diff --git a/dev.mjs b/dev.mjs index f22a68e4..7c232ca6 100644 --- a/dev.mjs +++ b/dev.mjs @@ -11,15 +11,13 @@ import path from 'path'; import { fileURLToPath } from 'url'; -import { createRequire } from 'module'; -import { statSync } from 'fs'; -import { execSync } from 'child_process'; import { createRestrictedFs, log, runNpm, runNpmAndWait, + runNpx, printHeader, printModeMenu, resolvePortConfiguration, @@ -28,11 +26,9 @@ import { startServerAndWait, ensureDependencies, prompt, + launchDockerContainers, } from './scripts/launcher-utils.mjs'; -const require = createRequire(import.meta.url); -const crossSpawn = require('cross-spawn'); - const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -47,89 +43,6 @@ const processes = { docker: null, }; -/** - * Sanitize a project name to be safe for use in shell commands and Docker image names. - * Converts to lowercase and removes any characters that aren't alphanumeric. - */ -function sanitizeProjectName(name) { - return name.toLowerCase().replace(/[^a-z0-9]/g, ''); -} - -/** - * Check if Docker images need to be rebuilt based on Dockerfile or package.json changes - */ -function shouldRebuildDockerImages() { - try { - const dockerfilePath = path.join(__dirname, 'Dockerfile'); - const packageJsonPath = path.join(__dirname, 'package.json'); - - // Get modification times of source files - const dockerfileMtime = statSync(dockerfilePath).mtimeMs; - const packageJsonMtime = statSync(packageJsonPath).mtimeMs; - const latestSourceMtime = Math.max(dockerfileMtime, packageJsonMtime); - - // Get project name from docker-compose config, falling back to directory name - let projectName; - try { - const composeConfig = execSync('docker compose config --format json', { - encoding: 'utf-8', - cwd: __dirname, - }); - const config = JSON.parse(composeConfig); - projectName = config.name; - } catch (error) { - // Fallback handled below - } - - // Sanitize project name (whether from config or fallback) - // This prevents command injection and ensures valid Docker image names - const sanitizedProjectName = sanitizeProjectName( - projectName || path.basename(__dirname) - ); - const serverImageName = `${sanitizedProjectName}_server`; - const uiImageName = `${sanitizedProjectName}_ui`; - - // Check if images exist and get their creation times - let needsRebuild = false; - - try { - // Check server image - const serverImageInfo = execSync( - `docker image inspect ${serverImageName} --format "{{.Created}}" 2>/dev/null || echo ""`, - { encoding: 'utf-8', cwd: __dirname } - ).trim(); - - // Check UI image - const uiImageInfo = execSync( - `docker image inspect ${uiImageName} --format "{{.Created}}" 2>/dev/null || echo ""`, - { encoding: 'utf-8', cwd: __dirname } - ).trim(); - - // If either image doesn't exist, we need to rebuild - if (!serverImageInfo || !uiImageInfo) { - return true; - } - - // Parse image creation times (ISO 8601 format) - const serverCreated = new Date(serverImageInfo).getTime(); - const uiCreated = new Date(uiImageInfo).getTime(); - const oldestImageTime = Math.min(serverCreated, uiCreated); - - // If source files are newer than images, rebuild - needsRebuild = latestSourceMtime > oldestImageTime; - } catch (error) { - // If images don't exist or inspect fails, rebuild - needsRebuild = true; - } - - return needsRebuild; - } catch (error) { - // If we can't check, err on the side of rebuilding - log('Could not check Docker image status, will rebuild to be safe', 'yellow'); - return true; - } -} - /** * Install Playwright browsers (dev-only dependency) */ @@ -137,10 +50,11 @@ async function installPlaywrightBrowsers() { log('Checking Playwright browsers...', 'yellow'); try { const exitCode = await new Promise((resolve) => { - const playwright = crossSpawn('npx', ['playwright', 'install', 'chromium'], { - stdio: 'inherit', - cwd: path.join(__dirname, 'apps', 'ui'), - }); + const playwright = runNpx( + ['playwright', 'install', 'chromium'], + { stdio: 'inherit' }, + path.join(__dirname, 'apps', 'ui') + ); playwright.on('close', (code) => resolve(code)); playwright.on('error', () => resolve(1)); }); @@ -256,47 +170,7 @@ async function main() { break; } else if (choice === '3') { console.log(''); - log('Launching Docker Container (Isolated Mode)...', 'blue'); - - // Check if Dockerfile or package.json changed and rebuild if needed - const needsRebuild = shouldRebuildDockerImages(); - const buildFlag = needsRebuild ? ['--build'] : []; - - if (needsRebuild) { - log('Dockerfile or package.json changed - rebuilding images...', 'yellow'); - } else { - log('Starting Docker containers...', 'yellow'); - } - console.log(''); - - // Check if ANTHROPIC_API_KEY is set - if (!process.env.ANTHROPIC_API_KEY) { - log('Warning: ANTHROPIC_API_KEY environment variable is not set.', 'yellow'); - log('The server will require an API key to function.', 'yellow'); - log('Set it with: export ANTHROPIC_API_KEY=your-key', 'yellow'); - console.log(''); - } - - // Start containers with docker-compose - // Will rebuild if Dockerfile or package.json changed - processes.docker = crossSpawn('docker', ['compose', 'up', ...buildFlag], { - stdio: 'inherit', - cwd: __dirname, - env: { - ...process.env, - }, - }); - - log('Docker containers starting...', 'blue'); - log('UI will be available at: http://localhost:3007', 'green'); - log('API will be available at: http://localhost:3008', 'green'); - console.log(''); - log('Press Ctrl+C to stop the containers.', 'yellow'); - - await new Promise((resolve) => { - processes.docker.on('close', resolve); - }); - + await launchDockerContainers({ baseDir: __dirname, processes }); break; } else { log('Invalid choice. Please enter 1, 2, or 3.', 'red'); diff --git a/scripts/launcher-utils.mjs b/scripts/launcher-utils.mjs index 215c0dc2..a1436569 100644 --- a/scripts/launcher-utils.mjs +++ b/scripts/launcher-utils.mjs @@ -13,7 +13,7 @@ */ import { execSync } from 'child_process'; -import fsNative from 'fs'; +import fsNative, { statSync } from 'fs'; import http from 'http'; import path from 'path'; import readline from 'readline'; @@ -662,3 +662,142 @@ export async function ensureDependencies(fs, baseDir) { }); } } + +// ============================================================================= +// Docker Utilities +// ============================================================================= + +/** + * Sanitize a project name to be safe for use in shell commands and Docker image names. + * Converts to lowercase and removes any characters that aren't alphanumeric. + * @param {string} name - Project name to sanitize + * @returns {string} - Sanitized project name + */ +export function sanitizeProjectName(name) { + return name.toLowerCase().replace(/[^a-z0-9]/g, ''); +} + +/** + * Check if Docker images need to be rebuilt based on Dockerfile or package.json changes + * @param {string} baseDir - Base directory containing Dockerfile and package.json + * @returns {boolean} - Whether images need to be rebuilt + */ +export function shouldRebuildDockerImages(baseDir) { + try { + const dockerfilePath = path.join(baseDir, 'Dockerfile'); + const packageJsonPath = path.join(baseDir, 'package.json'); + + // Get modification times of source files + const dockerfileMtime = statSync(dockerfilePath).mtimeMs; + const packageJsonMtime = statSync(packageJsonPath).mtimeMs; + const latestSourceMtime = Math.max(dockerfileMtime, packageJsonMtime); + + // Get project name from docker-compose config, falling back to directory name + let projectName; + try { + const composeConfig = execSync('docker compose config --format json', { + encoding: 'utf-8', + cwd: baseDir, + }); + const config = JSON.parse(composeConfig); + projectName = config.name; + } catch (error) { + // Fallback handled below + } + + // Sanitize project name (whether from config or fallback) + // This prevents command injection and ensures valid Docker image names + const sanitizedProjectName = sanitizeProjectName(projectName || path.basename(baseDir)); + const serverImageName = `${sanitizedProjectName}_server`; + const uiImageName = `${sanitizedProjectName}_ui`; + + // Check if images exist and get their creation times + let needsRebuild = false; + + try { + // Check server image + const serverImageInfo = execSync( + `docker image inspect ${serverImageName} --format "{{.Created}}" 2>/dev/null || echo ""`, + { encoding: 'utf-8', cwd: baseDir } + ).trim(); + + // Check UI image + const uiImageInfo = execSync( + `docker image inspect ${uiImageName} --format "{{.Created}}" 2>/dev/null || echo ""`, + { encoding: 'utf-8', cwd: baseDir } + ).trim(); + + // If either image doesn't exist, we need to rebuild + if (!serverImageInfo || !uiImageInfo) { + return true; + } + + // Parse image creation times (ISO 8601 format) + const serverCreated = new Date(serverImageInfo).getTime(); + const uiCreated = new Date(uiImageInfo).getTime(); + const oldestImageTime = Math.min(serverCreated, uiCreated); + + // If source files are newer than images, rebuild + needsRebuild = latestSourceMtime > oldestImageTime; + } catch (error) { + // If images don't exist or inspect fails, rebuild + needsRebuild = true; + } + + return needsRebuild; + } catch (error) { + // If we can't check, err on the side of rebuilding + log('Could not check Docker image status, will rebuild to be safe', 'yellow'); + return true; + } +} + +/** + * Launch Docker containers with docker-compose + * @param {object} options - Configuration options + * @param {string} options.baseDir - Base directory containing docker-compose.yml + * @param {object} options.processes - Processes object to track docker process + * @returns {Promise} + */ +export async function launchDockerContainers({ baseDir, processes }) { + log('Launching Docker Container (Isolated Mode)...', 'blue'); + + // Check if Dockerfile or package.json changed and rebuild if needed + const needsRebuild = shouldRebuildDockerImages(baseDir); + const buildFlag = needsRebuild ? ['--build'] : []; + + if (needsRebuild) { + log('Dockerfile or package.json changed - rebuilding images...', 'yellow'); + } else { + log('Starting Docker containers...', 'yellow'); + } + console.log(''); + + // Check if ANTHROPIC_API_KEY is set + if (!process.env.ANTHROPIC_API_KEY) { + log('Warning: ANTHROPIC_API_KEY environment variable is not set.', 'yellow'); + log('The server will require an API key to function.', 'yellow'); + log('Set it with: export ANTHROPIC_API_KEY=your-key', 'yellow'); + console.log(''); + } + + // Start containers with docker-compose + // Will rebuild if Dockerfile or package.json changed + processes.docker = crossSpawn('docker', ['compose', 'up', ...buildFlag], { + stdio: 'inherit', + cwd: baseDir, + env: { + ...process.env, + }, + }); + + log('Docker containers starting...', 'blue'); + log('UI will be available at: http://localhost:3007', 'green'); + log('API will be available at: http://localhost:3008', 'green'); + console.log(''); + log('Press Ctrl+C to stop the containers.', 'yellow'); + + await new Promise((resolve) => { + processes.docker.on('close', resolve); + }); +} diff --git a/start.mjs b/start.mjs index 29a6e04e..326bb4b7 100755 --- a/start.mjs +++ b/start.mjs @@ -18,13 +18,9 @@ import path from 'path'; import { fileURLToPath } from 'url'; -import { createRequire } from 'module'; -import { statSync } from 'fs'; -import { execSync } from 'child_process'; import { createRestrictedFs, log, - runNpm, runNpmAndWait, runNpx, printHeader, @@ -37,11 +33,9 @@ import { prompt, killProcessTree, sleep, + launchDockerContainers, } from './scripts/launcher-utils.mjs'; -const require = createRequire(import.meta.url); -const crossSpawn = require('cross-spawn'); - const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -56,89 +50,6 @@ const processes = { docker: null, }; -/** - * Sanitize a project name to be safe for use in shell commands and Docker image names. - * Converts to lowercase and removes any characters that aren't alphanumeric. - */ -function sanitizeProjectName(name) { - return name.toLowerCase().replace(/[^a-z0-9]/g, ''); -} - -/** - * Check if Docker images need to be rebuilt based on Dockerfile or package.json changes - */ -function shouldRebuildDockerImages() { - try { - const dockerfilePath = path.join(__dirname, 'Dockerfile'); - const packageJsonPath = path.join(__dirname, 'package.json'); - - // Get modification times of source files - const dockerfileMtime = statSync(dockerfilePath).mtimeMs; - const packageJsonMtime = statSync(packageJsonPath).mtimeMs; - const latestSourceMtime = Math.max(dockerfileMtime, packageJsonMtime); - - // Get project name from docker-compose config, falling back to directory name - let projectName; - try { - const composeConfig = execSync('docker compose config --format json', { - encoding: 'utf-8', - cwd: __dirname, - }); - const config = JSON.parse(composeConfig); - projectName = config.name; - } catch (error) { - // Fallback handled below - } - - // Sanitize project name (whether from config or fallback) - // This prevents command injection and ensures valid Docker image names - const sanitizedProjectName = sanitizeProjectName( - projectName || path.basename(__dirname) - ); - const serverImageName = `${sanitizedProjectName}_server`; - const uiImageName = `${sanitizedProjectName}_ui`; - - // Check if images exist and get their creation times - let needsRebuild = false; - - try { - // Check server image - const serverImageInfo = execSync( - `docker image inspect ${serverImageName} --format "{{.Created}}" 2>/dev/null || echo ""`, - { encoding: 'utf-8', cwd: __dirname } - ).trim(); - - // Check UI image - const uiImageInfo = execSync( - `docker image inspect ${uiImageName} --format "{{.Created}}" 2>/dev/null || echo ""`, - { encoding: 'utf-8', cwd: __dirname } - ).trim(); - - // If either image doesn't exist, we need to rebuild - if (!serverImageInfo || !uiImageInfo) { - return true; - } - - // Parse image creation times (ISO 8601 format) - const serverCreated = new Date(serverImageInfo).getTime(); - const uiCreated = new Date(uiImageInfo).getTime(); - const oldestImageTime = Math.min(serverCreated, uiCreated); - - // If source files are newer than images, rebuild - needsRebuild = latestSourceMtime > oldestImageTime; - } catch (error) { - // If images don't exist or inspect fails, rebuild - needsRebuild = true; - } - - return needsRebuild; - } catch (error) { - // If we can't check, err on the side of rebuilding - log('Could not check Docker image status, will rebuild to be safe', 'yellow'); - return true; - } -} - /** * Build all production artifacts */ @@ -315,47 +226,7 @@ async function main() { break; } else if (choice === '3') { console.log(''); - log('Launching Docker Container (Isolated Mode)...', 'blue'); - - // Check if Dockerfile or package.json changed and rebuild if needed - const needsRebuild = shouldRebuildDockerImages(); - const buildFlag = needsRebuild ? ['--build'] : []; - - if (needsRebuild) { - log('Dockerfile or package.json changed - rebuilding images...', 'yellow'); - } else { - log('Starting Docker containers...', 'yellow'); - } - console.log(''); - - // Check if ANTHROPIC_API_KEY is set - if (!process.env.ANTHROPIC_API_KEY) { - log('Warning: ANTHROPIC_API_KEY environment variable is not set.', 'yellow'); - log('The server will require an API key to function.', 'yellow'); - log('Set it with: export ANTHROPIC_API_KEY=your-key', 'yellow'); - console.log(''); - } - - // Start containers with docker-compose - // Will rebuild if Dockerfile or package.json changed - processes.docker = crossSpawn('docker', ['compose', 'up', ...buildFlag], { - stdio: 'inherit', - cwd: __dirname, - env: { - ...process.env, - }, - }); - - log('Docker containers starting...', 'blue'); - log('UI will be available at: http://localhost:3007', 'green'); - log('API will be available at: http://localhost:3008', 'green'); - console.log(''); - log('Press Ctrl+C to stop the containers.', 'yellow'); - - await new Promise((resolve) => { - processes.docker.on('close', resolve); - }); - + await launchDockerContainers({ baseDir: __dirname, processes }); break; } else { log('Invalid choice. Please enter 1, 2, or 3.', 'red');