frontmcp 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -29
- package/package.json +4 -4
- package/src/commands/build/exec/bin-meta.d.ts +49 -0
- package/src/commands/build/exec/bin-meta.js +68 -0
- package/src/commands/build/exec/bin-meta.js.map +1 -0
- package/src/commands/build/exec/cli-runtime/generate-cli-entry.js +195 -3
- package/src/commands/build/exec/cli-runtime/generate-cli-entry.js.map +1 -1
- package/src/commands/build/exec/cli-runtime/plugin-emitter.d.ts +160 -0
- package/src/commands/build/exec/cli-runtime/plugin-emitter.js +512 -0
- package/src/commands/build/exec/cli-runtime/plugin-emitter.js.map +1 -0
- package/src/commands/build/exec/cli-runtime/schema-extractor.d.ts +13 -1
- package/src/commands/build/exec/cli-runtime/schema-extractor.js +29 -3
- package/src/commands/build/exec/cli-runtime/schema-extractor.js.map +1 -1
- package/src/commands/build/exec/cli-runtime/skill-md-compose.d.ts +25 -0
- package/src/commands/build/exec/cli-runtime/skill-md-compose.js +63 -0
- package/src/commands/build/exec/cli-runtime/skill-md-compose.js.map +1 -0
- package/src/commands/build/exec/index.js +26 -0
- package/src/commands/build/exec/index.js.map +1 -1
- package/src/commands/build/exec/runner-script.js +16 -4
- package/src/commands/build/exec/runner-script.js.map +1 -1
- package/src/commands/dev/bridge/child-supervisor.d.ts +48 -0
- package/src/commands/dev/bridge/child-supervisor.js +228 -0
- package/src/commands/dev/bridge/child-supervisor.js.map +1 -0
- package/src/commands/dev/bridge/errors.d.ts +23 -0
- package/src/commands/dev/bridge/errors.js +34 -0
- package/src/commands/dev/bridge/errors.js.map +1 -0
- package/src/commands/dev/bridge/index.d.ts +30 -0
- package/src/commands/dev/bridge/index.js +220 -0
- package/src/commands/dev/bridge/index.js.map +1 -0
- package/src/commands/dev/bridge/log.d.ts +29 -0
- package/src/commands/dev/bridge/log.js +82 -0
- package/src/commands/dev/bridge/log.js.map +1 -0
- package/src/commands/dev/bridge/state-machine.d.ts +56 -0
- package/src/commands/dev/bridge/state-machine.js +245 -0
- package/src/commands/dev/bridge/state-machine.js.map +1 -0
- package/src/commands/dev/bridge/stdio-framer.d.ts +47 -0
- package/src/commands/dev/bridge/stdio-framer.js +128 -0
- package/src/commands/dev/bridge/stdio-framer.js.map +1 -0
- package/src/commands/dev/bridge/upstream-client.d.ts +49 -0
- package/src/commands/dev/bridge/upstream-client.js +159 -0
- package/src/commands/dev/bridge/upstream-client.js.map +1 -0
- package/src/commands/dev/bridge/watcher.d.ts +30 -0
- package/src/commands/dev/bridge/watcher.js +87 -0
- package/src/commands/dev/bridge/watcher.js.map +1 -0
- package/src/commands/dev/dev.d.ts +34 -1
- package/src/commands/dev/dev.js +168 -14
- package/src/commands/dev/dev.js.map +1 -1
- package/src/commands/dev/inspector.d.ts +13 -1
- package/src/commands/dev/inspector.js +77 -3
- package/src/commands/dev/inspector.js.map +1 -1
- package/src/commands/dev/port.d.ts +23 -0
- package/src/commands/dev/port.js +87 -0
- package/src/commands/dev/port.js.map +1 -0
- package/src/commands/dev/register.d.ts +1 -1
- package/src/commands/dev/register.js +28 -4
- package/src/commands/dev/register.js.map +1 -1
- package/src/commands/dev/test.d.ts +26 -1
- package/src/commands/dev/test.js +181 -64
- package/src/commands/dev/test.js.map +1 -1
- package/src/commands/eject/mcp-client.d.ts +25 -0
- package/src/commands/eject/mcp-client.js +74 -0
- package/src/commands/eject/mcp-client.js.map +1 -0
- package/src/commands/eject/register.d.ts +9 -0
- package/src/commands/eject/register.js +56 -0
- package/src/commands/eject/register.js.map +1 -0
- package/src/commands/install/install-claude-plugin.d.ts +13 -0
- package/src/commands/install/install-claude-plugin.js +327 -0
- package/src/commands/install/install-claude-plugin.js.map +1 -0
- package/src/commands/install/register.d.ts +16 -0
- package/src/commands/install/register.js +70 -0
- package/src/commands/install/register.js.map +1 -0
- package/src/commands/scaffold/create.js +52 -8
- package/src/commands/scaffold/create.js.map +1 -1
- package/src/commands/skills/from-entry.d.ts +31 -0
- package/src/commands/skills/from-entry.js +68 -0
- package/src/commands/skills/from-entry.js.map +1 -0
- package/src/commands/skills/install.d.ts +12 -0
- package/src/commands/skills/install.js +173 -8
- package/src/commands/skills/install.js.map +1 -1
- package/src/commands/skills/register.js +7 -3
- package/src/commands/skills/register.js.map +1 -1
- package/src/config/frontmcp-config.loader.d.ts +28 -0
- package/src/config/frontmcp-config.loader.js +146 -67
- package/src/config/frontmcp-config.loader.js.map +1 -1
- package/src/config/frontmcp-config.resolve.d.ts +67 -0
- package/src/config/frontmcp-config.resolve.js +118 -0
- package/src/config/frontmcp-config.resolve.js.map +1 -0
- package/src/config/frontmcp-config.schema.d.ts +207 -0
- package/src/config/frontmcp-config.schema.js +217 -1
- package/src/config/frontmcp-config.schema.js.map +1 -1
- package/src/config/frontmcp-config.types.d.ts +133 -0
- package/src/config/frontmcp-config.types.js.map +1 -1
- package/src/config/index.d.ts +2 -1
- package/src/config/index.js +3 -1
- package/src/config/index.js.map +1 -1
- package/src/core/args.d.ts +13 -0
- package/src/core/args.js.map +1 -1
- package/src/core/bridge.js +39 -0
- package/src/core/bridge.js.map +1 -1
- package/src/core/cli.d.ts +0 -6
- package/src/core/cli.js +23 -3
- package/src/core/cli.js.map +1 -1
- package/src/core/help.d.ts +1 -1
- package/src/core/help.js +27 -6
- package/src/core/help.js.map +1 -1
- package/src/core/program.d.ts +1 -1
- package/src/core/program.js +56 -12
- package/src/core/program.js.map +1 -1
- package/src/core/project-commands.d.ts +44 -0
- package/src/core/project-commands.js +216 -0
- package/src/core/project-commands.js.map +1 -0
- package/src/core/tsconfig.d.ts +20 -0
- package/src/core/tsconfig.js +41 -2
- package/src/core/tsconfig.js.map +1 -1
|
@@ -33,16 +33,20 @@ case "\${1:-}" in
|
|
|
33
33
|
cat <<EOF
|
|
34
34
|
${name} v${version} — FrontMCP server
|
|
35
35
|
|
|
36
|
-
This binary starts a long-running MCP HTTP server.
|
|
36
|
+
This binary starts a long-running MCP HTTP server, or an MCP stdio server with --stdio.
|
|
37
37
|
|
|
38
38
|
Usage:
|
|
39
|
-
${name} Start the server
|
|
39
|
+
${name} Start the HTTP server
|
|
40
|
+
${name} --stdio Serve over stdio (stdin/stdout JSON-RPC); binds no TCP port
|
|
40
41
|
${name} --help Show this help
|
|
41
42
|
${name} --version Show version
|
|
42
43
|
${name} --print-manifest Print the deployment manifest as JSON
|
|
43
44
|
|
|
44
45
|
Configure via environment variables, .env, or frontmcp.config.
|
|
45
46
|
|
|
47
|
+
Use --stdio for local MCP clients (Claude Desktop, Cursor). Example config:
|
|
48
|
+
{ "command": "${name}", "args": ["--stdio"] }
|
|
49
|
+
|
|
46
50
|
For a CLI-style binary that exposes tools/resources/prompts as subcommands,
|
|
47
51
|
build with: frontmcp build --target cli
|
|
48
52
|
EOF
|
|
@@ -56,10 +60,18 @@ EOF
|
|
|
56
60
|
cat "\${SCRIPT_DIR}/${name}.manifest.json"
|
|
57
61
|
exit 0
|
|
58
62
|
;;
|
|
63
|
+
--stdio)
|
|
64
|
+
# Serve over stdio (stdin/stdout JSON-RPC) instead of starting the HTTP
|
|
65
|
+
# server. FRONTMCP_STDIO=1 makes the @FrontMcp decorator connect the stdio
|
|
66
|
+
# transport and bind no TCP port (#448, #451); logs go to stderr and
|
|
67
|
+
# ~/.frontmcp/logs. Drop the flag and fall through to the exec below.
|
|
68
|
+
export FRONTMCP_STDIO=1
|
|
69
|
+
shift
|
|
70
|
+
;;
|
|
59
71
|
--*)
|
|
60
72
|
echo "Error: unsupported flag '\${1}' on the server runner."
|
|
61
|
-
echo "This binary is a long-running HTTP server; flag-style invocation is reserved." >&2
|
|
62
|
-
echo "Run with no args to start, or build with --target cli for a CLI binary." >&2
|
|
73
|
+
echo "This binary is a long-running HTTP server; flag-style invocation is reserved (except --stdio)." >&2
|
|
74
|
+
echo "Run with no args to start, --stdio to serve over stdio, or build with --target cli for a CLI binary." >&2
|
|
63
75
|
exit 2
|
|
64
76
|
;;
|
|
65
77
|
esac
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner-script.js","sourceRoot":"","sources":["../../../../../src/commands/build/exec/runner-script.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAWH,oDAGC;AAED,
|
|
1
|
+
{"version":3,"file":"runner-script.js","sourceRoot":"","sources":["../../../../../src/commands/build/exec/runner-script.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAWH,oDAGC;AAED,oDAyJC;AArKD;;;;;;GAMG;AACH,SAAgB,oBAAoB,CAAC,KAAa;IAChD,0EAA0E;IAC1E,OAAO,KAAK,CAAC,OAAO,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAC;AACjD,CAAC;AAED,SAAgB,oBAAoB,CAAC,MAA0B,EAAE,OAAiB,EAAE,OAAiB;IACnG,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;IACzB,MAAM,OAAO,GAAG,oBAAoB,CAAC,MAAM,CAAC,OAAO,IAAI,OAAO,CAAC,CAAC;IAEhE,yEAAyE;IACzE,6EAA6E;IAC7E,wEAAwE;IACxE,wEAAwE;IACxE,wBAAwB;IACxB,MAAM,eAAe,GAAG,CAAC,OAAO;QAC9B,CAAC,CAAC;;;;;EAKJ,IAAI,KAAK,OAAO;;;;;IAKd,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;;;;;kBAKU,IAAI;;;;;;;;YAQV,IAAI,IAAI,OAAO;;;;0BAID,IAAI;;;;;;;;;;;;;;;;;;CAkB7B;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,0DAA0D;IAC1D,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC;QAC3D,MAAM,OAAO,GAAG,OAAO;YACrB,CAAC,CAAC,KAAK,IAAI,qCAAqC;YAChD,CAAC,CAAC,KAAK,IAAI,wCAAwC,CAAC;QAEtD,OAAO;;;EAGT,OAAO;yCACgC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM;;;yBAGxC,MAAM;;;;;;EAM7B,eAAe;;;;;;;;;;;CAWhB,CAAC;IACA,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,UAAU,CAAC;IACrD,MAAM,YAAY,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IAElD,MAAM,MAAM,GAAG,OAAO;QACpB,CAAC,CAAC,kBAAkB,IAAI,gBAAgB;QACxC,CAAC,CAAC,kBAAkB,IAAI,YAAY,CAAC;IAEvC,MAAM,OAAO,GAAG,OAAO;QACrB,CAAC,CAAC,KAAK,IAAI,4BAA4B;QACvC,CAAC,CAAC,KAAK,IAAI,2BAA2B,CAAC;IAEzC,OAAO;;;EAGP,OAAO;yCACgC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM;;;UAG5D,MAAM;EACd,eAAe;;;;0BAIS,WAAW;;;;;6BAKR,YAAY;yBAChB,WAAW;;;;;;;uCAOG,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM;;;;;;;;;;;;;;8CActB,IAAI;;;;;;CAMjD,CAAC;AACF,CAAC;AAED,SAAS,eAAe,CAAC,OAAe;IACtC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACrC,OAAO,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC7C,CAAC","sourcesContent":["/**\n * Bash runner script generation.\n * The runner checks Node.js, loads .env, and runs the bundle.\n */\n\nimport { type FrontmcpExecConfig } from './config';\n\n/**\n * Defense-in-depth: scrub anything outside `[a-zA-Z0-9._+-]` from values that\n * the runner / installer scripts interpolate into bash. The user owns\n * `frontmcp.config`, so a malicious value would be self-inflicted, but the\n * generated scripts are committed into repos and downloaded by end-users —\n * so we keep them safe to copy/paste regardless of upstream config hygiene.\n */\nexport function sanitizeShellLiteral(value: string): string {\n // `-` placed last in the character class is literal, so no escape needed.\n return value.replace(/[^A-Za-z0-9._+-]/g, '_');\n}\n\nexport function generateRunnerScript(config: FrontmcpExecConfig, cliMode?: boolean, seaMode?: boolean): string {\n const name = config.name;\n const version = sanitizeShellLiteral(config.version || '0.0.0');\n\n // #377 — `--target node` runner used to silently exec the bundle for any\n // flag, so `./frontegg-bin --help` quietly booted the HTTP server. Intercept\n // help/version here so server-mode runners behave like a normal CLI for\n // those flags. CLI-mode runners pass everything through to the bundle's\n // own commander parser.\n const helpInterceptor = !cliMode\n ? `\n# Intercept standard CLI flags before booting the long-running server.\ncase \"\\${1:-}\" in\n -h|--help)\n cat <<EOF\n${name} v${version} — FrontMCP server\n\nThis binary starts a long-running MCP HTTP server, or an MCP stdio server with --stdio.\n\nUsage:\n ${name} Start the HTTP server\n ${name} --stdio Serve over stdio (stdin/stdout JSON-RPC); binds no TCP port\n ${name} --help Show this help\n ${name} --version Show version\n ${name} --print-manifest Print the deployment manifest as JSON\n\nConfigure via environment variables, .env, or frontmcp.config.\n\nUse --stdio for local MCP clients (Claude Desktop, Cursor). Example config:\n { \"command\": \"${name}\", \"args\": [\"--stdio\"] }\n\nFor a CLI-style binary that exposes tools/resources/prompts as subcommands,\nbuild with: frontmcp build --target cli\nEOF\n exit 0\n ;;\n --version)\n echo \"${name} ${version}\"\n exit 0\n ;;\n --print-manifest)\n cat \"\\${SCRIPT_DIR}/${name}.manifest.json\"\n exit 0\n ;;\n --stdio)\n # Serve over stdio (stdin/stdout JSON-RPC) instead of starting the HTTP\n # server. FRONTMCP_STDIO=1 makes the @FrontMcp decorator connect the stdio\n # transport and bind no TCP port (#448, #451); logs go to stderr and\n # ~/.frontmcp/logs. Drop the flag and fall through to the exec below.\n export FRONTMCP_STDIO=1\n shift\n ;;\n --*)\n echo \"Error: unsupported flag '\\${1}' on the server runner.\"\n echo \"This binary is a long-running HTTP server; flag-style invocation is reserved (except --stdio).\" >&2\n echo \"Run with no args to start, --stdio to serve over stdio, or build with --target cli for a CLI binary.\" >&2\n exit 2\n ;;\nesac\n`\n : '';\n\n // SEA mode: binary is self-contained, no Node.js required\n if (seaMode) {\n const binary = cliMode ? `${name}-cli-bin` : `${name}-bin`;\n const comment = cliMode\n ? `# ${name} — FrontMCP CLI (single executable)`\n : `# ${name} — FrontMCP Server (single executable)`;\n\n return `#!/usr/bin/env bash\nset -euo pipefail\n\n${comment}\n# Generated by frontmcp build --target ${cliMode ? 'cli' : 'node'}\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"\\${BASH_SOURCE[0]}\")\" && pwd)\"\nBINARY=\"\\${SCRIPT_DIR}/${binary}\"\n\nif [ ! -f \"\\${BINARY}\" ]; then\n echo \"Error: Binary not found at \\${BINARY}\"\n exit 1\nfi\n${helpInterceptor}\n# Load .env if present\nENV_FILE=\"\\${SCRIPT_DIR}/.env\"\nif [ -f \"\\${ENV_FILE}\" ]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ENV_FILE}\"\n set +a\nfi\n\nexec \"\\${BINARY}\" \"$@\"\n`;\n }\n\n const nodeVersion = config.nodeVersion || '>=22.0.0';\n const minNodeMajor = extractMinMajor(nodeVersion);\n\n const bundle = cliMode\n ? `\\${SCRIPT_DIR}/${name}-cli.bundle.js`\n : `\\${SCRIPT_DIR}/${name}.bundle.js`;\n\n const comment = cliMode\n ? `# ${name} — FrontMCP CLI Executable`\n : `# ${name} — FrontMCP Server Runner`;\n\n return `#!/usr/bin/env bash\nset -euo pipefail\n\n${comment}\n# Generated by frontmcp build --target ${cliMode ? 'cli --js' : 'node'}\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"\\${BASH_SOURCE[0]}\")\" && pwd)\"\nBUNDLE=\"${bundle}\"\n${helpInterceptor}\n# Check Node.js\nif ! command -v node &> /dev/null; then\n echo \"Error: Node.js is required but not installed.\"\n echo \"Install Node.js ${nodeVersion}: https://nodejs.org\"\n exit 1\nfi\n\nNODE_MAJOR=$(node -e \"console.log(process.versions.node.split('.')[0])\")\nif [ \"\\${NODE_MAJOR}\" -lt \"${minNodeMajor}\" ]; then\n echo \"Error: Node.js ${nodeVersion} required, found v$(node -v)\"\n exit 1\nfi\n\n# Check bundle exists\nif [ ! -f \"\\${BUNDLE}\" ]; then\n echo \"Error: Bundle not found at \\${BUNDLE}\"\n echo \"Run 'frontmcp build --target ${cliMode ? 'cli --js' : 'node'}' to create it.\"\n exit 1\nfi\n\n# Load .env if present\nENV_FILE=\"\\${SCRIPT_DIR}/.env\"\nif [ -f \"\\${ENV_FILE}\" ]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ENV_FILE}\"\n set +a\nfi\n\n# Enable Node.js compile cache for faster startup on warm runs\nCOMPILE_CACHE_DIR=\"\\${HOME}/.cache/frontmcp/${name}\"\nmkdir -p \"\\${COMPILE_CACHE_DIR}\" 2>/dev/null || true\nexport NODE_COMPILE_CACHE=\"\\${COMPILE_CACHE_DIR}\"\n\n# Run\nexec node \"\\${BUNDLE}\" \"$@\"\n`;\n}\n\nfunction extractMinMajor(version: string): number {\n const match = version.match(/(\\d+)/);\n return match ? parseInt(match[1], 10) : 22;\n}\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child supervisor for the dev bridge (issue #399).
|
|
3
|
+
*
|
|
4
|
+
* Owns the user-code subprocess. Spawns it, watches for the ready
|
|
5
|
+
* sentinel (or a TCP probe in HTTP mode), restarts it on watcher events,
|
|
6
|
+
* and surfaces lifecycle events to the state machine.
|
|
7
|
+
*
|
|
8
|
+
* Two modes:
|
|
9
|
+
*
|
|
10
|
+
* - **HTTP mode (default)**: `npx -y tsx --conditions node <entry>` with
|
|
11
|
+
* `stdio: 'pipe'`. The child boots a normal FrontMCP HTTP listener on
|
|
12
|
+
* `FRONTMCP_DEV_PORT`. The supervisor TCP-probes the port to confirm
|
|
13
|
+
* readiness, OR greps the child's stderr for the `__FRONTMCP_BOOTSTRAP_COMPLETE__`
|
|
14
|
+
* sentinel when `FRONTMCP_DEV_BOOTSTRAP_SENTINEL=1` is set.
|
|
15
|
+
*
|
|
16
|
+
* - **Pipe mode (`--serve`)**: `node --import tsx <entry>` with
|
|
17
|
+
* `stdio: ['pipe', 'pipe', 'pipe', 'ipc']`. `FRONTMCP_DEV_STDIO_FD=3`
|
|
18
|
+
* tells the SDK to point `runStdio` at the IPC pipe. Readiness =
|
|
19
|
+
* first `message` over the IPC channel.
|
|
20
|
+
*/
|
|
21
|
+
import { type ChildProcess } from 'node:child_process';
|
|
22
|
+
import type { BridgeLogger } from './log';
|
|
23
|
+
export type SupervisorMode = 'http' | 'pipe';
|
|
24
|
+
export interface ChildSupervisorOptions {
|
|
25
|
+
mode: SupervisorMode;
|
|
26
|
+
entry: string;
|
|
27
|
+
log: BridgeLogger;
|
|
28
|
+
/** Pinned session id passed to the child for HTTP-mode session continuity. */
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
/** Port for HTTP mode. Ignored in pipe mode. */
|
|
31
|
+
port?: number;
|
|
32
|
+
/** Called once the child is ready to accept traffic. */
|
|
33
|
+
onReady: (child: ChildProcess) => void | Promise<void>;
|
|
34
|
+
/** Called when the child exits (expected or otherwise). */
|
|
35
|
+
onExit: (reason: string) => void | Promise<void>;
|
|
36
|
+
/** Max time to wait for a child to become ready before giving up. */
|
|
37
|
+
readyTimeoutMs?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface ChildSupervisor {
|
|
40
|
+
start(): Promise<void>;
|
|
41
|
+
/** Kill the current child, spawn a replacement, wait for ready. */
|
|
42
|
+
restart(): Promise<void>;
|
|
43
|
+
/** Final shutdown. */
|
|
44
|
+
stop(): Promise<void>;
|
|
45
|
+
/** Current child handle (undefined when no child is running). */
|
|
46
|
+
current(): ChildProcess | undefined;
|
|
47
|
+
}
|
|
48
|
+
export declare function createChildSupervisor(options: ChildSupervisorOptions): ChildSupervisor;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Child supervisor for the dev bridge (issue #399).
|
|
4
|
+
*
|
|
5
|
+
* Owns the user-code subprocess. Spawns it, watches for the ready
|
|
6
|
+
* sentinel (or a TCP probe in HTTP mode), restarts it on watcher events,
|
|
7
|
+
* and surfaces lifecycle events to the state machine.
|
|
8
|
+
*
|
|
9
|
+
* Two modes:
|
|
10
|
+
*
|
|
11
|
+
* - **HTTP mode (default)**: `npx -y tsx --conditions node <entry>` with
|
|
12
|
+
* `stdio: 'pipe'`. The child boots a normal FrontMCP HTTP listener on
|
|
13
|
+
* `FRONTMCP_DEV_PORT`. The supervisor TCP-probes the port to confirm
|
|
14
|
+
* readiness, OR greps the child's stderr for the `__FRONTMCP_BOOTSTRAP_COMPLETE__`
|
|
15
|
+
* sentinel when `FRONTMCP_DEV_BOOTSTRAP_SENTINEL=1` is set.
|
|
16
|
+
*
|
|
17
|
+
* - **Pipe mode (`--serve`)**: `node --import tsx <entry>` with
|
|
18
|
+
* `stdio: ['pipe', 'pipe', 'pipe', 'ipc']`. `FRONTMCP_DEV_STDIO_FD=3`
|
|
19
|
+
* tells the SDK to point `runStdio` at the IPC pipe. Readiness =
|
|
20
|
+
* first `message` over the IPC channel.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.createChildSupervisor = createChildSupervisor;
|
|
24
|
+
const tslib_1 = require("tslib");
|
|
25
|
+
const node_child_process_1 = require("node:child_process");
|
|
26
|
+
const net = tslib_1.__importStar(require("node:net"));
|
|
27
|
+
const READY_SENTINEL = '__FRONTMCP_BOOTSTRAP_COMPLETE__';
|
|
28
|
+
function createChildSupervisor(options) {
|
|
29
|
+
const { mode, entry, log, sessionId, port, onReady, onExit, readyTimeoutMs = 30_000 } = options;
|
|
30
|
+
let current;
|
|
31
|
+
let killSignaled = false;
|
|
32
|
+
function buildEnv() {
|
|
33
|
+
const env = { ...process.env };
|
|
34
|
+
// The stderr-bootstrap sentinel is an HTTP-mode signal only: in pipe
|
|
35
|
+
// mode readiness MUST be the first IPC message from the child so we
|
|
36
|
+
// know the FD-3 channel is wired up. Enabling the sentinel in pipe
|
|
37
|
+
// mode lets probeReady() resolve before any IPC arrives and races
|
|
38
|
+
// the first forwarded request.
|
|
39
|
+
if (mode === 'http')
|
|
40
|
+
env['FRONTMCP_DEV_BOOTSTRAP_SENTINEL'] = '1';
|
|
41
|
+
if (sessionId)
|
|
42
|
+
env['FRONTMCP_DEV_FORCE_SESSION_ID'] = sessionId;
|
|
43
|
+
if (mode === 'http' && port)
|
|
44
|
+
env['FRONTMCP_DEV_PORT'] = String(port);
|
|
45
|
+
if (mode === 'pipe')
|
|
46
|
+
env['FRONTMCP_DEV_STDIO_FD'] = '3';
|
|
47
|
+
return env;
|
|
48
|
+
}
|
|
49
|
+
function spawnChild() {
|
|
50
|
+
const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
51
|
+
if (mode === 'http') {
|
|
52
|
+
// tsx as a loader; bridge owns the watcher (no --watch here).
|
|
53
|
+
return (0, node_child_process_1.spawn)(npxCmd, ['-y', 'tsx', '--conditions', 'node', entry], {
|
|
54
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
55
|
+
env: buildEnv(),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// Pipe mode: pair an IPC channel as FD 3 so the child can read/write
|
|
59
|
+
// JSON frames there. Node sets up the IPC machinery automatically when
|
|
60
|
+
// 'ipc' is the 4th stdio entry — the resulting FD lands at 3.
|
|
61
|
+
return (0, node_child_process_1.spawn)(npxCmd, ['-y', 'tsx', '--conditions', 'node', entry], {
|
|
62
|
+
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
63
|
+
env: buildEnv(),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function probeReady(child) {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
let resolved = false;
|
|
69
|
+
const cleanup = () => {
|
|
70
|
+
clearTimeout(deadlineTimer);
|
|
71
|
+
clearInterval(tcpProbeTimer);
|
|
72
|
+
child.stderr?.off('data', onStderr);
|
|
73
|
+
child.off('message', onMessage);
|
|
74
|
+
child.off('exit', onExitDuringBoot);
|
|
75
|
+
};
|
|
76
|
+
const deadlineTimer = setTimeout(() => {
|
|
77
|
+
if (resolved)
|
|
78
|
+
return;
|
|
79
|
+
resolved = true;
|
|
80
|
+
cleanup();
|
|
81
|
+
reject(new Error(`child did not become ready within ${readyTimeoutMs}ms`));
|
|
82
|
+
}, readyTimeoutMs).unref();
|
|
83
|
+
const onStderr = (chunk) => {
|
|
84
|
+
// Sentinel-on-stderr is only meaningful in HTTP mode (buildEnv
|
|
85
|
+
// sets `FRONTMCP_DEV_BOOTSTRAP_SENTINEL=1` only there). In pipe
|
|
86
|
+
// mode readiness comes from the first IPC message — ignore any
|
|
87
|
+
// stderr signal so we don't race the IPC handshake.
|
|
88
|
+
if (mode !== 'http')
|
|
89
|
+
return;
|
|
90
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf-8');
|
|
91
|
+
if (text.includes(READY_SENTINEL)) {
|
|
92
|
+
if (resolved)
|
|
93
|
+
return;
|
|
94
|
+
resolved = true;
|
|
95
|
+
cleanup();
|
|
96
|
+
resolve();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
child.stderr?.on('data', onStderr);
|
|
100
|
+
const onMessage = () => {
|
|
101
|
+
if (mode !== 'pipe' || resolved)
|
|
102
|
+
return;
|
|
103
|
+
// First IPC message from the child counts as ready in pipe mode.
|
|
104
|
+
resolved = true;
|
|
105
|
+
cleanup();
|
|
106
|
+
resolve();
|
|
107
|
+
};
|
|
108
|
+
if (mode === 'pipe')
|
|
109
|
+
child.on('message', onMessage);
|
|
110
|
+
const onExitDuringBoot = (code, signal) => {
|
|
111
|
+
if (resolved)
|
|
112
|
+
return;
|
|
113
|
+
resolved = true;
|
|
114
|
+
cleanup();
|
|
115
|
+
reject(new Error(`child exited during boot: code=${code} signal=${signal ?? 'null'}`));
|
|
116
|
+
};
|
|
117
|
+
child.once('exit', onExitDuringBoot);
|
|
118
|
+
// HTTP mode fallback: TCP probe in parallel with the sentinel scan.
|
|
119
|
+
const tcpProbeTimer = mode === 'http' && port
|
|
120
|
+
? setInterval(() => {
|
|
121
|
+
if (resolved)
|
|
122
|
+
return;
|
|
123
|
+
const sock = net.createConnection({ host: '127.0.0.1', port }, () => {
|
|
124
|
+
sock.end();
|
|
125
|
+
if (resolved)
|
|
126
|
+
return;
|
|
127
|
+
resolved = true;
|
|
128
|
+
cleanup();
|
|
129
|
+
resolve();
|
|
130
|
+
});
|
|
131
|
+
sock.once('error', () => sock.destroy());
|
|
132
|
+
sock.setTimeout(500, () => sock.destroy());
|
|
133
|
+
}, 250).unref()
|
|
134
|
+
: setInterval(() => undefined, 60_000).unref();
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
async function killCurrent() {
|
|
138
|
+
if (!current)
|
|
139
|
+
return;
|
|
140
|
+
killSignaled = true;
|
|
141
|
+
const dyingChild = current;
|
|
142
|
+
try {
|
|
143
|
+
dyingChild.kill('SIGTERM');
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// ignore
|
|
147
|
+
}
|
|
148
|
+
const forceKill = setTimeout(() => {
|
|
149
|
+
try {
|
|
150
|
+
dyingChild.kill('SIGKILL');
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// ignore
|
|
154
|
+
}
|
|
155
|
+
}, 2000).unref();
|
|
156
|
+
await new Promise((resolve) => {
|
|
157
|
+
if (dyingChild.exitCode !== null || dyingChild.signalCode !== null)
|
|
158
|
+
return resolve();
|
|
159
|
+
dyingChild.once('exit', () => resolve());
|
|
160
|
+
});
|
|
161
|
+
clearTimeout(forceKill);
|
|
162
|
+
killSignaled = false;
|
|
163
|
+
}
|
|
164
|
+
function wireExitHandler(child) {
|
|
165
|
+
child.once('exit', (code, signal) => {
|
|
166
|
+
const reason = killSignaled ? 'killed-for-restart' : `code=${code ?? 'null'} signal=${signal ?? 'null'}`;
|
|
167
|
+
log.warn('child-exited', { reason });
|
|
168
|
+
void onExit(reason);
|
|
169
|
+
});
|
|
170
|
+
// Forward child stderr to the log file (never to our stdout).
|
|
171
|
+
child.stderr?.on('data', (chunk) => {
|
|
172
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf-8');
|
|
173
|
+
for (const line of text.split('\n')) {
|
|
174
|
+
if (!line.trim())
|
|
175
|
+
continue;
|
|
176
|
+
if (line.includes(READY_SENTINEL))
|
|
177
|
+
continue;
|
|
178
|
+
log.info('child-stderr', { line: line.slice(0, 500) });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
current: () => current,
|
|
184
|
+
async start() {
|
|
185
|
+
log.info('child-spawn', { mode, entry, port: port ?? null });
|
|
186
|
+
const child = spawnChild();
|
|
187
|
+
current = child;
|
|
188
|
+
wireExitHandler(child);
|
|
189
|
+
// Clean up the spawned subprocess on any startup failure
|
|
190
|
+
// (probeReady timeout, onReady throw). Without this, a failed
|
|
191
|
+
// start would leave the child running and occupying the dev port
|
|
192
|
+
// or the IPC channel, breaking the next restart.
|
|
193
|
+
try {
|
|
194
|
+
await probeReady(child);
|
|
195
|
+
log.info('child-ready', { mode, pid: child.pid ?? null });
|
|
196
|
+
await onReady(child);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
await killCurrent();
|
|
200
|
+
current = undefined;
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
async restart() {
|
|
205
|
+
log.info('child-restart-start');
|
|
206
|
+
await killCurrent();
|
|
207
|
+
current = undefined;
|
|
208
|
+
const child = spawnChild();
|
|
209
|
+
current = child;
|
|
210
|
+
wireExitHandler(child);
|
|
211
|
+
try {
|
|
212
|
+
await probeReady(child);
|
|
213
|
+
log.info('child-restart-ready', { pid: child.pid ?? null });
|
|
214
|
+
await onReady(child);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
await killCurrent();
|
|
218
|
+
current = undefined;
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
async stop() {
|
|
223
|
+
await killCurrent();
|
|
224
|
+
current = undefined;
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=child-supervisor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"child-supervisor.js","sourceRoot":"","sources":["../../../../../src/commands/dev/bridge/child-supervisor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Child supervisor for the dev bridge (issue #399).\n *\n * Owns the user-code subprocess. Spawns it, watches for the ready\n * sentinel (or a TCP probe in HTTP mode), restarts it on watcher events,\n * and surfaces lifecycle events to the state machine.\n *\n * Two modes:\n *\n * - **HTTP mode (default)**: `npx -y tsx --conditions node <entry>` with\n * `stdio: 'pipe'`. The child boots a normal FrontMCP HTTP listener on\n * `FRONTMCP_DEV_PORT`. The supervisor TCP-probes the port to confirm\n * readiness, OR greps the child's stderr for the `__FRONTMCP_BOOTSTRAP_COMPLETE__`\n * sentinel when `FRONTMCP_DEV_BOOTSTRAP_SENTINEL=1` is set.\n *\n * - **Pipe mode (`--serve`)**: `node --import tsx <entry>` with\n * `stdio: ['pipe', 'pipe', 'pipe', 'ipc']`. `FRONTMCP_DEV_STDIO_FD=3`\n * tells the SDK to point `runStdio` at the IPC pipe. Readiness =\n * first `message` over the IPC channel.\n */\n\nimport { spawn, type ChildProcess } from 'node:child_process';\nimport * as net from 'node:net';\n\nimport type { BridgeLogger } from './log';\n\nexport type SupervisorMode = 'http' | 'pipe';\n\nexport interface ChildSupervisorOptions {\n mode: SupervisorMode;\n entry: string;\n log: BridgeLogger;\n /** Pinned session id passed to the child for HTTP-mode session continuity. */\n sessionId?: string;\n /** Port for HTTP mode. Ignored in pipe mode. */\n port?: number;\n /** Called once the child is ready to accept traffic. */\n onReady: (child: ChildProcess) => void | Promise<void>;\n /** Called when the child exits (expected or otherwise). */\n onExit: (reason: string) => void | Promise<void>;\n /** Max time to wait for a child to become ready before giving up. */\n readyTimeoutMs?: number;\n}\n\nexport interface ChildSupervisor {\n start(): Promise<void>;\n /** Kill the current child, spawn a replacement, wait for ready. */\n restart(): Promise<void>;\n /** Final shutdown. */\n stop(): Promise<void>;\n /** Current child handle (undefined when no child is running). */\n current(): ChildProcess | undefined;\n}\n\nconst READY_SENTINEL = '__FRONTMCP_BOOTSTRAP_COMPLETE__';\n\nexport function createChildSupervisor(options: ChildSupervisorOptions): ChildSupervisor {\n const { mode, entry, log, sessionId, port, onReady, onExit, readyTimeoutMs = 30_000 } = options;\n\n let current: ChildProcess | undefined;\n let killSignaled = false;\n\n function buildEnv(): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = { ...process.env };\n // The stderr-bootstrap sentinel is an HTTP-mode signal only: in pipe\n // mode readiness MUST be the first IPC message from the child so we\n // know the FD-3 channel is wired up. Enabling the sentinel in pipe\n // mode lets probeReady() resolve before any IPC arrives and races\n // the first forwarded request.\n if (mode === 'http') env['FRONTMCP_DEV_BOOTSTRAP_SENTINEL'] = '1';\n if (sessionId) env['FRONTMCP_DEV_FORCE_SESSION_ID'] = sessionId;\n if (mode === 'http' && port) env['FRONTMCP_DEV_PORT'] = String(port);\n if (mode === 'pipe') env['FRONTMCP_DEV_STDIO_FD'] = '3';\n return env;\n }\n\n function spawnChild(): ChildProcess {\n const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';\n if (mode === 'http') {\n // tsx as a loader; bridge owns the watcher (no --watch here).\n return spawn(npxCmd, ['-y', 'tsx', '--conditions', 'node', entry], {\n stdio: ['ignore', 'pipe', 'pipe'],\n env: buildEnv(),\n });\n }\n // Pipe mode: pair an IPC channel as FD 3 so the child can read/write\n // JSON frames there. Node sets up the IPC machinery automatically when\n // 'ipc' is the 4th stdio entry — the resulting FD lands at 3.\n return spawn(npxCmd, ['-y', 'tsx', '--conditions', 'node', entry], {\n stdio: ['ignore', 'pipe', 'pipe', 'ipc'],\n env: buildEnv(),\n });\n }\n\n async function probeReady(child: ChildProcess): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n let resolved = false;\n const cleanup = (): void => {\n clearTimeout(deadlineTimer);\n clearInterval(tcpProbeTimer);\n child.stderr?.off('data', onStderr);\n child.off('message', onMessage);\n child.off('exit', onExitDuringBoot);\n };\n\n const deadlineTimer = setTimeout(() => {\n if (resolved) return;\n resolved = true;\n cleanup();\n reject(new Error(`child did not become ready within ${readyTimeoutMs}ms`));\n }, readyTimeoutMs).unref();\n\n const onStderr = (chunk: Buffer | string): void => {\n // Sentinel-on-stderr is only meaningful in HTTP mode (buildEnv\n // sets `FRONTMCP_DEV_BOOTSTRAP_SENTINEL=1` only there). In pipe\n // mode readiness comes from the first IPC message — ignore any\n // stderr signal so we don't race the IPC handshake.\n if (mode !== 'http') return;\n const text = typeof chunk === 'string' ? chunk : chunk.toString('utf-8');\n if (text.includes(READY_SENTINEL)) {\n if (resolved) return;\n resolved = true;\n cleanup();\n resolve();\n }\n };\n child.stderr?.on('data', onStderr);\n\n const onMessage = (): void => {\n if (mode !== 'pipe' || resolved) return;\n // First IPC message from the child counts as ready in pipe mode.\n resolved = true;\n cleanup();\n resolve();\n };\n if (mode === 'pipe') child.on('message', onMessage);\n\n const onExitDuringBoot = (code: number | null, signal: NodeJS.Signals | null): void => {\n if (resolved) return;\n resolved = true;\n cleanup();\n reject(new Error(`child exited during boot: code=${code} signal=${signal ?? 'null'}`));\n };\n child.once('exit', onExitDuringBoot);\n\n // HTTP mode fallback: TCP probe in parallel with the sentinel scan.\n const tcpProbeTimer =\n mode === 'http' && port\n ? setInterval(() => {\n if (resolved) return;\n const sock = net.createConnection({ host: '127.0.0.1', port }, () => {\n sock.end();\n if (resolved) return;\n resolved = true;\n cleanup();\n resolve();\n });\n sock.once('error', () => sock.destroy());\n sock.setTimeout(500, () => sock.destroy());\n }, 250).unref()\n : setInterval(() => undefined, 60_000).unref();\n });\n }\n\n async function killCurrent(): Promise<void> {\n if (!current) return;\n killSignaled = true;\n const dyingChild = current;\n try {\n dyingChild.kill('SIGTERM');\n } catch {\n // ignore\n }\n const forceKill = setTimeout(() => {\n try {\n dyingChild.kill('SIGKILL');\n } catch {\n // ignore\n }\n }, 2000).unref();\n await new Promise<void>((resolve) => {\n if (dyingChild.exitCode !== null || dyingChild.signalCode !== null) return resolve();\n dyingChild.once('exit', () => resolve());\n });\n clearTimeout(forceKill);\n killSignaled = false;\n }\n\n function wireExitHandler(child: ChildProcess): void {\n child.once('exit', (code, signal) => {\n const reason = killSignaled ? 'killed-for-restart' : `code=${code ?? 'null'} signal=${signal ?? 'null'}`;\n log.warn('child-exited', { reason });\n void onExit(reason);\n });\n // Forward child stderr to the log file (never to our stdout).\n child.stderr?.on('data', (chunk: Buffer | string) => {\n const text = typeof chunk === 'string' ? chunk : chunk.toString('utf-8');\n for (const line of text.split('\\n')) {\n if (!line.trim()) continue;\n if (line.includes(READY_SENTINEL)) continue;\n log.info('child-stderr', { line: line.slice(0, 500) });\n }\n });\n }\n\n return {\n current: () => current,\n async start() {\n log.info('child-spawn', { mode, entry, port: port ?? null });\n const child = spawnChild();\n current = child;\n wireExitHandler(child);\n // Clean up the spawned subprocess on any startup failure\n // (probeReady timeout, onReady throw). Without this, a failed\n // start would leave the child running and occupying the dev port\n // or the IPC channel, breaking the next restart.\n try {\n await probeReady(child);\n log.info('child-ready', { mode, pid: child.pid ?? null });\n await onReady(child);\n } catch (err) {\n await killCurrent();\n current = undefined;\n throw err;\n }\n },\n async restart() {\n log.info('child-restart-start');\n await killCurrent();\n current = undefined;\n const child = spawnChild();\n current = child;\n wireExitHandler(child);\n try {\n await probeReady(child);\n log.info('child-restart-ready', { pid: child.pid ?? null });\n await onReady(child);\n } catch (err) {\n await killCurrent();\n current = undefined;\n throw err;\n }\n },\n async stop() {\n await killCurrent();\n current = undefined;\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC error codes emitted by the dev bridge (issue #399).
|
|
3
|
+
*
|
|
4
|
+
* Reserved in the implementation-defined `-32099 .. -32000` range so they
|
|
5
|
+
* never collide with the JSON-RPC 2.0 reserved range. Clients (Claude
|
|
6
|
+
* Code, etc.) receive these in `error.code` and can render structured
|
|
7
|
+
* feedback instead of sitting on an indefinite `Calling…` spinner.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEV_SERVER_UNREACHABLE = -32099;
|
|
10
|
+
export declare const DEV_BUFFER_FULL = -32098;
|
|
11
|
+
export declare const DEV_RELOAD_DEADLINE = -32097;
|
|
12
|
+
/** Human-readable label for each code. Used in the `error.message` field. */
|
|
13
|
+
export declare const DEV_ERROR_MESSAGE: Record<number, string>;
|
|
14
|
+
/** Build a JSON-RPC error response for a given inbound request id. */
|
|
15
|
+
export declare function makeDevError(id: string | number | null, code: number, data?: Record<string, unknown>): {
|
|
16
|
+
jsonrpc: '2.0';
|
|
17
|
+
id: string | number | null;
|
|
18
|
+
error: {
|
|
19
|
+
code: number;
|
|
20
|
+
message: string;
|
|
21
|
+
data?: Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEV_ERROR_MESSAGE = exports.DEV_RELOAD_DEADLINE = exports.DEV_BUFFER_FULL = exports.DEV_SERVER_UNREACHABLE = void 0;
|
|
4
|
+
exports.makeDevError = makeDevError;
|
|
5
|
+
/**
|
|
6
|
+
* JSON-RPC error codes emitted by the dev bridge (issue #399).
|
|
7
|
+
*
|
|
8
|
+
* Reserved in the implementation-defined `-32099 .. -32000` range so they
|
|
9
|
+
* never collide with the JSON-RPC 2.0 reserved range. Clients (Claude
|
|
10
|
+
* Code, etc.) receive these in `error.code` and can render structured
|
|
11
|
+
* feedback instead of sitting on an indefinite `Calling…` spinner.
|
|
12
|
+
*/
|
|
13
|
+
exports.DEV_SERVER_UNREACHABLE = -32099;
|
|
14
|
+
exports.DEV_BUFFER_FULL = -32098;
|
|
15
|
+
exports.DEV_RELOAD_DEADLINE = -32097;
|
|
16
|
+
/** Human-readable label for each code. Used in the `error.message` field. */
|
|
17
|
+
exports.DEV_ERROR_MESSAGE = {
|
|
18
|
+
[exports.DEV_SERVER_UNREACHABLE]: 'dev_server_unreachable',
|
|
19
|
+
[exports.DEV_BUFFER_FULL]: 'dev_buffer_full',
|
|
20
|
+
[exports.DEV_RELOAD_DEADLINE]: 'dev_reload_deadline',
|
|
21
|
+
};
|
|
22
|
+
/** Build a JSON-RPC error response for a given inbound request id. */
|
|
23
|
+
function makeDevError(id, code, data) {
|
|
24
|
+
return {
|
|
25
|
+
jsonrpc: '2.0',
|
|
26
|
+
id,
|
|
27
|
+
error: {
|
|
28
|
+
code,
|
|
29
|
+
message: exports.DEV_ERROR_MESSAGE[code] ?? 'dev_error',
|
|
30
|
+
...(data ? { data } : {}),
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../../../src/commands/dev/bridge/errors.ts"],"names":[],"mappings":";;;AAoBA,oCAkBC;AAtCD;;;;;;;GAOG;AACU,QAAA,sBAAsB,GAAG,CAAC,KAAK,CAAC;AAChC,QAAA,eAAe,GAAG,CAAC,KAAK,CAAC;AACzB,QAAA,mBAAmB,GAAG,CAAC,KAAK,CAAC;AAE1C,6EAA6E;AAChE,QAAA,iBAAiB,GAA2B;IACvD,CAAC,8BAAsB,CAAC,EAAE,wBAAwB;IAClD,CAAC,uBAAe,CAAC,EAAE,iBAAiB;IACpC,CAAC,2BAAmB,CAAC,EAAE,qBAAqB;CAC7C,CAAC;AAEF,sEAAsE;AACtE,SAAgB,YAAY,CAC1B,EAA0B,EAC1B,IAAY,EACZ,IAA8B;IAM9B,OAAO;QACL,OAAO,EAAE,KAAK;QACd,EAAE;QACF,KAAK,EAAE;YACL,IAAI;YACJ,OAAO,EAAE,yBAAiB,CAAC,IAAI,CAAC,IAAI,WAAW;YAC/C,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC1B;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * JSON-RPC error codes emitted by the dev bridge (issue #399).\n *\n * Reserved in the implementation-defined `-32099 .. -32000` range so they\n * never collide with the JSON-RPC 2.0 reserved range. Clients (Claude\n * Code, etc.) receive these in `error.code` and can render structured\n * feedback instead of sitting on an indefinite `Calling…` spinner.\n */\nexport const DEV_SERVER_UNREACHABLE = -32099;\nexport const DEV_BUFFER_FULL = -32098;\nexport const DEV_RELOAD_DEADLINE = -32097;\n\n/** Human-readable label for each code. Used in the `error.message` field. */\nexport const DEV_ERROR_MESSAGE: Record<number, string> = {\n [DEV_SERVER_UNREACHABLE]: 'dev_server_unreachable',\n [DEV_BUFFER_FULL]: 'dev_buffer_full',\n [DEV_RELOAD_DEADLINE]: 'dev_reload_deadline',\n};\n\n/** Build a JSON-RPC error response for a given inbound request id. */\nexport function makeDevError(\n id: string | number | null,\n code: number,\n data?: Record<string, unknown>,\n): {\n jsonrpc: '2.0';\n id: string | number | null;\n error: { code: number; message: string; data?: Record<string, unknown> };\n} {\n return {\n jsonrpc: '2.0',\n id,\n error: {\n code,\n message: DEV_ERROR_MESSAGE[code] ?? 'dev_error',\n ...(data ? { data } : {}),\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev stdio bridge entry point (issue #399).
|
|
3
|
+
*
|
|
4
|
+
* Wires the framer (stdio in/out), state machine (buffer + reload FSM),
|
|
5
|
+
* watcher (file-change source), child supervisor (user-code lifecycle),
|
|
6
|
+
* and upstream client (forwarding to the child) into a single
|
|
7
|
+
* long-lived process.
|
|
8
|
+
*
|
|
9
|
+
* Lifetime:
|
|
10
|
+
*
|
|
11
|
+
* 1. Parse options, resolve entry + log file path.
|
|
12
|
+
* 2. Pin a stable session id (uuid) so the same id survives child
|
|
13
|
+
* restarts.
|
|
14
|
+
* 3. Construct logger; open log file.
|
|
15
|
+
* 4. Construct state machine + framer + watcher + supervisor +
|
|
16
|
+
* upstream client (transport per `--serve`).
|
|
17
|
+
* 5. Spawn the first child, wait for ready, transition state to Ready.
|
|
18
|
+
* 6. Forward frames in both directions; watcher events trigger
|
|
19
|
+
* controlled restart.
|
|
20
|
+
* 7. SIGINT/SIGTERM → flush buffer with `dev_server_unreachable`,
|
|
21
|
+
* tear down child + watcher, exit cleanly.
|
|
22
|
+
*/
|
|
23
|
+
import type { ParsedArgs } from '../../../core/args';
|
|
24
|
+
import { type ChildSupervisor } from './child-supervisor';
|
|
25
|
+
import { type BridgeLogger } from './log';
|
|
26
|
+
import { type BridgeStateMachine } from './state-machine';
|
|
27
|
+
import { type StdioFramer } from './stdio-framer';
|
|
28
|
+
import { type UpstreamClient } from './upstream-client';
|
|
29
|
+
export declare function runDevBridge(opts: ParsedArgs): Promise<void>;
|
|
30
|
+
export { type BridgeLogger, type BridgeStateMachine, type ChildSupervisor, type StdioFramer, type UpstreamClient };
|