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 { 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');

View File

@@ -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<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 { 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');