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.
377 lines
11 KiB
JavaScript
Executable File
377 lines
11 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Automaker - Production Mode Launch Script
|
|
*
|
|
* This script runs the application in production mode (no Vite dev server).
|
|
* It builds everything if needed, then serves static files via vite preview.
|
|
*
|
|
* Key differences from dev.mjs:
|
|
* - Uses pre-built static files instead of Vite dev server (faster startup)
|
|
* - No HMR or hot reloading
|
|
* - Server runs from compiled dist/ directory
|
|
* - Uses "vite preview" to serve static UI files
|
|
*
|
|
* Usage: npm run start
|
|
*/
|
|
|
|
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,
|
|
createCleanupHandler,
|
|
setupSignalHandlers,
|
|
startServerAndWait,
|
|
ensureDependencies,
|
|
prompt,
|
|
killProcessTree,
|
|
sleep,
|
|
} 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, 'start.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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build all production artifacts
|
|
*/
|
|
async function ensureProductionBuilds() {
|
|
// Always build shared packages first to ensure they're up to date
|
|
log('Building shared packages...', 'blue');
|
|
try {
|
|
await runNpmAndWait(['run', 'build:packages'], { stdio: 'inherit' }, __dirname);
|
|
log('✓ Shared packages built', 'green');
|
|
} catch (error) {
|
|
log(`Failed to build shared packages: ${error.message}`, 'red');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Always rebuild server to ensure it's in sync with packages
|
|
log('Building server...', 'blue');
|
|
try {
|
|
await runNpmAndWait(
|
|
['run', 'build'],
|
|
{ stdio: 'inherit' },
|
|
path.join(__dirname, 'apps', 'server')
|
|
);
|
|
log('✓ Server built', 'green');
|
|
} catch (error) {
|
|
log(`Failed to build server: ${error.message}`, 'red');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Always rebuild UI to ensure it's in sync with latest code
|
|
log('Building UI...', 'blue');
|
|
try {
|
|
await runNpmAndWait(['run', 'build'], { stdio: 'inherit' }, __dirname);
|
|
log('✓ UI built', 'green');
|
|
console.log('');
|
|
} catch (error) {
|
|
log(`Failed to build UI: ${error.message}`, 'red');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main function
|
|
*/
|
|
async function main() {
|
|
// Change to script directory
|
|
process.chdir(__dirname);
|
|
|
|
printHeader('Automaker Production Mode');
|
|
|
|
// Ensure dependencies are installed
|
|
await ensureDependencies(fs, __dirname);
|
|
|
|
// Build production artifacts if needed
|
|
await ensureProductionBuilds();
|
|
|
|
// 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 (Production Mode)...', 'blue');
|
|
|
|
// Start the backend server in PRODUCTION mode
|
|
// Uses "npm run start" in apps/server which runs the compiled dist/
|
|
// NOT the Vite dev server (no HMR, faster startup)
|
|
processes.server = await startServerAndWait({
|
|
serverPort,
|
|
corsOriginEnv,
|
|
npmArgs: ['run', 'start'],
|
|
cwd: path.join(__dirname, 'apps', 'server'),
|
|
fs,
|
|
baseDir: __dirname,
|
|
});
|
|
|
|
if (!processes.server) {
|
|
await cleanup();
|
|
process.exit(1);
|
|
}
|
|
|
|
log(`Starting web server...`, 'blue');
|
|
|
|
// Start vite preview to serve pre-built static files
|
|
// This is NOT Vite dev server - it just serves the dist/ folder
|
|
// No HMR, no compilation, just static file serving
|
|
processes.web = runNpx(
|
|
['vite', 'preview', '--port', String(webPort)],
|
|
{
|
|
stdio: 'inherit',
|
|
env: {
|
|
VITE_SERVER_URL: `http://localhost:${serverPort}`,
|
|
},
|
|
},
|
|
path.join(__dirname, 'apps', 'ui')
|
|
);
|
|
|
|
log(`The application is available at: http://localhost:${webPort}`, 'green');
|
|
console.log('');
|
|
|
|
await new Promise((resolve) => {
|
|
processes.web.on('close', resolve);
|
|
});
|
|
|
|
break;
|
|
} else if (choice === '2') {
|
|
console.log('');
|
|
log('Launching Desktop Application (Production Mode)...', 'blue');
|
|
log('(Electron will start its own backend server)', 'yellow');
|
|
console.log('');
|
|
|
|
// Run electron directly with the built main.js
|
|
const electronMainPath = path.join(__dirname, 'apps', 'ui', 'dist-electron', 'main.js');
|
|
|
|
if (!fs.existsSync(electronMainPath)) {
|
|
log('Error: Electron main process not built. Run build first.', 'red');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Start vite preview to serve built static files for electron
|
|
// (Electron in non-packaged mode needs a server to load from)
|
|
log('Starting static file server...', 'blue');
|
|
processes.web = runNpx(
|
|
['vite', 'preview', '--port', String(webPort)],
|
|
{
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
env: {
|
|
VITE_SERVER_URL: `http://localhost:${serverPort}`,
|
|
},
|
|
},
|
|
path.join(__dirname, 'apps', 'ui')
|
|
);
|
|
|
|
// Wait for vite preview to start
|
|
await sleep(2000);
|
|
|
|
// Use electron from node_modules with NODE_ENV=production
|
|
// This ensures electron loads from the preview server, not Vite dev
|
|
processes.electron = runNpx(
|
|
['electron', electronMainPath],
|
|
{
|
|
stdio: 'inherit',
|
|
env: {
|
|
TEST_PORT: String(webPort),
|
|
PORT: String(serverPort),
|
|
VITE_DEV_SERVER_URL: `http://localhost:${webPort}`,
|
|
VITE_SERVER_URL: `http://localhost:${serverPort}`,
|
|
CORS_ORIGIN: corsOriginEnv,
|
|
NODE_ENV: 'production',
|
|
},
|
|
},
|
|
path.join(__dirname, 'apps', 'ui')
|
|
);
|
|
|
|
await new Promise((resolve) => {
|
|
processes.electron.on('close', () => {
|
|
// Also kill vite preview when electron closes
|
|
if (processes.web && !processes.web.killed && processes.web.pid) {
|
|
killProcessTree(processes.web.pid);
|
|
}
|
|
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);
|
|
});
|