From 90a388f5ab228c4dc391f37384846206cbe7adfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E6=9C=88?= Date: Wed, 13 May 2026 22:46:48 +0800 Subject: [PATCH] refactor(serve): remove tunnel/cloudflared, simplify to WS-only gateway - Delete tunnel.ts (startTunnel/cloudflared), rename to gateway.ts - Remove --no-tunnel, --tunnel-url flags - ServeOptions: drop noTunnel, tunnelUrl fields - Two modes: gateway (with WORKFLOW_GATEWAY_SECRET) or local-only - WS reverse connection is the only gateway transport --- .../commands/serve/{tunnel.ts => gateway.ts} | 42 +----- .../cli-workflow/src/commands/serve/serve.ts | 122 +++++++----------- .../cli-workflow/src/commands/serve/types.ts | 2 - packages/cli-workflow/src/skill.ts | 2 +- 4 files changed, 53 insertions(+), 115 deletions(-) rename packages/cli-workflow/src/commands/serve/{tunnel.ts => gateway.ts} (50%) diff --git a/packages/cli-workflow/src/commands/serve/tunnel.ts b/packages/cli-workflow/src/commands/serve/gateway.ts similarity index 50% rename from packages/cli-workflow/src/commands/serve/tunnel.ts rename to packages/cli-workflow/src/commands/serve/gateway.ts index 2c83e12..f3a0912 100644 --- a/packages/cli-workflow/src/commands/serve/tunnel.ts +++ b/packages/cli-workflow/src/commands/serve/gateway.ts @@ -1,43 +1,9 @@ import { printCliLine } from "../../cli-output.js"; -type TunnelHandle = { - process: ReturnType; - url: string; -}; - -export async function startTunnel(port: number): Promise { - const proc = Bun.spawn(["cloudflared", "tunnel", "--url", `http://localhost:${port}`], { - stdout: "pipe", - stderr: "pipe", - }); - - // cloudflared prints the URL to stderr - const reader = proc.stderr.getReader(); - const decoder = new TextDecoder(); - let buffer = ""; - const deadline = Date.now() + 30_000; - - while (Date.now() < deadline) { - const { done, value } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - const match = buffer.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/); - if (match) { - // Release the reader so stderr keeps flowing without backpressure - reader.releaseLock(); - return { process: proc, url: match[0] }; - } - } - - reader.releaseLock(); - proc.kill(); - return null; -} - export async function registerWithGateway( gatewayUrl: string, name: string, - tunnelUrl: string, + localUrl: string, secret: string, agentToken: string, ): Promise { @@ -45,7 +11,7 @@ export async function registerWithGateway( const resp = await fetch(`${gatewayUrl}/api/gateway/register`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name, url: tunnelUrl, secret, agentToken }), + body: JSON.stringify({ name, url: localUrl, secret, agentToken }), }); if (!resp.ok) { const body = await resp.text(); @@ -77,12 +43,12 @@ export async function unregisterFromGateway( export function startHeartbeat( gatewayUrl: string, name: string, - tunnelUrl: string, + localUrl: string, secret: string, agentToken: string, intervalMs: number, ): ReturnType { return setInterval(() => { - registerWithGateway(gatewayUrl, name, tunnelUrl, secret, agentToken).catch(() => {}); + registerWithGateway(gatewayUrl, name, localUrl, secret, agentToken).catch(() => {}); }, intervalMs); } diff --git a/packages/cli-workflow/src/commands/serve/serve.ts b/packages/cli-workflow/src/commands/serve/serve.ts index b41f3e7..3117f62 100644 --- a/packages/cli-workflow/src/commands/serve/serve.ts +++ b/packages/cli-workflow/src/commands/serve/serve.ts @@ -6,7 +6,7 @@ import { serve } from "bun"; import { printCliLine } from "../../cli-output.js"; import { createApp } from "./app.js"; -import { registerWithGateway, startHeartbeat, unregisterFromGateway } from "./tunnel.js"; +import { registerWithGateway, startHeartbeat, unregisterFromGateway } from "./gateway.js"; import type { ServeOptions } from "./types.js"; import { startGatewayWsClient } from "./ws-client.js"; @@ -52,8 +52,6 @@ function parseServeArgv(argv: string[]): Result { let port = 7860; let hostname = "127.0.0.1"; let name = osHostname().split(".")[0].toLowerCase(); - let noTunnel = false; - let tunnelUrl: string | null = null; let gatewayUrl = DEFAULT_GATEWAY_URL; const gatewaySecret = process.env.WORKFLOW_GATEWAY_SECRET ?? ""; const stringFlags: Record void> = { @@ -66,9 +64,6 @@ function parseServeArgv(argv: string[]): Result { "--gateway": (v) => { gatewayUrl = v; }, - "--tunnel-url": (v) => { - tunnelUrl = v; - }, }; for (let i = 0; i < argv.length; i++) { @@ -78,8 +73,6 @@ function parseServeArgv(argv: string[]): Result { if (!portResult.ok) return portResult; port = portResult.value; i++; - } else if (arg === "--no-tunnel") { - noTunnel = true; } else if (arg in stringFlags) { const r = requireNextArg(argv, i, arg); if (!r.ok) return r; @@ -88,7 +81,7 @@ function parseServeArgv(argv: string[]): Result { } } - return ok({ port, hostname, name, noTunnel, tunnelUrl, gatewayUrl, gatewaySecret }); + return ok({ port, hostname, name, gatewayUrl, gatewaySecret }); } export async function dispatchServe(storageRoot: string, argv: string[]): Promise { @@ -99,81 +92,62 @@ export async function dispatchServe(storageRoot: string, argv: string[]): Promis } const options = parsed.value; - const agentToken = options.noTunnel ? null : randomUUID(); - startServer(storageRoot, options, agentToken); - if (options.noTunnel) { - printCliLine("tunnel disabled (--no-tunnel)"); + if (options.gatewaySecret === "") { + // No gateway — local-only mode + startServer(storageRoot, options, null); + printCliLine("no WORKFLOW_GATEWAY_SECRET — running in local-only mode"); await new Promise(() => {}); return 0; } - let resolvedTunnelUrl: string; - let stopWsClient: (() => void) | null = null; + const agentToken = randomUUID(); + startServer(storageRoot, options, agentToken); - if (options.tunnelUrl !== null) { - resolvedTunnelUrl = options.tunnelUrl; - printCliLine(`using tunnel URL: ${resolvedTunnelUrl}`); - } else { - if (options.gatewaySecret === "") { - printCliLine( - "WORKFLOW_GATEWAY_SECRET not set — cannot use WebSocket gateway connection (set env or pass --tunnel-url)", - ); - await new Promise(() => {}); - return 0; - } - resolvedTunnelUrl = `http://127.0.0.1:${options.port}`; - const log = createLogger({ sink: { kind: "stderr" } }); - stopWsClient = startGatewayWsClient({ - gatewayUrl: options.gatewayUrl, - name: options.name, - secret: options.gatewaySecret, - localPort: options.port, - log, - }); - printCliLine("gateway WebSocket reverse connection (no cloudflared)"); + // Start WebSocket reverse connection to gateway + const log = createLogger({ sink: { kind: "stderr" } }); + const stopWsClient = startGatewayWsClient({ + gatewayUrl: options.gatewayUrl, + name: options.name, + secret: options.gatewaySecret, + localPort: options.port, + log, + }); + + printCliLine("connected to gateway via WebSocket"); + + // Register with gateway for discovery + const localUrl = `http://127.0.0.1:${options.port}`; + const registered = await registerWithGateway( + options.gatewayUrl, + options.name, + localUrl, + options.gatewaySecret, + agentToken, + ); + if (registered) { + printCliLine(`registered with gateway as "${options.name}"`); } - if (options.gatewaySecret) { - if (agentToken === null) { - printCliLine("internal error: agent token missing"); - await new Promise(() => {}); - return 1; - } - const token = agentToken; - const registered = await registerWithGateway( - options.gatewayUrl, - options.name, - resolvedTunnelUrl, - options.gatewaySecret, - token, - ); - if (registered) { - printCliLine(`registered with gateway as "${options.name}"`); - } + const heartbeatTimer = startHeartbeat( + options.gatewayUrl, + options.name, + localUrl, + options.gatewaySecret, + agentToken, + HEARTBEAT_INTERVAL_MS, + ); - const heartbeatTimer = startHeartbeat( - options.gatewayUrl, - options.name, - resolvedTunnelUrl, - options.gatewaySecret, - token, - HEARTBEAT_INTERVAL_MS, - ); + const cleanup = async () => { + clearInterval(heartbeatTimer); + stopWsClient(); + printCliLine("unregistering from gateway..."); + await unregisterFromGateway(options.gatewayUrl, options.name, options.gatewaySecret); + process.exit(0); + }; - const cleanup = async () => { - clearInterval(heartbeatTimer); - stopWsClient?.(); - printCliLine("unregistering from gateway..."); - await unregisterFromGateway(options.gatewayUrl, options.name, options.gatewaySecret); - process.exit(0); - }; - - process.on("SIGINT", cleanup); - process.on("SIGTERM", cleanup); - } else { - printCliLine("WORKFLOW_GATEWAY_SECRET not set — skipping gateway registration"); - } + process.on("SIGINT", cleanup); + process.on("SIGTERM", cleanup); await new Promise(() => {}); return 0; diff --git a/packages/cli-workflow/src/commands/serve/types.ts b/packages/cli-workflow/src/commands/serve/types.ts index 8c19cd7..56b5de3 100644 --- a/packages/cli-workflow/src/commands/serve/types.ts +++ b/packages/cli-workflow/src/commands/serve/types.ts @@ -2,8 +2,6 @@ export type ServeOptions = { port: number; hostname: string; name: string; - noTunnel: boolean; - tunnelUrl: string | null; gatewayUrl: string; gatewaySecret: string; }; diff --git a/packages/cli-workflow/src/skill.ts b/packages/cli-workflow/src/skill.ts index 3dd0c2f..f6842ed 100644 --- a/packages/cli-workflow/src/skill.ts +++ b/packages/cli-workflow/src/skill.ts @@ -90,7 +90,7 @@ ${commandSections.join("\n\n")} | Command | Args | Description | |---------|------|-------------| -| \`serve\` | \`[--port N] [--host ADDR] [--name NAME]\` | Start HTTP API server with auto-tunnel. \`--name\` registers with the gateway. | +| \`serve\` | \`[--port N] [--host ADDR] [--name NAME]\` | Start HTTP API server with WebSocket gateway connection. \`--name\` registers with the gateway. | ## Typical Workflow