frontmcp 1.3.0 → 1.4.1

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.
@@ -16,4 +16,20 @@ export declare function resolveDevPort(opts: {
16
16
  exit?: (code: number) => never;
17
17
  log?: (msg: string) => void;
18
18
  }): Promise<number>;
19
+ /**
20
+ * Build the environment handed to the spawned dev child.
21
+ *
22
+ * The resolved port is exported as `PORT`, and the configured
23
+ * `transport.http.path` (when set) as `FRONTMCP_HTTP_ENTRY_PATH` so the server
24
+ * mounts the MCP endpoint where the generated client URLs point (#446). Both are
25
+ * applied AFTER the inherited env so the dev-resolved values win for this run —
26
+ * the same precedence as `PORT`. A hard-coded `@FrontMcp({ http: { entryPath } })`
27
+ * in metadata still wins over the env (the SDK only reads it as a default).
28
+ */
29
+ export declare function buildDevChildEnv(params: {
30
+ effectiveEnv: NodeJS.ProcessEnv;
31
+ baseEnv: NodeJS.ProcessEnv;
32
+ port: number;
33
+ configHttpPath?: string;
34
+ }): NodeJS.ProcessEnv;
19
35
  export declare function runDev(opts: ParsedArgs): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveDevPort = resolveDevPort;
4
+ exports.buildDevChildEnv = buildDevChildEnv;
4
5
  exports.runDev = runDev;
5
6
  const tslib_1 = require("tslib");
6
7
  const child_process_1 = require("child_process");
@@ -70,6 +71,25 @@ async function resolveDevPort(opts) {
70
71
  log(line);
71
72
  return exit(1);
72
73
  }
74
+ /**
75
+ * Build the environment handed to the spawned dev child.
76
+ *
77
+ * The resolved port is exported as `PORT`, and the configured
78
+ * `transport.http.path` (when set) as `FRONTMCP_HTTP_ENTRY_PATH` so the server
79
+ * mounts the MCP endpoint where the generated client URLs point (#446). Both are
80
+ * applied AFTER the inherited env so the dev-resolved values win for this run —
81
+ * the same precedence as `PORT`. A hard-coded `@FrontMcp({ http: { entryPath } })`
82
+ * in metadata still wins over the env (the SDK only reads it as a default).
83
+ */
84
+ function buildDevChildEnv(params) {
85
+ const { effectiveEnv, baseEnv, port, configHttpPath } = params;
86
+ return {
87
+ ...effectiveEnv,
88
+ ...baseEnv,
89
+ PORT: String(port),
90
+ ...(configHttpPath !== undefined ? { FRONTMCP_HTTP_ENTRY_PATH: configHttpPath } : {}),
91
+ };
92
+ }
73
93
  async function runDev(opts) {
74
94
  // Issue #399 — `--stdio` runs the first-party watch-aware stdio bridge
75
95
  // instead of the legacy `tsx --watch + tsc --noEmit --watch` pair. The
@@ -120,11 +140,20 @@ async function runDev(opts) {
120
140
  showConflict: !!opts.showConflict,
121
141
  envPort: process.env['PORT'],
122
142
  });
143
+ // Issue #446 — honor the configured MCP mount path in dev. `transport.http.path`
144
+ // already drives the generated client URLs (eject); propagate it to the spawned
145
+ // server via FRONTMCP_HTTP_ENTRY_PATH so the endpoint is actually mounted there
146
+ // (the SDK's httpOptionsSchema.entryPath default reads this env). Same precedence
147
+ // caveat as PORT: a hard-coded `@FrontMcp({ http: { entryPath } })` still wins.
148
+ const configHttpPath = typeof cfg?.transport?.http?.path === 'string' ? cfg.transport.http.path : undefined;
123
149
  console.log(`${(0, colors_1.c)('cyan', '[dev]')} using entry: ${path.relative(cwd, entry)}`);
124
150
  if (resolved.configPath || resolved.configDir) {
125
151
  console.log(`${(0, colors_1.c)('gray', '[dev]')} config: ${resolved.configPath ?? resolved.configDir}`);
126
152
  }
127
153
  console.log(`${(0, colors_1.c)('cyan', '[dev]')} listening on port: ${port}`);
154
+ if (configHttpPath) {
155
+ console.log(`${(0, colors_1.c)('gray', '[dev]')} MCP endpoint path: ${configHttpPath}`);
156
+ }
128
157
  console.log(`${(0, colors_1.c)('gray', '[dev]')} starting ${(0, colors_1.c)('bold', 'tsx --watch')} and ${(0, colors_1.c)('bold', 'tsc --noEmit --watch')} (async type-checker)`);
129
158
  console.log(`${(0, colors_1.c)('gray', 'hint:')} press Ctrl+C to stop`);
130
159
  // Use --conditions node to ensure proper Node.js module resolution.
@@ -139,7 +168,12 @@ async function runDev(opts) {
139
168
  // Issue #400 — env overlays from `frontmcp.config.env.{shared,dev}` are
140
169
  // included via `resolved.effectiveEnv`. `.env`/`.env.local` already loaded
141
170
  // into `process.env` above, so they win (they're closer to deployment).
142
- const childEnv = { ...resolved.effectiveEnv, ...process.env, PORT: String(port) };
171
+ const childEnv = buildDevChildEnv({
172
+ effectiveEnv: resolved.effectiveEnv,
173
+ baseEnv: process.env,
174
+ port,
175
+ configHttpPath,
176
+ });
143
177
  const app = (0, child_process_1.spawn)(npxCmd, ['-y', 'tsx', '--conditions', 'node', '--watch', entry], {
144
178
  stdio: 'inherit',
145
179
  env: childEnv,
@@ -1 +1 @@
1
- {"version":3,"file":"dev.js","sourceRoot":"","sources":["../../../../src/commands/dev/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, type ChildProcess } from 'child_process';\nimport * as path from 'path';\n\nimport { resolveConfig } from '../../config';\nimport { type ParsedArgs } from '../../core/args';\nimport { c } from '../../core/colors';\nimport { loadDevEnv } from '../../shared/env';\nimport { resolveEntry } from '../../shared/fs';\nimport { findNextFreePort, isPortFree, lookupPortOwner } from './port';\n\nconst DEFAULT_DEV_PORT = 3000;\n\nfunction killQuiet(proc?: ChildProcess, signal: NodeJS.Signals = 'SIGINT') {\n try {\n if (proc && proc.exitCode === null && proc.signalCode === null) {\n proc.kill(signal);\n }\n } catch {\n // ignore\n }\n}\n\n/**\n * Resolve the port the dev child should bind to and report any conflict\n * clearly. Returns the chosen port — or never returns and exits the process\n * with a clear error when the port is busy and `--auto-port` was not set.\n *\n * Issue #398: previously the child crashed with a raw `EADDRINUSE` stack\n * trace; this helper turns that into a one-line message with a suggested\n * remediation and (optionally) the owning process.\n */\nexport async function resolveDevPort(opts: {\n port?: number;\n autoPort?: boolean;\n showConflict?: boolean;\n envPort?: string | undefined;\n exit?: (code: number) => never;\n log?: (msg: string) => void;\n}): Promise<number> {\n const exit = opts.exit ?? ((code: number) => process.exit(code) as never);\n const log = opts.log ?? ((msg: string) => console.error(msg));\n const explicit = opts.port ?? (opts.envPort !== undefined && opts.envPort !== '' ? Number(opts.envPort) : undefined);\n const port =\n explicit !== undefined && Number.isFinite(explicit) && (explicit as number) > 0\n ? (explicit as number)\n : DEFAULT_DEV_PORT;\n\n if (await isPortFree(port)) return port;\n\n if (opts.autoPort) {\n const alt = await findNextFreePort(port + 1);\n log(`${c('yellow', '[dev]')} port ${port} is in use; auto-picked ${alt}`);\n return alt;\n }\n\n // Build a clear, actionable error message.\n const lines = [\n `${c('red', '[dev]')} Port ${port} is already in use — refusing to start.`,\n `${c('gray', ' ')} Retry with one of:`,\n `${c('gray', ' ')} • ${c('bold', `frontmcp dev --port <other-port>`)}`,\n `${c('gray', ' ')} • ${c('bold', `frontmcp dev --auto-port`)} ${c('gray', '(pick the next free port automatically)')}`,\n `${c('gray', ' ')} • ${c('bold', `PORT=<other-port> frontmcp dev`)}`,\n ];\n if (opts.showConflict) {\n const owner = await lookupPortOwner(port);\n if (owner) {\n lines.push(`${c('gray', ' ')} Holder of ${port}:`);\n for (const row of owner.split('\\n')) lines.push(`${c('gray', ' ')} ${row}`);\n } else {\n lines.push(`${c('gray', ' ')} (could not identify the holder of port ${port})`);\n }\n } else {\n lines.push(`${c('gray', ' ')} (pass --show-conflict to print which process is holding the port)`);\n }\n for (const line of lines) log(line);\n return exit(1);\n}\n\nexport async function runDev(opts: ParsedArgs): Promise<void> {\n // Issue #399 — `--stdio` runs the first-party watch-aware stdio bridge\n // instead of the legacy `tsx --watch + tsc --noEmit --watch` pair. The\n // bridge owns process stdin/stdout (JSON-RPC frames only), holds the\n // upstream MCP session across child restarts, and replaces the\n // third-party `mcp-remote` recipe for the dev loop.\n if (opts.stdio) {\n const { runDevBridge } = await import('./bridge/index.js');\n return runDevBridge(opts);\n }\n\n const cwd = process.cwd();\n\n // Issue #400 — resolve frontmcp.config so `entry`, `transport.http.port`,\n // and `env.shared`/`env.dev` overlays apply. Precedence:\n // CLI flag > FRONTMCP_<NAME> env > frontmcp.config field > built-in default.\n const resolved = await resolveConfig({\n cwd,\n mode: 'dev',\n configPath: typeof opts.config === 'string' ? opts.config : undefined,\n });\n const cfg = resolved.config;\n\n const cliEntry = typeof opts.entry === 'string' ? opts.entry : undefined;\n const configEntry = typeof cfg?.entry === 'string' ? cfg.entry : undefined;\n const entry = await resolveEntry(cwd, cliEntry ?? configEntry);\n\n // Load .env and .env.local files (these win over config env overlays for\n // parity with existing behavior — file-based env is the deployment escape\n // hatch and shouldn't be silently overridden by committed config).\n loadDevEnv(cwd);\n\n // Resolve the port BEFORE spawning tsx so EADDRINUSE produces a clean\n // one-line error instead of a raw node:net stack trace (issue #398).\n //\n // Two caveats worth knowing about this pre-flight check:\n // 1. TOCTOU — between this probe returning and the child actually binding,\n // another process can grab the port. We accept that race: this is a\n // dev-time tool, the worst case reverts to the prior behaviour (the\n // child surfaces a raw EADDRINUSE), and the common case (port already\n // busy at startup) is the one we wanted to fix.\n // 2. The resolved port is exported as `PORT` to the child. It only takes\n // effect when the user's `@FrontMcp({ http: { port } })` reads\n // `process.env.PORT` (the SDK's `httpOptionsSchema` default does).\n // If the user's metadata HARD-CODES `http.port`, the child binds to\n // that hard-coded value and ignores PORT — the probe is then advisory\n // only. Documented in docs/frontmcp/deployment/local-dev-server.mdx.\n const cliPort = typeof opts.port === 'number' ? opts.port : opts.port ? Number(opts.port) : undefined;\n const configPort = cfg?.transport?.http?.port;\n const port = await resolveDevPort({\n port: cliPort ?? configPort,\n autoPort: !!opts.autoPort,\n showConflict: !!opts.showConflict,\n envPort: process.env['PORT'],\n });\n\n console.log(`${c('cyan', '[dev]')} using entry: ${path.relative(cwd, entry)}`);\n if (resolved.configPath || resolved.configDir) {\n console.log(`${c('gray', '[dev]')} config: ${resolved.configPath ?? resolved.configDir}`);\n }\n console.log(`${c('cyan', '[dev]')} listening on port: ${port}`);\n console.log(\n `${c('gray', '[dev]')} starting ${c('bold', 'tsx --watch')} and ${c(\n 'bold',\n 'tsc --noEmit --watch',\n )} (async type-checker)`,\n );\n console.log(`${c('gray', 'hint:')} press Ctrl+C to stop`);\n\n // Use --conditions node to ensure proper Node.js module resolution.\n // This helps with dynamic require() calls in packages like ioredis.\n // On Windows resolve npx.cmd directly — previously we passed shell:true\n // for the .cmd suffix, but that triggers Node DEP0190 (#381) every run.\n // spawn() resolves .cmd via CreateProcessW since Node 16, so no shell is\n // needed; on Unix spawn() works on 'npx' directly. SIGINT still\n // propagates cleanly because no intermediate shell sits between us and\n // the child process.\n const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';\n // Issue #400 — env overlays from `frontmcp.config.env.{shared,dev}` are\n // included via `resolved.effectiveEnv`. `.env`/`.env.local` already loaded\n // into `process.env` above, so they win (they're closer to deployment).\n const childEnv = { ...resolved.effectiveEnv, ...process.env, PORT: String(port) };\n const app = spawn(npxCmd, ['-y', 'tsx', '--conditions', 'node', '--watch', entry], {\n stdio: 'inherit',\n env: childEnv,\n });\n const checker = spawn(npxCmd, ['-y', 'tsc', '--noEmit', '--pretty', '--watch'], {\n stdio: 'inherit',\n env: childEnv,\n });\n\n const cleanup = (clearTimer = true) => {\n if (clearTimer) {\n clearForceKillTimer();\n }\n killQuiet(checker);\n killQuiet(app);\n };\n\n let forceKillTimer: NodeJS.Timeout | undefined;\n let appClosed = false;\n let checkerClosed = false;\n\n const clearForceKillTimer = () => {\n if (forceKillTimer) {\n clearTimeout(forceKillTimer);\n forceKillTimer = undefined;\n }\n };\n\n const markClosed = (child: 'app' | 'checker') => {\n if (child === 'app') {\n appClosed = true;\n } else {\n checkerClosed = true;\n }\n if (appClosed && checkerClosed) {\n clearForceKillTimer();\n }\n };\n\n process.once('SIGINT', () => {\n cleanup(false);\n // Force-kill after 2s if children haven't exited\n clearForceKillTimer();\n forceKillTimer = setTimeout(() => {\n killQuiet(checker, 'SIGKILL');\n killQuiet(app, 'SIGKILL');\n process.exit(0);\n }, 2000);\n forceKillTimer.unref();\n // Exit cleanly once both children have closed\n const tryExit = () => {\n if (appClosed && checkerClosed) {\n clearForceKillTimer();\n process.exit(0);\n }\n };\n app.once('close', () => {\n markClosed('app');\n tryExit();\n });\n checker.once('close', () => {\n markClosed('checker');\n tryExit();\n });\n });\n\n process.once('SIGTERM', () => {\n cleanup();\n process.exit(0);\n });\n\n let appExitCode: number | null = 0;\n await new Promise<void>((resolve, reject) => {\n app.on('close', (code) => {\n // Capture the child's exit code so it can propagate to the parent\n // shell. SIGINT/SIGTERM yield code=null with a signalCode — treat\n // those as 0 so Ctrl+C doesn't appear as a failure.\n appExitCode = typeof code === 'number' ? code : 0;\n markClosed('app');\n cleanup(false);\n resolve();\n });\n app.on('error', (err) => {\n clearForceKillTimer();\n cleanup();\n reject(err);\n });\n checker.on('close', () => {\n markClosed('checker');\n });\n checker.on('error', (err) => {\n clearForceKillTimer();\n cleanup();\n reject(err);\n });\n });\n\n // Propagate the child's exit code so CI / shells see real failures\n // instead of always-success.\n if (appExitCode && appExitCode !== 0) {\n process.exit(appExitCode);\n }\n}\n"]}
1
+ {"version":3,"file":"dev.js","sourceRoot":"","sources":["../../../../src/commands/dev/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, type ChildProcess } from 'child_process';\nimport * as path from 'path';\n\nimport { resolveConfig } from '../../config';\nimport { type ParsedArgs } from '../../core/args';\nimport { c } from '../../core/colors';\nimport { loadDevEnv } from '../../shared/env';\nimport { resolveEntry } from '../../shared/fs';\nimport { findNextFreePort, isPortFree, lookupPortOwner } from './port';\n\nconst DEFAULT_DEV_PORT = 3000;\n\nfunction killQuiet(proc?: ChildProcess, signal: NodeJS.Signals = 'SIGINT') {\n try {\n if (proc && proc.exitCode === null && proc.signalCode === null) {\n proc.kill(signal);\n }\n } catch {\n // ignore\n }\n}\n\n/**\n * Resolve the port the dev child should bind to and report any conflict\n * clearly. Returns the chosen port — or never returns and exits the process\n * with a clear error when the port is busy and `--auto-port` was not set.\n *\n * Issue #398: previously the child crashed with a raw `EADDRINUSE` stack\n * trace; this helper turns that into a one-line message with a suggested\n * remediation and (optionally) the owning process.\n */\nexport async function resolveDevPort(opts: {\n port?: number;\n autoPort?: boolean;\n showConflict?: boolean;\n envPort?: string | undefined;\n exit?: (code: number) => never;\n log?: (msg: string) => void;\n}): Promise<number> {\n const exit = opts.exit ?? ((code: number) => process.exit(code) as never);\n const log = opts.log ?? ((msg: string) => console.error(msg));\n const explicit = opts.port ?? (opts.envPort !== undefined && opts.envPort !== '' ? Number(opts.envPort) : undefined);\n const port =\n explicit !== undefined && Number.isFinite(explicit) && (explicit as number) > 0\n ? (explicit as number)\n : DEFAULT_DEV_PORT;\n\n if (await isPortFree(port)) return port;\n\n if (opts.autoPort) {\n const alt = await findNextFreePort(port + 1);\n log(`${c('yellow', '[dev]')} port ${port} is in use; auto-picked ${alt}`);\n return alt;\n }\n\n // Build a clear, actionable error message.\n const lines = [\n `${c('red', '[dev]')} Port ${port} is already in use — refusing to start.`,\n `${c('gray', ' ')} Retry with one of:`,\n `${c('gray', ' ')} • ${c('bold', `frontmcp dev --port <other-port>`)}`,\n `${c('gray', ' ')} • ${c('bold', `frontmcp dev --auto-port`)} ${c('gray', '(pick the next free port automatically)')}`,\n `${c('gray', ' ')} • ${c('bold', `PORT=<other-port> frontmcp dev`)}`,\n ];\n if (opts.showConflict) {\n const owner = await lookupPortOwner(port);\n if (owner) {\n lines.push(`${c('gray', ' ')} Holder of ${port}:`);\n for (const row of owner.split('\\n')) lines.push(`${c('gray', ' ')} ${row}`);\n } else {\n lines.push(`${c('gray', ' ')} (could not identify the holder of port ${port})`);\n }\n } else {\n lines.push(`${c('gray', ' ')} (pass --show-conflict to print which process is holding the port)`);\n }\n for (const line of lines) log(line);\n return exit(1);\n}\n\n/**\n * Build the environment handed to the spawned dev child.\n *\n * The resolved port is exported as `PORT`, and the configured\n * `transport.http.path` (when set) as `FRONTMCP_HTTP_ENTRY_PATH` so the server\n * mounts the MCP endpoint where the generated client URLs point (#446). Both are\n * applied AFTER the inherited env so the dev-resolved values win for this run —\n * the same precedence as `PORT`. A hard-coded `@FrontMcp({ http: { entryPath } })`\n * in metadata still wins over the env (the SDK only reads it as a default).\n */\nexport function buildDevChildEnv(params: {\n effectiveEnv: NodeJS.ProcessEnv;\n baseEnv: NodeJS.ProcessEnv;\n port: number;\n configHttpPath?: string;\n}): NodeJS.ProcessEnv {\n const { effectiveEnv, baseEnv, port, configHttpPath } = params;\n return {\n ...effectiveEnv,\n ...baseEnv,\n PORT: String(port),\n ...(configHttpPath !== undefined ? { FRONTMCP_HTTP_ENTRY_PATH: configHttpPath } : {}),\n };\n}\n\nexport async function runDev(opts: ParsedArgs): Promise<void> {\n // Issue #399 — `--stdio` runs the first-party watch-aware stdio bridge\n // instead of the legacy `tsx --watch + tsc --noEmit --watch` pair. The\n // bridge owns process stdin/stdout (JSON-RPC frames only), holds the\n // upstream MCP session across child restarts, and replaces the\n // third-party `mcp-remote` recipe for the dev loop.\n if (opts.stdio) {\n const { runDevBridge } = await import('./bridge/index.js');\n return runDevBridge(opts);\n }\n\n const cwd = process.cwd();\n\n // Issue #400 — resolve frontmcp.config so `entry`, `transport.http.port`,\n // and `env.shared`/`env.dev` overlays apply. Precedence:\n // CLI flag > FRONTMCP_<NAME> env > frontmcp.config field > built-in default.\n const resolved = await resolveConfig({\n cwd,\n mode: 'dev',\n configPath: typeof opts.config === 'string' ? opts.config : undefined,\n });\n const cfg = resolved.config;\n\n const cliEntry = typeof opts.entry === 'string' ? opts.entry : undefined;\n const configEntry = typeof cfg?.entry === 'string' ? cfg.entry : undefined;\n const entry = await resolveEntry(cwd, cliEntry ?? configEntry);\n\n // Load .env and .env.local files (these win over config env overlays for\n // parity with existing behavior — file-based env is the deployment escape\n // hatch and shouldn't be silently overridden by committed config).\n loadDevEnv(cwd);\n\n // Resolve the port BEFORE spawning tsx so EADDRINUSE produces a clean\n // one-line error instead of a raw node:net stack trace (issue #398).\n //\n // Two caveats worth knowing about this pre-flight check:\n // 1. TOCTOU — between this probe returning and the child actually binding,\n // another process can grab the port. We accept that race: this is a\n // dev-time tool, the worst case reverts to the prior behaviour (the\n // child surfaces a raw EADDRINUSE), and the common case (port already\n // busy at startup) is the one we wanted to fix.\n // 2. The resolved port is exported as `PORT` to the child. It only takes\n // effect when the user's `@FrontMcp({ http: { port } })` reads\n // `process.env.PORT` (the SDK's `httpOptionsSchema` default does).\n // If the user's metadata HARD-CODES `http.port`, the child binds to\n // that hard-coded value and ignores PORT — the probe is then advisory\n // only. Documented in docs/frontmcp/deployment/local-dev-server.mdx.\n const cliPort = typeof opts.port === 'number' ? opts.port : opts.port ? Number(opts.port) : undefined;\n const configPort = cfg?.transport?.http?.port;\n const port = await resolveDevPort({\n port: cliPort ?? configPort,\n autoPort: !!opts.autoPort,\n showConflict: !!opts.showConflict,\n envPort: process.env['PORT'],\n });\n\n // Issue #446 — honor the configured MCP mount path in dev. `transport.http.path`\n // already drives the generated client URLs (eject); propagate it to the spawned\n // server via FRONTMCP_HTTP_ENTRY_PATH so the endpoint is actually mounted there\n // (the SDK's httpOptionsSchema.entryPath default reads this env). Same precedence\n // caveat as PORT: a hard-coded `@FrontMcp({ http: { entryPath } })` still wins.\n const configHttpPath = typeof cfg?.transport?.http?.path === 'string' ? cfg.transport.http.path : undefined;\n\n console.log(`${c('cyan', '[dev]')} using entry: ${path.relative(cwd, entry)}`);\n if (resolved.configPath || resolved.configDir) {\n console.log(`${c('gray', '[dev]')} config: ${resolved.configPath ?? resolved.configDir}`);\n }\n console.log(`${c('cyan', '[dev]')} listening on port: ${port}`);\n if (configHttpPath) {\n console.log(`${c('gray', '[dev]')} MCP endpoint path: ${configHttpPath}`);\n }\n console.log(\n `${c('gray', '[dev]')} starting ${c('bold', 'tsx --watch')} and ${c(\n 'bold',\n 'tsc --noEmit --watch',\n )} (async type-checker)`,\n );\n console.log(`${c('gray', 'hint:')} press Ctrl+C to stop`);\n\n // Use --conditions node to ensure proper Node.js module resolution.\n // This helps with dynamic require() calls in packages like ioredis.\n // On Windows resolve npx.cmd directly — previously we passed shell:true\n // for the .cmd suffix, but that triggers Node DEP0190 (#381) every run.\n // spawn() resolves .cmd via CreateProcessW since Node 16, so no shell is\n // needed; on Unix spawn() works on 'npx' directly. SIGINT still\n // propagates cleanly because no intermediate shell sits between us and\n // the child process.\n const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';\n // Issue #400 — env overlays from `frontmcp.config.env.{shared,dev}` are\n // included via `resolved.effectiveEnv`. `.env`/`.env.local` already loaded\n // into `process.env` above, so they win (they're closer to deployment).\n const childEnv = buildDevChildEnv({\n effectiveEnv: resolved.effectiveEnv,\n baseEnv: process.env,\n port,\n configHttpPath,\n });\n const app = spawn(npxCmd, ['-y', 'tsx', '--conditions', 'node', '--watch', entry], {\n stdio: 'inherit',\n env: childEnv,\n });\n const checker = spawn(npxCmd, ['-y', 'tsc', '--noEmit', '--pretty', '--watch'], {\n stdio: 'inherit',\n env: childEnv,\n });\n\n const cleanup = (clearTimer = true) => {\n if (clearTimer) {\n clearForceKillTimer();\n }\n killQuiet(checker);\n killQuiet(app);\n };\n\n let forceKillTimer: NodeJS.Timeout | undefined;\n let appClosed = false;\n let checkerClosed = false;\n\n const clearForceKillTimer = () => {\n if (forceKillTimer) {\n clearTimeout(forceKillTimer);\n forceKillTimer = undefined;\n }\n };\n\n const markClosed = (child: 'app' | 'checker') => {\n if (child === 'app') {\n appClosed = true;\n } else {\n checkerClosed = true;\n }\n if (appClosed && checkerClosed) {\n clearForceKillTimer();\n }\n };\n\n process.once('SIGINT', () => {\n cleanup(false);\n // Force-kill after 2s if children haven't exited\n clearForceKillTimer();\n forceKillTimer = setTimeout(() => {\n killQuiet(checker, 'SIGKILL');\n killQuiet(app, 'SIGKILL');\n process.exit(0);\n }, 2000);\n forceKillTimer.unref();\n // Exit cleanly once both children have closed\n const tryExit = () => {\n if (appClosed && checkerClosed) {\n clearForceKillTimer();\n process.exit(0);\n }\n };\n app.once('close', () => {\n markClosed('app');\n tryExit();\n });\n checker.once('close', () => {\n markClosed('checker');\n tryExit();\n });\n });\n\n process.once('SIGTERM', () => {\n cleanup();\n process.exit(0);\n });\n\n let appExitCode: number | null = 0;\n await new Promise<void>((resolve, reject) => {\n app.on('close', (code) => {\n // Capture the child's exit code so it can propagate to the parent\n // shell. SIGINT/SIGTERM yield code=null with a signalCode — treat\n // those as 0 so Ctrl+C doesn't appear as a failure.\n appExitCode = typeof code === 'number' ? code : 0;\n markClosed('app');\n cleanup(false);\n resolve();\n });\n app.on('error', (err) => {\n clearForceKillTimer();\n cleanup();\n reject(err);\n });\n checker.on('close', () => {\n markClosed('checker');\n });\n checker.on('error', (err) => {\n clearForceKillTimer();\n cleanup();\n reject(err);\n });\n });\n\n // Propagate the child's exit code so CI / shells see real failures\n // instead of always-success.\n if (appExitCode && appExitCode !== 0) {\n process.exit(appExitCode);\n }\n}\n"]}
@@ -565,7 +565,7 @@ function generatePmSetupSteps(pm) {
565
565
  uses: pnpm/action-setup@v4
566
566
 
567
567
  - name: Setup Node.js
568
- uses: actions/setup-node@v4
568
+ uses: actions/setup-node@v6
569
569
  with:
570
570
  node-version: '24'
571
571
  cache: '${cfg.ghCache}'
@@ -575,7 +575,7 @@ function generatePmSetupSteps(pm) {
575
575
  }
576
576
  return `
577
577
  - name: Setup Node.js
578
- uses: actions/setup-node@v4
578
+ uses: actions/setup-node@v6
579
579
  with:
580
580
  node-version: '24'
581
581
  cache: '${cfg.ghCache}'
@@ -599,7 +599,7 @@ jobs:
599
599
  runs-on: ubuntu-latest
600
600
 
601
601
  steps:
602
- - uses: actions/checkout@v4
602
+ - uses: actions/checkout@v6
603
603
  ${generatePmSetupSteps(pm)}
604
604
 
605
605
  - name: Type check
@@ -625,7 +625,7 @@ jobs:
625
625
  runs-on: ubuntu-latest
626
626
 
627
627
  steps:
628
- - uses: actions/checkout@v4
628
+ - uses: actions/checkout@v6
629
629
  ${generatePmSetupSteps(pm)}
630
630
 
631
631
  - name: Build
@@ -655,7 +655,7 @@ jobs:
655
655
  packages: write
656
656
 
657
657
  steps:
658
- - uses: actions/checkout@v4
658
+ - uses: actions/checkout@v6
659
659
 
660
660
  - name: Log in to Container Registry
661
661
  uses: docker/login-action@v3
@@ -693,7 +693,7 @@ jobs:
693
693
  runs-on: ubuntu-latest
694
694
 
695
695
  steps:
696
- - uses: actions/checkout@v4
696
+ - uses: actions/checkout@v6
697
697
  ${generatePmSetupSteps(pm)}
698
698
 
699
699
  - name: Build
@@ -722,7 +722,7 @@ jobs:
722
722
  runs-on: ubuntu-latest
723
723
 
724
724
  steps:
725
- - uses: actions/checkout@v4
725
+ - uses: actions/checkout@v6
726
726
  ${generatePmSetupSteps(pm)}
727
727
 
728
728
  - name: Build
@@ -759,7 +759,7 @@ jobs:
759
759
  runs-on: ubuntu-latest
760
760
 
761
761
  steps:
762
- - uses: actions/checkout@v4
762
+ - uses: actions/checkout@v6
763
763
  ${generatePmSetupSteps(pm)}
764
764
 
765
765
  - name: Build