@akira-tl/forgerelay 0.9.3 → 0.10.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 (109) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +10 -4
  3. package/dist/cli/init.js +111 -24
  4. package/dist/cli/maintenance-prune.js +479 -0
  5. package/dist/cli/maintenance-retention.js +93 -0
  6. package/dist/cli/maintenance.js +598 -0
  7. package/dist/cli/setup-support.js +21 -0
  8. package/dist/cli/shell/setup.js +99 -0
  9. package/dist/cli.js +72 -16
  10. package/dist/mcp/hooks/hooks.js +6 -3
  11. package/dist/mcp/oauth/public-url.js +3 -0
  12. package/dist/mcp/oauth/router.js +14 -9
  13. package/dist/mcp/process/process-platform.js +24 -26
  14. package/dist/mcp/process/process-sessions.js +6 -3
  15. package/dist/mcp/server/transport/http-server.js +17 -9
  16. package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +6 -0
  17. package/dist/mcp/server/workspace/runtime/workspace-tools.js +1 -1
  18. package/dist/mcp/server-instructions.js +10 -1
  19. package/dist/runtime/config/config.js +60 -6
  20. package/dist/runtime/instructions/shell-instructions.js +100 -0
  21. package/dist/runtime/security/runtime-privilege.js +120 -0
  22. package/dist/runtime/shell/command-shell-runtime.js +235 -0
  23. package/dist/runtime/state/runtime-lease.js +109 -0
  24. package/dist/ui/.vite/manifest.json +276 -276
  25. package/dist/ui/activity-panel-app.html +2 -2
  26. package/dist/ui/assets/{activity-panel-app-Sl1Jro8v.js → activity-panel-app-BkODhDLg.js} +1 -1
  27. package/dist/ui/assets/{activity-panel-app-7Xe0alhm.css → activity-panel-app-DkfJzTZ3.css} +1 -1
  28. package/dist/ui/assets/{angular-html-B_yn91wW.js → angular-html-DjuELMdF.js} +1 -1
  29. package/dist/ui/assets/{angular-ts-DSgtloKz.js → angular-ts-2EXVZIIX.js} +1 -1
  30. package/dist/ui/assets/{apl-NatWtF8X.js → apl-Bb4IeT5V.js} +1 -1
  31. package/dist/ui/assets/{astro-BYFEVBuN.js → astro-Bjavu3nT.js} +1 -1
  32. package/dist/ui/assets/{blade-DoO7yvMR.js → blade-DKW2uHZo.js} +1 -1
  33. package/dist/ui/assets/{c-Rl2hMCcg.js → c-oLeHmzbj.js} +1 -1
  34. package/dist/ui/assets/{cobol-ByC5mWiB.js → cobol-CcqFMr5T.js} +1 -1
  35. package/dist/ui/assets/{coffee-Dez77Qov.js → coffee-BA20zqRF.js} +1 -1
  36. package/dist/ui/assets/{cpp-DtaAQT9h.js → cpp-D750WmDa.js} +1 -1
  37. package/dist/ui/assets/{crystal-Bgmbf8ec.js → crystal-Cv5I6OMI.js} +1 -1
  38. package/dist/ui/assets/{css-C1tjBtVN.js → css-CjPPWPeH.js} +1 -1
  39. package/dist/ui/assets/{edge-DeH6u8Wg.js → edge-aYRuudEZ.js} +1 -1
  40. package/dist/ui/assets/{elixir-_H3KnmWT.js → elixir-ChLitmz7.js} +1 -1
  41. package/dist/ui/assets/{elm-CsCe3U28.js → elm-Fs7L7u36.js} +1 -1
  42. package/dist/ui/assets/{erb-CbM-L2g8.js → erb-B-qXHxN1.js} +1 -1
  43. package/dist/ui/assets/{git-rebase-D0s5pDh9.js → git-rebase-DOVN5OEg.js} +1 -1
  44. package/dist/ui/assets/{glimmer-js-BfOToq_A.js → glimmer-js-BFt_vCTW.js} +1 -1
  45. package/dist/ui/assets/{glimmer-ts-CCUOkL2K.js → glimmer-ts-ufCHlR6C.js} +1 -1
  46. package/dist/ui/assets/{glsl-DH4ec9dE.js → glsl-Ex4JXGuC.js} +1 -1
  47. package/dist/ui/assets/{graphql-DilAMBoI.js → graphql-pQPXtBk5.js} +1 -1
  48. package/dist/ui/assets/{hack-BDkNDZlF.js → hack-B5hDguhd.js} +1 -1
  49. package/dist/ui/assets/{haml-D_QBp6K7.js → haml-BrNeI29U.js} +1 -1
  50. package/dist/ui/assets/{handlebars-DePUrTpC.js → handlebars-DCqTsL9-.js} +1 -1
  51. package/dist/ui/assets/{heavy-payload-DOwZpO6n.js → heavy-payload-DsmmHgvt.js} +3 -3
  52. package/dist/ui/assets/{html-By4dNnM4.js → html-D8k0CC9V.js} +1 -1
  53. package/dist/ui/assets/{html-derivative-DDw9Ef4Q.js → html-derivative-CC8flWmC.js} +1 -1
  54. package/dist/ui/assets/{http-B0QxWc9M.js → http-BYnWqNep.js} +1 -1
  55. package/dist/ui/assets/{hurl-vMQ4lda1.js → hurl-BND-z85s.js} +1 -1
  56. package/dist/ui/assets/{java-DYRe2lWG.js → java-Dug0oB9T.js} +1 -1
  57. package/dist/ui/assets/{javascript-CxyGqwcL.js → javascript-oDJ7WITC.js} +1 -1
  58. package/dist/ui/assets/{jinja-JUwV-fZt.js → jinja-Cv0t_F1U.js} +1 -1
  59. package/dist/ui/assets/{jison-7ZqCEvS8.js → jison-Car3MF55.js} +1 -1
  60. package/dist/ui/assets/{json-dbltgiiA.js → json-CEViCsf4.js} +1 -1
  61. package/dist/ui/assets/{jsx-oDopT7YQ.js → jsx-BfZ0zgSH.js} +1 -1
  62. package/dist/ui/assets/{julia-CkKiNSav.js → julia-CmX0b3Tn.js} +1 -1
  63. package/dist/ui/assets/{just-DuqCAKJ3.js → just-BuugLCVk.js} +1 -1
  64. package/dist/ui/assets/{latex-sudcRDSn.js → latex-FpTXqO03.js} +1 -1
  65. package/dist/ui/assets/{liquid-Di-08Js7.js → liquid-B37OzUFu.js} +1 -1
  66. package/dist/ui/assets/{lua-YlqdMoV0.js → lua-BHjcb8oq.js} +1 -1
  67. package/dist/ui/assets/{marko-CJRNyJDb.js → marko-CApj_fsH.js} +1 -1
  68. package/dist/ui/assets/{mdc-t12CzZHs.js → mdc-Ed5vQWq8.js} +1 -1
  69. package/dist/ui/assets/{nginx-57-b4XwT.js → nginx-u_rVEBox.js} +1 -1
  70. package/dist/ui/assets/{nim-BzrswYQa.js → nim-Ba9EdchT.js} +1 -1
  71. package/dist/ui/assets/{perl-D9JR_iIh.js → perl-BpU_6Wvz.js} +1 -1
  72. package/dist/ui/assets/{php-Py2gFHil.js → php-bKabofky.js} +1 -1
  73. package/dist/ui/assets/{pug-PzLLy0h1.js → pug-C_aLNkqe.js} +1 -1
  74. package/dist/ui/assets/{qml-By8fwgV4.js → qml-D9VisnRy.js} +1 -1
  75. package/dist/ui/assets/{r-CcezTbLq.js → r-CYqB1sLx.js} +1 -1
  76. package/dist/ui/assets/{razor-dG-vdhL_.js → razor-DNJKI8Rs.js} +1 -1
  77. package/dist/ui/assets/{regexp-BlsQ-N_8.js → regexp-DHrTHXkz.js} +1 -1
  78. package/dist/ui/assets/{rst-C2awDCat.js → rst-CbNdSFHv.js} +1 -1
  79. package/dist/ui/assets/{ruby-zA2W_49d.js → ruby-FyKG-pkF.js} +1 -1
  80. package/dist/ui/assets/{sas-D49bhyaN.js → sas-BbSPDl3X.js} +1 -1
  81. package/dist/ui/assets/{scss-BQb-cp01.js → scss-2nX9g6gD.js} +1 -1
  82. package/dist/ui/assets/{shellscript-Dga4H_Ni.js → shellscript-CmON6x2x.js} +1 -1
  83. package/dist/ui/assets/{shellsession-n3-5CqcF.js → shellsession-DtqIGlAX.js} +1 -1
  84. package/dist/ui/assets/{soy-XYBOf_BA.js → soy-BzUJMmJT.js} +1 -1
  85. package/dist/ui/assets/{sql-ET4u5Cpl.js → sql-CgVcFZmb.js} +1 -1
  86. package/dist/ui/assets/{stata-CLHcJHO_.js → stata-CNitlMot.js} +1 -1
  87. package/dist/ui/assets/{surrealql-BdjQ60T4.js → surrealql-BA8-xsRZ.js} +1 -1
  88. package/dist/ui/assets/{svelte-B2RPtKJe.js → svelte-El02yAxv.js} +1 -1
  89. package/dist/ui/assets/{templ-CrByPW_W.js → templ-DeUQ84aR.js} +1 -1
  90. package/dist/ui/assets/{tex-DZBkg363.js → tex-Cu-FqpPS.js} +1 -1
  91. package/dist/ui/assets/{ts-tags-DJR3Gphb.js → ts-tags-X42N6TGA.js} +1 -1
  92. package/dist/ui/assets/{tsx-CajzROyy.js → tsx-C1aPp9rY.js} +1 -1
  93. package/dist/ui/assets/{twig-BZjk5Zda.js → twig-Cj1Q4SQc.js} +1 -1
  94. package/dist/ui/assets/{typescript-B-EWToLj.js → typescript-Du9lVt1G.js} +1 -1
  95. package/dist/ui/assets/{vue-D_rHCCr8.js → vue-_uMGbFv8.js} +1 -1
  96. package/dist/ui/assets/{vue-html-CC4WlvVt.js → vue-html-D61naG-Q.js} +1 -1
  97. package/dist/ui/assets/{vue-vine-BgNMVgpk.js → vue-vine-CkFae4yV.js} +1 -1
  98. package/dist/ui/assets/{xml-BI86-WbH.js → xml-Cbwsc6ws.js} +1 -1
  99. package/dist/ui/assets/{xsl-DXeLZV4J.js → xsl-NyBx72XB.js} +1 -1
  100. package/dist/ui/assets/{yaml-60_PFlWa.js → yaml-Cgb6Uq9u.js} +1 -1
  101. package/dist/workspaces/context.js +32 -5
  102. package/dist/workspaces/presentation/workspace-presentation.js +1 -0
  103. package/dist/workspaces/sessions.js +2 -0
  104. package/dist/workspaces.js +2 -1
  105. package/docs/configuration.md +101 -14
  106. package/package.json +2 -2
  107. package/scripts/release/release-gate.test.mjs +1 -0
  108. package/scripts/release-proof.mjs +85 -9
  109. package/scripts/release-proof.test.mjs +79 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.10.0] - 2026-09-05
8
+
9
+ ### Added
10
+
11
+ - Added a first-class Command Shell Runtime identity with launch-shell/fallback selection, explicit pinned shell executable support, owner-visible diagnostics, and one shared shell contract for Agent commands and Hooks.
12
+ - Added editable ForgeRelay shell Instructions for PowerShell 7, Windows PowerShell 5.1, cmd, zsh, and fish. `forgerelay init` seeds exact-release-tag templates into the user config directory, preserves user edits, and projects loaded/disabled/unavailable state through the existing Workspace Instructions UI and context-delta path.
13
+
14
+ ### Security
15
+
16
+ - ForgeRelay now refuses elevated root/Administrator/SYSTEM-equivalent runtime authority by default across supported platforms. Intentional elevated startup requires the invocation-scoped `--allow-elevated` acknowledgement and always exposes a mandatory Agent safety notice.
17
+
18
+ ## [0.9.4] - 2026-09-04
19
+
20
+ ### Added
21
+
22
+ - Added owner-facing `forgerelay maintenance inspect` and `forgerelay maintenance prune` for the 0.9.4 retention contract. Durable Activity/Audit, Host Turn, and Bash history remains unlimited by default; configured retention is explicit prune authorization, whole Host Turn cohorts protect recent/nonterminal/running Bash/active Subagent work, and persistent Workspace identity, aliases, Task Lists, named checkpoints, managed worktrees/branches, and non-empty private Workspace state remain protected.
23
+ - Added a runtime state lease so `serve` and destructive maintenance cannot operate on the same state directory concurrently. Retained Activity payloads sharing segments with pruned history are compacted before unreferenced old segments are removed, and repeat prune is idempotent.
24
+
25
+ ### Fixed
26
+
27
+ - Made routed `publicBaseUrl` path prefixes real inbound MCP/OAuth/health/App deployment boundaries, split setup network modes into safe loopback proxy versus direct LAN binds, and replaced permissive proxy trust with explicit trusted proxy sources. This fixes reverse-proxy `X-Forwarded-For` failures without enabling spoofable global `trust proxy` behavior.
28
+
7
29
  ## [0.9.3] - 2026-09-04
8
30
 
9
31
  ### Added
package/README.md CHANGED
@@ -55,8 +55,10 @@ http://127.0.0.1:7676/mcp
55
55
 
56
56
  If the MCP host cannot reach localhost, put ForgeRelay behind a public HTTPS
57
57
  tunnel or reverse proxy such as Cloudflare Tunnel, ngrok, Pinggy, Tailscale
58
- Funnel, or your own proxy. During setup, enter the public base URL before the
59
- final `/mcp`; routed prefixes are allowed:
58
+ Funnel, or your own proxy. Setup separates **Direct LAN** (`0.0.0.0`) from
59
+ **HTTPS reverse proxy / tunnel** (`127.0.0.1` with loopback-only proxy trust), so
60
+ you do not need to choose a bind address manually. During setup, enter the public
61
+ base URL before the final `/mcp`; routed prefixes are allowed:
60
62
 
61
63
  ```text
62
64
  https://your-tunnel-host.example.com/forgerelay/main
@@ -69,7 +71,11 @@ https://your-tunnel-host.example.com/forgerelay/main/mcp
69
71
  ```
70
72
 
71
73
  `publicBaseUrl` may also be an ordered list when multiple public entries are
72
- valid; each entry may use its own route and the first is canonical.
74
+ valid; each entry may use its own route and the first is canonical for generated
75
+ metadata/links. Every configured pathname is an accepted inbound operational route
76
+ boundary. If the only configured route is `/forgerelay/main`, MCP/OAuth/health/App
77
+ routes are served below that prefix rather than in parallel at naked `/mcp`,
78
+ `/authorize`, or `/healthz` paths.
73
79
 
74
80
  ForgeRelay uses an Owner-password OAuth approval flow. `forgerelay init` prints
75
81
  the password and stores it in the active config directory. New installations use:
@@ -216,7 +222,7 @@ Hook 是 ForgeRelay 的自动生命周期规则。首选方式是一个 Hook 一
216
222
  }
217
223
  ```
218
224
 
219
- 命中 `BeforeTool` 后,Hook 只快速验证 clean working tree(含 untracked)、tag 与 package version 一致,以及本地 tag 指向当前 HEAD;成功才继续原始 `git push`,失败则直接阻断。Hook 不运行、也不要求本地 CI。tag 推送后由 GitHub Actions 的 Linux/macOS/Windows 矩阵执行权威验证,全部通过后才进入发布。`npm run release:verify` 仅作为可选的本地云端复现工具。Hook 结果会回到 Agent,Agent 应向用户说明重要 Hook 是否通过或阻断了操作。`report:false` 可以隐藏不重要的成功报告,但阻断失败始终可见。
225
+ 命中 `BeforeTool` 后,普通 tag push 的 Hook 只快速验证 clean working tree(含 untracked)、tag 与 package version 一致,以及本地 tag 指向当前 HEAD;成功才继续原始 `git push`,失败则直接阻断。force release-tag push 始终拒绝。对于失败发布后显式执行的 `git push --delete origin vX.Y.Z`,Hook 仅在当前 HEAD 有有效 `release:verify` proof,且联网确认 npm 对应版本与 GitHub Release 都不存在时允许删除;任一不可撤回产物存在或远端状态无法确认都会拒绝。因此仅有 tag 时可以重建同版本,一旦 GitHub Release 或包仓库版本存在就进入不可变状态。tag 推送后由 GitHub Actions 的 Linux/macOS/Windows 矩阵执行权威验证,全部通过后才进入发布。Hook 结果会回到 Agent,Agent 应向用户说明重要 Hook 是否通过或阻断了操作。`report:false` 可以隐藏不重要的成功报告,但阻断失败始终可见。
220
226
 
221
227
  旧的 inline `hooks` 和聚合 `hooks.json` 仍兼容;新配置建议都用独立 `hooks/*.json` 文件。
222
228
 
package/dist/cli/init.js CHANGED
@@ -2,10 +2,13 @@ import { resolve } from "node:path";
2
2
  import * as prompts from "@clack/prompts";
3
3
  import { publicEndpointUrl } from "../mcp/oauth/public-url.js";
4
4
  import { expandHomePath } from "../mcp/filesystem/roots.js";
5
+ import { detectLauncherCommandShell, } from "../runtime/shell/command-shell-runtime.js";
6
+ import { seedShellInstructionFiles, shellInstructionFamiliesToSeed, } from "../runtime/instructions/shell-instructions.js";
5
7
  import { installManagedLanguageServers, installedManagedLanguageServers, managedLanguageServerOptions, } from "../lsp/runtime/managed-language-servers.js";
6
8
  import { generateInstanceId, generateOwnerToken, loadForgeRelayFiles, resolveSubagentsFlag, writeForgeRelayAuth, writeForgeRelayConfig, } from "../runtime/config/user-config.js";
7
- import { classifyClientFacingBaseUrl, compactPublicBaseUrlConfig, hasInsecureLanBaseUrl, isLoopbackBindAddress, normalizePublicBaseUrlsInput, SetupCancelledError, textPrompt, validateBindAddress, validateClientFacingBaseUrls, validatePort, } from "./setup-support.js";
8
- export async function runInit({ force }) {
9
+ import { classifyClientFacingBaseUrl, compactPublicBaseUrlConfig, hasInsecureLanBaseUrl, isLoopbackBindAddress, normalizePublicBaseUrlsInput, setupBindAddress, SetupCancelledError, textPrompt, validateHttpsProxyBaseUrls, validateLanClientFacingBaseUrls, validatePort, } from "./setup-support.js";
10
+ import { commandShellCompatibilityWarning, commandShellSetupOptions, customPinnedPreference, defaultCommandShellSetupChoice, followLauncherPreference, pinnedFamilyPreference, preservePinnedPreference, shellFamiliesForCustomSelection, } from "./shell/setup.js";
11
+ export async function runInit({ force, version }) {
9
12
  const files = loadForgeRelayFiles();
10
13
  if (!force && files.configExists && files.authExists) {
11
14
  prompts.log.info(`ForgeRelay is already configured at ${files.dir}`);
@@ -36,9 +39,11 @@ export async function runInit({ force }) {
36
39
  const existingPublicBaseUrls = Array.isArray(files.config.publicBaseUrl)
37
40
  ? files.config.publicBaseUrl
38
41
  : files.config.publicBaseUrl ? [files.config.publicBaseUrl] : [];
39
- const defaultNetworkMode = !isLoopbackBindAddress(files.config.host ?? "127.0.0.1") || existingPublicBaseUrls.length > 0
40
- ? "network"
41
- : "local";
42
+ const defaultNetworkMode = !isLoopbackBindAddress(files.config.host ?? "127.0.0.1")
43
+ ? "lan"
44
+ : existingPublicBaseUrls.some((baseUrl) => new URL(baseUrl).protocol === "https:")
45
+ ? "proxy"
46
+ : "local";
42
47
  const selectedMode = await prompts.select({
43
48
  message: "How should clients reach this ForgeRelay instance?",
44
49
  initialValue: defaultNetworkMode,
@@ -54,40 +59,53 @@ export async function runInit({ force }) {
54
59
  hint: "Bind to loopback; another ForgeRelay reaches it through an SSH tunnel.",
55
60
  },
56
61
  {
57
- value: "network",
58
- label: "LAN / HTTPS proxy",
59
- hint: "Expose through a LAN address or an HTTPS reverse proxy/tunnel.",
62
+ value: "lan",
63
+ label: "Direct LAN",
64
+ hint: "Bind to 0.0.0.0; clients connect directly over a trusted private LAN.",
65
+ },
66
+ {
67
+ value: "proxy",
68
+ label: "HTTPS reverse proxy / tunnel",
69
+ hint: "Bind to 127.0.0.1; a local trusted proxy publishes the HTTPS endpoint.",
60
70
  },
61
71
  ],
62
72
  });
63
73
  if (prompts.isCancel(selectedMode))
64
74
  throw new SetupCancelledError();
65
75
  const networkMode = selectedMode;
66
- let host = "127.0.0.1";
76
+ const host = setupBindAddress(networkMode);
67
77
  let publicBaseUrl = null;
68
78
  let clientFacingBaseUrls = [`http://127.0.0.1:${port}`];
69
- if (networkMode === "network") {
70
- const defaultHost = files.config.host && !isLoopbackBindAddress(files.config.host)
71
- ? files.config.host
72
- : "0.0.0.0";
73
- host = await textPrompt({
74
- message: "Which address should ForgeRelay bind to? Use 0.0.0.0 for direct LAN, or 127.0.0.1 behind a local reverse proxy.",
75
- placeholder: defaultHost,
76
- defaultValue: defaultHost,
77
- validate: validateBindAddress,
78
- });
79
- const defaultClientFacing = existingPublicBaseUrls.join(", ");
79
+ const trustedProxies = networkMode === "proxy" ? ["loopback"] : undefined;
80
+ if (networkMode === "lan" || networkMode === "proxy") {
81
+ const validateBaseUrls = networkMode === "lan"
82
+ ? validateLanClientFacingBaseUrls
83
+ : validateHttpsProxyBaseUrls;
84
+ const existingClientFacing = existingPublicBaseUrls.join(", ");
85
+ const defaultClientFacing = existingClientFacing && validateBaseUrls(existingClientFacing) === undefined
86
+ ? existingClientFacing
87
+ : "";
88
+ if (networkMode === "proxy") {
89
+ prompts.note([
90
+ "The public URL may include a path prefix, for example https://example.com/forgerelay/debug.",
91
+ "That prefix is the deployment route boundary, so MCP, OAuth, health, and App assets are served below it.",
92
+ ].join("\n"), "Routed public URL");
93
+ }
80
94
  clientFacingBaseUrls = normalizePublicBaseUrlsInput(await textPrompt({
81
95
  message: defaultClientFacing
82
96
  ? `What client-facing base URLs should clients use? Press Enter to keep ${defaultClientFacing}`
83
- : "What client-facing base URL should clients use?",
84
- placeholder: defaultClientFacing || `http://192.168.1.20:${port} or https://forge.example.com`,
97
+ : networkMode === "lan"
98
+ ? "What direct LAN base URL should clients use?"
99
+ : "What HTTPS public base URL should clients use?",
100
+ placeholder: defaultClientFacing || (networkMode === "lan"
101
+ ? `http://192.168.1.20:${port}`
102
+ : "https://example.com/forgerelay/debug"),
85
103
  defaultValue: defaultClientFacing,
86
- validate: validateClientFacingBaseUrls,
104
+ validate: validateBaseUrls,
87
105
  }));
88
106
  for (const baseUrl of clientFacingBaseUrls)
89
107
  classifyClientFacingBaseUrl(baseUrl);
90
- if (hasInsecureLanBaseUrl(clientFacingBaseUrls)) {
108
+ if (networkMode === "lan" && hasInsecureLanBaseUrl(clientFacingBaseUrls)) {
91
109
  prompts.note([
92
110
  "Plain HTTP does not encrypt the ForgeRelay Owner approval flow or MCP bearer tokens.",
93
111
  "Use this only on a trusted private LAN. Prefer SSH relay or HTTPS when the network is not fully trusted.",
@@ -101,6 +119,64 @@ export async function runInit({ force }) {
101
119
  }
102
120
  publicBaseUrl = compactPublicBaseUrlConfig(clientFacingBaseUrls);
103
121
  }
122
+ const launcherShell = detectLauncherCommandShell();
123
+ const selectedShellChoice = await prompts.select({
124
+ message: "Which command shell should Agent commands and Hooks use?",
125
+ initialValue: defaultCommandShellSetupChoice(files.config.commandShell),
126
+ options: commandShellSetupOptions(process.platform, files.config.commandShell, launcherShell),
127
+ });
128
+ if (prompts.isCancel(selectedShellChoice))
129
+ throw new SetupCancelledError();
130
+ let commandShell;
131
+ const shellChoice = selectedShellChoice;
132
+ if (shellChoice === "follow-launcher") {
133
+ commandShell = followLauncherPreference(files.config.commandShell, launcherShell);
134
+ }
135
+ else if (shellChoice === "keep-pinned") {
136
+ commandShell = preservePinnedPreference(files.config.commandShell);
137
+ }
138
+ else if (shellChoice === "custom") {
139
+ const customFamily = await prompts.select({
140
+ message: "Which command language does the custom executable implement?",
141
+ options: shellFamiliesForCustomSelection(),
142
+ });
143
+ if (prompts.isCancel(customFamily))
144
+ throw new SetupCancelledError();
145
+ const executable = await textPrompt({
146
+ message: "What is the command-shell executable path?",
147
+ placeholder: process.platform === "win32" ? "D:\\Portable\\PowerShell\\pwsh.exe" : "/opt/custom/bin/zsh",
148
+ defaultValue: "",
149
+ validate: (value) => value?.trim() ? undefined : "Enter an executable path.",
150
+ });
151
+ commandShell = customPinnedPreference(customFamily, executable);
152
+ }
153
+ else {
154
+ commandShell = pinnedFamilyPreference(shellChoice);
155
+ }
156
+ const shellWarning = commandShellCompatibilityWarning(commandShell.family);
157
+ if (shellWarning)
158
+ prompts.note(shellWarning, "Command shell compatibility");
159
+ const shellInstructionFamilies = shellInstructionFamiliesToSeed(process.platform, commandShell.family);
160
+ let shellInstructions = files.config.shellInstructions;
161
+ let shellInstructionSeedResults = [];
162
+ if (shellInstructionFamilies.length > 0) {
163
+ const shellInstructionsAnswer = await prompts.confirm({
164
+ message: "Load ForgeRelay-provided extra Instructions for this command shell?",
165
+ initialValue: files.config.shellInstructions !== false,
166
+ });
167
+ if (prompts.isCancel(shellInstructionsAnswer))
168
+ throw new SetupCancelledError();
169
+ shellInstructions = shellInstructionsAnswer === true;
170
+ shellInstructionSeedResults = await seedShellInstructionFiles({
171
+ configDir: files.dir,
172
+ version,
173
+ families: shellInstructionFamilies,
174
+ });
175
+ const failedShellInstructions = shellInstructionSeedResults.filter((result) => result.status === "failed");
176
+ if (failedShellInstructions.length > 0) {
177
+ prompts.note(failedShellInstructions.map((result) => `${result.family}: ${result.error ?? "download failed"}\n${result.sourceUrl}`).join("\n\n"), "Shell Instructions unavailable");
178
+ }
179
+ }
104
180
  const installedManaged = installedManagedLanguageServers(files.dir);
105
181
  const selectedManaged = await prompts.multiselect({
106
182
  message: "Which Language Servers should ForgeRelay manage with npm?",
@@ -135,13 +211,17 @@ export async function runInit({ force }) {
135
211
  }
136
212
  }
137
213
  const config = {
214
+ ...files.config,
138
215
  host,
139
216
  port,
140
217
  allowedRoots,
141
218
  publicBaseUrl,
142
219
  allowedHosts: files.config.allowedHosts,
220
+ trustedProxies,
143
221
  workflowInstructions: files.config.workflowInstructions,
144
222
  appendInstructions: files.config.appendInstructions,
223
+ commandShell,
224
+ shellInstructions,
145
225
  subagents: resolveSubagentsFlag(files.config),
146
226
  languageServers: files.config.languageServers,
147
227
  allowAgentLanguageServerInstall,
@@ -157,6 +237,13 @@ export async function runInit({ force }) {
157
237
  `Config: ${configPath}`,
158
238
  `Auth: ${authPath}`,
159
239
  `Bind: http://${config.host}:${config.port}`,
240
+ `Command shell: ${commandShell.mode} ${commandShell.family} (${commandShell.executable})`,
241
+ ...(shellInstructionFamilies.length > 0
242
+ ? [
243
+ `Shell Instructions: ${shellInstructions ? "enabled" : "disabled"}`,
244
+ ...shellInstructionSeedResults.map((result) => `Shell Instructions ${result.family}: ${result.status} (${result.path})`),
245
+ ]
246
+ : []),
160
247
  ...clientFacingBaseUrls.map((baseUrl, index) => `${index === 0 ? "Client-facing MCP URL" : "Client-facing MCP alias"}: ${publicEndpointUrl(baseUrl, "mcp").toString()}`),
161
248
  ];
162
249
  if (networkMode === "ssh") {