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.
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,128 @@
1
+ "use strict";
2
+ /**
3
+ * Newline-delimited JSON-RPC framer for the dev bridge (issue #399).
4
+ *
5
+ * Reads `process.stdin` and yields complete JSON-RPC frames. Writes
6
+ * complete frames to `process.stdout` (atomic per `\n` boundary). Parser
7
+ * state is preserved across chunks so frames split arbitrarily on the
8
+ * wire still parse.
9
+ *
10
+ * MCP stdio framing per spec is newline-delimited JSON (`\n`-terminated
11
+ * UTF-8). We do NOT implement LSP-style `Content-Length` framing — it's
12
+ * not in the MCP spec and adding it would silently shadow real JSON
13
+ * bodies that happen to start with `C`.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.createStdioFramer = createStdioFramer;
17
+ const errors_1 = require("./errors");
18
+ /**
19
+ * Build a newline-delimited JSON framer bound to the supplied streams.
20
+ *
21
+ * Parse errors (malformed JSON between newlines) emit a `-32700` Parse
22
+ * error response back on the output stream and continue — a malformed
23
+ * frame must not kill the bridge.
24
+ */
25
+ function createStdioFramer(options) {
26
+ const { input, output, log, onFrame } = options;
27
+ let buffer = '';
28
+ // True while `output.write` is signalling backpressure. Read by `onData`
29
+ // to pause the inbound stream so we don't keep buffering frames when the
30
+ // outbound side can't keep up.
31
+ let paused = false;
32
+ let drainResolvers = [];
33
+ function flushBuffer() {
34
+ let nl;
35
+ while ((nl = buffer.indexOf('\n')) >= 0) {
36
+ const raw = buffer.slice(0, nl).replace(/\r$/, '');
37
+ buffer = buffer.slice(nl + 1);
38
+ if (raw.length === 0)
39
+ continue;
40
+ let parsed;
41
+ try {
42
+ parsed = JSON.parse(raw);
43
+ }
44
+ catch (err) {
45
+ log.warn('parse-error', { raw: raw.slice(0, 200), error: err.message });
46
+ // `makeDevError` returns a strict JSON-RPC error shape that
47
+ // structurally satisfies `JsonRpcFrame`; the dev bridge owns both
48
+ // sides of this constructor, so write() trusts the payload.
49
+ void write((0, errors_1.makeDevError)(null, -32700, { reason: 'parse_error' }));
50
+ continue;
51
+ }
52
+ if (!parsed || typeof parsed !== 'object') {
53
+ // Per JSON-RPC 2.0, every frame must be an object. A bare `42` or
54
+ // `"string"` line is just as invalid as garbage JSON — emit the
55
+ // same -32700 envelope so the client doesn't get inconsistent
56
+ // behaviour depending on whether the parser stage or the shape
57
+ // check rejected the input.
58
+ log.warn('parse-error', { raw: raw.slice(0, 200), reason: 'not_object' });
59
+ void write((0, errors_1.makeDevError)(null, -32700, { reason: 'not_object' }));
60
+ continue;
61
+ }
62
+ // Don't drop the onFrame promise. `onFrame` is user-supplied (the
63
+ // state machine in the production wiring) and can reject — if we
64
+ // void the rejection, Node bubbles it as an unhandledRejection and
65
+ // crashes the bridge. Funnel it through the logger instead.
66
+ void Promise.resolve(onFrame(parsed)).catch((err) => {
67
+ log.error('on-frame-error', {
68
+ error: err instanceof Error ? err.message : String(err),
69
+ });
70
+ });
71
+ }
72
+ }
73
+ function onData(chunk) {
74
+ buffer += typeof chunk === 'string' ? chunk : chunk.toString('utf-8');
75
+ flushBuffer();
76
+ }
77
+ function write(frame) {
78
+ return new Promise((resolve) => {
79
+ const line = JSON.stringify(frame) + '\n';
80
+ const ok = output.write(line);
81
+ if (ok)
82
+ return resolve();
83
+ // Backpressure: pause inbound parsing AND the input stream so we
84
+ // stop accumulating frames in `buffer` until drain. `input.pause()`
85
+ // is a no-op on streams that don't support flow-mode pausing.
86
+ if (!paused) {
87
+ paused = true;
88
+ input.pause?.();
89
+ }
90
+ drainResolvers.push(resolve);
91
+ });
92
+ }
93
+ function onDrain() {
94
+ if (paused) {
95
+ paused = false;
96
+ input.resume?.();
97
+ }
98
+ const resolvers = drainResolvers;
99
+ drainResolvers = [];
100
+ for (const r of resolvers)
101
+ r();
102
+ }
103
+ return {
104
+ start: () => {
105
+ input.setEncoding?.('utf-8');
106
+ input.on('data', onData);
107
+ output.on('drain', onDrain);
108
+ },
109
+ write,
110
+ stop: () => {
111
+ input.off('data', onData);
112
+ output.off('drain', onDrain);
113
+ // Settle any queued write() promises so callers blocked on
114
+ // backpressure don't hang past shutdown. Resume the input stream
115
+ // symmetrically (we may have paused it on backpressure) so the
116
+ // caller doesn't inherit a paused stdin.
117
+ if (paused) {
118
+ paused = false;
119
+ input.resume?.();
120
+ }
121
+ const resolvers = drainResolvers;
122
+ drainResolvers = [];
123
+ for (const r of resolvers)
124
+ r();
125
+ },
126
+ };
127
+ }
128
+ //# sourceMappingURL=stdio-framer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio-framer.js","sourceRoot":"","sources":["../../../../../src/commands/dev/bridge/stdio-framer.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;AAqCH,8CAsGC;AAvID,qCAAwC;AA0BxC;;;;;;GAMG;AACH,SAAgB,iBAAiB,CAAC,OAA2B;IAC3D,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IAChD,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,yEAAyE;IACzE,yEAAyE;IACzE,+BAA+B;IAC/B,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,cAAc,GAAsB,EAAE,CAAC;IAE3C,SAAS,WAAW;QAClB,IAAI,EAAU,CAAC;QACf,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YACxC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACnD,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;YAC9B,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YAC/B,IAAI,MAAe,CAAC;YACpB,IAAI,CAAC;gBACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC3B,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,KAAK,EAAG,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;gBACnF,4DAA4D;gBAC5D,kEAAkE;gBAClE,4DAA4D;gBAC5D,KAAK,KAAK,CAAC,IAAA,qBAAY,EAAC,IAAI,EAAE,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC;gBAClE,SAAS;YACX,CAAC;YACD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;gBAC1C,kEAAkE;gBAClE,gEAAgE;gBAChE,8DAA8D;gBAC9D,+DAA+D;gBAC/D,4BAA4B;gBAC5B,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;gBAC1E,KAAK,KAAK,CAAC,IAAA,qBAAY,EAAC,IAAI,EAAE,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC;gBACjE,SAAS;YACX,CAAC;YACD,kEAAkE;YAClE,iEAAiE;YACjE,mEAAmE;YACnE,4DAA4D;YAC5D,KAAK,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,MAAsB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;gBAC3E,GAAG,CAAC,KAAK,CAAC,gBAAgB,EAAE;oBAC1B,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;iBACxD,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,SAAS,MAAM,CAAC,KAAsB;QACpC,MAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACtE,WAAW,EAAE,CAAC;IAChB,CAAC;IAED,SAAS,KAAK,CAAC,KAAmB;QAChC,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YACnC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;YAC1C,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC9B,IAAI,EAAE;gBAAE,OAAO,OAAO,EAAE,CAAC;YACzB,iEAAiE;YACjE,oEAAoE;YACpE,8DAA8D;YAC9D,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,GAAG,IAAI,CAAC;gBACd,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;YAClB,CAAC;YACD,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC/B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,SAAS,OAAO;QACd,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,GAAG,KAAK,CAAC;YACf,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;QACnB,CAAC;QACD,MAAM,SAAS,GAAG,cAAc,CAAC;QACjC,cAAc,GAAG,EAAE,CAAC;QACpB,KAAK,MAAM,CAAC,IAAI,SAAS;YAAE,CAAC,EAAE,CAAC;IACjC,CAAC;IAED,OAAO;QACL,KAAK,EAAE,GAAG,EAAE;YACV,KAAK,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,CAAC;YAC7B,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YACzB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC9B,CAAC;QACD,KAAK;QACL,IAAI,EAAE,GAAG,EAAE;YACT,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAC1B,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC7B,2DAA2D;YAC3D,iEAAiE;YACjE,+DAA+D;YAC/D,yCAAyC;YACzC,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,GAAG,KAAK,CAAC;gBACf,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;YACnB,CAAC;YACD,MAAM,SAAS,GAAG,cAAc,CAAC;YACjC,cAAc,GAAG,EAAE,CAAC;YACpB,KAAK,MAAM,CAAC,IAAI,SAAS;gBAAE,CAAC,EAAE,CAAC;QACjC,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Newline-delimited JSON-RPC framer for the dev bridge (issue #399).\n *\n * Reads `process.stdin` and yields complete JSON-RPC frames. Writes\n * complete frames to `process.stdout` (atomic per `\\n` boundary). Parser\n * state is preserved across chunks so frames split arbitrarily on the\n * wire still parse.\n *\n * MCP stdio framing per spec is newline-delimited JSON (`\\n`-terminated\n * UTF-8). We do NOT implement LSP-style `Content-Length` framing — it's\n * not in the MCP spec and adding it would silently shadow real JSON\n * bodies that happen to start with `C`.\n */\n\nimport type { Readable, Writable } from 'node:stream';\n\nimport { makeDevError } from './errors';\nimport type { BridgeLogger } from './log';\n\nexport interface JsonRpcFrame {\n jsonrpc: '2.0';\n id?: string | number | null;\n method?: string;\n params?: unknown;\n result?: unknown;\n error?: { code: number; message: string; data?: unknown };\n}\n\nexport interface StdioFramerOptions {\n input: Readable;\n output: Writable;\n log: BridgeLogger;\n onFrame: (frame: JsonRpcFrame) => void | Promise<void>;\n}\n\nexport interface StdioFramer {\n start(): void;\n /** Write a single frame as a newline-terminated JSON line. Resolves on `'drain'` when backpressure kicks in. */\n write(frame: JsonRpcFrame): Promise<void>;\n stop(): void;\n}\n\n/**\n * Build a newline-delimited JSON framer bound to the supplied streams.\n *\n * Parse errors (malformed JSON between newlines) emit a `-32700` Parse\n * error response back on the output stream and continue — a malformed\n * frame must not kill the bridge.\n */\nexport function createStdioFramer(options: StdioFramerOptions): StdioFramer {\n const { input, output, log, onFrame } = options;\n let buffer = '';\n // True while `output.write` is signalling backpressure. Read by `onData`\n // to pause the inbound stream so we don't keep buffering frames when the\n // outbound side can't keep up.\n let paused = false;\n let drainResolvers: Array<() => void> = [];\n\n function flushBuffer(): void {\n let nl: number;\n while ((nl = buffer.indexOf('\\n')) >= 0) {\n const raw = buffer.slice(0, nl).replace(/\\r$/, '');\n buffer = buffer.slice(nl + 1);\n if (raw.length === 0) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n log.warn('parse-error', { raw: raw.slice(0, 200), error: (err as Error).message });\n // `makeDevError` returns a strict JSON-RPC error shape that\n // structurally satisfies `JsonRpcFrame`; the dev bridge owns both\n // sides of this constructor, so write() trusts the payload.\n void write(makeDevError(null, -32700, { reason: 'parse_error' }));\n continue;\n }\n if (!parsed || typeof parsed !== 'object') {\n // Per JSON-RPC 2.0, every frame must be an object. A bare `42` or\n // `\"string\"` line is just as invalid as garbage JSON — emit the\n // same -32700 envelope so the client doesn't get inconsistent\n // behaviour depending on whether the parser stage or the shape\n // check rejected the input.\n log.warn('parse-error', { raw: raw.slice(0, 200), reason: 'not_object' });\n void write(makeDevError(null, -32700, { reason: 'not_object' }));\n continue;\n }\n // Don't drop the onFrame promise. `onFrame` is user-supplied (the\n // state machine in the production wiring) and can reject — if we\n // void the rejection, Node bubbles it as an unhandledRejection and\n // crashes the bridge. Funnel it through the logger instead.\n void Promise.resolve(onFrame(parsed as JsonRpcFrame)).catch((err: unknown) => {\n log.error('on-frame-error', {\n error: err instanceof Error ? err.message : String(err),\n });\n });\n }\n }\n\n function onData(chunk: Buffer | string): void {\n buffer += typeof chunk === 'string' ? chunk : chunk.toString('utf-8');\n flushBuffer();\n }\n\n function write(frame: JsonRpcFrame): Promise<void> {\n return new Promise<void>((resolve) => {\n const line = JSON.stringify(frame) + '\\n';\n const ok = output.write(line);\n if (ok) return resolve();\n // Backpressure: pause inbound parsing AND the input stream so we\n // stop accumulating frames in `buffer` until drain. `input.pause()`\n // is a no-op on streams that don't support flow-mode pausing.\n if (!paused) {\n paused = true;\n input.pause?.();\n }\n drainResolvers.push(resolve);\n });\n }\n\n function onDrain(): void {\n if (paused) {\n paused = false;\n input.resume?.();\n }\n const resolvers = drainResolvers;\n drainResolvers = [];\n for (const r of resolvers) r();\n }\n\n return {\n start: () => {\n input.setEncoding?.('utf-8');\n input.on('data', onData);\n output.on('drain', onDrain);\n },\n write,\n stop: () => {\n input.off('data', onData);\n output.off('drain', onDrain);\n // Settle any queued write() promises so callers blocked on\n // backpressure don't hang past shutdown. Resume the input stream\n // symmetrically (we may have paused it on backpressure) so the\n // caller doesn't inherit a paused stdin.\n if (paused) {\n paused = false;\n input.resume?.();\n }\n const resolvers = drainResolvers;\n drainResolvers = [];\n for (const r of resolvers) r();\n },\n };\n}\n"]}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Upstream MCP client for the dev bridge (issue #399).
3
+ *
4
+ * Two transport variants:
5
+ *
6
+ * - **HTTP mode**: speaks streamable-HTTP JSON-RPC to the child's HTTP
7
+ * listener at `http://127.0.0.1:<port>/`. Carries the bridge-pinned
8
+ * `mcp-session-id` header on every request so session continuity
9
+ * survives reload.
10
+ *
11
+ * - **Pipe mode (--serve)**: writes/reads newline-delimited JSON-RPC
12
+ * frames on a pair of FDs paired with the child via `child_process`
13
+ * IPC. The child opens these FDs because `FRONTMCP_DEV_STDIO_FD` is
14
+ * set; the bridge writes requests, reads responses, no HTTP layer.
15
+ */
16
+ import type { ChildProcess } from 'node:child_process';
17
+ import type { BridgeLogger } from './log';
18
+ import type { JsonRpcFrame } from './stdio-framer';
19
+ export interface UpstreamClient {
20
+ send(frame: JsonRpcFrame): Promise<void>;
21
+ /** Stop background tasks (SSE listener, pipe parser). */
22
+ close(): Promise<void>;
23
+ }
24
+ export interface UpstreamClientOptions {
25
+ log: BridgeLogger;
26
+ /** Called for every frame the upstream child sends back. */
27
+ onFrame: (frame: JsonRpcFrame) => void | Promise<void>;
28
+ /** Session id pinned by the bridge for HTTP mode. */
29
+ sessionId?: string;
30
+ }
31
+ export interface HttpUpstreamOptions extends UpstreamClientOptions {
32
+ /** Loopback URL of the user-code HTTP server. */
33
+ url: string;
34
+ }
35
+ export declare function createHttpUpstream(options: HttpUpstreamOptions): UpstreamClient;
36
+ export interface PipeUpstreamOptions extends UpstreamClientOptions {
37
+ /** The forked child; we write to / read from the IPC pipe (FD 3). */
38
+ child: ChildProcess;
39
+ }
40
+ /**
41
+ * Pipe mode: the child speaks JSON-RPC on FD 3 (set via
42
+ * `FRONTMCP_DEV_STDIO_FD=3`). We use Node's IPC channel for the same wire
43
+ * — `child.send(...)` forwards a structured message and `child.on('message', …)`
44
+ * yields whatever the child writes back.
45
+ *
46
+ * (Node's IPC wraps JSON over a pipe internally; the framing is consistent
47
+ * with what `runStdio` would write if it were pointed at FD 3.)
48
+ */
49
+ export declare function createPipeUpstream(options: PipeUpstreamOptions): UpstreamClient;
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ /**
3
+ * Upstream MCP client for the dev bridge (issue #399).
4
+ *
5
+ * Two transport variants:
6
+ *
7
+ * - **HTTP mode**: speaks streamable-HTTP JSON-RPC to the child's HTTP
8
+ * listener at `http://127.0.0.1:<port>/`. Carries the bridge-pinned
9
+ * `mcp-session-id` header on every request so session continuity
10
+ * survives reload.
11
+ *
12
+ * - **Pipe mode (--serve)**: writes/reads newline-delimited JSON-RPC
13
+ * frames on a pair of FDs paired with the child via `child_process`
14
+ * IPC. The child opens these FDs because `FRONTMCP_DEV_STDIO_FD` is
15
+ * set; the bridge writes requests, reads responses, no HTTP layer.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.createHttpUpstream = createHttpUpstream;
19
+ exports.createPipeUpstream = createPipeUpstream;
20
+ function createHttpUpstream(options) {
21
+ const { log, url, onFrame, sessionId } = options;
22
+ // Track every in-flight request — `close()` must abort all of them,
23
+ // not just whichever happened to be assigned last. A shared scalar
24
+ // would let concurrent sends clobber each other's controller.
25
+ const abortControllers = new Set();
26
+ async function send(frame) {
27
+ const abortController = new AbortController();
28
+ abortControllers.add(abortController);
29
+ const signal = abortController.signal;
30
+ try {
31
+ const headers = {
32
+ 'content-type': 'application/json',
33
+ accept: 'application/json, text/event-stream',
34
+ };
35
+ if (sessionId)
36
+ headers['mcp-session-id'] = sessionId;
37
+ const res = await fetch(url, {
38
+ method: 'POST',
39
+ headers,
40
+ body: JSON.stringify(frame),
41
+ signal,
42
+ });
43
+ if (!res.ok) {
44
+ // Reject so the FSM can clear inflight bookkeeping and synthesise
45
+ // a JSON-RPC error response back to the client. Silently swallowing
46
+ // a non-OK status here leaves callers hanging.
47
+ log.warn('http-upstream-non-ok', { status: res.status, method: frame.method });
48
+ throw new Error(`upstream returned ${res.status} for ${frame.method ?? 'request'}`);
49
+ }
50
+ const contentType = res.headers.get('content-type') ?? '';
51
+ if (contentType.includes('text/event-stream')) {
52
+ // SSE: parse `data: <json>` lines until the stream ends, forward each.
53
+ const reader = res.body?.getReader();
54
+ if (!reader)
55
+ return;
56
+ const decoder = new TextDecoder();
57
+ let buffer = '';
58
+ while (true) {
59
+ const { done, value } = await reader.read();
60
+ if (done)
61
+ break;
62
+ buffer += decoder.decode(value, { stream: true });
63
+ let nl;
64
+ while ((nl = buffer.indexOf('\n')) >= 0) {
65
+ const line = buffer.slice(0, nl).trim();
66
+ buffer = buffer.slice(nl + 1);
67
+ if (!line.startsWith('data:'))
68
+ continue;
69
+ const json = line.slice(5).trim();
70
+ if (!json)
71
+ continue;
72
+ try {
73
+ const parsed = JSON.parse(json);
74
+ await onFrame(parsed);
75
+ }
76
+ catch (err) {
77
+ log.warn('sse-parse-error', { error: err.message, raw: json.slice(0, 200) });
78
+ }
79
+ }
80
+ }
81
+ }
82
+ else {
83
+ // Single JSON response.
84
+ const body = (await res.json());
85
+ await onFrame(body);
86
+ }
87
+ }
88
+ catch (err) {
89
+ // AbortError surfaces when close() interrupts an in-flight request
90
+ // during reload; that's expected, so log it at info-not-error. We
91
+ // still rethrow so the FSM can clear inflight bookkeeping and
92
+ // synthesise an error response on its end.
93
+ if (err.name === 'AbortError') {
94
+ log.info('http-upstream-aborted', { method: frame.method });
95
+ }
96
+ else {
97
+ log.error('http-upstream-error', { error: err.message, method: frame.method });
98
+ }
99
+ throw err;
100
+ }
101
+ finally {
102
+ abortControllers.delete(abortController);
103
+ }
104
+ }
105
+ return {
106
+ send,
107
+ close: async () => {
108
+ for (const ac of abortControllers)
109
+ ac.abort();
110
+ abortControllers.clear();
111
+ },
112
+ };
113
+ }
114
+ /**
115
+ * Pipe mode: the child speaks JSON-RPC on FD 3 (set via
116
+ * `FRONTMCP_DEV_STDIO_FD=3`). We use Node's IPC channel for the same wire
117
+ * — `child.send(...)` forwards a structured message and `child.on('message', …)`
118
+ * yields whatever the child writes back.
119
+ *
120
+ * (Node's IPC wraps JSON over a pipe internally; the framing is consistent
121
+ * with what `runStdio` would write if it were pointed at FD 3.)
122
+ */
123
+ function createPipeUpstream(options) {
124
+ const { log, child, onFrame } = options;
125
+ function handleMessage(msg) {
126
+ if (!msg || typeof msg !== 'object') {
127
+ log.warn('pipe-upstream-non-object', { type: typeof msg });
128
+ return;
129
+ }
130
+ void onFrame(msg);
131
+ }
132
+ child.on('message', handleMessage);
133
+ async function send(frame) {
134
+ if (!child.connected) {
135
+ log.warn('pipe-upstream-disconnected', { method: frame.method });
136
+ throw new Error(`pipe upstream disconnected for ${frame.method ?? 'request'}`);
137
+ }
138
+ await new Promise((resolve, reject) => {
139
+ child.send(frame, (err) => {
140
+ if (err)
141
+ reject(err);
142
+ else
143
+ resolve();
144
+ });
145
+ }).catch((err) => {
146
+ // Reject so the FSM can produce a JSON-RPC error response back to
147
+ // the client rather than leaving the request stuck inflight.
148
+ log.warn('pipe-upstream-send-error', { error: err.message, method: frame.method });
149
+ throw err;
150
+ });
151
+ }
152
+ return {
153
+ send,
154
+ close: async () => {
155
+ child.off('message', handleMessage);
156
+ },
157
+ };
158
+ }
159
+ //# sourceMappingURL=upstream-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstream-client.js","sourceRoot":"","sources":["../../../../../src/commands/dev/bridge/upstream-client.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Upstream MCP client for the dev bridge (issue #399).\n *\n * Two transport variants:\n *\n * - **HTTP mode**: speaks streamable-HTTP JSON-RPC to the child's HTTP\n * listener at `http://127.0.0.1:<port>/`. Carries the bridge-pinned\n * `mcp-session-id` header on every request so session continuity\n * survives reload.\n *\n * - **Pipe mode (--serve)**: writes/reads newline-delimited JSON-RPC\n * frames on a pair of FDs paired with the child via `child_process`\n * IPC. The child opens these FDs because `FRONTMCP_DEV_STDIO_FD` is\n * set; the bridge writes requests, reads responses, no HTTP layer.\n */\n\nimport type { ChildProcess } from 'node:child_process';\n\nimport type { BridgeLogger } from './log';\nimport type { JsonRpcFrame } from './stdio-framer';\n\nexport interface UpstreamClient {\n send(frame: JsonRpcFrame): Promise<void>;\n /** Stop background tasks (SSE listener, pipe parser). */\n close(): Promise<void>;\n}\n\nexport interface UpstreamClientOptions {\n log: BridgeLogger;\n /** Called for every frame the upstream child sends back. */\n onFrame: (frame: JsonRpcFrame) => void | Promise<void>;\n /** Session id pinned by the bridge for HTTP mode. */\n sessionId?: string;\n}\n\n// ─── HTTP mode ──────────────────────────────────────────────────────────\n\nexport interface HttpUpstreamOptions extends UpstreamClientOptions {\n /** Loopback URL of the user-code HTTP server. */\n url: string;\n}\n\nexport function createHttpUpstream(options: HttpUpstreamOptions): UpstreamClient {\n const { log, url, onFrame, sessionId } = options;\n // Track every in-flight request — `close()` must abort all of them,\n // not just whichever happened to be assigned last. A shared scalar\n // would let concurrent sends clobber each other's controller.\n const abortControllers = new Set<AbortController>();\n\n async function send(frame: JsonRpcFrame): Promise<void> {\n const abortController = new AbortController();\n abortControllers.add(abortController);\n const signal = abortController.signal;\n try {\n const headers: Record<string, string> = {\n 'content-type': 'application/json',\n accept: 'application/json, text/event-stream',\n };\n if (sessionId) headers['mcp-session-id'] = sessionId;\n\n const res = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(frame),\n signal,\n });\n\n if (!res.ok) {\n // Reject so the FSM can clear inflight bookkeeping and synthesise\n // a JSON-RPC error response back to the client. Silently swallowing\n // a non-OK status here leaves callers hanging.\n log.warn('http-upstream-non-ok', { status: res.status, method: frame.method });\n throw new Error(`upstream returned ${res.status} for ${frame.method ?? 'request'}`);\n }\n\n const contentType = res.headers.get('content-type') ?? '';\n if (contentType.includes('text/event-stream')) {\n // SSE: parse `data: <json>` lines until the stream ends, forward each.\n const reader = res.body?.getReader();\n if (!reader) return;\n const decoder = new TextDecoder();\n let buffer = '';\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n let nl: number;\n while ((nl = buffer.indexOf('\\n')) >= 0) {\n const line = buffer.slice(0, nl).trim();\n buffer = buffer.slice(nl + 1);\n if (!line.startsWith('data:')) continue;\n const json = line.slice(5).trim();\n if (!json) continue;\n try {\n const parsed = JSON.parse(json) as JsonRpcFrame;\n await onFrame(parsed);\n } catch (err) {\n log.warn('sse-parse-error', { error: (err as Error).message, raw: json.slice(0, 200) });\n }\n }\n }\n } else {\n // Single JSON response.\n const body = (await res.json()) as JsonRpcFrame;\n await onFrame(body);\n }\n } catch (err) {\n // AbortError surfaces when close() interrupts an in-flight request\n // during reload; that's expected, so log it at info-not-error. We\n // still rethrow so the FSM can clear inflight bookkeeping and\n // synthesise an error response on its end.\n if ((err as { name?: string }).name === 'AbortError') {\n log.info('http-upstream-aborted', { method: frame.method });\n } else {\n log.error('http-upstream-error', { error: (err as Error).message, method: frame.method });\n }\n throw err;\n } finally {\n abortControllers.delete(abortController);\n }\n }\n\n return {\n send,\n close: async () => {\n for (const ac of abortControllers) ac.abort();\n abortControllers.clear();\n },\n };\n}\n\n// ─── Pipe mode (--serve) ────────────────────────────────────────────────\n\nexport interface PipeUpstreamOptions extends UpstreamClientOptions {\n /** The forked child; we write to / read from the IPC pipe (FD 3). */\n child: ChildProcess;\n}\n\n/**\n * Pipe mode: the child speaks JSON-RPC on FD 3 (set via\n * `FRONTMCP_DEV_STDIO_FD=3`). We use Node's IPC channel for the same wire\n * — `child.send(...)` forwards a structured message and `child.on('message', …)`\n * yields whatever the child writes back.\n *\n * (Node's IPC wraps JSON over a pipe internally; the framing is consistent\n * with what `runStdio` would write if it were pointed at FD 3.)\n */\nexport function createPipeUpstream(options: PipeUpstreamOptions): UpstreamClient {\n const { log, child, onFrame } = options;\n\n function handleMessage(msg: unknown): void {\n if (!msg || typeof msg !== 'object') {\n log.warn('pipe-upstream-non-object', { type: typeof msg });\n return;\n }\n void onFrame(msg as JsonRpcFrame);\n }\n\n child.on('message', handleMessage);\n\n async function send(frame: JsonRpcFrame): Promise<void> {\n if (!child.connected) {\n log.warn('pipe-upstream-disconnected', { method: frame.method });\n throw new Error(`pipe upstream disconnected for ${frame.method ?? 'request'}`);\n }\n await new Promise<void>((resolve, reject) => {\n child.send(frame, (err) => {\n if (err) reject(err);\n else resolve();\n });\n }).catch((err: Error) => {\n // Reject so the FSM can produce a JSON-RPC error response back to\n // the client rather than leaving the request stuck inflight.\n log.warn('pipe-upstream-send-error', { error: err.message, method: frame.method });\n throw err;\n });\n }\n\n return {\n send,\n close: async () => {\n child.off('message', handleMessage);\n },\n };\n}\n"]}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * File watcher for the dev bridge (issue #399).
3
+ *
4
+ * In bridge mode, `tsx` is used as a loader (no `--watch`) — the bridge
5
+ * owns the watcher so it can emit a `reload-start` signal to the state
6
+ * machine. Today's `tsx --watch` owns the watcher internally, which is
7
+ * why it can't coordinate with anyone else.
8
+ *
9
+ * Uses `fs.watch` (Node built-in, no dep) with a 150ms debounce. The
10
+ * default ignore list filters `node_modules`, `.git`, `dist`, and the
11
+ * non-TS extensions that don't trigger source-of-truth reloads.
12
+ */
13
+ import type { BridgeLogger } from './log';
14
+ export interface DevWatcherOptions {
15
+ /** Directory to watch (typically the entry's parent). */
16
+ rootDir: string;
17
+ /** Debounce window in ms — multiple events within this window collapse. */
18
+ debounceMs?: number;
19
+ /** Glob fragments to skip. Default: node_modules, .git, dist, .frontmcp. */
20
+ ignore?: string[];
21
+ /** Extensions that trigger reload. Default: .ts, .tsx, .js, .mjs, .cjs. */
22
+ triggerExtensions?: string[];
23
+ log: BridgeLogger;
24
+ onChange: (relativePath: string) => void;
25
+ }
26
+ export interface DevWatcher {
27
+ start(): void;
28
+ stop(): void;
29
+ }
30
+ export declare function createDevWatcher(options: DevWatcherOptions): DevWatcher;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ /**
3
+ * File watcher for the dev bridge (issue #399).
4
+ *
5
+ * In bridge mode, `tsx` is used as a loader (no `--watch`) — the bridge
6
+ * owns the watcher so it can emit a `reload-start` signal to the state
7
+ * machine. Today's `tsx --watch` owns the watcher internally, which is
8
+ * why it can't coordinate with anyone else.
9
+ *
10
+ * Uses `fs.watch` (Node built-in, no dep) with a 150ms debounce. The
11
+ * default ignore list filters `node_modules`, `.git`, `dist`, and the
12
+ * non-TS extensions that don't trigger source-of-truth reloads.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.createDevWatcher = createDevWatcher;
16
+ const tslib_1 = require("tslib");
17
+ const fs = tslib_1.__importStar(require("node:fs"));
18
+ const path = tslib_1.__importStar(require("node:path"));
19
+ const DEFAULT_IGNORE = ['node_modules', '.git', 'dist', '.frontmcp'];
20
+ const DEFAULT_EXTS = ['.ts', '.tsx', '.js', '.mjs', '.cjs'];
21
+ function createDevWatcher(options) {
22
+ const debounce = options.debounceMs ?? 150;
23
+ const ignore = options.ignore ?? DEFAULT_IGNORE;
24
+ const exts = options.triggerExtensions ?? DEFAULT_EXTS;
25
+ const log = options.log;
26
+ let timer;
27
+ let lastTrigger;
28
+ let watcher;
29
+ function shouldIgnore(rel) {
30
+ if (ignore.some((seg) => rel.split(path.sep).includes(seg)))
31
+ return true;
32
+ const ext = path.extname(rel);
33
+ return ext.length > 0 && !exts.includes(ext);
34
+ }
35
+ function fire(rel) {
36
+ lastTrigger = rel;
37
+ if (timer)
38
+ clearTimeout(timer);
39
+ timer = setTimeout(() => {
40
+ timer = undefined;
41
+ const triggerLabel = lastTrigger ?? '(unknown)';
42
+ log.reloadEvent('watcher', { trigger: triggerLabel });
43
+ options.onChange(triggerLabel);
44
+ }, debounce);
45
+ }
46
+ return {
47
+ start() {
48
+ try {
49
+ watcher = fs.watch(options.rootDir, { recursive: true }, (_event, filename) => {
50
+ if (!filename)
51
+ return;
52
+ // Node's `fs.watch` types `filename` as `string | null` here
53
+ // (no `encoding: 'buffer'` option set), so a `typeof filename ===
54
+ // 'string'` narrow leaves the else-branch as `never` and trips
55
+ // TS2339. `String(filename)` is defensive for any future widening
56
+ // (e.g., a `Buffer` arriving via a polyfill) without confusing
57
+ // the type checker.
58
+ const rel = String(filename);
59
+ if (shouldIgnore(rel))
60
+ return;
61
+ fire(rel);
62
+ });
63
+ watcher.on('error', (err) => {
64
+ log.warn('watcher-error', { error: err.message });
65
+ });
66
+ log.info('watcher-started', { rootDir: options.rootDir, debounceMs: debounce });
67
+ }
68
+ catch (err) {
69
+ log.error('watcher-start-failed', { error: err.message });
70
+ }
71
+ },
72
+ stop() {
73
+ if (timer) {
74
+ clearTimeout(timer);
75
+ timer = undefined;
76
+ }
77
+ try {
78
+ watcher?.close();
79
+ }
80
+ catch {
81
+ // ignore
82
+ }
83
+ log.info('watcher-stopped');
84
+ },
85
+ };
86
+ }
87
+ //# sourceMappingURL=watcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watcher.js","sourceRoot":"","sources":["../../../../../src/commands/dev/bridge/watcher.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * File watcher for the dev bridge (issue #399).\n *\n * In bridge mode, `tsx` is used as a loader (no `--watch`) — the bridge\n * owns the watcher so it can emit a `reload-start` signal to the state\n * machine. Today's `tsx --watch` owns the watcher internally, which is\n * why it can't coordinate with anyone else.\n *\n * Uses `fs.watch` (Node built-in, no dep) with a 150ms debounce. The\n * default ignore list filters `node_modules`, `.git`, `dist`, and the\n * non-TS extensions that don't trigger source-of-truth reloads.\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { BridgeLogger } from './log';\n\nexport interface DevWatcherOptions {\n /** Directory to watch (typically the entry's parent). */\n rootDir: string;\n /** Debounce window in ms — multiple events within this window collapse. */\n debounceMs?: number;\n /** Glob fragments to skip. Default: node_modules, .git, dist, .frontmcp. */\n ignore?: string[];\n /** Extensions that trigger reload. Default: .ts, .tsx, .js, .mjs, .cjs. */\n triggerExtensions?: string[];\n log: BridgeLogger;\n onChange: (relativePath: string) => void;\n}\n\nexport interface DevWatcher {\n start(): void;\n stop(): void;\n}\n\nconst DEFAULT_IGNORE = ['node_modules', '.git', 'dist', '.frontmcp'];\nconst DEFAULT_EXTS = ['.ts', '.tsx', '.js', '.mjs', '.cjs'];\n\nexport function createDevWatcher(options: DevWatcherOptions): DevWatcher {\n const debounce = options.debounceMs ?? 150;\n const ignore = options.ignore ?? DEFAULT_IGNORE;\n const exts = options.triggerExtensions ?? DEFAULT_EXTS;\n const log = options.log;\n let timer: NodeJS.Timeout | undefined;\n let lastTrigger: string | undefined;\n let watcher: fs.FSWatcher | undefined;\n\n function shouldIgnore(rel: string): boolean {\n if (ignore.some((seg) => rel.split(path.sep).includes(seg))) return true;\n const ext = path.extname(rel);\n return ext.length > 0 && !exts.includes(ext);\n }\n\n function fire(rel: string): void {\n lastTrigger = rel;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n timer = undefined;\n const triggerLabel = lastTrigger ?? '(unknown)';\n log.reloadEvent('watcher', { trigger: triggerLabel });\n options.onChange(triggerLabel);\n }, debounce);\n }\n\n return {\n start() {\n try {\n watcher = fs.watch(options.rootDir, { recursive: true }, (_event, filename) => {\n if (!filename) return;\n // Node's `fs.watch` types `filename` as `string | null` here\n // (no `encoding: 'buffer'` option set), so a `typeof filename ===\n // 'string'` narrow leaves the else-branch as `never` and trips\n // TS2339. `String(filename)` is defensive for any future widening\n // (e.g., a `Buffer` arriving via a polyfill) without confusing\n // the type checker.\n const rel = String(filename);\n if (shouldIgnore(rel)) return;\n fire(rel);\n });\n watcher.on('error', (err) => {\n log.warn('watcher-error', { error: err.message });\n });\n log.info('watcher-started', { rootDir: options.rootDir, debounceMs: debounce });\n } catch (err) {\n log.error('watcher-start-failed', { error: (err as Error).message });\n }\n },\n stop() {\n if (timer) {\n clearTimeout(timer);\n timer = undefined;\n }\n try {\n watcher?.close();\n } catch {\n // ignore\n }\n log.info('watcher-stopped');\n },\n };\n}\n"]}
@@ -1,2 +1,19 @@
1
- import { ParsedArgs } from '../../core/args';
1
+ import { type ParsedArgs } from '../../core/args';
2
+ /**
3
+ * Resolve the port the dev child should bind to and report any conflict
4
+ * clearly. Returns the chosen port — or never returns and exits the process
5
+ * with a clear error when the port is busy and `--auto-port` was not set.
6
+ *
7
+ * Issue #398: previously the child crashed with a raw `EADDRINUSE` stack
8
+ * trace; this helper turns that into a one-line message with a suggested
9
+ * remediation and (optionally) the owning process.
10
+ */
11
+ export declare function resolveDevPort(opts: {
12
+ port?: number;
13
+ autoPort?: boolean;
14
+ showConflict?: boolean;
15
+ envPort?: string | undefined;
16
+ exit?: (code: number) => never;
17
+ log?: (msg: string) => void;
18
+ }): Promise<number>;
2
19
  export declare function runDev(opts: ParsedArgs): Promise<void>;