@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.
- package/CHANGELOG.md +22 -0
- package/README.md +10 -4
- package/dist/cli/init.js +111 -24
- package/dist/cli/maintenance-prune.js +479 -0
- package/dist/cli/maintenance-retention.js +93 -0
- package/dist/cli/maintenance.js +598 -0
- package/dist/cli/setup-support.js +21 -0
- package/dist/cli/shell/setup.js +99 -0
- package/dist/cli.js +72 -16
- package/dist/mcp/hooks/hooks.js +6 -3
- package/dist/mcp/oauth/public-url.js +3 -0
- package/dist/mcp/oauth/router.js +14 -9
- package/dist/mcp/process/process-platform.js +24 -26
- package/dist/mcp/process/process-sessions.js +6 -3
- package/dist/mcp/server/transport/http-server.js +17 -9
- package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +6 -0
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +1 -1
- package/dist/mcp/server-instructions.js +10 -1
- package/dist/runtime/config/config.js +60 -6
- package/dist/runtime/instructions/shell-instructions.js +100 -0
- package/dist/runtime/security/runtime-privilege.js +120 -0
- package/dist/runtime/shell/command-shell-runtime.js +235 -0
- package/dist/runtime/state/runtime-lease.js +109 -0
- package/dist/ui/.vite/manifest.json +276 -276
- package/dist/ui/activity-panel-app.html +2 -2
- package/dist/ui/assets/{activity-panel-app-Sl1Jro8v.js → activity-panel-app-BkODhDLg.js} +1 -1
- package/dist/ui/assets/{activity-panel-app-7Xe0alhm.css → activity-panel-app-DkfJzTZ3.css} +1 -1
- package/dist/ui/assets/{angular-html-B_yn91wW.js → angular-html-DjuELMdF.js} +1 -1
- package/dist/ui/assets/{angular-ts-DSgtloKz.js → angular-ts-2EXVZIIX.js} +1 -1
- package/dist/ui/assets/{apl-NatWtF8X.js → apl-Bb4IeT5V.js} +1 -1
- package/dist/ui/assets/{astro-BYFEVBuN.js → astro-Bjavu3nT.js} +1 -1
- package/dist/ui/assets/{blade-DoO7yvMR.js → blade-DKW2uHZo.js} +1 -1
- package/dist/ui/assets/{c-Rl2hMCcg.js → c-oLeHmzbj.js} +1 -1
- package/dist/ui/assets/{cobol-ByC5mWiB.js → cobol-CcqFMr5T.js} +1 -1
- package/dist/ui/assets/{coffee-Dez77Qov.js → coffee-BA20zqRF.js} +1 -1
- package/dist/ui/assets/{cpp-DtaAQT9h.js → cpp-D750WmDa.js} +1 -1
- package/dist/ui/assets/{crystal-Bgmbf8ec.js → crystal-Cv5I6OMI.js} +1 -1
- package/dist/ui/assets/{css-C1tjBtVN.js → css-CjPPWPeH.js} +1 -1
- package/dist/ui/assets/{edge-DeH6u8Wg.js → edge-aYRuudEZ.js} +1 -1
- package/dist/ui/assets/{elixir-_H3KnmWT.js → elixir-ChLitmz7.js} +1 -1
- package/dist/ui/assets/{elm-CsCe3U28.js → elm-Fs7L7u36.js} +1 -1
- package/dist/ui/assets/{erb-CbM-L2g8.js → erb-B-qXHxN1.js} +1 -1
- package/dist/ui/assets/{git-rebase-D0s5pDh9.js → git-rebase-DOVN5OEg.js} +1 -1
- package/dist/ui/assets/{glimmer-js-BfOToq_A.js → glimmer-js-BFt_vCTW.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-CCUOkL2K.js → glimmer-ts-ufCHlR6C.js} +1 -1
- package/dist/ui/assets/{glsl-DH4ec9dE.js → glsl-Ex4JXGuC.js} +1 -1
- package/dist/ui/assets/{graphql-DilAMBoI.js → graphql-pQPXtBk5.js} +1 -1
- package/dist/ui/assets/{hack-BDkNDZlF.js → hack-B5hDguhd.js} +1 -1
- package/dist/ui/assets/{haml-D_QBp6K7.js → haml-BrNeI29U.js} +1 -1
- package/dist/ui/assets/{handlebars-DePUrTpC.js → handlebars-DCqTsL9-.js} +1 -1
- package/dist/ui/assets/{heavy-payload-DOwZpO6n.js → heavy-payload-DsmmHgvt.js} +3 -3
- package/dist/ui/assets/{html-By4dNnM4.js → html-D8k0CC9V.js} +1 -1
- package/dist/ui/assets/{html-derivative-DDw9Ef4Q.js → html-derivative-CC8flWmC.js} +1 -1
- package/dist/ui/assets/{http-B0QxWc9M.js → http-BYnWqNep.js} +1 -1
- package/dist/ui/assets/{hurl-vMQ4lda1.js → hurl-BND-z85s.js} +1 -1
- package/dist/ui/assets/{java-DYRe2lWG.js → java-Dug0oB9T.js} +1 -1
- package/dist/ui/assets/{javascript-CxyGqwcL.js → javascript-oDJ7WITC.js} +1 -1
- package/dist/ui/assets/{jinja-JUwV-fZt.js → jinja-Cv0t_F1U.js} +1 -1
- package/dist/ui/assets/{jison-7ZqCEvS8.js → jison-Car3MF55.js} +1 -1
- package/dist/ui/assets/{json-dbltgiiA.js → json-CEViCsf4.js} +1 -1
- package/dist/ui/assets/{jsx-oDopT7YQ.js → jsx-BfZ0zgSH.js} +1 -1
- package/dist/ui/assets/{julia-CkKiNSav.js → julia-CmX0b3Tn.js} +1 -1
- package/dist/ui/assets/{just-DuqCAKJ3.js → just-BuugLCVk.js} +1 -1
- package/dist/ui/assets/{latex-sudcRDSn.js → latex-FpTXqO03.js} +1 -1
- package/dist/ui/assets/{liquid-Di-08Js7.js → liquid-B37OzUFu.js} +1 -1
- package/dist/ui/assets/{lua-YlqdMoV0.js → lua-BHjcb8oq.js} +1 -1
- package/dist/ui/assets/{marko-CJRNyJDb.js → marko-CApj_fsH.js} +1 -1
- package/dist/ui/assets/{mdc-t12CzZHs.js → mdc-Ed5vQWq8.js} +1 -1
- package/dist/ui/assets/{nginx-57-b4XwT.js → nginx-u_rVEBox.js} +1 -1
- package/dist/ui/assets/{nim-BzrswYQa.js → nim-Ba9EdchT.js} +1 -1
- package/dist/ui/assets/{perl-D9JR_iIh.js → perl-BpU_6Wvz.js} +1 -1
- package/dist/ui/assets/{php-Py2gFHil.js → php-bKabofky.js} +1 -1
- package/dist/ui/assets/{pug-PzLLy0h1.js → pug-C_aLNkqe.js} +1 -1
- package/dist/ui/assets/{qml-By8fwgV4.js → qml-D9VisnRy.js} +1 -1
- package/dist/ui/assets/{r-CcezTbLq.js → r-CYqB1sLx.js} +1 -1
- package/dist/ui/assets/{razor-dG-vdhL_.js → razor-DNJKI8Rs.js} +1 -1
- package/dist/ui/assets/{regexp-BlsQ-N_8.js → regexp-DHrTHXkz.js} +1 -1
- package/dist/ui/assets/{rst-C2awDCat.js → rst-CbNdSFHv.js} +1 -1
- package/dist/ui/assets/{ruby-zA2W_49d.js → ruby-FyKG-pkF.js} +1 -1
- package/dist/ui/assets/{sas-D49bhyaN.js → sas-BbSPDl3X.js} +1 -1
- package/dist/ui/assets/{scss-BQb-cp01.js → scss-2nX9g6gD.js} +1 -1
- package/dist/ui/assets/{shellscript-Dga4H_Ni.js → shellscript-CmON6x2x.js} +1 -1
- package/dist/ui/assets/{shellsession-n3-5CqcF.js → shellsession-DtqIGlAX.js} +1 -1
- package/dist/ui/assets/{soy-XYBOf_BA.js → soy-BzUJMmJT.js} +1 -1
- package/dist/ui/assets/{sql-ET4u5Cpl.js → sql-CgVcFZmb.js} +1 -1
- package/dist/ui/assets/{stata-CLHcJHO_.js → stata-CNitlMot.js} +1 -1
- package/dist/ui/assets/{surrealql-BdjQ60T4.js → surrealql-BA8-xsRZ.js} +1 -1
- package/dist/ui/assets/{svelte-B2RPtKJe.js → svelte-El02yAxv.js} +1 -1
- package/dist/ui/assets/{templ-CrByPW_W.js → templ-DeUQ84aR.js} +1 -1
- package/dist/ui/assets/{tex-DZBkg363.js → tex-Cu-FqpPS.js} +1 -1
- package/dist/ui/assets/{ts-tags-DJR3Gphb.js → ts-tags-X42N6TGA.js} +1 -1
- package/dist/ui/assets/{tsx-CajzROyy.js → tsx-C1aPp9rY.js} +1 -1
- package/dist/ui/assets/{twig-BZjk5Zda.js → twig-Cj1Q4SQc.js} +1 -1
- package/dist/ui/assets/{typescript-B-EWToLj.js → typescript-Du9lVt1G.js} +1 -1
- package/dist/ui/assets/{vue-D_rHCCr8.js → vue-_uMGbFv8.js} +1 -1
- package/dist/ui/assets/{vue-html-CC4WlvVt.js → vue-html-D61naG-Q.js} +1 -1
- package/dist/ui/assets/{vue-vine-BgNMVgpk.js → vue-vine-CkFae4yV.js} +1 -1
- package/dist/ui/assets/{xml-BI86-WbH.js → xml-Cbwsc6ws.js} +1 -1
- package/dist/ui/assets/{xsl-DXeLZV4J.js → xsl-NyBx72XB.js} +1 -1
- package/dist/ui/assets/{yaml-60_PFlWa.js → yaml-Cgb6Uq9u.js} +1 -1
- package/dist/workspaces/context.js +32 -5
- package/dist/workspaces/presentation/workspace-presentation.js +1 -0
- package/dist/workspaces/sessions.js +2 -0
- package/dist/workspaces.js +2 -1
- package/docs/configuration.md +101 -14
- package/package.json +2 -2
- package/scripts/release/release-gate.test.mjs +1 -0
- package/scripts/release-proof.mjs +85 -9
- 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.
|
|
59
|
-
|
|
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`
|
|
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,
|
|
8
|
-
|
|
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")
|
|
40
|
-
? "
|
|
41
|
-
: "
|
|
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: "
|
|
58
|
-
label: "LAN
|
|
59
|
-
hint: "
|
|
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
|
-
|
|
76
|
+
const host = setupBindAddress(networkMode);
|
|
67
77
|
let publicBaseUrl = null;
|
|
68
78
|
let clientFacingBaseUrls = [`http://127.0.0.1:${port}`];
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
:
|
|
84
|
-
|
|
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:
|
|
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") {
|