#!/usr/bin/env node import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { createDbPool } from '../db.mjs'; import { buildExecutorLaunchPlan, createLlmProviderService } from '../llm-providers.mjs'; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); function loadEnvFile(filePath) { if (!fs.existsSync(filePath)) return; for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eq = trimmed.indexOf('='); if (eq < 0) continue; const key = trimmed.slice(0, eq).trim(); const value = trimmed.slice(eq + 1).trim(); if (!process.env[key]) process.env[key] = value; } } function parseArgs(argv) { if (argv[0] === '--help' || argv[0] === '-h') { return { help: true, executor: '' }; } const [executor, ...rest] = argv; const out = { executor: String(executor ?? '').trim(), cwd: process.cwd(), mode: 'headless', instruction: '', purpose: 'default', }; for (let i = 0; i < rest.length; i += 1) { const item = rest[i]; if (item === '--cwd') out.cwd = String(rest[++i] ?? out.cwd); else if (item === '--mode') out.mode = String(rest[++i] ?? out.mode); else if (item === '--instruction') out.instruction = String(rest[++i] ?? out.instruction); else if (item === '--purpose') out.purpose = String(rest[++i] ?? out.purpose); else if (item === '--help' || item === '-h') out.help = true; } return out; } function printHelp() { console.log([ 'Usage:', ' node scripts/launch-executor.mjs [--cwd ] [--mode headless|serve] [--instruction ] [--purpose default]', '', 'Examples:', ' node scripts/launch-executor.mjs aider --cwd /path/to/repo --instruction "fix login button"', ' node scripts/launch-executor.mjs openhands --mode serve --cwd /path/to/repo', ' node scripts/launch-executor.mjs openhands --mode headless --cwd /path/to/repo --instruction "add search"', ].join('\n')); } loadEnvFile(path.join(root, '.env')); loadEnvFile(path.join(root, '.env.local')); const parsed = parseArgs(process.argv.slice(2)); if (parsed.help || !parsed.executor) { printHelp(); process.exit(parsed.help ? 0 : 1); } const pool = createDbPool(); const svc = createLlmProviderService(pool, { apiTarget: process.env.TKMIND_API_TARGET ?? 'https://127.0.0.1:18006', apiSecret: process.env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret', }); const plan = await svc.getExecutorLaunchPlan(parsed.executor, { cwd: path.resolve(parsed.cwd), mode: parsed.mode, instruction: parsed.instruction, purpose: parsed.purpose, includeSecret: true, }); if (!plan.ok) { console.error(plan.message ?? '执行器启动计划不可用'); await pool.end(); process.exit(1); } console.log(`==> ${plan.executorLabel ?? plan.executor} launch plan`); console.log(`cwd: ${plan.cwd}`); console.log(`cmd: ${plan.command} ${plan.args?.join(' ') ?? ''}`.trim()); if (plan.notes?.length) { for (const note of plan.notes) console.log(`note: ${note}`); } const child = spawn(plan.command, plan.args ?? [], { cwd: plan.cwd, env: { ...process.env, ...(plan.env ?? {}) }, stdio: 'inherit', }); const shutdown = (code = 0) => { child.kill('SIGTERM'); setTimeout(() => process.exit(code), 200); }; process.on('SIGINT', () => shutdown(0)); process.on('SIGTERM', () => shutdown(0)); child.on('exit', async (code, signal) => { await pool.end().catch(() => {}); if (signal) { process.exit(0); return; } process.exit(code ?? 0); }); child.on('error', async (err) => { console.error(err instanceof Error ? err.message : String(err)); await pool.end().catch(() => {}); process.exit(1); });