frontmcp 1.2.1 → 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.
- 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/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 +18 -1
- package/src/commands/dev/dev.js +134 -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 +44 -0
- 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
|
@@ -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
|