refactor: streamline Docker container management and enhance utility functions

- Removed redundant Docker image rebuilding logic from `dev.mjs` and `start.mjs`, centralizing it in the new `launchDockerContainers` function within `launcher-utils.mjs`.
- Introduced `sanitizeProjectName` and `shouldRebuildDockerImages` functions to improve project name handling and Docker image management.
- Updated the Docker launch process to provide clearer logging and ensure proper handling of environment variables, enhancing the overall development experience.
This commit is contained in:
webdevcody
2026-01-05 21:50:12 -05:00
parent bd5176165d
commit 84b582ffa7
3 changed files with 150 additions and 266 deletions

142
dev.mjs
View File

@@ -11,15 +11,13 @@
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { createRequire } from 'module';
import { statSync } from 'fs';
import { execSync } from 'child_process';
import { import {
createRestrictedFs, createRestrictedFs,
log, log,
runNpm, runNpm,
runNpmAndWait, runNpmAndWait,
runNpx,
printHeader, printHeader,
printModeMenu, printModeMenu,
resolvePortConfiguration, resolvePortConfiguration,
@@ -28,11 +26,9 @@ import {
startServerAndWait, startServerAndWait,
ensureDependencies, ensureDependencies,
prompt, prompt,
launchDockerContainers,
} from './scripts/launcher-utils.mjs'; } from './scripts/launcher-utils.mjs';
const require = createRequire(import.meta.url);
const crossSpawn = require('cross-spawn');
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@@ -47,89 +43,6 @@ const processes = {
docker: null, 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) * Install Playwright browsers (dev-only dependency)
*/ */
@@ -137,10 +50,11 @@ async function installPlaywrightBrowsers() {
log('Checking Playwright browsers...', 'yellow'); log('Checking Playwright browsers...', 'yellow');
try { try {
const exitCode = await new Promise((resolve) => { const exitCode = await new Promise((resolve) => {
const playwright = crossSpawn('npx', ['playwright', 'install', 'chromium'], { const playwright = runNpx(
stdio: 'inherit', ['playwright', 'install', 'chromium'],
cwd: path.join(__dirname, 'apps', 'ui'), { stdio: 'inherit' },
}); path.join(__dirname, 'apps', 'ui')
);
playwright.on('close', (code) => resolve(code)); playwright.on('close', (code) => resolve(code));
playwright.on('error', () => resolve(1)); playwright.on('error', () => resolve(1));
}); });
@@ -256,47 +170,7 @@ async function main() {
break; break;
} else if (choice === '3') { } else if (choice === '3') {
console.log(''); console.log('');
log('Launching Docker Container (Isolated Mode)...', 'blue'); await launchDockerContainers({ baseDir: __dirname, processes });
// 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);
});
break; break;
} else { } else {
log('Invalid choice. Please enter 1, 2, or 3.', 'red'); log('Invalid choice. Please enter 1, 2, or 3.', 'red');

View File

@@ -13,7 +13,7 @@
*/ */
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import fsNative from 'fs'; import fsNative, { statSync } from 'fs';
import http from 'http'; import http from 'http';
import path from 'path'; import path from 'path';
import readline from 'readline'; 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<void>}
*/
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);
});
}

133
start.mjs
View File

@@ -18,13 +18,9 @@
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { createRequire } from 'module';
import { statSync } from 'fs';
import { execSync } from 'child_process';
import { import {
createRestrictedFs, createRestrictedFs,
log, log,
runNpm,
runNpmAndWait, runNpmAndWait,
runNpx, runNpx,
printHeader, printHeader,
@@ -37,11 +33,9 @@ import {
prompt, prompt,
killProcessTree, killProcessTree,
sleep, sleep,
launchDockerContainers,
} from './scripts/launcher-utils.mjs'; } from './scripts/launcher-utils.mjs';
const require = createRequire(import.meta.url);
const crossSpawn = require('cross-spawn');
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@@ -56,89 +50,6 @@ const processes = {
docker: null, 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 * Build all production artifacts
*/ */
@@ -315,47 +226,7 @@ async function main() {
break; break;
} else if (choice === '3') { } else if (choice === '3') {
console.log(''); console.log('');
log('Launching Docker Container (Isolated Mode)...', 'blue'); await launchDockerContainers({ baseDir: __dirname, processes });
// 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);
});
break; break;
} else { } else {
log('Invalid choice. Please enter 1, 2, or 3.', 'red'); log('Invalid choice. Please enter 1, 2, or 3.', 'red');