diff --git a/dev/local/cli.ts b/dev/local/cli.ts index 45c0d3e2a..70e444a69 100644 --- a/dev/local/cli.ts +++ b/dev/local/cli.ts @@ -142,7 +142,12 @@ async function cmdUp(targets: string[], repoRoot: string): Promise { const SIDEBAR_WIDTH = 40; // --- Start capture services first (tunnel, stripe) and wait for output --- - const captureServiceSet = new Set(['kiloclaw-tunnel', 'kiloclaw-stripe', 'app-builder-tunnel']); + const captureServiceSet = new Set([ + 'kiloclaw-tunnel', + 'kiloclaw-worker-tunnel', + 'kiloclaw-stripe', + 'app-builder-tunnel', + ]); const captureServices = serviceNames.filter(n => captureServiceSet.has(n)); const otherServices = serviceNames.filter(n => !captureServiceSet.has(n)); diff --git a/dev/local/scripts/start-tunnel.ts b/dev/local/scripts/start-tunnel.ts index 4bd70f469..341098a12 100644 --- a/dev/local/scripts/start-tunnel.ts +++ b/dev/local/scripts/start-tunnel.ts @@ -6,11 +6,29 @@ import * as path from 'node:path'; const repoRoot = path.resolve(import.meta.dirname, '../../..'); const devVarsPath = path.join(repoRoot, 'kiloclaw/.dev.vars'); -type TunnelConfig = { - tunnelName: string; - tunnelHostname: string; +type TunnelMode = { + nameKey: string; + hostnameKey: string; + onUrl: ((url: string) => void) | null; }; +const TUNNEL_MODES = { + nextjs: { + nameKey: 'TUNNEL_NAME', + hostnameKey: 'TUNNEL_HOSTNAME', + onUrl: (url: string) => + updateEnvValue(devVarsPath, 'KILOCODE_API_BASE_URL', `${url}/api/gateway/`), + }, + worker: { + nameKey: 'WORKER_TUNNEL_NAME', + hostnameKey: 'WORKER_TUNNEL_HOSTNAME', + onUrl: (url: string) => + updateEnvValue(devVarsPath, 'KILOCLAW_CHECKIN_URL', `${url}/api/controller/checkin`), + }, +} satisfies Record; + +type ModeName = keyof typeof TUNNEL_MODES; + function parseConfFile(filePath: string): Record { if (!fs.existsSync(filePath)) return {}; const result: Record = {}; @@ -25,19 +43,10 @@ function parseConfFile(filePath: string): Record { return result; } -function loadTunnelConfig(): TunnelConfig { +function loadConf(): Record { const globalPath = path.join(os.homedir(), '.config/kiloclaw/dev-start.conf'); const localPath = path.join(repoRoot, 'kiloclaw/scripts/.dev-start.conf'); - - const merged = { - ...parseConfFile(globalPath), - ...parseConfFile(localPath), - }; - - return { - tunnelName: merged['TUNNEL_NAME'] ?? '', - tunnelHostname: merged['TUNNEL_HOSTNAME'] ?? '', - }; + return { ...parseConfFile(globalPath), ...parseConfFile(localPath) }; } function updateEnvValue(filePath: string, key: string, value: string): void { @@ -68,30 +77,33 @@ if (spawnSync('cloudflared', ['version'], { stdio: 'ignore' }).error) { process.exit(1); } -const port = process.argv[2] ?? '3000'; -const config = loadTunnelConfig(); +const modeArg = process.argv[2]; +const modeName: ModeName = modeArg === 'worker' ? 'worker' : 'nextjs'; +// For worker mode: argv[2]='worker', argv[3]=port +// For nextjs mode: argv[2]=port (no mode prefix) +const port = modeName === 'worker' ? (process.argv[3] ?? '8795') : (modeArg ?? '3000'); +const mode = TUNNEL_MODES[modeName]; +const conf = loadConf(); +const tunnelName = conf[mode.nameKey] ?? ''; +const tunnelHostname = conf[mode.hostnameKey] ?? ''; -let command: string; let args: string[]; let urlPattern: RegExp | null = null; -if (config.tunnelName) { - command = 'cloudflared'; - args = ['tunnel', 'run', config.tunnelName]; - console.log(`Named tunnel: ${config.tunnelName} -> ${config.tunnelHostname}`); +if (tunnelName) { + args = ['tunnel', 'run', tunnelName]; + console.log(`Named tunnel: ${tunnelName} -> ${tunnelHostname}`); - if (config.tunnelHostname) { - const apiUrl = `https://${config.tunnelHostname}/api/gateway/`; - updateEnvValue(devVarsPath, 'KILOCODE_API_BASE_URL', apiUrl); + if (mode.onUrl && tunnelHostname) { + mode.onUrl(`https://${tunnelHostname}`); } } else { - command = 'cloudflared'; args = ['tunnel', '--url', `http://localhost:${port}`]; urlPattern = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/; console.log(`Starting quick tunnel -> http://localhost:${port}...`); } -const child = spawn(command, args, { +const child = spawn('cloudflared', args, { stdio: ['ignore', 'pipe', 'pipe'], }); @@ -103,11 +115,12 @@ function handleOutput(data: Buffer) { if (!match) return; const url = match[0]; - const apiUrl = `${url}/api/gateway/`; - updateEnvValue(devVarsPath, 'KILOCODE_API_BASE_URL', apiUrl); + mode.onUrl?.(url); console.log(`\nTunnel URL: ${url}`); - console.log(`Set KILOCODE_API_BASE_URL=${apiUrl}`); + if (mode.onUrl) { + console.log(`Set KILOCODE_API_BASE_URL=${url}/api/gateway/`); + } // Only capture once urlPattern = null; diff --git a/dev/local/services.ts b/dev/local/services.ts index e63ba05a6..ba42edc38 100644 --- a/dev/local/services.ts +++ b/dev/local/services.ts @@ -100,8 +100,12 @@ const serviceMeta: Record = { }, // kiloclaw 'kiloclaw-tunnel': { group: 'kiloclaw', dependsOn: [] }, + 'kiloclaw-worker-tunnel': { group: 'kiloclaw', dependsOn: [] }, 'kiloclaw-stripe': { group: 'kiloclaw', dependsOn: [] }, - kiloclaw: { group: 'kiloclaw', dependsOn: ['postgres', 'kiloclaw-tunnel'] }, + kiloclaw: { + group: 'kiloclaw', + dependsOn: ['postgres', 'kiloclaw-tunnel', 'kiloclaw-worker-tunnel'], + }, // observability 'cloudflare-o11y': { group: 'observability', dependsOn: ['nextjs'] }, 'cloudflare-ai-attribution': { group: 'observability', dependsOn: [] }, @@ -250,6 +254,20 @@ function buildServiceDefs(): ServiceDef[] { continue; } + if (name === 'kiloclaw-worker-tunnel') { + const workerPort = readWranglerPort(path.join(repoRoot, 'kiloclaw')) + portOffset; + defs.push({ + name, + type: 'process', + dir: '.', + port: 0, + dependsOn: meta.dependsOn, + command: ['tsx', 'dev/local/scripts/start-tunnel.ts', 'worker', String(workerPort)], + group: meta.group, + }); + continue; + } + if (name === 'kiloclaw-stripe') { defs.push({ name,