Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion dev/local/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,12 @@ async function cmdUp(targets: string[], repoRoot: string): Promise<void> {
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',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: kiloclaw-worker-tunnel is started as a capture service but never awaited

cmdUp only snapshots and waits for KILOCODE_API_BASE_URL, STRIPE_WEBHOOK_SECRET, and BUILDER_HOSTNAME. After adding kiloclaw-worker-tunnel here, the code still never waits for KILOCLAW_CHECKIN_URL to change, so kiloclaw can start before the worker tunnel writes its endpoint into kiloclaw/.dev.vars. On a fresh quick-tunnel startup that leaves the worker booting with a stale or missing check-in URL.

'kiloclaw-stripe',
'app-builder-tunnel',
]);
const captureServices = serviceNames.filter(n => captureServiceSet.has(n));
const otherServices = serviceNames.filter(n => !captureServiceSet.has(n));

Expand Down
71 changes: 42 additions & 29 deletions dev/local/scripts/start-tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, TunnelMode>;

type ModeName = keyof typeof TUNNEL_MODES;

function parseConfFile(filePath: string): Record<string, string> {
if (!fs.existsSync(filePath)) return {};
const result: Record<string, string> = {};
Expand All @@ -25,19 +43,10 @@ function parseConfFile(filePath: string): Record<string, string> {
return result;
}

function loadTunnelConfig(): TunnelConfig {
function loadConf(): Record<string, string> {
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 {
Expand Down Expand Up @@ -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'],
});

Expand All @@ -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;
Expand Down
20 changes: 19 additions & 1 deletion dev/local/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,12 @@ const serviceMeta: Record<string, ServiceMeta> = {
},
// 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: [] },
Expand Down Expand Up @@ -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,
Expand Down
Loading