mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
Merge pull request #162 from leonvanzyl/main
feat: add cross-platform dev script (Windows/macOS/Linux support)
This commit is contained in:
414
init.mjs
Normal file
414
init.mjs
Normal file
@@ -0,0 +1,414 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Automaker - Cross-Platform Development Environment Setup and Launch Script
|
||||
*
|
||||
* This script works on Windows, macOS, and Linux.
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import http from 'http';
|
||||
import path from 'path';
|
||||
import readline from 'readline';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createRequire } from 'module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const treeKill = require('tree-kill');
|
||||
const crossSpawn = require('cross-spawn');
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Colors for terminal output (works on modern terminals including Windows)
|
||||
const colors = {
|
||||
green: '\x1b[0;32m',
|
||||
blue: '\x1b[0;34m',
|
||||
yellow: '\x1b[1;33m',
|
||||
red: '\x1b[0;31m',
|
||||
reset: '\x1b[0m',
|
||||
};
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// Track background processes for cleanup
|
||||
let serverProcess = null;
|
||||
let webProcess = null;
|
||||
let electronProcess = null;
|
||||
|
||||
/**
|
||||
* Print colored output
|
||||
*/
|
||||
function log(message, color = 'reset') {
|
||||
console.log(`${colors[color]}${message}${colors.reset}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the header banner
|
||||
*/
|
||||
function printHeader() {
|
||||
console.log('╔═══════════════════════════════════════════════════════╗');
|
||||
console.log('║ Automaker Development Environment ║');
|
||||
console.log('╚═══════════════════════════════════════════════════════╝');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command synchronously and return stdout
|
||||
*/
|
||||
function execCommand(command, options = {}) {
|
||||
try {
|
||||
return execSync(command, { encoding: 'utf8', stdio: 'pipe', ...options }).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get process IDs using a specific port (cross-platform)
|
||||
*/
|
||||
function getProcessesOnPort(port) {
|
||||
const pids = new Set();
|
||||
|
||||
if (isWindows) {
|
||||
// Windows: Use netstat to find PIDs
|
||||
try {
|
||||
const output = execCommand(`netstat -ano | findstr :${port}`);
|
||||
if (output) {
|
||||
const lines = output.split('\n');
|
||||
for (const line of lines) {
|
||||
// Match lines with LISTENING or ESTABLISHED on our port
|
||||
const match = line.match(/:\d+\s+.*?(\d+)\s*$/);
|
||||
if (match) {
|
||||
const pid = parseInt(match[1], 10);
|
||||
if (pid > 0) pids.add(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
} else {
|
||||
// Unix: Use lsof
|
||||
try {
|
||||
const output = execCommand(`lsof -ti:${port}`);
|
||||
if (output) {
|
||||
output.split('\n').forEach(pid => {
|
||||
const parsed = parseInt(pid.trim(), 10);
|
||||
if (parsed > 0) pids.add(parsed);
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(pids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill a process by PID (cross-platform)
|
||||
*/
|
||||
function killProcess(pid) {
|
||||
try {
|
||||
if (isWindows) {
|
||||
execCommand(`taskkill /F /PID ${pid}`);
|
||||
} else {
|
||||
process.kill(pid, 'SIGKILL');
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill processes on a port and wait for it to be freed
|
||||
*/
|
||||
async function killPort(port) {
|
||||
const pids = getProcessesOnPort(port);
|
||||
|
||||
if (pids.length === 0) {
|
||||
log(`✓ Port ${port} is available`, 'green');
|
||||
return true;
|
||||
}
|
||||
|
||||
log(`Killing process(es) on port ${port}: ${pids.join(', ')}`, 'yellow');
|
||||
|
||||
for (const pid of pids) {
|
||||
killProcess(pid);
|
||||
}
|
||||
|
||||
// Wait for port to be freed (max 5 seconds)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await sleep(500);
|
||||
const remainingPids = getProcessesOnPort(port);
|
||||
if (remainingPids.length === 0) {
|
||||
log(`✓ Port ${port} is now free`, 'green');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
log(`Warning: Port ${port} may still be in use`, 'red');
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep for a given number of milliseconds
|
||||
*/
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the server health endpoint is responding
|
||||
*/
|
||||
function checkHealth() {
|
||||
return new Promise((resolve) => {
|
||||
const req = http.get('http://localhost:3008/api/health', (res) => {
|
||||
resolve(res.statusCode === 200);
|
||||
});
|
||||
req.on('error', () => resolve(false));
|
||||
req.setTimeout(2000, () => {
|
||||
req.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user for input
|
||||
*/
|
||||
function prompt(question) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run npm command using cross-spawn for Windows compatibility
|
||||
*/
|
||||
function runNpm(args, options = {}) {
|
||||
const spawnOptions = {
|
||||
stdio: 'inherit',
|
||||
cwd: __dirname,
|
||||
...options,
|
||||
};
|
||||
// cross-spawn handles Windows .cmd files automatically
|
||||
return crossSpawn('npm', args, spawnOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run npx command using cross-spawn for Windows compatibility
|
||||
*/
|
||||
function runNpx(args, options = {}) {
|
||||
const spawnOptions = {
|
||||
stdio: 'inherit',
|
||||
cwd: __dirname,
|
||||
...options,
|
||||
};
|
||||
// cross-spawn handles Windows .cmd files automatically
|
||||
return crossSpawn('npx', args, spawnOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill a process tree using tree-kill
|
||||
*/
|
||||
function killProcessTree(pid) {
|
||||
return new Promise((resolve) => {
|
||||
if (!pid) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
treeKill(pid, 'SIGTERM', (err) => {
|
||||
if (err) {
|
||||
// Try force kill if graceful termination fails
|
||||
treeKill(pid, 'SIGKILL', () => resolve());
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup function to kill all spawned processes
|
||||
*/
|
||||
async function cleanup() {
|
||||
console.log('\nCleaning up...');
|
||||
|
||||
const killPromises = [];
|
||||
|
||||
if (serverProcess && !serverProcess.killed && serverProcess.pid) {
|
||||
killPromises.push(killProcessTree(serverProcess.pid));
|
||||
}
|
||||
|
||||
if (webProcess && !webProcess.killed && webProcess.pid) {
|
||||
killPromises.push(killProcessTree(webProcess.pid));
|
||||
}
|
||||
|
||||
if (electronProcess && !electronProcess.killed && electronProcess.pid) {
|
||||
killPromises.push(killProcessTree(electronProcess.pid));
|
||||
}
|
||||
|
||||
await Promise.all(killPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
async function main() {
|
||||
// Change to script directory
|
||||
process.chdir(__dirname);
|
||||
|
||||
printHeader();
|
||||
|
||||
// Check if node_modules exists
|
||||
if (!fs.existsSync(path.join(__dirname, 'node_modules'))) {
|
||||
log('Installing dependencies...', 'blue');
|
||||
const install = runNpm(['install'], { stdio: 'inherit' });
|
||||
await new Promise((resolve, reject) => {
|
||||
install.on('close', (code) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`npm install failed with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Install Playwright browsers from apps/app where @playwright/test is installed
|
||||
log('Checking Playwright browsers...', 'yellow');
|
||||
try {
|
||||
await new Promise((resolve) => {
|
||||
const playwright = crossSpawn(
|
||||
'npx',
|
||||
['playwright', 'install', 'chromium'],
|
||||
{ stdio: 'ignore', cwd: path.join(__dirname, 'apps', 'app') }
|
||||
);
|
||||
playwright.on('close', () => resolve());
|
||||
playwright.on('error', () => resolve());
|
||||
});
|
||||
} catch {
|
||||
// Ignore errors - Playwright install is optional
|
||||
}
|
||||
|
||||
// Kill any existing processes on required ports
|
||||
log('Checking for processes on ports 3007 and 3008...', 'yellow');
|
||||
await killPort(3007);
|
||||
await killPort(3008);
|
||||
console.log('');
|
||||
|
||||
// Show menu
|
||||
console.log('═══════════════════════════════════════════════════════');
|
||||
console.log(' Select Application Mode:');
|
||||
console.log('═══════════════════════════════════════════════════════');
|
||||
console.log(' 1) Web Application (Browser)');
|
||||
console.log(' 2) Desktop Application (Electron)');
|
||||
console.log('═══════════════════════════════════════════════════════');
|
||||
console.log('');
|
||||
|
||||
// Setup cleanup handlers
|
||||
let cleaningUp = false;
|
||||
const handleExit = async (signal) => {
|
||||
if (cleaningUp) return;
|
||||
cleaningUp = true;
|
||||
await cleanup();
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGINT', () => handleExit('SIGINT'));
|
||||
process.on('SIGTERM', () => handleExit('SIGTERM'));
|
||||
|
||||
// Prompt for choice
|
||||
while (true) {
|
||||
const choice = await prompt('Enter your choice (1 or 2): ');
|
||||
|
||||
if (choice === '1') {
|
||||
console.log('');
|
||||
log('Launching Web Application...', 'blue');
|
||||
|
||||
// Start the backend server
|
||||
log('Starting backend server on port 3008...', 'blue');
|
||||
|
||||
// Create logs directory
|
||||
if (!fs.existsSync(path.join(__dirname, 'logs'))) {
|
||||
fs.mkdirSync(path.join(__dirname, 'logs'), { recursive: true });
|
||||
}
|
||||
|
||||
// Start server in background
|
||||
const logStream = fs.createWriteStream(path.join(__dirname, 'logs', 'server.log'));
|
||||
serverProcess = runNpm(['run', 'dev:server'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
serverProcess.stdout?.pipe(logStream);
|
||||
serverProcess.stderr?.pipe(logStream);
|
||||
|
||||
log('Waiting for server to be ready...', 'yellow');
|
||||
|
||||
// Wait for server health check
|
||||
const maxRetries = 30;
|
||||
let serverReady = false;
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
if (await checkHealth()) {
|
||||
serverReady = true;
|
||||
break;
|
||||
}
|
||||
process.stdout.write('.');
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
if (!serverReady) {
|
||||
log('Error: Server failed to start', 'red');
|
||||
console.log('Check logs/server.log for details');
|
||||
cleanup();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log('✓ Server is ready!', 'green');
|
||||
log(`The application will be available at: http://localhost:3007`, 'green');
|
||||
console.log('');
|
||||
|
||||
// Start web app
|
||||
webProcess = runNpm(['run', 'dev:web'], { stdio: 'inherit' });
|
||||
await new Promise((resolve) => {
|
||||
webProcess.on('close', resolve);
|
||||
});
|
||||
|
||||
break;
|
||||
} else if (choice === '2') {
|
||||
console.log('');
|
||||
log('Launching Desktop Application...', 'blue');
|
||||
log('(Electron will start its own backend server)', 'yellow');
|
||||
console.log('');
|
||||
|
||||
electronProcess = runNpm(['run', 'dev:electron'], { stdio: 'inherit' });
|
||||
await new Promise((resolve) => {
|
||||
electronProcess.on('close', resolve);
|
||||
});
|
||||
|
||||
break;
|
||||
} else {
|
||||
log('Invalid choice. Please enter 1 or 2.', 'red');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run main function
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
cleanup();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -13,7 +13,8 @@
|
||||
"libs/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6"
|
||||
"cross-spawn": "^7.0.6",
|
||||
"tree-kill": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"apps/app": {
|
||||
@@ -8223,14 +8224,6 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/trim-lines": {
|
||||
"version": "3.0.1",
|
||||
"license": "MIT",
|
||||
@@ -15171,6 +15164,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"postinstall": "node -e \"const fs=require('fs');if(process.platform==='darwin'){['darwin-arm64','darwin-x64'].forEach(a=>{const p='node_modules/node-pty/prebuilds/'+a+'/spawn-helper';if(fs.existsSync(p))fs.chmodSync(p,0o755)})}\"",
|
||||
"dev": "./init.sh",
|
||||
"dev": "node init.mjs",
|
||||
"dev:web": "npm run dev:web --workspace=apps/app",
|
||||
"dev:electron": "npm run dev:electron --workspace=apps/app",
|
||||
"dev:electron:debug": "npm run dev:electron:debug --workspace=apps/app",
|
||||
@@ -32,6 +32,7 @@
|
||||
"lint:lockfile": "! grep -q 'git+ssh://' package-lock.json || (echo 'Error: package-lock.json contains git+ssh:// URLs. Run: git config --global url.\"https://github.com/\".insteadOf \"git@github.com:\"' && exit 1)"
|
||||
},
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6"
|
||||
"cross-spawn": "^7.0.6",
|
||||
"tree-kill": "^1.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user