From d4eca3055abf752b09d3da87c51882ed07038c9d Mon Sep 17 00:00:00 2001 From: underscope Date: Tue, 10 Feb 2026 15:57:04 +0100 Subject: [PATCH] Add apple container bootstrap script --- docker-compose.dev.yaml | 6 +- package.json | 3 + scripts/containers.js | 169 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 scripts/containers.js diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 6c3eb30d1..270556f80 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -1,7 +1,7 @@ services: author-dev-postgres: container_name: 'author-dev-postgres' - image: 'postgres:15.1' + image: 'docker.io/library/postgres:15.1' environment: POSTGRES_DB: 'tailor_dev' POSTGRES_USER: 'dev' @@ -13,14 +13,14 @@ services: restart: 'no' author-dev-redis: container_name: 'author-dev-redis' - image: redis:7.4.0 + image: docker.io/library/redis:7.4.0 expose: - '6379' ports: - '6379:6379' author-dev-localstack: container_name: author-dev-localstack - image: localstack/localstack + image: docker.io/localstack/localstack expose: - '4566' ports: diff --git a/package.json b/package.json index 5d5f970fd..cb20c7b66 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,9 @@ "scripts": { "dev": "node ./scripts/boot.js", "dc": "docker compose -f docker-compose.dev.yaml up & pnpm dev", + "ac:up": "node ./scripts/containers.js start", + "ac:down": "node ./scripts/containers.js stop", + "ac:status": "node ./scripts/containers.js status", "build": "dotenv -- pnpm -r build", "start": "cd ./apps/backend && pnpm start", "dcs": "docker compose -f docker-compose.dev.yaml up & pnpm start", diff --git a/scripts/containers.js b/scripts/containers.js new file mode 100644 index 000000000..5b5eb1213 --- /dev/null +++ b/scripts/containers.js @@ -0,0 +1,169 @@ +import chalk from 'chalk'; +import dotenv from 'dotenv'; +import { execaCommand } from 'execa'; +import fs from 'node:fs/promises'; +import ora from 'ora'; +import path from 'node:path'; + +const configPath = path.join(process.cwd(), '.env'); +const config = dotenv.parse(await fs.readFile(configPath, 'utf-8')); + +const SERVICES = [ + { + name: 'author-dev-postgres', + image: 'docker.io/library/postgres:15.1', + port: '5432:5432', + env: { + POSTGRES_DB: config.DATABASE_NAME || 'tailor_dev', + POSTGRES_USER: config.DATABASE_USER || 'dev', + POSTGRES_PASSWORD: config.DATABASE_PASSWORD || 'dev', + }, + }, + { + name: 'author-dev-redis', + image: 'docker.io/library/redis:7.4.0', + port: '6379:6379', + }, + { + name: 'author-dev-minio', + image: 'docker.io/minio/minio', + port: '4566:9000', + env: { + MINIO_ROOT_USER: config.STORAGE_KEY || 'test', + MINIO_ROOT_PASSWORD: config.STORAGE_SECRET || 'test', + }, + args: 'server /data', + }, +]; + +const run = (cmd) => execaCommand(cmd, { shell: true }); + +const ensureCLI = async () => { + try { + await run('which container'); + } catch { + console.error(chalk.red('Apple Container CLI not found.')); + console.error(chalk.dim('Install via: brew install container')); + process.exit(1); + } +}; + +await ensureCLI(); + +const runQuiet = async (cmd) => { + try { + return await run(cmd); + } catch { + return null; + } +}; + +const status = async () => { + const output = await listRunning(); + console.log(output || chalk.dim(' No containers running.')); +}; + +const listRunning = async () => { + const result = await runQuiet('container list'); + return result?.stdout || ''; +}; + +const isRuntimeUp = async () => !!(await listRunning()); + +const startRuntime = async () => { + const spinner = ora('Checking runtime...').start(); + if (await isRuntimeUp()) { + spinner.succeed('Runtime is running'); + return; + } + spinner.text = 'Starting runtime...'; + try { + await run('container system start'); + spinner.succeed('Runtime started'); + } catch (err) { + spinner.fail('Failed to start runtime'); + throw err; + } +}; + +const startService = async ({ name, image, port, env, args }, running) => { + const label = chalk.cyan(name); + const spinner = ora(`Starting ${label}...`).start(); + if (running.includes(name)) { + spinner.info(`${label} already running`); + return; + } + await runQuiet(`container rm ${name}`); + const envFlags = Object.entries(env || {}) + .map(([k, v]) => `-e ${k}=${v}`) + .join(' '); + const flags = [`-d`, `--name ${name}`, `-p ${port}`, envFlags, image, args] + .filter(Boolean) + .join(' '); + try { + await run(`container run ${flags}`); + spinner.succeed(`${label} started`); + } catch (err) { + spinner.fail(`${label} failed to start`); + throw err; + } +}; + +const start = async () => { + await startRuntime(); + console.log(); + const running = await listRunning(); + await Promise.all(SERVICES.map((svc) => startService(svc, running))); + console.log(); + await status(); +}; + +const stopService = async (name, running) => { + const label = chalk.cyan(name); + const spinner = ora(`Stopping ${label}...`).start(); + if (running.includes(name)) await runQuiet(`container stop ${name}`); + await runQuiet(`container rm ${name}`); + spinner.succeed(`${label} stopped`); +}; + +const stop = async () => { + const running = await listRunning(); + await Promise.all(SERVICES.map(({ name }) => stopService(name, running))); +}; + +const restart = async () => { + await stop(); + console.log(); + await start(); +}; + +const logs = async (name) => { + const target = name || SERVICES[0].name; + const result = await run(`container logs ${target}`); + console.log(result.stdout); +}; + +// ---------------------- CLI -------------------------- + +const [command, ...args] = process.argv.slice(2); + +const commands = { + start, + up: start, + stop, + down: stop, + status, + ps: status, + restart, + logs, +}; +const handler = commands[command || 'start']; + +if (!handler) { + console.log( + `Usage: node containers.js {${Object.keys(commands).join('|')}} [name]`, + ); + process.exit(1); +} + +await handler(args[0]);