frontmcp 1.2.0 → 1.3.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.
Files changed (108) hide show
  1. package/package.json +4 -4
  2. package/src/commands/build/exec/bin-meta.d.ts +49 -0
  3. package/src/commands/build/exec/bin-meta.js +68 -0
  4. package/src/commands/build/exec/bin-meta.js.map +1 -0
  5. package/src/commands/build/exec/cli-runtime/generate-cli-entry.js +195 -3
  6. package/src/commands/build/exec/cli-runtime/generate-cli-entry.js.map +1 -1
  7. package/src/commands/build/exec/cli-runtime/plugin-emitter.d.ts +160 -0
  8. package/src/commands/build/exec/cli-runtime/plugin-emitter.js +512 -0
  9. package/src/commands/build/exec/cli-runtime/plugin-emitter.js.map +1 -0
  10. package/src/commands/build/exec/cli-runtime/schema-extractor.d.ts +13 -1
  11. package/src/commands/build/exec/cli-runtime/schema-extractor.js +29 -3
  12. package/src/commands/build/exec/cli-runtime/schema-extractor.js.map +1 -1
  13. package/src/commands/build/exec/cli-runtime/skill-md-compose.d.ts +25 -0
  14. package/src/commands/build/exec/cli-runtime/skill-md-compose.js +63 -0
  15. package/src/commands/build/exec/cli-runtime/skill-md-compose.js.map +1 -0
  16. package/src/commands/build/exec/index.js +26 -0
  17. package/src/commands/build/exec/index.js.map +1 -1
  18. package/src/commands/dev/bridge/child-supervisor.d.ts +48 -0
  19. package/src/commands/dev/bridge/child-supervisor.js +228 -0
  20. package/src/commands/dev/bridge/child-supervisor.js.map +1 -0
  21. package/src/commands/dev/bridge/errors.d.ts +23 -0
  22. package/src/commands/dev/bridge/errors.js +34 -0
  23. package/src/commands/dev/bridge/errors.js.map +1 -0
  24. package/src/commands/dev/bridge/index.d.ts +30 -0
  25. package/src/commands/dev/bridge/index.js +220 -0
  26. package/src/commands/dev/bridge/index.js.map +1 -0
  27. package/src/commands/dev/bridge/log.d.ts +29 -0
  28. package/src/commands/dev/bridge/log.js +82 -0
  29. package/src/commands/dev/bridge/log.js.map +1 -0
  30. package/src/commands/dev/bridge/state-machine.d.ts +56 -0
  31. package/src/commands/dev/bridge/state-machine.js +245 -0
  32. package/src/commands/dev/bridge/state-machine.js.map +1 -0
  33. package/src/commands/dev/bridge/stdio-framer.d.ts +47 -0
  34. package/src/commands/dev/bridge/stdio-framer.js +128 -0
  35. package/src/commands/dev/bridge/stdio-framer.js.map +1 -0
  36. package/src/commands/dev/bridge/upstream-client.d.ts +49 -0
  37. package/src/commands/dev/bridge/upstream-client.js +159 -0
  38. package/src/commands/dev/bridge/upstream-client.js.map +1 -0
  39. package/src/commands/dev/bridge/watcher.d.ts +30 -0
  40. package/src/commands/dev/bridge/watcher.js +87 -0
  41. package/src/commands/dev/bridge/watcher.js.map +1 -0
  42. package/src/commands/dev/dev.d.ts +18 -1
  43. package/src/commands/dev/dev.js +134 -14
  44. package/src/commands/dev/dev.js.map +1 -1
  45. package/src/commands/dev/inspector.d.ts +13 -1
  46. package/src/commands/dev/inspector.js +77 -3
  47. package/src/commands/dev/inspector.js.map +1 -1
  48. package/src/commands/dev/port.d.ts +23 -0
  49. package/src/commands/dev/port.js +87 -0
  50. package/src/commands/dev/port.js.map +1 -0
  51. package/src/commands/dev/register.d.ts +1 -1
  52. package/src/commands/dev/register.js +28 -4
  53. package/src/commands/dev/register.js.map +1 -1
  54. package/src/commands/dev/test.d.ts +26 -1
  55. package/src/commands/dev/test.js +181 -64
  56. package/src/commands/dev/test.js.map +1 -1
  57. package/src/commands/eject/mcp-client.d.ts +25 -0
  58. package/src/commands/eject/mcp-client.js +74 -0
  59. package/src/commands/eject/mcp-client.js.map +1 -0
  60. package/src/commands/eject/register.d.ts +9 -0
  61. package/src/commands/eject/register.js +56 -0
  62. package/src/commands/eject/register.js.map +1 -0
  63. package/src/commands/install/install-claude-plugin.d.ts +13 -0
  64. package/src/commands/install/install-claude-plugin.js +327 -0
  65. package/src/commands/install/install-claude-plugin.js.map +1 -0
  66. package/src/commands/install/register.d.ts +16 -0
  67. package/src/commands/install/register.js +70 -0
  68. package/src/commands/install/register.js.map +1 -0
  69. package/src/commands/scaffold/create.js +44 -0
  70. package/src/commands/scaffold/create.js.map +1 -1
  71. package/src/commands/skills/from-entry.d.ts +31 -0
  72. package/src/commands/skills/from-entry.js +68 -0
  73. package/src/commands/skills/from-entry.js.map +1 -0
  74. package/src/commands/skills/install.d.ts +12 -0
  75. package/src/commands/skills/install.js +173 -8
  76. package/src/commands/skills/install.js.map +1 -1
  77. package/src/commands/skills/register.js +7 -3
  78. package/src/commands/skills/register.js.map +1 -1
  79. package/src/config/frontmcp-config.loader.d.ts +28 -0
  80. package/src/config/frontmcp-config.loader.js +146 -67
  81. package/src/config/frontmcp-config.loader.js.map +1 -1
  82. package/src/config/frontmcp-config.resolve.d.ts +67 -0
  83. package/src/config/frontmcp-config.resolve.js +118 -0
  84. package/src/config/frontmcp-config.resolve.js.map +1 -0
  85. package/src/config/frontmcp-config.schema.d.ts +207 -0
  86. package/src/config/frontmcp-config.schema.js +217 -1
  87. package/src/config/frontmcp-config.schema.js.map +1 -1
  88. package/src/config/frontmcp-config.types.d.ts +133 -0
  89. package/src/config/frontmcp-config.types.js.map +1 -1
  90. package/src/config/index.d.ts +2 -1
  91. package/src/config/index.js +3 -1
  92. package/src/config/index.js.map +1 -1
  93. package/src/core/args.d.ts +13 -0
  94. package/src/core/args.js.map +1 -1
  95. package/src/core/bridge.js +39 -0
  96. package/src/core/bridge.js.map +1 -1
  97. package/src/core/cli.d.ts +0 -6
  98. package/src/core/cli.js +23 -3
  99. package/src/core/cli.js.map +1 -1
  100. package/src/core/help.d.ts +1 -1
  101. package/src/core/help.js +27 -6
  102. package/src/core/help.js.map +1 -1
  103. package/src/core/program.d.ts +1 -1
  104. package/src/core/program.js +56 -12
  105. package/src/core/program.js.map +1 -1
  106. package/src/core/project-commands.d.ts +44 -0
  107. package/src/core/project-commands.js +216 -0
  108. package/src/core/project-commands.js.map +1 -0
@@ -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 };
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+ /**
3
+ * Dev stdio bridge entry point (issue #399).
4
+ *
5
+ * Wires the framer (stdio in/out), state machine (buffer + reload FSM),
6
+ * watcher (file-change source), child supervisor (user-code lifecycle),
7
+ * and upstream client (forwarding to the child) into a single
8
+ * long-lived process.
9
+ *
10
+ * Lifetime:
11
+ *
12
+ * 1. Parse options, resolve entry + log file path.
13
+ * 2. Pin a stable session id (uuid) so the same id survives child
14
+ * restarts.
15
+ * 3. Construct logger; open log file.
16
+ * 4. Construct state machine + framer + watcher + supervisor +
17
+ * upstream client (transport per `--serve`).
18
+ * 5. Spawn the first child, wait for ready, transition state to Ready.
19
+ * 6. Forward frames in both directions; watcher events trigger
20
+ * controlled restart.
21
+ * 7. SIGINT/SIGTERM → flush buffer with `dev_server_unreachable`,
22
+ * tear down child + watcher, exit cleanly.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.runDevBridge = runDevBridge;
26
+ const tslib_1 = require("tslib");
27
+ const path = tslib_1.__importStar(require("node:path"));
28
+ const utils_1 = require("@frontmcp/utils");
29
+ const fs_1 = require("../../../shared/fs");
30
+ const child_supervisor_1 = require("./child-supervisor");
31
+ const log_1 = require("./log");
32
+ const state_machine_1 = require("./state-machine");
33
+ const stdio_framer_1 = require("./stdio-framer");
34
+ const upstream_client_1 = require("./upstream-client");
35
+ const watcher_1 = require("./watcher");
36
+ const DEFAULT_PORT = 3000;
37
+ const DEFAULT_LOG_FILE = path.join('.frontmcp', 'dev.log');
38
+ function normalizeOptions(opts, entry) {
39
+ const mode = opts.serve ? 'pipe' : 'http';
40
+ const port = typeof opts.port === 'number' ? opts.port : DEFAULT_PORT;
41
+ const bufferSize = typeof opts.bufferSize === 'number' && opts.bufferSize > 0 ? opts.bufferSize : 8;
42
+ const reloadDeadlineMs = typeof opts.reloadDeadlineMs === 'number' && opts.reloadDeadlineMs > 0 ? opts.reloadDeadlineMs : 30_000;
43
+ const logFile = typeof opts.logFile === 'string' && opts.logFile.length > 0 ? opts.logFile : DEFAULT_LOG_FILE;
44
+ return { entry, mode, port, bufferSize, reloadDeadlineMs, logFile };
45
+ }
46
+ async function runDevBridge(opts) {
47
+ const cwd = process.cwd();
48
+ const entry = await (0, fs_1.resolveEntry)(cwd, opts.entry);
49
+ const runtime = normalizeOptions(opts, entry);
50
+ const log = await (0, log_1.createBridgeLogger)({ filePath: runtime.logFile });
51
+ log.info('bridge-start', {
52
+ entry: runtime.entry,
53
+ mode: runtime.mode,
54
+ port: runtime.port,
55
+ bufferSize: runtime.bufferSize,
56
+ reloadDeadlineMs: runtime.reloadDeadlineMs,
57
+ });
58
+ // Pinned session id — child reads from FRONTMCP_DEV_FORCE_SESSION_ID so
59
+ // session continuity works across restarts (memory or Redis store both OK).
60
+ const sessionId = (0, utils_1.randomUUID)();
61
+ // Only `upstream` is reassigned during runtime (on every child restart).
62
+ // The rest are constructed exactly once below and referenced through
63
+ // closures that fire after all bindings exist.
64
+ let upstream;
65
+ function buildUpstreamForChild(child) {
66
+ if (runtime.mode === 'http') {
67
+ return (0, upstream_client_1.createHttpUpstream)({
68
+ url: `http://127.0.0.1:${runtime.port}/`,
69
+ log,
70
+ sessionId,
71
+ onFrame: (frame) => fsm.relayUpstream(frame),
72
+ });
73
+ }
74
+ return (0, upstream_client_1.createPipeUpstream)({
75
+ child,
76
+ log,
77
+ sessionId,
78
+ onFrame: (frame) => fsm.relayUpstream(frame),
79
+ });
80
+ }
81
+ // ─── construct framer + FSM. Closures bind to each other by reference,
82
+ // so referencing `fsm`/`framer` inside a callback executed at runtime
83
+ // is safe even though `framer` is declared first textually. ───
84
+ const framer = (0, stdio_framer_1.createStdioFramer)({
85
+ input: process.stdin,
86
+ output: process.stdout,
87
+ log,
88
+ onFrame: (frame) => fsm.enqueue(frame),
89
+ });
90
+ const fsm = (0, state_machine_1.createBridgeStateMachine)({
91
+ log,
92
+ bufferSize: runtime.bufferSize,
93
+ reloadDeadlineMs: runtime.reloadDeadlineMs,
94
+ respond: (frame) => framer.write(frame),
95
+ forward: async (frame) => {
96
+ if (!upstream) {
97
+ log.warn('forward-without-upstream', { method: frame.method });
98
+ return;
99
+ }
100
+ await upstream.send(frame);
101
+ },
102
+ });
103
+ framer.start();
104
+ // ─── supervisor → boots first child, then attaches upstream ───
105
+ const supervisor = (0, child_supervisor_1.createChildSupervisor)({
106
+ mode: runtime.mode,
107
+ entry: runtime.entry,
108
+ log,
109
+ sessionId,
110
+ port: runtime.mode === 'http' ? runtime.port : undefined,
111
+ onReady: async (child) => {
112
+ // Close any previous upstream (reload path). A rejection here MUST
113
+ // NOT block re-binding — the child is up and ready, and leaving the
114
+ // bridge without an upstream would strand every subsequent RPC.
115
+ try {
116
+ await upstream?.close();
117
+ }
118
+ catch (err) {
119
+ log.error('upstream-stop-error', {
120
+ error: err instanceof Error ? err.message : String(err),
121
+ });
122
+ }
123
+ upstream = buildUpstreamForChild(child);
124
+ fsm.onChildReady();
125
+ },
126
+ onExit: (reason) => {
127
+ // Don't drop the close() promise — an in-flight HTTP request or SSE
128
+ // body read can reject (e.g. AbortError when we abort it ourselves),
129
+ // and an unhandled rejection here would crash the bridge on the
130
+ // next tick. Hand the rejection to the logger; the child is dead
131
+ // either way so we always proceed to onChildExit.
132
+ const closingUpstream = upstream;
133
+ upstream = undefined;
134
+ void closingUpstream?.close().catch((err) => {
135
+ log.error('upstream-stop-error', {
136
+ error: err instanceof Error ? err.message : String(err),
137
+ });
138
+ });
139
+ fsm.onChildExit(reason);
140
+ },
141
+ });
142
+ fsm.onBootStart();
143
+ try {
144
+ await supervisor.start();
145
+ }
146
+ catch (err) {
147
+ log.error('initial-boot-failed', { error: err.message });
148
+ fsm.onReloadDeadline();
149
+ // Stay running so the watcher can retry once the user fixes the source.
150
+ }
151
+ // ─── watcher → restart on file change ───
152
+ // Watch the project root (cwd), not just the entry's directory: shared
153
+ // helpers, `frontmcp.config.ts`, and `tsconfig.json` all live above
154
+ // `src/main.ts` and must trigger a reload too. The recursive watcher
155
+ // already debounces and filters via `shouldIgnore` so the wider scope
156
+ // doesn't generate spurious reloads.
157
+ const watcher = (0, watcher_1.createDevWatcher)({
158
+ rootDir: cwd,
159
+ log,
160
+ onChange: (trigger) => {
161
+ fsm.onWatcherEvent(trigger);
162
+ void (async () => {
163
+ // Defensive — `supervisor` is captured in this closure after the
164
+ // initial assignment above, but a runtime check keeps the
165
+ // non-null assertion off the call site.
166
+ if (!supervisor) {
167
+ log.error('restart-failed', { error: 'supervisor not initialised', trigger });
168
+ return;
169
+ }
170
+ try {
171
+ await supervisor.restart();
172
+ }
173
+ catch (err) {
174
+ log.error('restart-failed', { error: err.message });
175
+ }
176
+ })();
177
+ },
178
+ });
179
+ watcher.start();
180
+ // ─── teardown wiring ───
181
+ let stopping = false;
182
+ async function shutdown(signal) {
183
+ if (stopping)
184
+ return;
185
+ stopping = true;
186
+ log.info('bridge-stop', { signal });
187
+ try {
188
+ await fsm.stop();
189
+ }
190
+ catch (err) {
191
+ log.error('fsm-stop-error', { error: err.message });
192
+ }
193
+ try {
194
+ watcher.stop();
195
+ }
196
+ catch (err) {
197
+ log.error('watcher-stop-error', { error: err.message });
198
+ }
199
+ try {
200
+ await upstream?.close();
201
+ }
202
+ catch (err) {
203
+ log.error('upstream-stop-error', { error: err.message });
204
+ }
205
+ try {
206
+ await supervisor.stop();
207
+ }
208
+ catch (err) {
209
+ log.error('supervisor-stop-error', { error: err.message });
210
+ }
211
+ framer.stop();
212
+ await log.close();
213
+ process.exit(0);
214
+ }
215
+ process.once('SIGINT', () => void shutdown('SIGINT'));
216
+ process.once('SIGTERM', () => void shutdown('SIGTERM'));
217
+ // Bridge runs until SIGINT/SIGTERM — keep the event loop alive via the
218
+ // open stdin + watcher.
219
+ }
220
+ //# sourceMappingURL=index.js.map