2026-08-13 14:32:54 -05:00
|
|
|
import { app } from 'electron'
|
|
|
|
|
import { execFile } from 'child_process'
|
|
|
|
|
import { promisify } from 'util'
|
|
|
|
|
import { mkdir, readFile, writeFile, access, rm, readdir } from 'fs/promises'
|
|
|
|
|
import { join } from 'path'
|
2026-08-22 04:11:44 -05:00
|
|
|
import type {
|
|
|
|
|
AuroraProjectDetail,
|
|
|
|
|
AuroraProjectSummary,
|
|
|
|
|
AuroraInstalledModule,
|
|
|
|
|
AuroraModuleManifest,
|
|
|
|
|
AuroraStackOptions,
|
|
|
|
|
AuroraRuntimeEngine
|
|
|
|
|
} from '../shared/types'
|
2026-08-22 03:02:24 -05:00
|
|
|
import type { AuroraNativePorts } from './native/portAllocator'
|
2026-08-22 04:11:44 -05:00
|
|
|
import { allocateNativePorts } from './native/portAllocator'
|
|
|
|
|
import {
|
|
|
|
|
nativeProjectStatus,
|
|
|
|
|
provisionNativeProject,
|
|
|
|
|
stopNativeProject,
|
|
|
|
|
type NativeProjectDefinition
|
|
|
|
|
} from './native/nativeProject'
|
|
|
|
|
import { getRuntimeStatus } from './nativeRuntime'
|
|
|
|
|
import {
|
|
|
|
|
CORE_VERSION,
|
|
|
|
|
MODULE_API_VERSION,
|
|
|
|
|
getModuleManifest,
|
|
|
|
|
getModuleRegistry
|
|
|
|
|
} from './moduleRegistry'
|
2026-08-13 14:32:54 -05:00
|
|
|
|
|
|
|
|
const execFileAsync = promisify(execFile)
|
|
|
|
|
const EXTRA_PATH_DIRS = ['/opt/homebrew/bin', '/usr/local/bin', '/opt/local/bin']
|
|
|
|
|
export const AURORA_ENV = { ...process.env, PATH: [...EXTRA_PATH_DIRS, process.env.PATH].join(':') }
|
|
|
|
|
|
2026-08-14 20:50:08 -05:00
|
|
|
export type AuroraConfig = {
|
2026-08-13 14:32:54 -05:00
|
|
|
name: string
|
|
|
|
|
type: string
|
|
|
|
|
docroot: string
|
|
|
|
|
php: string
|
|
|
|
|
node: string
|
2026-08-14 13:38:50 -05:00
|
|
|
webserver: 'nginx' | 'apache'
|
2026-08-14 13:47:25 -05:00
|
|
|
database: 'mariadb' | 'mysql' | 'postgres'
|
2026-08-13 14:32:54 -05:00
|
|
|
databaseVersion: string
|
|
|
|
|
modules: string[]
|
|
|
|
|
moduleSettings?: Record<string, Record<string, string | number | boolean>>
|
|
|
|
|
primaryProtocol?: 'http' | 'https'
|
|
|
|
|
wordpressMultisite?: 'none' | 'subdirectory' | 'subdomain'
|
2026-08-14 12:08:21 -05:00
|
|
|
moduleMetadata?: Record<string, string | number | boolean>
|
2026-08-13 14:32:54 -05:00
|
|
|
xdebug?: boolean
|
2026-08-22 03:02:24 -05:00
|
|
|
runtimeEngine?: AuroraRuntimeEngine
|
|
|
|
|
nativePorts?: AuroraNativePorts
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Registry = { projects: Record<string, string> }
|
|
|
|
|
const configDir = (root: string): string => join(root, '.aurora')
|
|
|
|
|
const configPath = (root: string): string => join(configDir(root), 'config.json')
|
|
|
|
|
const composePath = (root: string): string => join(configDir(root), 'compose.yaml')
|
|
|
|
|
const phpDockerfilePath = (root: string): string => join(configDir(root), 'Dockerfile.php')
|
2026-08-14 13:38:50 -05:00
|
|
|
const apacheConfigPath = (root: string): string => join(configDir(root), 'httpd.conf')
|
2026-08-22 04:11:44 -05:00
|
|
|
const adminerAutoLoginPath = (root: string): string =>
|
|
|
|
|
join(configDir(root), 'adminer-auto-login.php')
|
2026-08-13 14:32:54 -05:00
|
|
|
const registryPath = (): string => join(app.getPath('userData'), 'projects.json')
|
|
|
|
|
const routerDir = (): string => join(app.getPath('userData'), 'router')
|
|
|
|
|
const routerConfigPath = (): string => join(routerDir(), 'dynamic.yaml')
|
|
|
|
|
const certDir = (): string => join(app.getPath('userData'), 'certificates')
|
|
|
|
|
const caDir = (): string => join(certDir(), 'ca')
|
|
|
|
|
const projectsCertDir = (): string => join(certDir(), 'projects')
|
|
|
|
|
const caKeyPath = (): string => join(caDir(), 'aurora-root-ca.key')
|
|
|
|
|
const caCertPath = (): string => join(caDir(), 'aurora-root-ca.crt')
|
|
|
|
|
const projectCertPath = (name: string): string => join(projectsCertDir(), `${safeName(name)}.crt`)
|
|
|
|
|
const projectKeyPath = (name: string): string => join(projectsCertDir(), `${safeName(name)}.key`)
|
2026-08-22 04:11:44 -05:00
|
|
|
const safeName = (name: string): string =>
|
|
|
|
|
name
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.replace(/[^a-z0-9_-]+/g, '-')
|
|
|
|
|
.replace(/^-+|-+$/g, '') || 'project'
|
2026-08-13 14:32:54 -05:00
|
|
|
const projectHost = (name: string): string => `${safeName(name)}.aurora.localhost`
|
2026-08-22 04:11:44 -05:00
|
|
|
export const projectUrls = (name: string): { http: string; https: string } => ({
|
|
|
|
|
http: `http://${projectHost(name)}`,
|
|
|
|
|
https: `https://${projectHost(name)}`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function nativeDefinition(root: string, config: AuroraConfig): NativeProjectDefinition {
|
|
|
|
|
if (!config.nativePorts)
|
|
|
|
|
throw new Error(`Native project '${config.name}' has no allocated ports.`)
|
|
|
|
|
return { name: config.name, root, docroot: config.docroot, ports: config.nativePorts }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function urlsForConfig(config: AuroraConfig): { http: string; https: string } {
|
|
|
|
|
if (config.runtimeEngine === 'native' && config.nativePorts)
|
|
|
|
|
return {
|
|
|
|
|
http: `http://127.0.0.1:${config.nativePorts.http}`,
|
|
|
|
|
https: `http://127.0.0.1:${config.nativePorts.http}`
|
|
|
|
|
}
|
|
|
|
|
return projectUrls(config.name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function assertNativeStack(
|
|
|
|
|
stack: Partial<AuroraStackOptions>,
|
|
|
|
|
phpVersion: string
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const status = await getRuntimeStatus()
|
|
|
|
|
if (!status.native.available)
|
|
|
|
|
throw new Error(status.native.reason || 'Aurora Native runtime is not installed.')
|
|
|
|
|
const versions = new Map(
|
|
|
|
|
status.native.components.map((component) => [component.id, component.version])
|
|
|
|
|
)
|
|
|
|
|
if (!versions.get('php')?.startsWith(`${phpVersion}.`))
|
|
|
|
|
throw new Error(`Installed native runtime does not provide PHP ${phpVersion}.`)
|
|
|
|
|
if (stack.webServer === 'apache')
|
|
|
|
|
throw new Error(
|
|
|
|
|
'The first Aurora Native runtime supports nginx; Apache will be added as a separate runtime component.'
|
|
|
|
|
)
|
|
|
|
|
if (stack.database && stack.database !== 'mariadb')
|
|
|
|
|
throw new Error('The first Aurora Native runtime supports MariaDB 11.8 only.')
|
|
|
|
|
if (!versions.get('mariadb')?.startsWith('11.8.'))
|
|
|
|
|
throw new Error('Installed native runtime does not provide MariaDB 11.8.')
|
|
|
|
|
if (!versions.has('nginx') || !versions.has('wp-cli'))
|
|
|
|
|
throw new Error('Installed native runtime is missing nginx or WP-CLI.')
|
|
|
|
|
if (stack.adminer !== false || stack.redis || stack.mailpit || stack.xdebug)
|
|
|
|
|
throw new Error('Adminer, Redis, Mailpit, and Xdebug are not enabled for Aurora Native yet.')
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
|
|
|
|
|
async function loadRegistry(): Promise<Registry> {
|
2026-08-22 04:11:44 -05:00
|
|
|
try {
|
|
|
|
|
return JSON.parse(await readFile(registryPath(), 'utf8')) as Registry
|
|
|
|
|
} catch {
|
|
|
|
|
return { projects: {} }
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
async function saveRegistry(registry: Registry): Promise<void> {
|
|
|
|
|
await mkdir(app.getPath('userData'), { recursive: true })
|
|
|
|
|
await writeFile(registryPath(), JSON.stringify(registry, null, 2))
|
|
|
|
|
}
|
|
|
|
|
async function readConfig(root: string): Promise<AuroraConfig> {
|
|
|
|
|
return JSON.parse(await readFile(configPath(root), 'utf8')) as AuroraConfig
|
|
|
|
|
}
|
|
|
|
|
async function writeConfig(root: string, config: AuroraConfig): Promise<void> {
|
|
|
|
|
await mkdir(configDir(root), { recursive: true })
|
|
|
|
|
await writeFile(configPath(root), JSON.stringify(config, null, 2))
|
2026-08-22 04:11:44 -05:00
|
|
|
if ((config.runtimeEngine ?? 'container') === 'container') {
|
|
|
|
|
await writeAdminerAutoLogin(root, config)
|
|
|
|
|
await writeFile(composePath(root), await renderCompose(config))
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
2026-08-14 13:26:14 -05:00
|
|
|
async function writeAdminerAutoLogin(root: string, config: AuroraConfig): Promise<void> {
|
|
|
|
|
const driver = config.database === 'postgres' ? 'pgsql' : 'server'
|
2026-08-22 04:11:44 -05:00
|
|
|
await writeFile(
|
|
|
|
|
adminerAutoLoginPath(root),
|
|
|
|
|
`<?php
|
2026-08-14 13:26:14 -05:00
|
|
|
// Generated by Aurora Core. Adminer is exposed only through the project's
|
|
|
|
|
// localhost route; credentials stay in this server-side container bootstrap.
|
|
|
|
|
if (empty($_GET['username']) && empty($_POST['auth'])) {
|
|
|
|
|
$_POST['auth'] = [
|
|
|
|
|
'driver' => '${driver}',
|
|
|
|
|
'server' => 'db',
|
|
|
|
|
'username' => 'db',
|
|
|
|
|
'password' => 'db',
|
|
|
|
|
'db' => 'db',
|
|
|
|
|
'permanent' => '1',
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
return new class {
|
|
|
|
|
public function credentials() { return ['db', 'db', 'db']; }
|
|
|
|
|
public function database() { return 'db'; }
|
|
|
|
|
public function login($login, $password) { return true; }
|
|
|
|
|
};
|
2026-08-22 04:11:44 -05:00
|
|
|
`
|
|
|
|
|
)
|
2026-08-14 13:26:14 -05:00
|
|
|
}
|
|
|
|
|
|
2026-08-13 14:32:54 -05:00
|
|
|
function indent(lines: string, spaces = 4): string {
|
|
|
|
|
const pad = ' '.repeat(spaces)
|
2026-08-22 04:11:44 -05:00
|
|
|
return lines
|
|
|
|
|
.split('\n')
|
|
|
|
|
.map((line) => (line ? pad + line : line))
|
|
|
|
|
.join('\n')
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
2026-08-22 04:11:44 -05:00
|
|
|
async function renderModuleService(
|
|
|
|
|
module: AuroraModuleManifest,
|
|
|
|
|
config: AuroraConfig
|
|
|
|
|
): Promise<string> {
|
2026-08-13 14:32:54 -05:00
|
|
|
if (!module.compose) return ''
|
|
|
|
|
const { service, image, ports = [], dependsOn = [] } = module.compose
|
|
|
|
|
const settings = config.moduleSettings?.[module.id] ?? {}
|
|
|
|
|
const lines = [`${service}:`, ` image: ${image}`]
|
|
|
|
|
if (module.id === 'redis' && settings.persistence !== false) {
|
|
|
|
|
lines.push(' volumes:', ' - redis_data:/data')
|
|
|
|
|
}
|
|
|
|
|
if (ports.length) {
|
|
|
|
|
lines.push(' ports:')
|
|
|
|
|
for (const port of ports) lines.push(` - "127.0.0.1::${port}"`)
|
|
|
|
|
}
|
|
|
|
|
if (dependsOn.length) lines.push(' depends_on:', ...dependsOn.map((dep) => ` - ${dep}`))
|
2026-08-22 04:11:44 -05:00
|
|
|
if (module.id === 'mailpit')
|
|
|
|
|
lines.push(
|
|
|
|
|
' networks:',
|
|
|
|
|
' default:',
|
|
|
|
|
' aurora-router:',
|
|
|
|
|
' aliases:',
|
|
|
|
|
` - aurora-${safeName(config.name)}-mailpit`
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
return indent(lines.join('\n'), 2)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function renderCompose(c: AuroraConfig): Promise<string> {
|
2026-08-22 04:11:44 -05:00
|
|
|
const db =
|
|
|
|
|
c.database === 'postgres'
|
|
|
|
|
? ` db:\n image: postgres:${c.databaseVersion}\n environment:\n POSTGRES_DB: db\n POSTGRES_USER: db\n POSTGRES_PASSWORD: db\n volumes:\n - db_data:/var/lib/postgresql/data\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U db -d db\"]\n interval: 3s\n timeout: 3s\n retries: 20`
|
|
|
|
|
: c.database === 'mysql'
|
|
|
|
|
? ` db:\n image: mysql:${c.databaseVersion}\n environment:\n MYSQL_DATABASE: db\n MYSQL_USER: db\n MYSQL_PASSWORD: db\n MYSQL_ROOT_PASSWORD: root\n volumes:\n - db_data:/var/lib/mysql\n healthcheck:\n test: [\"CMD-SHELL\", \"mysqladmin ping -h localhost -uroot -proot --silent\"]\n interval: 3s\n timeout: 3s\n retries: 20`
|
|
|
|
|
: ` db:\n image: mariadb:${c.databaseVersion}\n environment:\n MARIADB_DATABASE: db\n MARIADB_USER: db\n MARIADB_PASSWORD: db\n MARIADB_ROOT_PASSWORD: root\n volumes:\n - db_data:/var/lib/mysql\n healthcheck:\n test: [\"CMD\", \"healthcheck.sh\", \"--connect\", \"--innodb_initialized\"]\n interval: 3s\n timeout: 3s\n retries: 20`
|
2026-08-14 12:08:21 -05:00
|
|
|
const coreServices: Record<string, AuroraModuleManifest> = {
|
2026-08-22 04:11:44 -05:00
|
|
|
redis: {
|
|
|
|
|
id: 'redis',
|
|
|
|
|
name: 'Redis',
|
|
|
|
|
version: '1.0.0',
|
|
|
|
|
category: 'service',
|
|
|
|
|
description: '',
|
|
|
|
|
dependencies: [],
|
|
|
|
|
conflicts: [],
|
|
|
|
|
settings: [],
|
|
|
|
|
aurora: { core: CORE_VERSION, moduleApi: MODULE_API_VERSION },
|
|
|
|
|
compose: { service: 'redis', image: 'redis:8-alpine', ports: [6379] }
|
|
|
|
|
},
|
|
|
|
|
mailpit: {
|
|
|
|
|
id: 'mailpit',
|
|
|
|
|
name: 'Mailpit',
|
|
|
|
|
version: '1.0.0',
|
|
|
|
|
category: 'tool',
|
|
|
|
|
description: '',
|
|
|
|
|
dependencies: [],
|
|
|
|
|
conflicts: [],
|
|
|
|
|
settings: [],
|
|
|
|
|
aurora: { core: CORE_VERSION, moduleApi: MODULE_API_VERSION },
|
|
|
|
|
compose: { service: 'mailpit', image: 'axllent/mailpit:latest', ports: [8025, 1025] }
|
|
|
|
|
}
|
2026-08-14 12:08:21 -05:00
|
|
|
}
|
2026-08-22 04:11:44 -05:00
|
|
|
const manifests = await Promise.all(
|
|
|
|
|
c.modules
|
|
|
|
|
.filter((id) => id !== 'adminer')
|
|
|
|
|
.map((id) => coreServices[id] ?? getModuleManifest(id))
|
|
|
|
|
)
|
|
|
|
|
const extras = (await Promise.all(manifests.map((m) => renderModuleService(m, c)))).filter(
|
|
|
|
|
Boolean
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
const volumes = [' db_data:']
|
2026-08-22 04:11:44 -05:00
|
|
|
if (c.modules.includes('redis') && c.moduleSettings?.redis?.persistence !== false)
|
|
|
|
|
volumes.push(' redis_data:')
|
|
|
|
|
const adminer = c.modules.includes('adminer')
|
|
|
|
|
? ` adminer:\n image: adminer:5\n environment:\n ADMINER_DEFAULT_SERVER: db\n volumes:\n - ./adminer-auto-login.php:/var/www/html/plugins-enabled/aurora-auto-login.php:ro\n networks:\n default:\n aurora-router:\n aliases:\n - aurora-${safeName(c.name)}-adminer\n depends_on:\n db:\n condition: service_healthy`
|
|
|
|
|
: ''
|
2026-08-14 13:38:50 -05:00
|
|
|
const webImage = c.webserver === 'apache' ? 'httpd:2.4-alpine' : 'nginx:1.29-alpine'
|
2026-08-22 04:11:44 -05:00
|
|
|
const webConfigMount =
|
|
|
|
|
c.webserver === 'apache'
|
|
|
|
|
? ' - ./httpd.conf:/usr/local/apache2/conf/httpd.conf:ro'
|
|
|
|
|
: ' - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro'
|
2026-08-14 13:38:50 -05:00
|
|
|
return `name: aurora-${safeName(c.name)}\nservices:\n web:\n image: ${webImage}\n working_dir: /var/www/html\n volumes:\n - ../:/var/www/html\n${webConfigMount}\n ports:\n - "127.0.0.1::80"\n networks:\n default:\n aurora-router:\n aliases:\n - aurora-${safeName(c.name)}-web\n depends_on:\n php:\n condition: service_started\n db:\n condition: service_healthy\n php:\n build:\n context: .\n dockerfile: Dockerfile.php\n args:\n PHP_VERSION: ${c.php}\n working_dir: /var/www/html\n volumes:\n - ../:/var/www/html\n node:\n image: node:${c.node}-alpine\n working_dir: /var/www/html\n volumes:\n - ../:/var/www/html\n command: ["sh", "-c", "sleep infinity"]\n${db}${adminer ? `\n${adminer}` : ''}${extras.length ? `\n${extras.join('\n')}` : ''}\nvolumes:\n${volumes.join('\n')}\nnetworks:\n aurora-router:\n external: true\n name: aurora-router\n`
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
async function writePhpDockerfile(root: string, xdebug = false): Promise<void> {
|
2026-08-22 04:11:44 -05:00
|
|
|
await writeFile(
|
|
|
|
|
phpDockerfilePath(root),
|
|
|
|
|
`ARG PHP_VERSION=8.4
|
2026-08-13 14:32:54 -05:00
|
|
|
FROM php:${'${PHP_VERSION}'}-fpm-alpine
|
2026-08-14 22:41:10 -05:00
|
|
|
RUN apk add --no-cache icu-dev libzip-dev libpng-dev libjpeg-turbo-dev freetype-dev oniguruma-dev postgresql-dev curl-dev libxml2-dev \
|
2026-08-13 14:32:54 -05:00
|
|
|
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
2026-08-14 22:41:10 -05:00
|
|
|
&& docker-php-ext-install -j2 mysqli pdo_mysql pdo_pgsql intl zip gd mbstring curl dom opcache
|
2026-08-13 14:32:54 -05:00
|
|
|
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
|
|
|
|
|
${xdebug ? 'RUN apk add --no-cache $PHPIZE_DEPS linux-headers && pecl install xdebug && docker-php-ext-enable xdebug' : ''}
|
2026-08-22 04:11:44 -05:00
|
|
|
`
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function writeNginx(root: string, docroot: string): Promise<void> {
|
|
|
|
|
const webroot = docroot ? `/var/www/html/${docroot}` : '/var/www/html'
|
2026-08-22 04:11:44 -05:00
|
|
|
await writeFile(
|
|
|
|
|
join(configDir(root), 'nginx.conf'),
|
|
|
|
|
`map $http_x_forwarded_proto $aurora_https {
|
2026-08-13 14:32:54 -05:00
|
|
|
default off;
|
|
|
|
|
https on;
|
|
|
|
|
}
|
|
|
|
|
server {
|
|
|
|
|
listen 80;
|
|
|
|
|
server_name _;
|
|
|
|
|
root ${webroot};
|
|
|
|
|
index index.php index.html;
|
|
|
|
|
location / { try_files $uri $uri/ /index.php?$query_string; }
|
|
|
|
|
location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param HTTPS $aurora_https; fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto; fastcgi_param HTTP_X_FORWARDED_HOST $http_x_forwarded_host; fastcgi_pass php:9000; }
|
|
|
|
|
}
|
2026-08-22 04:11:44 -05:00
|
|
|
`
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
2026-08-14 13:38:50 -05:00
|
|
|
async function writeApache(root: string, docroot: string): Promise<void> {
|
|
|
|
|
const webroot = docroot ? `/var/www/html/${docroot}` : '/var/www/html'
|
2026-08-22 04:11:44 -05:00
|
|
|
await writeFile(
|
|
|
|
|
apacheConfigPath(root),
|
|
|
|
|
`ServerRoot "/usr/local/apache2"
|
2026-08-14 13:38:50 -05:00
|
|
|
Listen 80
|
|
|
|
|
LoadModule mpm_event_module modules/mod_mpm_event.so
|
|
|
|
|
LoadModule authn_core_module modules/mod_authn_core.so
|
|
|
|
|
LoadModule authz_core_module modules/mod_authz_core.so
|
|
|
|
|
LoadModule dir_module modules/mod_dir.so
|
|
|
|
|
LoadModule mime_module modules/mod_mime.so
|
|
|
|
|
LoadModule proxy_module modules/mod_proxy.so
|
|
|
|
|
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
|
|
|
|
|
LoadModule rewrite_module modules/mod_rewrite.so
|
2026-08-14 16:06:59 -05:00
|
|
|
LoadModule unixd_module modules/mod_unixd.so
|
|
|
|
|
LoadModule log_config_module modules/mod_log_config.so
|
2026-08-14 17:05:14 -05:00
|
|
|
LoadModule setenvif_module modules/mod_setenvif.so
|
2026-08-14 13:38:50 -05:00
|
|
|
User daemon
|
|
|
|
|
Group daemon
|
|
|
|
|
ServerName localhost
|
|
|
|
|
DocumentRoot "${webroot}"
|
|
|
|
|
DirectoryIndex index.php index.html
|
2026-08-14 17:05:14 -05:00
|
|
|
SetEnvIf X-Forwarded-Proto "^https$" HTTPS=on
|
|
|
|
|
SetEnvIf X-Forwarded-Port "^443$" SERVER_PORT=443
|
2026-08-14 13:38:50 -05:00
|
|
|
<Directory "${webroot}">
|
|
|
|
|
Options Indexes FollowSymLinks
|
|
|
|
|
AllowOverride All
|
|
|
|
|
Require all granted
|
|
|
|
|
RewriteEngine On
|
|
|
|
|
RewriteCond %{REQUEST_FILENAME} !-f
|
|
|
|
|
RewriteCond %{REQUEST_FILENAME} !-d
|
|
|
|
|
RewriteRule ^ index.php [QSA,L]
|
|
|
|
|
</Directory>
|
|
|
|
|
ProxyPassMatch ^/(.*\\.php(?:/.*)?)$ fcgi://php:9000/var/www/html/$1
|
|
|
|
|
ErrorLog /proc/self/fd/2
|
|
|
|
|
LogFormat "%h %l %u %t \\"%r\\" %>s %b" combined
|
|
|
|
|
CustomLog /proc/self/fd/1 combined
|
2026-08-22 04:11:44 -05:00
|
|
|
`
|
|
|
|
|
)
|
2026-08-14 13:38:50 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function writeWebServerConfig(root: string, config: AuroraConfig): Promise<void> {
|
|
|
|
|
if (config.webserver === 'apache') await writeApache(root, config.docroot)
|
|
|
|
|
else await writeNginx(root, config.docroot)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-22 04:11:44 -05:00
|
|
|
export async function createProject(
|
|
|
|
|
root: string,
|
|
|
|
|
name: string,
|
|
|
|
|
type: string,
|
|
|
|
|
docroot: string,
|
|
|
|
|
stack?: Partial<AuroraStackOptions>
|
|
|
|
|
): Promise<void> {
|
2026-08-13 14:32:54 -05:00
|
|
|
await mkdir(root, { recursive: true })
|
2026-08-14 12:08:21 -05:00
|
|
|
if (!type) throw new Error('Install and select an application module before creating a project')
|
|
|
|
|
const application = await getModuleManifest(type)
|
2026-08-22 04:11:44 -05:00
|
|
|
if (application.category !== 'application')
|
|
|
|
|
throw new Error(`Module '${type}' cannot create application projects`)
|
2026-08-14 12:08:21 -05:00
|
|
|
const normalizedType = application.id
|
|
|
|
|
const defaultDocroot = docroot || application.defaults?.docroot || ''
|
|
|
|
|
const modules = [normalizedType]
|
2026-08-13 14:32:54 -05:00
|
|
|
if (stack?.adminer !== false) modules.push('adminer')
|
|
|
|
|
if (stack?.redis) modules.push('redis')
|
|
|
|
|
if (stack?.mailpit) modules.push('mailpit')
|
2026-08-14 22:41:10 -05:00
|
|
|
const phpVersion = stack?.phpVersion || application.creation?.phpVersions?.[0] || '8.4'
|
2026-08-22 04:11:44 -05:00
|
|
|
if (
|
|
|
|
|
application.creation?.phpVersions?.length &&
|
|
|
|
|
!application.creation.phpVersions.includes(phpVersion)
|
|
|
|
|
)
|
|
|
|
|
throw new Error(`${application.name} does not support PHP ${phpVersion}`)
|
2026-08-22 03:02:24 -05:00
|
|
|
const runtimeEngine = stack?.runtimeEngine ?? 'container'
|
2026-08-22 04:11:44 -05:00
|
|
|
if (runtimeEngine === 'native') await assertNativeStack(stack ?? {}, phpVersion)
|
|
|
|
|
const config: AuroraConfig = {
|
|
|
|
|
name,
|
|
|
|
|
type: normalizedType,
|
|
|
|
|
docroot: defaultDocroot,
|
|
|
|
|
php: phpVersion,
|
|
|
|
|
node: stack?.nodeVersion || '24',
|
|
|
|
|
webserver: stack?.webServer || 'nginx',
|
|
|
|
|
database: stack?.database || 'mariadb',
|
|
|
|
|
databaseVersion: stack?.databaseVersion || '11.8',
|
|
|
|
|
modules,
|
|
|
|
|
moduleSettings: {},
|
|
|
|
|
primaryProtocol: runtimeEngine === 'native' ? 'http' : 'https',
|
|
|
|
|
xdebug: stack?.xdebug === true,
|
|
|
|
|
runtimeEngine,
|
|
|
|
|
nativePorts: runtimeEngine === 'native' ? await allocateNativePorts() : undefined
|
|
|
|
|
}
|
|
|
|
|
await writeConfig(root, config)
|
|
|
|
|
await writeWebServerConfig(root, config)
|
|
|
|
|
await writePhpDockerfile(root, config.xdebug)
|
|
|
|
|
if (runtimeEngine === 'native') await provisionNativeProject(nativeDefinition(root, config))
|
|
|
|
|
const reg = await loadRegistry()
|
|
|
|
|
reg.projects[name] = root
|
|
|
|
|
await saveRegistry(reg)
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
export async function unregisterProject(name: string, deleteFiles: boolean): Promise<void> {
|
2026-08-22 04:11:44 -05:00
|
|
|
const reg = await loadRegistry()
|
|
|
|
|
const root = reg.projects[name]
|
|
|
|
|
delete reg.projects[name]
|
|
|
|
|
await saveRegistry(reg)
|
2026-08-13 14:32:54 -05:00
|
|
|
if (deleteFiles && root) await rm(root, { recursive: true, force: true })
|
|
|
|
|
}
|
|
|
|
|
async function composeJson(root: string): Promise<any[]> {
|
2026-08-22 04:11:44 -05:00
|
|
|
try {
|
|
|
|
|
const { stdout } = await execFileAsync(
|
|
|
|
|
'docker',
|
|
|
|
|
['compose', '-f', composePath(root), 'ps', '--all', '--format', 'json'],
|
|
|
|
|
{ env: AURORA_ENV, maxBuffer: 8 * 1024 * 1024 }
|
|
|
|
|
)
|
|
|
|
|
return stdout
|
|
|
|
|
.trim()
|
|
|
|
|
.split('\n')
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.map((x) => JSON.parse(x))
|
|
|
|
|
} catch {
|
|
|
|
|
return []
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
export async function listProjects(): Promise<AuroraProjectSummary[]> {
|
2026-08-22 04:11:44 -05:00
|
|
|
const reg = await loadRegistry()
|
|
|
|
|
const out: AuroraProjectSummary[] = []
|
|
|
|
|
const availableModules = new Set((await getModuleRegistry()).map((module) => module.id))
|
2026-08-13 14:32:54 -05:00
|
|
|
for (const [name, root] of Object.entries(reg.projects)) {
|
2026-08-22 04:11:44 -05:00
|
|
|
try {
|
|
|
|
|
await access(configPath(root))
|
|
|
|
|
const c = await readConfig(root)
|
|
|
|
|
const ps = c.runtimeEngine === 'native' ? [] : await composeJson(root)
|
|
|
|
|
const nativeStatus =
|
|
|
|
|
c.runtimeEngine === 'native' ? await nativeProjectStatus(nativeDefinition(root, c)) : null
|
|
|
|
|
const running =
|
|
|
|
|
nativeStatus?.running ?? (ps.length > 0 && ps.every((p) => p.State === 'running'))
|
|
|
|
|
const urls = urlsForConfig(c)
|
|
|
|
|
const primary = c.primaryProtocol === 'http' ? urls.http : urls.https
|
2026-08-14 12:08:21 -05:00
|
|
|
const moduleAvailable = availableModules.has(c.type)
|
2026-08-22 04:11:44 -05:00
|
|
|
out.push({
|
|
|
|
|
name,
|
|
|
|
|
status: running ? 'running' : 'stopped',
|
|
|
|
|
status_desc: moduleAvailable
|
|
|
|
|
? running
|
|
|
|
|
? 'Running'
|
|
|
|
|
: 'Stopped'
|
|
|
|
|
: `Missing application module: ${c.type}`,
|
|
|
|
|
type: c.type,
|
|
|
|
|
approot: root,
|
|
|
|
|
shortroot: root,
|
|
|
|
|
docroot: c.docroot,
|
|
|
|
|
primary_url: primary,
|
|
|
|
|
httpurl: urls.http,
|
|
|
|
|
httpsurl: urls.https,
|
|
|
|
|
mutagen_enabled: false,
|
|
|
|
|
module_available: moduleAvailable,
|
|
|
|
|
missing_module_id: moduleAvailable ? undefined : c.type,
|
|
|
|
|
runtime_engine: c.runtimeEngine ?? 'container',
|
|
|
|
|
native_ports: c.nativePorts
|
|
|
|
|
})
|
|
|
|
|
} catch {
|
|
|
|
|
/* stale registry entry */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
export async function describeProject(name: string): Promise<AuroraProjectDetail> {
|
2026-08-22 04:11:44 -05:00
|
|
|
const reg = await loadRegistry()
|
|
|
|
|
const root = reg.projects[name]
|
|
|
|
|
if (!root) throw new Error(`Aurora project '${name}' not found`)
|
|
|
|
|
const c = await readConfig(root)
|
|
|
|
|
const ps = c.runtimeEngine === 'native' ? [] : await composeJson(root)
|
|
|
|
|
const nativeStatus =
|
|
|
|
|
c.runtimeEngine === 'native' ? await nativeProjectStatus(nativeDefinition(root, c)) : null
|
|
|
|
|
const running = nativeStatus?.running ?? (ps.length > 0 && ps.every((p) => p.State === 'running'))
|
|
|
|
|
const currentRouterStatus =
|
|
|
|
|
c.runtimeEngine === 'native' ? (running ? 'running' : 'stopped') : await routerStatus()
|
|
|
|
|
const urlSet = urlsForConfig(c)
|
|
|
|
|
const primary = c.primaryProtocol === 'http' ? urlSet.http : urlSet.https
|
|
|
|
|
const services: Record<string, any> = {}
|
|
|
|
|
for (const p of ps)
|
|
|
|
|
services[p.Service] = {
|
|
|
|
|
short_name: p.Service,
|
|
|
|
|
full_name: p.Name,
|
|
|
|
|
status: p.State,
|
|
|
|
|
image: p.Image,
|
|
|
|
|
exposed_ports: '',
|
|
|
|
|
host_ports: '',
|
|
|
|
|
host_ports_mapping: []
|
|
|
|
|
}
|
|
|
|
|
if (nativeStatus)
|
|
|
|
|
for (const [service, status] of Object.entries(nativeStatus.services))
|
|
|
|
|
services[service] = {
|
|
|
|
|
short_name: service,
|
|
|
|
|
full_name: `${c.name}:${service}`,
|
|
|
|
|
status,
|
|
|
|
|
image: 'Aurora Native',
|
|
|
|
|
exposed_ports: '',
|
|
|
|
|
host_ports: '',
|
|
|
|
|
host_ports_mapping: []
|
|
|
|
|
}
|
|
|
|
|
const moduleMetadata = {
|
|
|
|
|
...(c.wordpressMultisite ? { multisite: c.wordpressMultisite } : {}),
|
|
|
|
|
...c.moduleMetadata
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
name,
|
|
|
|
|
status: running ? 'running' : 'stopped',
|
|
|
|
|
status_desc: running ? 'Running' : 'Stopped',
|
|
|
|
|
type: c.type,
|
|
|
|
|
approot: root,
|
|
|
|
|
shortroot: root,
|
|
|
|
|
docroot: c.docroot,
|
|
|
|
|
primary_url: primary,
|
|
|
|
|
httpurl: urlSet.http,
|
|
|
|
|
httpsurl: urlSet.https,
|
|
|
|
|
mutagen_enabled: false,
|
|
|
|
|
database_type: c.database,
|
|
|
|
|
database_version: c.databaseVersion,
|
|
|
|
|
dbinfo: {
|
|
|
|
|
database_type: c.database,
|
|
|
|
|
database_version: c.databaseVersion,
|
|
|
|
|
dbPort: c.database === 'postgres' ? '5432' : '3306',
|
|
|
|
|
dbname: 'db',
|
|
|
|
|
host: c.runtimeEngine === 'native' ? '127.0.0.1' : 'db',
|
|
|
|
|
password: 'db',
|
|
|
|
|
published_port: c.nativePorts?.database ?? 0,
|
|
|
|
|
username: 'db'
|
|
|
|
|
},
|
|
|
|
|
hostname: c.runtimeEngine === 'native' ? '127.0.0.1' : projectHost(c.name),
|
|
|
|
|
hostnames: [c.runtimeEngine === 'native' ? '127.0.0.1' : projectHost(c.name)],
|
|
|
|
|
httpURLs: [urlSet.http],
|
|
|
|
|
httpsURLs: c.runtimeEngine === 'native' ? [] : [urlSet.https],
|
|
|
|
|
urls: c.runtimeEngine === 'native' ? [urlSet.http] : [urlSet.http, urlSet.https],
|
|
|
|
|
php_version: c.php,
|
|
|
|
|
nodejs_version: c.node,
|
|
|
|
|
webserver_type: c.webserver,
|
|
|
|
|
router: c.runtimeEngine === 'native' ? 'native' : 'file',
|
|
|
|
|
router_status: currentRouterStatus,
|
|
|
|
|
certificate_status: c.runtimeEngine === 'native' ? 'missing' : await certificateStatus(c.name),
|
|
|
|
|
ca_trust_status: c.runtimeEngine === 'native' ? 'unknown' : await caTrustStatus(),
|
|
|
|
|
firefox_trust_status: c.runtimeEngine === 'native' ? 'unknown' : await firefoxTrustStatus(),
|
|
|
|
|
chromium_trust_status: c.runtimeEngine === 'native' ? 'unknown' : await chromiumTrustStatus(),
|
|
|
|
|
module_metadata: moduleMetadata,
|
|
|
|
|
adminer_url:
|
|
|
|
|
c.runtimeEngine === 'container' && c.modules.includes('adminer')
|
|
|
|
|
? `https://adminer.${projectHost(c.name)}`
|
|
|
|
|
: undefined,
|
|
|
|
|
services,
|
|
|
|
|
xdebug_enabled: c.xdebug === true,
|
|
|
|
|
runtime_engine: c.runtimeEngine ?? 'container',
|
|
|
|
|
native_ports: c.nativePorts
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
2026-08-22 04:11:44 -05:00
|
|
|
export async function updateEnvironment(
|
|
|
|
|
root: string,
|
|
|
|
|
updates: {
|
|
|
|
|
phpVersion?: string
|
|
|
|
|
nodeVersion?: string
|
|
|
|
|
webserverType?: string
|
|
|
|
|
database?: string
|
|
|
|
|
xdebugEnabled?: boolean
|
|
|
|
|
primaryProtocol?: 'http' | 'https'
|
|
|
|
|
}
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const c = await readConfig(root)
|
|
|
|
|
if (updates.phpVersion) c.php = updates.phpVersion
|
|
|
|
|
if (updates.nodeVersion) c.node = updates.nodeVersion
|
|
|
|
|
if (updates.webserverType === 'nginx' || updates.webserverType === 'nginx-fpm')
|
|
|
|
|
c.webserver = 'nginx'
|
|
|
|
|
if (updates.webserverType === 'apache' || updates.webserverType === 'apache-fpm')
|
|
|
|
|
c.webserver = 'apache'
|
|
|
|
|
if (typeof updates.xdebugEnabled === 'boolean') c.xdebug = updates.xdebugEnabled
|
|
|
|
|
if (updates.database) {
|
|
|
|
|
const [kind, version] = updates.database.split(':')
|
|
|
|
|
if (kind === 'mariadb' || kind === 'mysql' || kind === 'postgres') {
|
|
|
|
|
c.database = kind
|
|
|
|
|
c.databaseVersion = version || c.databaseVersion
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (updates.primaryProtocol === 'http' || updates.primaryProtocol === 'https')
|
|
|
|
|
c.primaryProtocol = updates.primaryProtocol
|
|
|
|
|
await writeConfig(root, c)
|
|
|
|
|
await writeWebServerConfig(root, c)
|
|
|
|
|
await writePhpDockerfile(root, c.xdebug === true)
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
2026-08-22 04:11:44 -05:00
|
|
|
export async function getProjectConfig(root: string): Promise<AuroraConfig> {
|
|
|
|
|
return readConfig(root)
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
|
2026-08-22 04:11:44 -05:00
|
|
|
export async function setProjectModuleMetadata(
|
|
|
|
|
root: string,
|
|
|
|
|
metadata: Record<string, string | number | boolean>
|
|
|
|
|
): Promise<void> {
|
2026-08-14 12:08:21 -05:00
|
|
|
const config = await readConfig(root)
|
|
|
|
|
config.moduleMetadata = { ...config.moduleMetadata, ...metadata }
|
|
|
|
|
await writeConfig(root, config)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 14:32:54 -05:00
|
|
|
export async function routerStatus(): Promise<'running' | 'provider-error' | 'stopped'> {
|
|
|
|
|
try {
|
2026-08-22 04:11:44 -05:00
|
|
|
const { stdout } = await execFileAsync(
|
|
|
|
|
'docker',
|
|
|
|
|
['inspect', '-f', '{{.State.Running}}', 'aurora-router'],
|
|
|
|
|
{ env: AURORA_ENV }
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
if (stdout.trim() !== 'true') return 'stopped'
|
2026-08-22 04:11:44 -05:00
|
|
|
const { stdout: logs = '', stderr: logErrors = '' } = await execFileAsync(
|
|
|
|
|
'docker',
|
|
|
|
|
['logs', '--tail', '40', 'aurora-router'],
|
|
|
|
|
{ env: AURORA_ENV, maxBuffer: 2 * 1024 * 1024 }
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
const recentLogs = `${logs}\n${logErrors}`
|
2026-08-22 04:11:44 -05:00
|
|
|
if (
|
|
|
|
|
recentLogs.includes('Error while building configuration') ||
|
|
|
|
|
recentLogs.includes('field not found, node:')
|
|
|
|
|
)
|
|
|
|
|
return 'provider-error'
|
2026-08-13 14:32:54 -05:00
|
|
|
return 'running'
|
2026-08-22 04:11:44 -05:00
|
|
|
} catch {
|
|
|
|
|
return 'stopped'
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function routerRunning(): Promise<boolean> {
|
|
|
|
|
return (await routerStatus()) === 'running'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function listModules(): Promise<AuroraModuleManifest[]> {
|
|
|
|
|
return getModuleRegistry()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function listInstalledModules(name: string): Promise<AuroraInstalledModule[]> {
|
|
|
|
|
const reg = await loadRegistry()
|
|
|
|
|
const root = reg.projects[name]
|
|
|
|
|
if (!root) throw new Error(`Aurora project '${name}' not found`)
|
|
|
|
|
const config = await readConfig(root)
|
|
|
|
|
const manifests = await Promise.all(config.modules.map((id) => getModuleManifest(id)))
|
2026-08-22 04:11:44 -05:00
|
|
|
return manifests.map((module) => ({
|
|
|
|
|
id: module.id,
|
|
|
|
|
name: module.name,
|
|
|
|
|
version: module.version,
|
|
|
|
|
category: module.category
|
|
|
|
|
}))
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function setModule(
|
|
|
|
|
name: string,
|
|
|
|
|
moduleId: string,
|
|
|
|
|
install: boolean,
|
|
|
|
|
settings: Record<string, string | number | boolean> = {}
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const reg = await loadRegistry()
|
|
|
|
|
const root = reg.projects[name]
|
|
|
|
|
if (!root) throw new Error(`Aurora project '${name}' not found`)
|
|
|
|
|
const config = await readConfig(root)
|
|
|
|
|
const module = await getModuleManifest(moduleId)
|
|
|
|
|
config.moduleSettings ??= {}
|
|
|
|
|
|
|
|
|
|
if (install) {
|
|
|
|
|
const conflict = module.conflicts.find((id) => config.modules.includes(id))
|
2026-08-22 04:11:44 -05:00
|
|
|
if (conflict)
|
|
|
|
|
throw new Error(
|
|
|
|
|
`${module.name} conflicts with the installed '${conflict}' application module.`
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
for (const dependency of module.dependencies) {
|
|
|
|
|
if (!config.modules.includes(dependency)) config.modules.push(dependency)
|
|
|
|
|
}
|
|
|
|
|
if (!config.modules.includes(moduleId)) config.modules.push(moduleId)
|
2026-08-22 04:11:44 -05:00
|
|
|
config.moduleSettings[moduleId] = Object.fromEntries(
|
|
|
|
|
module.settings.map((item) => [item.id, item.default])
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
Object.assign(config.moduleSettings[moduleId], settings)
|
|
|
|
|
if (module.category === 'application') {
|
|
|
|
|
config.type = moduleId
|
|
|
|
|
if (module.defaults?.docroot !== undefined) config.docroot = module.defaults.docroot
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2026-08-22 04:11:44 -05:00
|
|
|
const dependents = (await getModuleRegistry()).filter(
|
|
|
|
|
(item) => item.dependencies.includes(moduleId) && config.modules.includes(item.id)
|
|
|
|
|
)
|
|
|
|
|
if (dependents.length)
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Cannot remove ${module.name}; required by ${dependents.map((item) => item.name).join(', ')}.`
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
config.modules = config.modules.filter((id) => id !== moduleId)
|
|
|
|
|
delete config.moduleSettings[moduleId]
|
|
|
|
|
if (config.type === moduleId) config.type = 'generic'
|
|
|
|
|
}
|
|
|
|
|
await writeConfig(root, config)
|
2026-08-14 13:38:50 -05:00
|
|
|
await writeWebServerConfig(root, config)
|
2026-08-13 14:32:54 -05:00
|
|
|
await writePhpDockerfile(root, config.xdebug === true)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-14 12:08:21 -05:00
|
|
|
export async function scaffoldApplicationModule(_name: string, moduleId: string): Promise<void> {
|
2026-08-13 14:32:54 -05:00
|
|
|
const module = await getModuleManifest(moduleId)
|
|
|
|
|
if (module.category !== 'application') return
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-22 04:11:44 -05:00
|
|
|
export async function getProjectRoot(name: string): Promise<string> {
|
|
|
|
|
const r = await loadRegistry()
|
|
|
|
|
if (!r.projects[name]) throw new Error(`Project ${name} not found`)
|
|
|
|
|
return r.projects[name]
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
|
2026-08-22 04:11:44 -05:00
|
|
|
export async function getNativeProjectDefinition(
|
|
|
|
|
name: string
|
|
|
|
|
): Promise<NativeProjectDefinition | null> {
|
|
|
|
|
const root = await getProjectRoot(name)
|
|
|
|
|
const config = await readConfig(root)
|
|
|
|
|
return config.runtimeEngine === 'native' ? nativeDefinition(root, config) : null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getProjectConfigByRoot(
|
|
|
|
|
root: string
|
|
|
|
|
): Promise<{ database: 'mariadb' | 'mysql' | 'postgres'; databaseVersion: string; name: string }> {
|
2026-08-13 14:32:54 -05:00
|
|
|
const config = await readConfig(root)
|
|
|
|
|
return { database: config.database, databaseVersion: config.databaseVersion, name: config.name }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function ensureCertificateAuthority(): Promise<void> {
|
|
|
|
|
await mkdir(caDir(), { recursive: true })
|
2026-08-22 04:11:44 -05:00
|
|
|
try {
|
|
|
|
|
await access(caKeyPath())
|
|
|
|
|
await access(caCertPath())
|
|
|
|
|
return
|
|
|
|
|
} catch {
|
|
|
|
|
/* create below */
|
|
|
|
|
}
|
|
|
|
|
await execFileAsync(
|
|
|
|
|
'openssl',
|
|
|
|
|
[
|
|
|
|
|
'req',
|
|
|
|
|
'-x509',
|
|
|
|
|
'-newkey',
|
|
|
|
|
'rsa:3072',
|
|
|
|
|
'-sha256',
|
|
|
|
|
'-days',
|
|
|
|
|
'3650',
|
|
|
|
|
'-nodes',
|
|
|
|
|
'-keyout',
|
|
|
|
|
caKeyPath(),
|
|
|
|
|
'-out',
|
|
|
|
|
caCertPath(),
|
|
|
|
|
'-subj',
|
|
|
|
|
'/CN=Aurora Dockside Local Development CA',
|
|
|
|
|
'-addext',
|
|
|
|
|
'basicConstraints=critical,CA:TRUE',
|
|
|
|
|
'-addext',
|
|
|
|
|
'keyUsage=critical,keyCertSign,cRLSign'
|
|
|
|
|
],
|
|
|
|
|
{ env: AURORA_ENV, maxBuffer: 8 * 1024 * 1024 }
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function ensureProjectCertificate(name: string): Promise<void> {
|
|
|
|
|
await ensureCertificateAuthority()
|
|
|
|
|
await mkdir(projectsCertDir(), { recursive: true })
|
2026-08-22 04:11:44 -05:00
|
|
|
const cert = projectCertPath(name)
|
|
|
|
|
const key = projectKeyPath(name)
|
|
|
|
|
try {
|
|
|
|
|
await access(cert)
|
|
|
|
|
await access(key)
|
|
|
|
|
return
|
|
|
|
|
} catch {
|
|
|
|
|
/* create below */
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
const host = projectHost(name)
|
|
|
|
|
const csr = join(projectsCertDir(), `${safeName(name)}.csr`)
|
2026-08-22 04:11:44 -05:00
|
|
|
await execFileAsync(
|
|
|
|
|
'openssl',
|
|
|
|
|
[
|
|
|
|
|
'req',
|
|
|
|
|
'-new',
|
|
|
|
|
'-newkey',
|
|
|
|
|
'rsa:2048',
|
|
|
|
|
'-nodes',
|
|
|
|
|
'-keyout',
|
|
|
|
|
key,
|
|
|
|
|
'-out',
|
|
|
|
|
csr,
|
|
|
|
|
'-subj',
|
|
|
|
|
`/CN=${host}`,
|
|
|
|
|
'-addext',
|
|
|
|
|
`subjectAltName=DNS:${host},DNS:*.${host}`
|
|
|
|
|
],
|
|
|
|
|
{ env: AURORA_ENV, maxBuffer: 8 * 1024 * 1024 }
|
|
|
|
|
)
|
|
|
|
|
await execFileAsync(
|
|
|
|
|
'openssl',
|
|
|
|
|
[
|
|
|
|
|
'x509',
|
|
|
|
|
'-req',
|
|
|
|
|
'-in',
|
|
|
|
|
csr,
|
|
|
|
|
'-CA',
|
|
|
|
|
caCertPath(),
|
|
|
|
|
'-CAkey',
|
|
|
|
|
caKeyPath(),
|
|
|
|
|
'-CAcreateserial',
|
|
|
|
|
'-out',
|
|
|
|
|
cert,
|
|
|
|
|
'-days',
|
|
|
|
|
'825',
|
|
|
|
|
'-sha256',
|
|
|
|
|
'-copy_extensions',
|
|
|
|
|
'copy'
|
|
|
|
|
],
|
|
|
|
|
{ env: AURORA_ENV, maxBuffer: 8 * 1024 * 1024 }
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
await rm(csr, { force: true })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function certificateStatus(name: string): Promise<'generated' | 'missing'> {
|
2026-08-22 04:11:44 -05:00
|
|
|
try {
|
|
|
|
|
await access(projectCertPath(name))
|
|
|
|
|
await access(projectKeyPath(name))
|
|
|
|
|
return 'generated'
|
|
|
|
|
} catch {
|
|
|
|
|
return 'missing'
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function caTrustStatus(): Promise<'trusted' | 'not-trusted' | 'unknown'> {
|
|
|
|
|
if (process.platform !== 'linux') return 'unknown'
|
2026-08-22 04:11:44 -05:00
|
|
|
try {
|
|
|
|
|
await access('/usr/local/share/ca-certificates/aurora-dockside-local-ca.crt')
|
|
|
|
|
return 'trusted'
|
|
|
|
|
} catch {
|
|
|
|
|
return 'not-trusted'
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const firefoxRoots = (): string[] => {
|
|
|
|
|
const home = process.env.HOME || app.getPath('home')
|
2026-08-22 04:11:44 -05:00
|
|
|
return [
|
|
|
|
|
join(home, '.mozilla', 'firefox'),
|
|
|
|
|
join(home, 'snap', 'firefox', 'common', '.mozilla', 'firefox')
|
|
|
|
|
]
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function pathExists(path: string): Promise<boolean> {
|
2026-08-22 04:11:44 -05:00
|
|
|
try {
|
|
|
|
|
await access(path)
|
|
|
|
|
return true
|
|
|
|
|
} catch {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function firefoxProfiles(): Promise<string[]> {
|
|
|
|
|
const profiles: string[] = []
|
|
|
|
|
for (const root of firefoxRoots()) {
|
|
|
|
|
// Prefer the active/default profile declared by Firefox itself.
|
|
|
|
|
try {
|
|
|
|
|
const ini = await readFile(join(root, 'profiles.ini'), 'utf8')
|
2026-08-22 04:11:44 -05:00
|
|
|
const sections = ini
|
|
|
|
|
.split(/^\s*\[/m)
|
|
|
|
|
.map((section, index) => (index === 0 ? section : '[' + section))
|
2026-08-13 14:32:54 -05:00
|
|
|
for (const section of sections) {
|
|
|
|
|
if (!/^\[Profile\d+\]/m.test(section) || !/^Default=1\s*$/m.test(section)) continue
|
|
|
|
|
const match = section.match(/^Path=(.+)\s*$/m)
|
|
|
|
|
if (!match) continue
|
|
|
|
|
const profile = join(root, match[1].trim())
|
|
|
|
|
if (await pathExists(join(profile, 'cert9.db'))) profiles.push(profile)
|
|
|
|
|
}
|
2026-08-22 04:11:44 -05:00
|
|
|
} catch {
|
|
|
|
|
/* no profiles.ini */
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
|
|
|
|
|
// Fall back to every NSS profile, but never add duplicates.
|
|
|
|
|
try {
|
|
|
|
|
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
|
|
|
if (!entry.isDirectory()) continue
|
|
|
|
|
const dir = join(root, entry.name)
|
2026-08-22 04:11:44 -05:00
|
|
|
if (!profiles.includes(dir) && (await pathExists(join(dir, 'cert9.db')))) profiles.push(dir)
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
2026-08-22 04:11:44 -05:00
|
|
|
} catch {
|
|
|
|
|
/* Firefox root does not exist */
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
return profiles
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function hasCertutil(): Promise<boolean> {
|
2026-08-22 04:11:44 -05:00
|
|
|
try {
|
|
|
|
|
await execFileAsync('certutil', ['-L', '-d', 'sql:/dev/null'], {
|
|
|
|
|
env: AURORA_ENV,
|
|
|
|
|
maxBuffer: 1024 * 1024
|
|
|
|
|
})
|
|
|
|
|
return true
|
|
|
|
|
} catch (e: any) {
|
2026-08-13 14:32:54 -05:00
|
|
|
return e?.code !== 'ENOENT'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function nssHasTrustedAuroraCA(db: string): Promise<boolean> {
|
|
|
|
|
try {
|
2026-08-22 04:11:44 -05:00
|
|
|
const { stdout } = await execFileAsync('certutil', ['-L', '-d', `sql:${db}`], {
|
|
|
|
|
env: AURORA_ENV,
|
|
|
|
|
maxBuffer: 1024 * 1024
|
|
|
|
|
})
|
|
|
|
|
const line = stdout
|
|
|
|
|
.split(/\r?\n/)
|
|
|
|
|
.find((value) => value.includes('Aurora Dockside Local Development CA'))
|
2026-08-13 14:32:54 -05:00
|
|
|
return Boolean(line && /\bC,,\s*$/.test(line.trim()))
|
2026-08-22 04:11:44 -05:00
|
|
|
} catch {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
2026-08-22 04:11:44 -05:00
|
|
|
export async function firefoxTrustStatus(): Promise<
|
|
|
|
|
'trusted' | 'not-trusted' | 'unavailable' | 'unknown'
|
|
|
|
|
> {
|
2026-08-13 14:32:54 -05:00
|
|
|
if (process.platform !== 'linux') return 'unknown'
|
|
|
|
|
if (!(await hasCertutil())) return 'unavailable'
|
|
|
|
|
const profiles = await firefoxProfiles()
|
|
|
|
|
if (!profiles.length) return 'unknown'
|
|
|
|
|
for (const profile of profiles) if (!(await nssHasTrustedAuroraCA(profile))) return 'not-trusted'
|
|
|
|
|
return 'trusted'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const chromiumNssDb = (): string => join(process.env.HOME || app.getPath('home'), '.pki', 'nssdb')
|
|
|
|
|
|
|
|
|
|
async function chromiumTrustTargetExists(): Promise<boolean> {
|
|
|
|
|
const db = chromiumNssDb()
|
|
|
|
|
if (await pathExists(join(db, 'cert9.db'))) return true
|
2026-08-22 04:11:44 -05:00
|
|
|
for (const command of [
|
|
|
|
|
'google-chrome',
|
|
|
|
|
'google-chrome-stable',
|
|
|
|
|
'chromium',
|
|
|
|
|
'chromium-browser',
|
|
|
|
|
'brave-browser',
|
|
|
|
|
'microsoft-edge',
|
|
|
|
|
'vivaldi'
|
|
|
|
|
]) {
|
|
|
|
|
try {
|
|
|
|
|
await execFileAsync('which', [command], { env: AURORA_ENV, maxBuffer: 1024 * 1024 })
|
|
|
|
|
return true
|
|
|
|
|
} catch {
|
|
|
|
|
/* try next */
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-22 04:11:44 -05:00
|
|
|
export async function chromiumTrustStatus(): Promise<
|
|
|
|
|
'trusted' | 'not-trusted' | 'unavailable' | 'unknown'
|
|
|
|
|
> {
|
2026-08-13 14:32:54 -05:00
|
|
|
if (process.platform !== 'linux') return 'unknown'
|
|
|
|
|
if (!(await chromiumTrustTargetExists())) return 'unknown'
|
|
|
|
|
if (!(await hasCertutil())) return 'unavailable'
|
2026-08-22 04:11:44 -05:00
|
|
|
return (await nssHasTrustedAuroraCA(chromiumNssDb())) ? 'trusted' : 'not-trusted'
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function installIntoNssDb(db: string): Promise<void> {
|
|
|
|
|
await mkdir(db, { recursive: true })
|
2026-08-22 04:11:44 -05:00
|
|
|
if (!(await pathExists(join(db, 'cert9.db')))) {
|
|
|
|
|
await execFileAsync('certutil', ['-N', '--empty-password', '-d', `sql:${db}`], {
|
|
|
|
|
env: AURORA_ENV,
|
|
|
|
|
maxBuffer: 1024 * 1024
|
|
|
|
|
})
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
2026-08-22 04:11:44 -05:00
|
|
|
try {
|
|
|
|
|
await execFileAsync(
|
|
|
|
|
'certutil',
|
|
|
|
|
['-D', '-d', `sql:${db}`, '-n', 'Aurora Dockside Local Development CA'],
|
|
|
|
|
{ env: AURORA_ENV, maxBuffer: 1024 * 1024 }
|
|
|
|
|
)
|
|
|
|
|
} catch {
|
|
|
|
|
/* absent is fine */
|
|
|
|
|
}
|
|
|
|
|
await execFileAsync(
|
|
|
|
|
'certutil',
|
|
|
|
|
[
|
|
|
|
|
'-A',
|
|
|
|
|
'-d',
|
|
|
|
|
`sql:${db}`,
|
|
|
|
|
'-n',
|
|
|
|
|
'Aurora Dockside Local Development CA',
|
|
|
|
|
'-t',
|
|
|
|
|
'C,,',
|
|
|
|
|
'-i',
|
|
|
|
|
caCertPath()
|
|
|
|
|
],
|
|
|
|
|
{ env: AURORA_ENV, maxBuffer: 1024 * 1024 }
|
|
|
|
|
)
|
|
|
|
|
if (!(await nssHasTrustedAuroraCA(db)))
|
|
|
|
|
throw new Error(`Aurora CA import verification failed for NSS database: ${db}`)
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function trustAuroraCA(): Promise<void> {
|
|
|
|
|
await ensureCertificateAuthority()
|
2026-08-22 04:11:44 -05:00
|
|
|
if (process.platform !== 'linux')
|
|
|
|
|
throw new Error('Automatic CA trust is currently implemented for Linux only.')
|
2026-08-13 14:32:54 -05:00
|
|
|
|
|
|
|
|
const script = `install -m 0644 "${caCertPath().replace(/"/g, '\\"')}" /usr/local/share/ca-certificates/aurora-dockside-local-ca.crt && update-ca-certificates`
|
2026-08-22 04:11:44 -05:00
|
|
|
await execFileAsync('pkexec', ['sh', '-c', script], {
|
|
|
|
|
env: AURORA_ENV,
|
|
|
|
|
maxBuffer: 8 * 1024 * 1024
|
|
|
|
|
})
|
2026-08-13 14:32:54 -05:00
|
|
|
|
|
|
|
|
if (!(await hasCertutil())) {
|
2026-08-22 04:11:44 -05:00
|
|
|
await execFileAsync(
|
|
|
|
|
'pkexec',
|
|
|
|
|
['sh', '-c', 'apt-get update && apt-get install -y libnss3-tools'],
|
|
|
|
|
{ env: AURORA_ENV, maxBuffer: 32 * 1024 * 1024 }
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// These are the exact stores verified on Ubuntu: Snap/native Firefox profiles
|
|
|
|
|
// and the shared Chromium NSS database at ~/.pki/nssdb.
|
|
|
|
|
const profiles = await firefoxProfiles()
|
|
|
|
|
for (const profile of profiles) await installIntoNssDb(profile)
|
|
|
|
|
|
|
|
|
|
if (await chromiumTrustTargetExists()) await installIntoNssDb(chromiumNssDb())
|
|
|
|
|
|
|
|
|
|
const firefox = await firefoxTrustStatus()
|
|
|
|
|
const chromium = await chromiumTrustStatus()
|
2026-08-22 04:11:44 -05:00
|
|
|
if (profiles.length && firefox !== 'trusted')
|
|
|
|
|
throw new Error('Aurora CA could not be verified in the active Firefox NSS trust store.')
|
|
|
|
|
if ((await chromiumTrustTargetExists()) && chromium !== 'trusted')
|
|
|
|
|
throw new Error('Aurora CA could not be verified in ~/.pki/nssdb for Chromium-family browsers.')
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function writeRouterConfig(): Promise<void> {
|
|
|
|
|
const registry = await loadRegistry()
|
|
|
|
|
const routers: string[] = []
|
|
|
|
|
const services: string[] = []
|
|
|
|
|
for (const [name, root] of Object.entries(registry.projects)) {
|
|
|
|
|
try {
|
|
|
|
|
const config = await readConfig(root)
|
|
|
|
|
await ensureProjectCertificate(config.name || name)
|
|
|
|
|
const safe = safeName(config.name || name)
|
|
|
|
|
const host = projectHost(config.name || name)
|
2026-08-22 04:11:44 -05:00
|
|
|
const rule =
|
|
|
|
|
config.moduleMetadata?.routingWildcard === true
|
|
|
|
|
? `Host(\`${host}\`) || HostRegexp(\`^[a-z0-9-]+\\.${host.replace(/\./g, '\\.')}$\`)`
|
|
|
|
|
: `Host(\`${host}\`)`
|
2026-08-13 14:32:54 -05:00
|
|
|
routers.push(
|
|
|
|
|
` aurora-${safe}-http:\n rule: '${rule}'\n entryPoints: [web]\n service: aurora-${safe}`,
|
|
|
|
|
` aurora-${safe}-https:\n rule: '${rule}'\n entryPoints: [websecure]\n service: aurora-${safe}\n tls: {}`
|
|
|
|
|
)
|
2026-08-22 04:11:44 -05:00
|
|
|
services.push(
|
|
|
|
|
` aurora-${safe}:\n loadBalancer:\n servers:\n - url: http://aurora-${safe}-web:80`
|
2026-08-13 14:32:54 -05:00
|
|
|
)
|
2026-08-22 04:11:44 -05:00
|
|
|
if (config.modules.includes('adminer'))
|
|
|
|
|
routers.push(
|
|
|
|
|
` aurora-${safe}-adminer-http:\n rule: 'Host(\`adminer.${host}\`)'\n entryPoints: [web]\n service: aurora-${safe}-adminer`,
|
|
|
|
|
` aurora-${safe}-adminer-https:\n rule: 'Host(\`adminer.${host}\`)'\n entryPoints: [websecure]\n service: aurora-${safe}-adminer\n tls: {}`
|
|
|
|
|
)
|
|
|
|
|
if (config.modules.includes('adminer'))
|
|
|
|
|
services.push(
|
|
|
|
|
` aurora-${safe}-adminer:\n loadBalancer:\n servers:\n - url: http://aurora-${safe}-adminer:8080`
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
if (config.modules.includes('mailpit')) {
|
|
|
|
|
routers.push(
|
|
|
|
|
` aurora-${safe}-mailpit-http:\n rule: 'Host(\`mail.${host}\`)'\n entryPoints: [web]\n service: aurora-${safe}-mailpit`,
|
|
|
|
|
` aurora-${safe}-mailpit-https:\n rule: 'Host(\`mail.${host}\`)'\n entryPoints: [websecure]\n service: aurora-${safe}-mailpit\n tls: {}`
|
|
|
|
|
)
|
2026-08-22 04:11:44 -05:00
|
|
|
services.push(
|
|
|
|
|
` aurora-${safe}-mailpit:\n loadBalancer:\n servers:\n - url: http://aurora-${safe}-mailpit:8025`
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
2026-08-22 04:11:44 -05:00
|
|
|
} catch {
|
|
|
|
|
/* ignore stale registry entries */
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
const tlsCerts: string[] = []
|
|
|
|
|
for (const [name, root] of Object.entries(registry.projects)) {
|
2026-08-22 04:11:44 -05:00
|
|
|
try {
|
|
|
|
|
const config = await readConfig(root)
|
|
|
|
|
const safe = safeName(config.name || name)
|
|
|
|
|
tlsCerts.push(
|
|
|
|
|
` - certFile: /etc/traefik/certs/${safe}.crt\n keyFile: /etc/traefik/certs/${safe}.key`
|
|
|
|
|
)
|
|
|
|
|
} catch {
|
|
|
|
|
/* stale */
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
const dynamic = `http:\n routers:\n${routers.length ? routers.join('\n') : ' {}'}\n services:\n${services.length ? services.join('\n') : ' {}'}\n${tlsCerts.length ? `tls:\n certificates:\n${tlsCerts.join('\n')}\n` : ''}`
|
|
|
|
|
await mkdir(routerDir(), { recursive: true })
|
|
|
|
|
await writeFile(routerConfigPath(), dynamic)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function ensureRouter(): Promise<void> {
|
2026-08-22 04:11:44 -05:00
|
|
|
try {
|
|
|
|
|
await execFileAsync('docker', ['network', 'inspect', 'aurora-router'], { env: AURORA_ENV })
|
|
|
|
|
} catch {
|
|
|
|
|
await execFileAsync('docker', ['network', 'create', 'aurora-router'], { env: AURORA_ENV })
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
|
|
|
|
|
await writeRouterConfig()
|
|
|
|
|
|
|
|
|
|
try {
|
2026-08-22 04:11:44 -05:00
|
|
|
const { stdout } = await execFileAsync(
|
|
|
|
|
'docker',
|
|
|
|
|
['inspect', '-f', '{{.State.Running}} {{json .Config.Cmd}}', 'aurora-router'],
|
|
|
|
|
{ env: AURORA_ENV }
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
const fileProvider = stdout.includes('--providers.file.directory=/etc/traefik/dynamic')
|
|
|
|
|
if (stdout.trimStart().startsWith('true') && fileProvider) return
|
2026-08-22 04:11:44 -05:00
|
|
|
await execFileAsync('docker', ['rm', '-f', 'aurora-router'], { env: AURORA_ENV }).catch(
|
|
|
|
|
() => undefined
|
|
|
|
|
)
|
|
|
|
|
} catch {
|
|
|
|
|
/* router does not exist yet */
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
|
2026-08-22 04:11:44 -05:00
|
|
|
await execFileAsync(
|
|
|
|
|
'docker',
|
|
|
|
|
[
|
|
|
|
|
'run',
|
|
|
|
|
'-d',
|
|
|
|
|
'--name',
|
|
|
|
|
'aurora-router',
|
|
|
|
|
'--restart',
|
|
|
|
|
'unless-stopped',
|
|
|
|
|
'--network',
|
|
|
|
|
'aurora-router',
|
|
|
|
|
'-p',
|
|
|
|
|
'127.0.0.1:80:80',
|
|
|
|
|
'-p',
|
|
|
|
|
'127.0.0.1:443:443',
|
|
|
|
|
'-v',
|
|
|
|
|
`${routerDir()}:/etc/traefik/dynamic:ro`,
|
|
|
|
|
'-v',
|
|
|
|
|
`${projectsCertDir()}:/etc/traefik/certs:ro`,
|
|
|
|
|
'traefik:v3.5',
|
|
|
|
|
'--providers.file.directory=/etc/traefik/dynamic',
|
|
|
|
|
'--providers.file.watch=true',
|
|
|
|
|
'--entrypoints.web.address=:80',
|
|
|
|
|
'--entrypoints.websecure.address=:443',
|
|
|
|
|
'--api.dashboard=false',
|
|
|
|
|
'--log.level=INFO'
|
|
|
|
|
],
|
|
|
|
|
{ env: AURORA_ENV, maxBuffer: 8 * 1024 * 1024 }
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function powerOffProjects(): Promise<void> {
|
|
|
|
|
const registry = await loadRegistry()
|
2026-08-22 04:11:44 -05:00
|
|
|
await Promise.allSettled(
|
|
|
|
|
Object.values(registry.projects).map(async (root) => {
|
|
|
|
|
try {
|
|
|
|
|
const config = await readConfig(root)
|
|
|
|
|
if (config.runtimeEngine === 'native') {
|
|
|
|
|
await stopNativeProject(nativeDefinition(root, config))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
await access(composePath(root))
|
|
|
|
|
await execFileAsync('docker', ['compose', '-f', composePath(root), 'down'], {
|
|
|
|
|
env: AURORA_ENV,
|
|
|
|
|
cwd: root
|
|
|
|
|
})
|
|
|
|
|
} catch {
|
|
|
|
|
/* stale project or Docker unavailable */
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
}
|
2026-08-14 14:00:07 -05:00
|
|
|
|
|
|
|
|
export interface RuntimeImageRefreshResult {
|
|
|
|
|
checkedProjects: number
|
|
|
|
|
refreshedProjects: number
|
|
|
|
|
failures: string[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function refreshRuntimeImages(): Promise<RuntimeImageRefreshResult> {
|
|
|
|
|
const registry = await loadRegistry()
|
|
|
|
|
const roots = [...new Set(Object.values(registry.projects))]
|
|
|
|
|
const failures: string[] = []
|
|
|
|
|
let refreshedProjects = 0
|
|
|
|
|
|
|
|
|
|
// Refresh the shared router image without replacing a currently running
|
|
|
|
|
// router. The new revision is used the next time it is recreated.
|
|
|
|
|
try {
|
|
|
|
|
await execFileAsync('docker', ['pull', 'traefik:v3.5'], {
|
|
|
|
|
env: AURORA_ENV,
|
|
|
|
|
maxBuffer: 16 * 1024 * 1024
|
|
|
|
|
})
|
|
|
|
|
} catch (error) {
|
|
|
|
|
failures.push(`router: ${error instanceof Error ? error.message : String(error)}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const root of roots) {
|
|
|
|
|
try {
|
|
|
|
|
await access(composePath(root))
|
2026-08-14 15:54:42 -05:00
|
|
|
const config = await readConfig(root)
|
2026-08-14 14:00:07 -05:00
|
|
|
// Pulls Node, nginx/Apache, MySQL/MariaDB/PostgreSQL, Adminer, Redis,
|
|
|
|
|
// Mailpit, and module-provided images. Running containers are not
|
|
|
|
|
// restarted, so an update cannot interrupt current work.
|
|
|
|
|
await execFileAsync(
|
|
|
|
|
'docker',
|
|
|
|
|
['compose', '-f', composePath(root), 'pull', '--ignore-buildable', '--policy', 'always'],
|
|
|
|
|
{ cwd: root, env: AURORA_ENV, maxBuffer: 32 * 1024 * 1024 }
|
|
|
|
|
)
|
2026-08-14 15:54:42 -05:00
|
|
|
// PHP is built locally by Aurora. Refresh only its selected upstream
|
|
|
|
|
// base here; compiling in the background can collide with a user-started
|
|
|
|
|
// project build. Docker will consume the refreshed base on the next
|
|
|
|
|
// normal project build.
|
2026-08-22 04:11:44 -05:00
|
|
|
await execFileAsync('docker', ['pull', `php:${config.php}-fpm-alpine`], {
|
|
|
|
|
env: AURORA_ENV,
|
|
|
|
|
maxBuffer: 32 * 1024 * 1024
|
|
|
|
|
})
|
2026-08-14 14:00:07 -05:00
|
|
|
refreshedProjects += 1
|
|
|
|
|
} catch (error) {
|
|
|
|
|
failures.push(`${root}: ${error instanceof Error ? error.message : String(error)}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { checkedProjects: roots.length, refreshedProjects, failures }
|
|
|
|
|
}
|