frontmcp 1.2.1 → 1.4.0

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