mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
- Introduced a new markdown file summarizing various attempts to install the Cursor CLI in Docker, detailing approaches, results, and key learnings. - Updated Dockerfile to ensure proper installation of Cursor CLI for the non-root user, including necessary PATH adjustments for interactive shells. - Enhanced entrypoint script to manage OAuth tokens for both Claude and Cursor CLIs, ensuring correct permissions and directory setups. - Added scripts for extracting OAuth tokens from macOS Keychain and Linux JSON files for seamless integration with Docker. - Updated docker-compose files to support persistent storage for CLI configurations and authentication tokens. These changes improve the development workflow and provide clear guidance on CLI installation and authentication processes.
318 lines
9.1 KiB
JavaScript
318 lines
9.1 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Automaker - Development Mode Launch Script
|
|
*
|
|
* This script starts the application in development mode with hot reloading.
|
|
* It uses Vite dev server for fast HMR during development.
|
|
*
|
|
* Usage: npm run dev
|
|
*/
|
|
|
|
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,
|
|
printHeader,
|
|
printModeMenu,
|
|
resolvePortConfiguration,
|
|
createCleanupHandler,
|
|
setupSignalHandlers,
|
|
startServerAndWait,
|
|
ensureDependencies,
|
|
prompt,
|
|
} 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);
|
|
|
|
// Create restricted fs for this script's directory
|
|
const fs = createRestrictedFs(__dirname, 'dev.mjs');
|
|
|
|
// Track background processes for cleanup
|
|
const processes = {
|
|
server: null,
|
|
web: null,
|
|
electron: null,
|
|
docker: null,
|
|
};
|
|
|
|
/**
|
|
* 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 image names from docker-compose config
|
|
let serverImageName, uiImageName;
|
|
try {
|
|
const composeConfig = execSync('docker compose config --format json', {
|
|
encoding: 'utf-8',
|
|
cwd: __dirname,
|
|
});
|
|
const config = JSON.parse(composeConfig);
|
|
|
|
// Docker Compose generates image names as <project>_<service>
|
|
// Get project name from config or default to directory name
|
|
const projectName =
|
|
config.name ||
|
|
path
|
|
.basename(__dirname)
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]/g, '');
|
|
serverImageName = `${projectName}_server`;
|
|
uiImageName = `${projectName}_ui`;
|
|
} catch (error) {
|
|
// Fallback to default naming convention
|
|
const projectName = path
|
|
.basename(__dirname)
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]/g, '');
|
|
serverImageName = `${projectName}_server`;
|
|
uiImageName = `${projectName}_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)
|
|
*/
|
|
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'),
|
|
});
|
|
playwright.on('close', (code) => resolve(code));
|
|
playwright.on('error', () => resolve(1));
|
|
});
|
|
|
|
if (exitCode === 0) {
|
|
log('Playwright browsers ready', 'green');
|
|
} else {
|
|
log('Playwright installation failed (browser automation may not work)', 'yellow');
|
|
}
|
|
} catch {
|
|
log('Playwright installation skipped', 'yellow');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main function
|
|
*/
|
|
async function main() {
|
|
// Change to script directory
|
|
process.chdir(__dirname);
|
|
|
|
printHeader('Automaker Development Environment');
|
|
|
|
// Ensure dependencies are installed
|
|
await ensureDependencies(fs, __dirname);
|
|
|
|
// Install Playwright browsers (dev-only)
|
|
await installPlaywrightBrowsers();
|
|
|
|
// Resolve port configuration (check/kill/change ports)
|
|
const { webPort, serverPort, corsOriginEnv } = await resolvePortConfiguration();
|
|
|
|
// Show mode selection menu
|
|
printModeMenu();
|
|
|
|
// Setup cleanup handlers
|
|
const cleanup = createCleanupHandler(processes);
|
|
setupSignalHandlers(cleanup);
|
|
|
|
// Prompt for choice
|
|
while (true) {
|
|
const choice = await prompt('Enter your choice (1, 2, or 3): ');
|
|
|
|
if (choice === '1') {
|
|
console.log('');
|
|
log('Launching Web Application (Development Mode)...', 'blue');
|
|
|
|
// Build shared packages once
|
|
log('Building shared packages...', 'blue');
|
|
await runNpmAndWait(['run', 'build:packages'], { stdio: 'inherit' }, __dirname);
|
|
|
|
// Start the backend server in dev mode
|
|
processes.server = await startServerAndWait({
|
|
serverPort,
|
|
corsOriginEnv,
|
|
npmArgs: ['run', '_dev:server'],
|
|
cwd: __dirname,
|
|
fs,
|
|
baseDir: __dirname,
|
|
});
|
|
|
|
if (!processes.server) {
|
|
await cleanup();
|
|
process.exit(1);
|
|
}
|
|
|
|
log(`The application will be available at: http://localhost:${webPort}`, 'green');
|
|
console.log('');
|
|
|
|
// Start web app with Vite dev server (HMR enabled)
|
|
processes.web = runNpm(
|
|
['run', '_dev:web'],
|
|
{
|
|
stdio: 'inherit',
|
|
env: {
|
|
TEST_PORT: String(webPort),
|
|
VITE_SERVER_URL: `http://localhost:${serverPort}`,
|
|
},
|
|
},
|
|
__dirname
|
|
);
|
|
|
|
await new Promise((resolve) => {
|
|
processes.web.on('close', resolve);
|
|
});
|
|
|
|
break;
|
|
} else if (choice === '2') {
|
|
console.log('');
|
|
log('Launching Desktop Application (Development Mode)...', 'blue');
|
|
log('(Electron will start its own backend server)', 'yellow');
|
|
console.log('');
|
|
|
|
// Pass selected ports through to Vite + Electron backend
|
|
processes.electron = runNpm(
|
|
['run', 'dev:electron'],
|
|
{
|
|
stdio: 'inherit',
|
|
env: {
|
|
TEST_PORT: String(webPort),
|
|
PORT: String(serverPort),
|
|
VITE_SERVER_URL: `http://localhost:${serverPort}`,
|
|
CORS_ORIGIN: corsOriginEnv,
|
|
},
|
|
},
|
|
__dirname
|
|
);
|
|
|
|
await new Promise((resolve) => {
|
|
processes.electron.on('close', resolve);
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
break;
|
|
} else {
|
|
log('Invalid choice. Please enter 1, 2, or 3.', 'red');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run main function
|
|
main().catch(async (err) => {
|
|
console.error(err);
|
|
const cleanup = createCleanupHandler(processes);
|
|
try {
|
|
await cleanup();
|
|
} catch (cleanupErr) {
|
|
console.error('Cleanup error:', cleanupErr);
|
|
}
|
|
process.exit(1);
|
|
});
|