#!/usr/bin/env node const { spawn } = require('child_process'); const chokidar = require('chokidar'); const path = require('path'); // Colors for output const colors = { reset: '\x1b[0m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m' }; // App configurations - now only need paths, colors, and nx project names const apps = { shade: { path: 'apps/shade', color: colors.white, nxName: '@tryghost/shade' }, design: { path: 'apps/admin-x-design-system', color: colors.cyan, nxName: '@tryghost/admin-x-design-system' }, framework: { path: 'apps/admin-x-framework', color: colors.magenta, nxName: '@tryghost/admin-x-framework' }, activitypub: { path: 'apps/admin-x-activitypub', color: colors.red, nxName: '@tryghost/admin-x-activitypub' }, settings: { path: 'apps/admin-x-settings', color: colors.green, nxName: '@tryghost/admin-x-settings' }, posts: { path: 'apps/posts', color: colors.yellow, nxName: '@tryghost/posts' }, stats: { path: 'apps/stats', color: colors.blue, nxName: '@tryghost/stats' } }; // Track all child processes and watchers for cleanup const activeProcesses = new Set(); const activeWatchers = new Set(); function log(appName, message) { const app = apps[appName]; console.log(`${app.color}[${appName}]${colors.reset} ${message}`); } function buildAllProjects(triggerApp) { return new Promise((resolve) => { log(triggerApp, 'Running nx run-many to rebuild all projects...'); const allProjects = Object.values(apps).map(app => app.nxName).join(','); const child = spawn('pnpm', ['nx', 'run-many', '-t', 'build', `--projects=${allProjects}`], { cwd: '/home/ghost', stdio: 'pipe', env: { ...process.env, NX_DAEMON: 'false' } }); activeProcesses.add(child); child.stdout.on('data', (data) => { data.toString().split('\n').forEach(line => { if (line.trim()) log(triggerApp, `nx: ${line}`); }); }); child.stderr.on('data', (data) => { data.toString().split('\n').forEach(line => { if (line.trim()) log(triggerApp, `nx: ${line}`); }); }); child.on('close', (code) => { activeProcesses.delete(child); if (code === 0) { log(triggerApp, 'All builds complete'); } else { log(triggerApp, `Some builds failed with code ${code}`); } resolve(); }); child.on('error', (error) => { activeProcesses.delete(child); log(triggerApp, `Build error: ${error.message}`); resolve(); }); }); } function startWatching() { const watchPaths = Object.values(apps).map(app => path.join('/home/ghost', app.path, 'src')); console.log('Watching all project src folders for changes...'); const watcher = chokidar.watch(watchPaths, { persistent: true, ignoreInitial: true, usePolling: true, interval: 1000 }); // Track the watcher for cleanup activeWatchers.add(watcher); let rebuildTimer; watcher.on('all', (event, filePath) => { const relativePath = path.relative('/home/ghost', filePath); // Find which project changed for better logging const changedProject = Object.keys(apps).find(name => filePath.includes(apps[name].path) ) || 'unknown'; log(changedProject, `Change detected: ${event} ${relativePath}`); // Debounce rebuilds clearTimeout(rebuildTimer); rebuildTimer = setTimeout(async () => { await buildAllProjects(changedProject); }, 500); }); watcher.on('error', (error) => { console.log(`Watcher error: ${error.message}`); console.log('Exiting process - Docker will restart the service'); process.exit(1); }); watcher.on('close', () => { console.log('Watcher closed unexpectedly'); console.log('Exiting process - Docker will restart the service'); process.exit(1); }); return watcher; } async function main() { console.log('Starting admin apps build and watch system...'); try { // Phase 1: Build everything with nx handling dependency order and parallelization const allProjects = Object.values(apps).map(app => app.nxName).join(','); const child = spawn('pnpm', ['nx', 'run-many', '-t', 'build', `--projects=${allProjects}`], { cwd: '/home/ghost', stdio: 'pipe', env: { ...process.env, NX_DAEMON: 'false' } }); activeProcesses.add(child); child.stdout.on('data', (data) => { data.toString().split('\n').forEach(line => { if (line.trim()) console.log(`[nx] ${line}`); }); }); child.stderr.on('data', (data) => { data.toString().split('\n').forEach(line => { if (line.trim()) console.log(`[nx] ${line}`); }); }); await new Promise((resolve, reject) => { child.on('close', (code) => { activeProcesses.delete(child); if (code === 0) { console.log('\nAll projects built successfully!'); resolve(); } else { console.log(`\nSome builds failed, but continuing with watch processes...`); resolve(); // Don't crash the watch system if some builds fail } }); child.on('error', (error) => { activeProcesses.delete(child); reject(error); }); }); // Phase 2: Start single watcher for all projects // Single watcher for all projects - any change triggers nx run-many (with caching) const watcher = startWatching(); console.log('\nAll watch processes started. Press Ctrl+C to stop.'); // Keep the process alive await new Promise(() => { }); } catch (error) { console.error('Failed to start:', error.message); process.exit(1); } } // Graceful shutdown handler function cleanup() { console.log('\nShutting down...'); // Kill all active child processes for (const child of activeProcesses) { try { child.kill('SIGTERM'); // Force kill after 1 second if still running setTimeout(() => { if (!child.killed) { child.kill('SIGKILL'); } }, 1000); } catch (error) { // Process might already be dead } } // Close all watchers for (const watcher of activeWatchers) { try { watcher.close(); } catch (error) { // Watcher might already be closed } } console.log('Cleanup complete.'); process.exit(0); } // Handle various termination signals process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); process.on('SIGQUIT', cleanup); // Handle uncaught exceptions to ensure cleanup process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); cleanup(); }); process.on('unhandledRejection', (reason) => { console.error('Unhandled rejection:', reason); cleanup(); }); main();