daemon-cmd.js
1 #!/usr/bin/env node 2 'use strict' 3 /* eslint-disable @typescript-eslint/no-require-imports */ 4 5 const crypto = require('node:crypto') 6 const fs = require('node:fs') 7 const net = require('node:net') 8 const path = require('node:path') 9 const { spawn } = require('node:child_process') 10 11 const { 12 BROWSER_PROFILES_DIR, 13 DATA_DIR, 14 PKG_ROOT, 15 SWARMCLAW_HOME, 16 WORKSPACE_DIR, 17 resolvePackageBuildRoot, 18 } = require('./server-cmd.js') 19 20 function printHelp() { 21 const help = ` 22 Usage: swarmclaw daemon run [options] 23 24 Run the detached SwarmClaw runtime daemon outside the public web process. 25 26 Options: 27 -d, --detach Start daemon in background 28 --port <port> Admin port to bind on localhost (default: random) 29 --token <token> Admin bearer token (default: random) 30 -h, --help Show this help message 31 32 Other daemon controls remain available through the API-backed CLI: 33 swarmclaw daemon status 34 swarmclaw daemon start 35 swarmclaw daemon stop 36 swarmclaw daemon health-check 37 `.trim() 38 console.log(help) 39 } 40 41 function resolveRoot() { 42 const buildRoot = process.env.SWARMCLAW_BUILD_ROOT || resolvePackageBuildRoot(PKG_ROOT) 43 if (fs.existsSync(path.join(buildRoot, 'src', 'lib', 'server', 'daemon', 'daemon-runtime.ts'))) return buildRoot 44 return PKG_ROOT 45 } 46 47 function resolveEntry(root) { 48 const entry = path.join(root, 'src', 'lib', 'server', 'daemon', 'daemon-runtime.ts') 49 if (!fs.existsSync(entry)) { 50 throw new Error(`Daemon runtime entry not found at ${entry}`) 51 } 52 return entry 53 } 54 55 function reservePort() { 56 return new Promise((resolve, reject) => { 57 const server = net.createServer() 58 server.once('error', reject) 59 server.listen(0, '127.0.0.1', () => { 60 const address = server.address() 61 if (!address || typeof address === 'string') { 62 server.close(() => reject(new Error('Failed to reserve daemon port.'))) 63 return 64 } 65 const port = address.port 66 server.close((err) => { 67 if (err) reject(err) 68 else resolve(port) 69 }) 70 }) 71 }) 72 } 73 74 function buildEnv(root, port, token) { 75 return { 76 ...process.env, 77 SWARMCLAW_HOME, 78 DATA_DIR, 79 WORKSPACE_DIR, 80 BROWSER_PROFILES_DIR, 81 SWARMCLAW_PACKAGE_ROOT: PKG_ROOT, 82 SWARMCLAW_BUILD_ROOT: root, 83 SWARMCLAW_RUNTIME_ROLE: 'daemon', 84 SWARMCLAW_DAEMON_BACKGROUND_SERVICES: '1', 85 SWARMCLAW_DAEMON_ADMIN_PORT: String(port), 86 SWARMCLAW_DAEMON_ADMIN_TOKEN: token, 87 } 88 } 89 90 async function runDaemon(options) { 91 const root = resolveRoot() 92 const entry = resolveEntry(root) 93 const port = options.port || await reservePort() 94 const token = options.token || crypto.randomBytes(24).toString('hex') 95 const env = buildEnv(root, port, token) 96 const args = ['--no-warnings', '--import', 'tsx', entry, '--port', String(port), '--token', token] 97 98 if (options.detach) { 99 const logPath = path.join(SWARMCLAW_HOME, 'daemon.log') 100 fs.mkdirSync(path.dirname(logPath), { recursive: true }) 101 const logStream = fs.openSync(logPath, 'a') 102 const child = spawn(process.execPath, args, { 103 cwd: root, 104 detached: true, 105 env, 106 stdio: ['ignore', logStream, logStream], 107 }) 108 child.unref() 109 console.log(`[swarmclaw] Daemon started in background (PID: ${child.pid})`) 110 console.log(`[swarmclaw] Admin port: ${port}`) 111 console.log(`[swarmclaw] Logs: ${logPath}`) 112 return 113 } 114 115 console.log(`[swarmclaw] Starting daemon runtime on 127.0.0.1:${port}`) 116 const child = spawn(process.execPath, args, { 117 cwd: root, 118 env, 119 stdio: 'inherit', 120 }) 121 child.on('exit', (code) => { 122 process.exit(code || 0) 123 }) 124 for (const signal of ['SIGINT', 'SIGTERM']) { 125 process.on(signal, () => child.kill(signal)) 126 } 127 } 128 129 async function main(args = process.argv.slice(3)) { 130 let detach = false 131 let port = null 132 let token = '' 133 let command = 'run' 134 135 for (let index = 0; index < args.length; index += 1) { 136 const arg = args[index] 137 if (arg === 'run') { 138 command = 'run' 139 } else if (arg === '-d' || arg === '--detach') { 140 detach = true 141 } else if (arg === '--port' && index + 1 < args.length) { 142 port = Number.parseInt(args[index + 1], 10) 143 index += 1 144 } else if (arg === '--token' && index + 1 < args.length) { 145 token = args[index + 1] || '' 146 index += 1 147 } else if (arg === '-h' || arg === '--help' || arg === 'help') { 148 printHelp() 149 return 150 } else { 151 throw new Error(`Unknown daemon argument: ${arg}`) 152 } 153 } 154 155 if (command !== 'run') { 156 throw new Error(`Unsupported daemon command: ${command}`) 157 } 158 159 await runDaemon({ detach, port, token }) 160 } 161 162 if (require.main === module) { 163 void main().catch((err) => { 164 console.error(`[swarmclaw] ${err?.message || String(err)}`) 165 process.exit(1) 166 }) 167 } 168 169 module.exports = { main }