@akira-tl/forgerelay 1.1.2 → 1.2.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 (170) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +14 -0
  3. package/dist/activity/runtime/mcp-query-tools.js +15 -5
  4. package/dist/cli/config/inspect.js +331 -0
  5. package/dist/cli/config/migrate.js +351 -0
  6. package/dist/cli/init/setup-config.js +41 -0
  7. package/dist/cli/init.js +128 -130
  8. package/dist/cli/maintenance.js +15 -5
  9. package/dist/cli/mcp/external-mcp.js +14 -10
  10. package/dist/cli/mcp/status.js +11 -6
  11. package/dist/cli.js +40 -7
  12. package/dist/lsp/language-server-config.js +66 -144
  13. package/dist/lsp/runtime/manager.js +35 -4
  14. package/dist/mcp/hooks/config.js +141 -0
  15. package/dist/mcp/hooks/external-mcp-transform.js +22 -13
  16. package/dist/mcp/hooks/hook-cli.js +62 -52
  17. package/dist/mcp/hooks/hooks.js +111 -85
  18. package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +8 -5
  19. package/dist/mcp/operations/external-mcp/external-mcp.js +9 -2
  20. package/dist/mcp/server/core/capability-support.js +7 -0
  21. package/dist/runtime/config/config.js +142 -64
  22. package/dist/runtime/config/definition/catalog.js +12 -0
  23. package/dist/runtime/config/definition/definition.js +115 -0
  24. package/dist/runtime/config/definition/external-mcp.js +58 -0
  25. package/dist/runtime/config/definition/general-config.js +258 -0
  26. package/dist/runtime/config/definition/language-servers.js +116 -0
  27. package/dist/runtime/config/definition/schema.js +113 -0
  28. package/dist/runtime/config/definition/types.js +2 -0
  29. package/dist/runtime/config/external-mcp-auth-store.js +105 -29
  30. package/dist/runtime/config/external-mcp-config.js +39 -0
  31. package/dist/runtime/config/external-mcp-registry.js +284 -106
  32. package/dist/runtime/config/inspection/live.js +77 -0
  33. package/dist/runtime/config/resolution/general.js +93 -0
  34. package/dist/runtime/config/resolution/hooks.js +315 -0
  35. package/dist/runtime/config/resolution/language-servers.js +237 -0
  36. package/dist/runtime/config/resolution/project-sources.js +159 -0
  37. package/dist/runtime/config/resolution/resolver.js +485 -0
  38. package/dist/runtime/config/resolution/types.js +1 -0
  39. package/dist/runtime/config/runtime/config-runtime.js +74 -0
  40. package/dist/runtime/config/runtime/source-refresh.js +263 -0
  41. package/dist/runtime/config/user-config.js +38 -31
  42. package/dist/runtime/security/project-execution-trust.js +34 -0
  43. package/dist/runtime/testing/server-fixture.js +9 -8
  44. package/dist/server.js +10 -6
  45. package/dist/subagents/profiles.js +336 -53
  46. package/dist/subagents/sessions/execution.js +6 -35
  47. package/dist/subagents/sessions/manager.js +14 -2
  48. package/dist/ui/.vite/manifest.json +275 -275
  49. package/dist/ui/activity-panel-app.html +1 -1
  50. package/dist/ui/assets/{activity-panel-app-Bw7ZFX2g.js → activity-panel-app-BeDiwd7D.js} +3 -3
  51. package/dist/ui/assets/{angular-html-DeUNP12X.js → angular-html-DuoCE7nn.js} +1 -1
  52. package/dist/ui/assets/{angular-ts-B3v8FKi_.js → angular-ts-D5tGk0hL.js} +1 -1
  53. package/dist/ui/assets/{apl-Bs3iQm4P.js → apl-lciOTPbw.js} +1 -1
  54. package/dist/ui/assets/{astro-D6GHecBY.js → astro-vJbqfNnJ.js} +1 -1
  55. package/dist/ui/assets/{blade-BMNbVOHv.js → blade-C7655Q4K.js} +1 -1
  56. package/dist/ui/assets/{c-BquuA6bv.js → c-DdHp1Jrw.js} +1 -1
  57. package/dist/ui/assets/{cobol-BuZ6kfps.js → cobol-DgvzQxZP.js} +1 -1
  58. package/dist/ui/assets/{coffee-DgC5uBzL.js → coffee-BIjVUOUd.js} +1 -1
  59. package/dist/ui/assets/{cpp-BdvWqCaB.js → cpp-CU8tGzvI.js} +1 -1
  60. package/dist/ui/assets/{crystal-0Q5qBvNI.js → crystal-CUQAwsg9.js} +1 -1
  61. package/dist/ui/assets/{css-Be_T8idh.js → css-DtMTqD3F.js} +1 -1
  62. package/dist/ui/assets/{edge-DaGLdgh_.js → edge-DoCX9RnL.js} +1 -1
  63. package/dist/ui/assets/{elixir-CMgiFIzB.js → elixir-DJxV9_U6.js} +1 -1
  64. package/dist/ui/assets/{elm-B-ufMlMC.js → elm-DIgWqK4r.js} +1 -1
  65. package/dist/ui/assets/{erb-CaGiOZM-.js → erb-BHxP0c_e.js} +1 -1
  66. package/dist/ui/assets/{git-rebase-CpwUWCzK.js → git-rebase-HbI_Vo8d.js} +1 -1
  67. package/dist/ui/assets/{glimmer-js-BRxccvnE.js → glimmer-js-CthY4Ty_.js} +1 -1
  68. package/dist/ui/assets/{glimmer-ts-CgQbPzlT.js → glimmer-ts-B2-_A1Xz.js} +1 -1
  69. package/dist/ui/assets/{glsl-D7JmHFr0.js → glsl-D9V9gq98.js} +1 -1
  70. package/dist/ui/assets/{graphql-C8n0eUBK.js → graphql-CddeAaS3.js} +1 -1
  71. package/dist/ui/assets/{hack-lVN7KVQv.js → hack-D_loGkOT.js} +1 -1
  72. package/dist/ui/assets/{haml-h18lG0pS.js → haml-BoWDczd0.js} +1 -1
  73. package/dist/ui/assets/{handlebars-lRFjRPfj.js → handlebars-ZoSOvw9y.js} +1 -1
  74. package/dist/ui/assets/{heavy-payload-Cqx9MOkH.js → heavy-payload-DO34LwoZ.js} +3 -3
  75. package/dist/ui/assets/{html-DtILqTE1.js → html-CE-obXVo.js} +1 -1
  76. package/dist/ui/assets/{html-derivative-CSTSmPup.js → html-derivative-Cd-smAbW.js} +1 -1
  77. package/dist/ui/assets/{http-DNI8fsbY.js → http-CZrkbrSl.js} +1 -1
  78. package/dist/ui/assets/{hurl-DgRi60JG.js → hurl-CxTuxBt1.js} +1 -1
  79. package/dist/ui/assets/{java-CW9pZ5bh.js → java-BshLjCat.js} +1 -1
  80. package/dist/ui/assets/{javascript-Cvg2UrWL.js → javascript-D2pxQTiX.js} +1 -1
  81. package/dist/ui/assets/{jinja-CZw_S1FB.js → jinja-kbfPP7-F.js} +1 -1
  82. package/dist/ui/assets/{jison-C56u_FZv.js → jison-C2mt9l1M.js} +1 -1
  83. package/dist/ui/assets/{json-DW2UTv1q.js → json-DU_rg_AU.js} +1 -1
  84. package/dist/ui/assets/{jsx-Zj5gobdD.js → jsx-D_8aQMlx.js} +1 -1
  85. package/dist/ui/assets/{julia-Cvcrre-L.js → julia-BmefTaVV.js} +1 -1
  86. package/dist/ui/assets/{just-8CZ2u6Tj.js → just-DXV8vk1S.js} +1 -1
  87. package/dist/ui/assets/{latex-D289AXWG.js → latex-Cjm2MvGW.js} +1 -1
  88. package/dist/ui/assets/{liquid-CFEX3SkZ.js → liquid-DyQDlo0u.js} +1 -1
  89. package/dist/ui/assets/{lua-BKpJ0ybO.js → lua-B2A1vzzy.js} +1 -1
  90. package/dist/ui/assets/{marko-D9maZ0a6.js → marko-B9vOkXOU.js} +1 -1
  91. package/dist/ui/assets/{mdc-rM9l0qWR.js → mdc-D42m6spU.js} +1 -1
  92. package/dist/ui/assets/{nginx-C9w6p2C9.js → nginx-TpfVxSUY.js} +1 -1
  93. package/dist/ui/assets/{nim-BRePHk0J.js → nim-CJr5T1FK.js} +1 -1
  94. package/dist/ui/assets/{perl-CskTmiPQ.js → perl-cJZcsdm9.js} +1 -1
  95. package/dist/ui/assets/{php-DCU_FvNI.js → php-CSZL5KRT.js} +1 -1
  96. package/dist/ui/assets/{pug-XLaVxTvv.js → pug-B1eMKzuA.js} +1 -1
  97. package/dist/ui/assets/{qml-CaONKSl-.js → qml-Dl2_7zc9.js} +1 -1
  98. package/dist/ui/assets/{r-BCVR0ZE3.js → r-F0aaO9gG.js} +1 -1
  99. package/dist/ui/assets/{razor-DDnGXxnU.js → razor-BPnkSMCw.js} +1 -1
  100. package/dist/ui/assets/{regexp-DWGTYYVx.js → regexp-BnGHMFbv.js} +1 -1
  101. package/dist/ui/assets/{rst-Dz3GWvQm.js → rst-B3CO7WYO.js} +1 -1
  102. package/dist/ui/assets/{ruby-DC3jGtMY.js → ruby-B6VMzT1T.js} +1 -1
  103. package/dist/ui/assets/{sas-7mNpvsjZ.js → sas-Df2lHVuU.js} +1 -1
  104. package/dist/ui/assets/{scss-uBsoCJVy.js → scss-4XxI6sgu.js} +1 -1
  105. package/dist/ui/assets/{shellscript-BCN9N_Jf.js → shellscript-DYASCN4r.js} +1 -1
  106. package/dist/ui/assets/{shellsession-Hsw46VXV.js → shellsession-Br6AGJO_.js} +1 -1
  107. package/dist/ui/assets/{soy-D6KV69HU.js → soy-p3ZNq1We.js} +1 -1
  108. package/dist/ui/assets/{sql-CVzVQkJh.js → sql-CAdv2OXL.js} +1 -1
  109. package/dist/ui/assets/{stata-BwUjXNhI.js → stata-Dx7yUbG_.js} +1 -1
  110. package/dist/ui/assets/{surrealql-CgloWqRa.js → surrealql-6YsE8HQa.js} +1 -1
  111. package/dist/ui/assets/{svelte-DcC3n6IC.js → svelte-YgzedwpD.js} +1 -1
  112. package/dist/ui/assets/{templ-DOFfnWTJ.js → templ-Bahqx3uD.js} +1 -1
  113. package/dist/ui/assets/{tex-ln89VPaX.js → tex-o6fTQjkH.js} +1 -1
  114. package/dist/ui/assets/{ts-tags-CWMzCstH.js → ts-tags-BO-Co0Ju.js} +1 -1
  115. package/dist/ui/assets/{tsx-DgzfiGeK.js → tsx-DtQe5AoH.js} +1 -1
  116. package/dist/ui/assets/{twig-Bi2itZL9.js → twig-BcUs6Kqc.js} +1 -1
  117. package/dist/ui/assets/{typescript-CQ24f4zW.js → typescript-BPtM8h4u.js} +1 -1
  118. package/dist/ui/assets/{vue-BS8jE5u6.js → vue-5KkGUzJJ.js} +1 -1
  119. package/dist/ui/assets/{vue-html-o38xqhyB.js → vue-html-Df_Xfsz9.js} +1 -1
  120. package/dist/ui/assets/{vue-vine-D2NvKv50.js → vue-vine-hUIe_u_Y.js} +1 -1
  121. package/dist/ui/assets/{xml-_44rx9_2.js → xml-C3Jy7as5.js} +1 -1
  122. package/dist/ui/assets/{xsl-B5QWQPoi.js → xsl-BnF9OuFo.js} +1 -1
  123. package/dist/ui/assets/{yaml-CKL2pEWe.js → yaml-DyrFgCou.js} +1 -1
  124. package/dist/workspaces/conversation-test-support.js +3 -11
  125. package/dist/workspaces/sessions.js +2 -0
  126. package/dist/workspaces/state/project-context.js +221 -0
  127. package/dist/workspaces.js +3 -1
  128. package/docs/chatgpt-coding-workflow.md +8 -3
  129. package/docs/configuration.md +99 -41
  130. package/docs/gotchas.md +24 -5
  131. package/docs/security.md +14 -3
  132. package/docs/setup.md +12 -7
  133. package/package.json +12 -3
  134. package/schemas/v1/config.project-local.schema.json +14 -0
  135. package/schemas/v1/config.project.schema.json +14 -0
  136. package/schemas/v1/config.user.schema.json +538 -0
  137. package/schemas/v1/hooks.project-local.schema.json +118 -0
  138. package/schemas/v1/hooks.project.schema.json +118 -0
  139. package/schemas/v1/hooks.user.schema.json +118 -0
  140. package/schemas/v1/language-servers.project-local.schema.json +230 -0
  141. package/schemas/v1/language-servers.project.schema.json +230 -0
  142. package/schemas/v1/language-servers.user.schema.json +230 -0
  143. package/schemas/v1/mcp.project-local.schema.json +143 -0
  144. package/schemas/v1/mcp.project.schema.json +143 -0
  145. package/schemas/v1/mcp.user.schema.json +143 -0
  146. package/scripts/ci/architecture.mjs +54 -4
  147. package/scripts/ci/cmd-acceptance.mjs +9 -9
  148. package/scripts/ci/config-v2-product-acceptance.mjs +639 -0
  149. package/scripts/ci/gates/acceptance-artifact.mjs +66 -0
  150. package/scripts/ci/gates/core-contract.mjs +27 -0
  151. package/scripts/ci/gates/platform-tests.mjs +38 -0
  152. package/scripts/ci/gates/prepare-windows-product.mjs +57 -0
  153. package/scripts/ci/gates/test-shard.mjs +71 -0
  154. package/scripts/ci/powershell51-acceptance.mjs +9 -9
  155. package/scripts/ci/pwsh-acceptance.mjs +9 -9
  156. package/scripts/ci/verify.mjs +2 -0
  157. package/scripts/ci/windows-product-acceptance.mjs +9 -10
  158. package/scripts/config/schema-text.mjs +4 -0
  159. package/scripts/config/schema-text.test.mjs +18 -0
  160. package/scripts/config/schema.mjs +74 -0
  161. package/scripts/lsp/interop.mjs +4 -1
  162. package/scripts/release/pack.mjs +64 -4
  163. package/scripts/release/release-gate.test.mjs +93 -27
  164. package/scripts/release-parity.mjs +19 -2
  165. package/templates/instructions/cmd.md +12 -0
  166. package/templates/instructions/fish.md +11 -0
  167. package/templates/instructions/powershell.md +14 -0
  168. package/templates/instructions/pwsh.md +14 -0
  169. package/templates/instructions/zsh.md +11 -0
  170. package/dist/runtime/instructions/powershell-skill.js +0 -65
package/dist/cli/init.js CHANGED
@@ -4,14 +4,16 @@ import { publicEndpointUrl } from "../mcp/oauth/public-url.js";
4
4
  import { expandHomePath } from "../mcp/filesystem/roots.js";
5
5
  import { detectLauncherCommandShell, } from "../runtime/shell/command-shell-runtime.js";
6
6
  import { seedShellInstructionFiles, shellInstructionFamiliesToSeed, } from "../runtime/instructions/shell-instructions.js";
7
- import { seedPowerShellSkill, } from "../runtime/instructions/powershell-skill.js";
8
7
  import { installManagedLanguageServers, installedManagedLanguageServers, managedLanguageServerOptions, } from "../lsp/runtime/managed-language-servers.js";
9
- import { generateInstanceId, generateOwnerToken, loadForgeRelayFiles, resolveSubagentsFlag, writeForgeRelayAuth, writeForgeRelayConfig, } from "../runtime/config/user-config.js";
10
- import { classifyClientFacingBaseUrl, compactPublicBaseUrlConfig, hasInsecureLanBaseUrl, isLoopbackBindAddress, normalizePublicBaseUrlsInput, setupBindAddress, SetupCancelledError, textPrompt, validateHttpsProxyBaseUrls, validateLanClientFacingBaseUrls, validatePort, } from "./setup-support.js";
8
+ import { generateInstanceId, generateOwnerToken, loadForgeRelayFiles, writeForgeRelayAuth, writeForgeRelayConfig, } from "../runtime/config/user-config.js";
9
+ import { generalConfigDefinition } from "../runtime/config/definition/general-config.js";
10
+ import { configSchemaId } from "../runtime/config/definition/schema.js";
11
+ import { classifyClientFacingBaseUrl, compactPublicBaseUrlConfig, hasInsecureLanBaseUrl, isLoopbackBindAddress, normalizePublicBaseUrlsInput, SetupCancelledError, textPrompt, validateHttpsProxyBaseUrls, validateLanClientFacingBaseUrls, validatePort, } from "./setup-support.js";
11
12
  import { commandShellCompatibilityWarning, commandShellSetupOptions, customPinnedPreference, defaultCommandShellSetupChoice, followLauncherPreference, pinnedFamilyPreference, preservePinnedPreference, shellFamiliesForCustomSelection, } from "./shell/setup.js";
12
- export async function runInit({ force, version }) {
13
+ import { applySetupConfig } from "./init/setup-config.js";
14
+ export async function runInit({ force, advanced, version }) {
13
15
  const files = loadForgeRelayFiles();
14
- if (!force && files.configExists && files.authExists) {
16
+ if (!force && !advanced && files.configExists && files.authExists) {
15
17
  prompts.log.info(`ForgeRelay is already configured at ${files.dir}`);
16
18
  prompts.log.info("Run `forgerelay init --force` to update it.");
17
19
  return;
@@ -30,13 +32,14 @@ export async function runInit({ force, version }) {
30
32
  .map((root) => resolve(expandHomePath(root.trim())))
31
33
  .filter(Boolean);
32
34
  const defaultPort = String(files.config.port ?? 7676);
33
- const portAnswer = await textPrompt({
34
- message: `Which local port should ForgeRelay use? Press Enter to use ${defaultPort}`,
35
- placeholder: defaultPort,
36
- defaultValue: defaultPort,
37
- validate: validatePort,
38
- });
39
- const port = Number(portAnswer);
35
+ const port = advanced
36
+ ? Number(await textPrompt({
37
+ message: `Which local port should ForgeRelay use? Press Enter to use ${defaultPort}`,
38
+ placeholder: defaultPort,
39
+ defaultValue: defaultPort,
40
+ validate: validatePort,
41
+ }))
42
+ : Number(defaultPort);
40
43
  const existingPublicBaseUrls = Array.isArray(files.config.publicBaseUrl)
41
44
  ? files.config.publicBaseUrl
42
45
  : files.config.publicBaseUrl ? [files.config.publicBaseUrl] : [];
@@ -74,10 +77,8 @@ export async function runInit({ force, version }) {
74
77
  if (prompts.isCancel(selectedMode))
75
78
  throw new SetupCancelledError();
76
79
  const networkMode = selectedMode;
77
- const host = setupBindAddress(networkMode);
78
- let publicBaseUrl = null;
80
+ let publicBaseUrl;
79
81
  let clientFacingBaseUrls = [`http://127.0.0.1:${port}`];
80
- const trustedProxies = networkMode === "proxy" ? ["loopback"] : undefined;
81
82
  if (networkMode === "lan" || networkMode === "proxy") {
82
83
  const validateBaseUrls = networkMode === "lan"
83
84
  ? validateLanClientFacingBaseUrls
@@ -120,123 +121,118 @@ export async function runInit({ force, version }) {
120
121
  }
121
122
  publicBaseUrl = compactPublicBaseUrlConfig(clientFacingBaseUrls);
122
123
  }
123
- const launcherShell = detectLauncherCommandShell();
124
- const selectedShellChoice = await prompts.select({
125
- message: "Which command shell should Agent commands and Hooks use?",
126
- initialValue: defaultCommandShellSetupChoice(files.config.commandShell),
127
- options: commandShellSetupOptions(process.platform, files.config.commandShell, launcherShell),
128
- });
129
- if (prompts.isCancel(selectedShellChoice))
130
- throw new SetupCancelledError();
131
- let commandShell;
132
- const shellChoice = selectedShellChoice;
133
- if (shellChoice === "follow-launcher") {
134
- commandShell = followLauncherPreference(files.config.commandShell, launcherShell);
135
- }
136
- else if (shellChoice === "keep-pinned") {
137
- commandShell = preservePinnedPreference(files.config.commandShell);
138
- }
139
- else if (shellChoice === "custom") {
140
- const customFamily = await prompts.select({
141
- message: "Which command language does the custom executable implement?",
142
- options: shellFamiliesForCustomSelection(),
143
- });
144
- if (prompts.isCancel(customFamily))
145
- throw new SetupCancelledError();
146
- const executable = await textPrompt({
147
- message: "What is the command-shell executable path?",
148
- placeholder: process.platform === "win32" ? "D:\\Portable\\PowerShell\\pwsh.exe" : "/opt/custom/bin/zsh",
149
- defaultValue: "",
150
- validate: (value) => value?.trim() ? undefined : "Enter an executable path.",
151
- });
152
- commandShell = customPinnedPreference(customFamily, executable);
153
- }
154
- else {
155
- commandShell = pinnedFamilyPreference(shellChoice);
156
- }
157
- const shellWarning = commandShellCompatibilityWarning(commandShell.family);
158
- if (shellWarning)
159
- prompts.note(shellWarning, "Command shell compatibility");
160
- const shellInstructionFamilies = shellInstructionFamiliesToSeed(process.platform, commandShell.family);
161
- let shellInstructions = files.config.shellInstructions;
124
+ let advancedSelection;
125
+ let commandShell = files.config.commandShell;
126
+ let shellInstructionFamilies = [];
127
+ let shellInstructions = files.config.shellInstructions ?? false;
162
128
  let shellInstructionSeedResults = [];
163
- let powerShellSkillSeedResult;
164
- if (shellInstructionFamilies.length > 0) {
165
- const shellInstructionsAnswer = await prompts.confirm({
166
- message: "Load ForgeRelay-provided extra Instructions for this command shell?",
167
- initialValue: files.config.shellInstructions !== false,
129
+ if (advanced) {
130
+ const launcherShell = detectLauncherCommandShell();
131
+ const selectedShellChoice = await prompts.select({
132
+ message: "Which command shell should Agent commands and Hooks use?",
133
+ initialValue: defaultCommandShellSetupChoice(files.config.commandShell),
134
+ options: commandShellSetupOptions(process.platform, files.config.commandShell, launcherShell),
168
135
  });
169
- if (prompts.isCancel(shellInstructionsAnswer))
136
+ if (prompts.isCancel(selectedShellChoice))
170
137
  throw new SetupCancelledError();
171
- shellInstructions = shellInstructionsAnswer === true;
172
- shellInstructionSeedResults = await seedShellInstructionFiles({
173
- configDir: files.dir,
174
- version,
175
- families: shellInstructionFamilies,
176
- });
177
- const failedShellInstructions = shellInstructionSeedResults.filter((result) => result.status === "failed");
178
- if (failedShellInstructions.length > 0) {
179
- prompts.note(failedShellInstructions.map((result) => `${result.family}: ${result.error ?? "download failed"}\n${result.sourceUrl}`).join("\n\n"), "Shell Instructions unavailable");
138
+ const shellChoice = selectedShellChoice;
139
+ if (shellChoice === "follow-launcher") {
140
+ commandShell = followLauncherPreference(files.config.commandShell, launcherShell);
180
141
  }
181
- }
182
- if (commandShell.family === "pwsh") {
183
- powerShellSkillSeedResult = await seedPowerShellSkill({
184
- configDir: files.dir,
185
- version,
186
- });
187
- if (powerShellSkillSeedResult.status === "failed") {
188
- prompts.note(`${powerShellSkillSeedResult.error ?? "download failed"}\n${powerShellSkillSeedResult.sourceUrl}`, "PowerShell Skill unavailable");
142
+ else if (shellChoice === "keep-pinned") {
143
+ commandShell = preservePinnedPreference(files.config.commandShell);
189
144
  }
190
- }
191
- const installedManaged = installedManagedLanguageServers(files.dir);
192
- const selectedManaged = await prompts.multiselect({
193
- message: "Which Language Servers should ForgeRelay manage with npm?",
194
- options: managedLanguageServerOptions(),
195
- initialValues: installedManaged,
196
- required: false,
197
- });
198
- if (prompts.isCancel(selectedManaged))
199
- throw new SetupCancelledError();
200
- const managedIds = selectedManaged;
201
- const allowAgentInstallAnswer = await prompts.confirm({
202
- message: "Allow Agents to install or update ForgeRelay-managed Language Servers on demand?",
203
- initialValue: files.config.allowAgentLanguageServerInstall === true,
204
- });
205
- if (prompts.isCancel(allowAgentInstallAnswer))
206
- throw new SetupCancelledError();
207
- const allowAgentLanguageServerInstall = allowAgentInstallAnswer === true;
208
- if (managedIds.length > 0) {
209
- prompts.note([
210
- "ForgeRelay installs these optional Language Servers into its private config directory, not global npm.",
211
- "Rust Analyzer, gopls, and clangd remain external toolchain/system installations and are auto-detected when available.",
212
- ].join("\n"), "Managed Language Servers");
213
- const spinner = prompts.spinner();
214
- spinner.start("Installing managed Language Servers with npm");
215
- try {
216
- const installed = await installManagedLanguageServers(managedIds, files.dir);
217
- spinner.stop(`Installed ${installed.installed.join(", ")} under ${installed.root}`);
145
+ else if (shellChoice === "custom") {
146
+ const customFamily = await prompts.select({
147
+ message: "Which command language does the custom executable implement?",
148
+ options: shellFamiliesForCustomSelection(),
149
+ });
150
+ if (prompts.isCancel(customFamily))
151
+ throw new SetupCancelledError();
152
+ const executable = await textPrompt({
153
+ message: "What is the command-shell executable path?",
154
+ placeholder: process.platform === "win32" ? "D:\\Portable\\PowerShell\\pwsh.exe" : "/opt/custom/bin/zsh",
155
+ defaultValue: "",
156
+ validate: (value) => value?.trim() ? undefined : "Enter an executable path.",
157
+ });
158
+ commandShell = customPinnedPreference(customFamily, executable);
218
159
  }
219
- catch (error) {
220
- spinner.stop("Managed Language Server installation failed");
221
- throw error;
160
+ else {
161
+ commandShell = pinnedFamilyPreference(shellChoice);
222
162
  }
163
+ const shellWarning = commandShellCompatibilityWarning(commandShell.family);
164
+ if (shellWarning)
165
+ prompts.note(shellWarning, "Command shell compatibility");
166
+ shellInstructionFamilies = shellInstructionFamiliesToSeed(process.platform, commandShell.family);
167
+ if (shellInstructionFamilies.length > 0) {
168
+ const shellInstructionsAnswer = await prompts.confirm({
169
+ message: "Enable ForgeRelay Runtime Shell Instructions for this command shell?",
170
+ initialValue: files.config.shellInstructions === true,
171
+ });
172
+ if (prompts.isCancel(shellInstructionsAnswer))
173
+ throw new SetupCancelledError();
174
+ shellInstructions = shellInstructionsAnswer === true;
175
+ if (shellInstructions) {
176
+ shellInstructionSeedResults = await seedShellInstructionFiles({
177
+ configDir: files.dir,
178
+ version,
179
+ families: shellInstructionFamilies,
180
+ });
181
+ const failedShellInstructions = shellInstructionSeedResults.filter((result) => result.status === "failed");
182
+ if (failedShellInstructions.length > 0) {
183
+ prompts.note(failedShellInstructions.map((result) => `${result.family}: ${result.error ?? "download failed"}\n${result.sourceUrl}`).join("\n\n"), "Shell Instructions unavailable");
184
+ }
185
+ }
186
+ }
187
+ const installedManaged = installedManagedLanguageServers(files.dir);
188
+ const selectedManaged = await prompts.multiselect({
189
+ message: "Which Language Servers should ForgeRelay manage with npm?",
190
+ options: managedLanguageServerOptions(),
191
+ initialValues: installedManaged,
192
+ required: false,
193
+ });
194
+ if (prompts.isCancel(selectedManaged))
195
+ throw new SetupCancelledError();
196
+ const managedIds = selectedManaged;
197
+ const allowAgentInstallAnswer = await prompts.confirm({
198
+ message: "Allow Agents to install or update ForgeRelay-managed Language Servers on demand?",
199
+ initialValue: files.config.allowAgentLanguageServerInstall === true,
200
+ });
201
+ if (prompts.isCancel(allowAgentInstallAnswer))
202
+ throw new SetupCancelledError();
203
+ const allowAgentLanguageServerInstall = allowAgentInstallAnswer === true;
204
+ if (managedIds.length > 0) {
205
+ prompts.note([
206
+ "ForgeRelay installs these optional Language Servers into its private config directory, not global npm.",
207
+ "Rust Analyzer, gopls, and clangd remain external toolchain/system installations and are auto-detected when available.",
208
+ ].join("\n"), "Managed Language Servers");
209
+ const spinner = prompts.spinner();
210
+ spinner.start("Installing managed Language Servers with npm");
211
+ try {
212
+ const installed = await installManagedLanguageServers(managedIds, files.dir);
213
+ spinner.stop(`Installed ${installed.installed.join(", ")} under ${installed.root}`);
214
+ }
215
+ catch (error) {
216
+ spinner.stop("Managed Language Server installation failed");
217
+ throw error;
218
+ }
219
+ }
220
+ advancedSelection = {
221
+ port,
222
+ commandShell,
223
+ shellInstructions,
224
+ allowAgentLanguageServerInstall,
225
+ };
223
226
  }
224
- const config = {
225
- ...files.config,
226
- host,
227
- port,
227
+ const config = applySetupConfig(files.config, {
228
+ schema: configSchemaId(generalConfigDefinition, "user"),
228
229
  allowedRoots,
229
- publicBaseUrl,
230
- allowedHosts: files.config.allowedHosts,
231
- trustedProxies,
232
- workflowInstructions: files.config.workflowInstructions,
233
- appendInstructions: files.config.appendInstructions,
234
- commandShell,
235
- shellInstructions,
236
- subagents: resolveSubagentsFlag(files.config),
237
- languageServers: files.config.languageServers,
238
- allowAgentLanguageServerInstall,
239
- };
230
+ network: {
231
+ mode: networkMode,
232
+ ...(publicBaseUrl === undefined ? {} : { publicBaseUrl }),
233
+ },
234
+ ...(advancedSelection ? { advanced: advancedSelection } : {}),
235
+ });
240
236
  const auth = {
241
237
  ...files.auth,
242
238
  ownerToken: files.auth.ownerToken ?? generateOwnerToken(),
@@ -244,24 +240,26 @@ export async function runInit({ force, version }) {
244
240
  };
245
241
  const configPath = writeForgeRelayConfig(config);
246
242
  const authPath = await writeForgeRelayAuth(auth);
243
+ const bindHost = config.host ?? "127.0.0.1";
244
+ const bindPort = config.port ?? 7676;
247
245
  const lines = [
248
246
  `Config: ${configPath}`,
249
247
  `Auth: ${authPath}`,
250
- `Bind: http://${config.host}:${config.port}`,
251
- `Command shell: ${commandShell.mode} ${commandShell.family} (${commandShell.executable})`,
252
- ...(shellInstructionFamilies.length > 0
248
+ `Bind: http://${bindHost}:${bindPort}`,
249
+ "OAuth mode: Owner-password approval",
250
+ ...(advancedSelection && commandShell
251
+ ? [`Command shell: ${commandShell.mode} ${commandShell.family} (${commandShell.executable})`]
252
+ : []),
253
+ ...(advancedSelection && shellInstructionFamilies.length > 0
253
254
  ? [
254
255
  `Shell Instructions: ${shellInstructions ? "enabled" : "disabled"}`,
255
256
  ...shellInstructionSeedResults.map((result) => `Shell Instructions ${result.family}: ${result.status} (${result.path})`),
256
257
  ]
257
258
  : []),
258
- ...(powerShellSkillSeedResult
259
- ? [`PowerShell Skill: ${powerShellSkillSeedResult.status} (${powerShellSkillSeedResult.path})`]
260
- : []),
261
259
  ...clientFacingBaseUrls.map((baseUrl, index) => `${index === 0 ? "Client-facing MCP URL" : "Client-facing MCP alias"}: ${publicEndpointUrl(baseUrl, "mcp").toString()}`),
262
260
  ];
263
261
  if (networkMode === "ssh") {
264
- lines.push(`SSH relay: forgerelay auth -J <ssh-host> 127.0.0.1:${port} --ssh-auth`);
262
+ lines.push(`SSH relay: forgerelay auth -J <ssh-host> 127.0.0.1:${bindPort} --ssh-auth`);
265
263
  }
266
264
  prompts.note(lines.join("\n"), "ForgeRelay configured");
267
265
  prompts.note([
@@ -8,6 +8,8 @@ import { databasePath } from "../runtime/state/db/client.js";
8
8
  import { bashBytesExpression, columnExists, eligibleActivityCte, tableExists, } from "./maintenance-retention.js";
9
9
  import { printMaintenancePruneReport, pruneMaintenanceState, } from "./maintenance-prune.js";
10
10
  import { forgerelayConfigPath, } from "../runtime/config/user-config.js";
11
+ import { resolveGeneralConfig } from "../runtime/config/resolution/general.js";
12
+ import { assertConfigResolutionValid } from "../runtime/config/resolution/resolver.js";
11
13
  const DAY_MS = 24 * 60 * 60 * 1_000;
12
14
  const MAX_RETENTION_DAYS = 36_500;
13
15
  const MAX_CHECKPOINT_STATE_BYTES = 1024 * 1024;
@@ -44,9 +46,17 @@ export function runMaintenanceCommand(args, env = process.env) {
44
46
  printMaintenancePruneReport(report);
45
47
  }
46
48
  export function inspectMaintenanceState(env = process.env, now = new Date()) {
47
- const config = readMaintenanceConfig(env);
48
- const stateDir = resolveMaintenanceStateDir(config, env);
49
- const policy = resolveMaintenanceRetentionPolicy(config.retention, env);
49
+ const configPath = forgerelayConfigPath(env);
50
+ const persistedConfig = readMaintenanceConfig(env);
51
+ const resolution = resolveGeneralConfig({
52
+ env,
53
+ user: persistedConfig,
54
+ userSourcePath: configPath,
55
+ });
56
+ assertConfigResolutionValid(resolution);
57
+ const config = resolution.values;
58
+ const stateDir = resolveMaintenanceStateDir(config);
59
+ const policy = resolveMaintenanceRetentionPolicy(config.retention, {});
50
60
  const cutoff = policy.historyDays === null
51
61
  ? null
52
62
  : new Date(now.getTime() - policy.historyDays * DAY_MS).toISOString();
@@ -113,8 +123,8 @@ function readMaintenanceConfig(env) {
113
123
  throw new Error(`Unable to read ForgeRelay maintenance config ${path}: ${errorMessage(error)}`);
114
124
  }
115
125
  }
116
- function resolveMaintenanceStateDir(config, env) {
117
- const configured = env.FORGERELAY_STATE_DIR ?? config.stateDir ?? join(homedir(), ".local", "share", "forgerelay");
126
+ function resolveMaintenanceStateDir(config) {
127
+ const configured = config.stateDir ?? join(homedir(), ".local", "share", "forgerelay");
118
128
  return resolve(expandHomePath(String(configured)));
119
129
  }
120
130
  function openReadOnlyDatabaseSnapshot(stateDir) {
@@ -77,7 +77,7 @@ async function runExternalMcpList(options, dependencies) {
77
77
  if (options.rest.length > 0) {
78
78
  throw new Error("Usage: forgerelay mcp list [--project <path>|--global]");
79
79
  }
80
- const scope = resolveExternalMcpScope(options, dependencies);
80
+ const scope = await resolveExternalMcpScope(options, dependencies);
81
81
  const status = inspectExternalMcpStatus(scope);
82
82
  console.log(formatExternalMcpList(status));
83
83
  if (status.configIssues > 0) {
@@ -85,7 +85,7 @@ async function runExternalMcpList(options, dependencies) {
85
85
  }
86
86
  }
87
87
  async function runExternalMcpTest(options, dependencies) {
88
- const scope = resolveExternalMcpScope(options, dependencies);
88
+ const scope = await resolveExternalMcpScope(options, dependencies);
89
89
  const initialStatus = inspectExternalMcpStatus(scope);
90
90
  const serverStatus = findExternalMcpServerStatus(initialStatus, options.server);
91
91
  if (!serverStatus)
@@ -99,7 +99,10 @@ async function runExternalMcpTest(options, dependencies) {
99
99
  printTestTarget(scope, serverStatus);
100
100
  const gateway = new ExternalMcpGateway(scope.config.mediaMaxBytes, scope.store);
101
101
  try {
102
- const probe = await gateway.probe(scope.snapshot.servers, options.server, undefined, { workspaceRoot: scope.projectRoot, origins: scope.snapshot.origins });
102
+ const probe = await gateway.probe(scope.snapshot.servers, options.server, undefined, {
103
+ ...(scope.project ? { project: { id: scope.project.id, projectRoot: scope.project.projectRoot } } : {}),
104
+ origins: scope.snapshot.origins,
105
+ });
103
106
  console.log("Connection: ok");
104
107
  console.log(`Protocol: ${probe.protocolEra}` +
105
108
  (probe.protocolVersion ? ` · ${probe.protocolVersion}` : ""));
@@ -148,11 +151,11 @@ function cliArgument(value) {
148
151
  return /^[A-Za-z0-9_./:\\-]+$/.test(value) ? value : JSON.stringify(value);
149
152
  }
150
153
  function formatCredentialScope(source, projectRoot) {
151
- return source === "project" ? `project · ${projectRoot}` : "global";
154
+ return source === "project" || source === "project-local" ? `project · ${projectRoot}` : "global";
152
155
  }
153
156
  async function runExternalMcpAuth(options, dependencies) {
154
157
  assertInteractive(dependencies);
155
- const target = resolveExternalMcpCliTarget(options, dependencies);
158
+ const target = await resolveExternalMcpCliTarget(options, dependencies);
156
159
  if (hasStaticAuthorizationHeader(target.serverConfig.headers)) {
157
160
  throw new Error(`External MCP ${target.server} uses a static Authorization header; remove that header before using OAuth authentication.`);
158
161
  }
@@ -160,7 +163,7 @@ async function runExternalMcpAuth(options, dependencies) {
160
163
  console.log(`Source: ${target.source}`);
161
164
  console.log(`Transport: ${target.serverConfig.transport}`);
162
165
  console.log(`Credential scope: ${formatCredentialScope(target.source, target.projectRoot)}`);
163
- const identity = externalMcpCredentialIdentity(target.source, target.server, target.projectRoot);
166
+ const identity = externalMcpCredentialIdentity(target.source, target.server, target.project);
164
167
  const existing = target.store.read(identity);
165
168
  const receiver = await (dependencies.createLoopbackReceiver ?? createLoopbackReceiver)(target.serverConfig.oauth?.callbackPort);
166
169
  let authorizationUrl;
@@ -224,11 +227,11 @@ async function runExternalMcpAuth(options, dependencies) {
224
227
  }
225
228
  }
226
229
  async function runExternalMcpLogout(options, dependencies) {
227
- const target = resolveExternalMcpCliTarget(options, dependencies);
230
+ const target = await resolveExternalMcpCliTarget(options, dependencies);
228
231
  console.log(`Logging out External MCP ${target.server}`);
229
232
  console.log(`Source: ${target.source}`);
230
233
  console.log(`Credential scope: ${formatCredentialScope(target.source, target.projectRoot)}`);
231
- const identity = externalMcpCredentialIdentity(target.source, target.server, target.projectRoot);
234
+ const identity = externalMcpCredentialIdentity(target.source, target.server, target.project);
232
235
  let existing;
233
236
  await target.store.withIdentityLock(identity, async () => {
234
237
  existing = target.store.read(identity);
@@ -254,8 +257,8 @@ async function runExternalMcpLogout(options, dependencies) {
254
257
  else
255
258
  console.log("Remote revocation: failed; the local credential was still removed.");
256
259
  }
257
- function resolveExternalMcpCliTarget(options, dependencies) {
258
- const scope = resolveExternalMcpScope(options, dependencies);
260
+ async function resolveExternalMcpCliTarget(options, dependencies) {
261
+ const scope = await resolveExternalMcpScope(options, dependencies);
259
262
  const serverConfig = scope.snapshot.servers[options.server];
260
263
  const source = scope.snapshot.origins[options.server];
261
264
  if (!serverConfig || !source) {
@@ -271,6 +274,7 @@ function resolveExternalMcpCliTarget(options, dependencies) {
271
274
  return {
272
275
  store: scope.store,
273
276
  projectRoot: scope.projectRoot,
277
+ ...(scope.project ? { project: { id: scope.project.id, projectRoot: scope.project.projectRoot } } : {}),
274
278
  server: options.server,
275
279
  source,
276
280
  serverConfig,
@@ -1,9 +1,10 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
+ import { ProjectContextResolver } from "../../workspaces/state/project-context.js";
3
4
  import { loadConfig } from "../../runtime/config/config.js";
4
5
  import { ExternalMcpCredentialStore, externalMcpCredentialIdentity, } from "../../runtime/config/external-mcp-auth-store.js";
5
6
  import { ExternalMcpConfigRegistry, } from "../../runtime/config/external-mcp-registry.js";
6
- export function resolveExternalMcpScope(request, dependencies = {}) {
7
+ export async function resolveExternalMcpScope(request, dependencies = {}) {
7
8
  if (request.global && request.projectRoot) {
8
9
  throw new Error("--global and --project cannot be used together.");
9
10
  }
@@ -12,7 +13,7 @@ export function resolveExternalMcpScope(request, dependencies = {}) {
12
13
  const cwd = resolve(dependencies.cwd ?? process.cwd());
13
14
  const registry = new ExternalMcpConfigRegistry({
14
15
  configDir: config.configDir,
15
- legacyServers: config.mcpServers,
16
+ sourceRuntime: config.configRuntime.sources,
16
17
  });
17
18
  const store = new ExternalMcpCredentialStore({ configDir: config.configDir });
18
19
  if (request.global) {
@@ -27,13 +28,15 @@ export function resolveExternalMcpScope(request, dependencies = {}) {
27
28
  };
28
29
  }
29
30
  const selected = selectProjectRoot(request.projectRoot, cwd, env);
31
+ const project = await new ProjectContextResolver(config.configDir).resolve(selected.root);
30
32
  return {
31
33
  mode: "project",
32
- projectRoot: selected.root,
34
+ projectRoot: project.projectRoot,
35
+ project,
33
36
  projectSelection: selected.selection,
34
37
  config,
35
38
  registry,
36
- snapshot: registry.resolve(selected.root),
39
+ snapshot: registry.resolve(project),
37
40
  store,
38
41
  };
39
42
  }
@@ -79,7 +82,7 @@ export function formatExternalMcpList(status) {
79
82
  }
80
83
  lines.push("", "Config:");
81
84
  for (const source of status.scope.snapshot.sources) {
82
- lines.push(` ${source.source.padEnd(7)} ${source.path} · ${formatConfigSourceState(source, status)}`);
85
+ lines.push(` ${source.source.padEnd(13)} ${source.path} · ${formatConfigSourceState(source, status)}`);
83
86
  }
84
87
  if (status.credentialStore === "invalid") {
85
88
  lines.push(` auth ${status.scope.store.filePath} · invalid`);
@@ -111,6 +114,8 @@ export function formatExternalMcpList(status) {
111
114
  for (const diagnostic of status.scope.snapshot.diagnostics) {
112
115
  const source = status.scope.snapshot.sources.find((candidate) => candidate.source === diagnostic.source);
113
116
  lines.push(` ${diagnostic.source}: ${diagnostic.message}`);
117
+ if (diagnostic.severity !== "error")
118
+ continue;
114
119
  if (source?.usingLastKnownGood) {
115
120
  lines.push(" Using this process's last-known-good configuration.");
116
121
  }
@@ -195,7 +200,7 @@ function serverStatus(scope, name, source, config, credentialStoreInvalid) {
195
200
  authState: "not-detected",
196
201
  };
197
202
  }
198
- const identity = externalMcpCredentialIdentity(source, name, scope.projectRoot);
203
+ const identity = externalMcpCredentialIdentity(source, name, scope.project ? { id: scope.project.id, projectRoot: scope.project.projectRoot } : undefined);
199
204
  let record;
200
205
  try {
201
206
  record = scope.store.read(identity);
package/dist/cli.js CHANGED
@@ -11,6 +11,8 @@ import * as prompts from "@clack/prompts";
11
11
  import { loadConfig } from "./runtime/config/config.js";
12
12
  import { acquireRuntimeLease } from "./runtime/state/runtime-lease.js";
13
13
  import { runInit } from "./cli/init.js";
14
+ import { runConfigMigration } from "./cli/config/migrate.js";
15
+ import { runConfigInspection } from "./cli/config/inspect.js";
14
16
  import { runMaintenanceCommand } from "./cli/maintenance.js";
15
17
  import { runExternalMcpCommand } from "./cli/mcp/external-mcp.js";
16
18
  import { formatExternalMcpDoctor, inspectExternalMcpStatus, resolveExternalMcpScope, } from "./cli/mcp/status.js";
@@ -47,14 +49,16 @@ async function main(argv) {
47
49
  await serve(runtimePrivilege);
48
50
  return;
49
51
  }
50
- case "init":
51
- await runInit({ force: args.includes("--force"), version: installedForgeRelayVersion() });
52
+ case "init": {
53
+ const initOptions = parseInitCommandArgs(args);
54
+ await runInit({ ...initOptions, version: installedForgeRelayVersion() });
52
55
  return;
56
+ }
53
57
  case "doctor":
54
58
  await runDoctor();
55
59
  return;
56
60
  case "config":
57
- runConfigCommand(args);
61
+ await runConfigCommand(args);
58
62
  return;
59
63
  case "hooks":
60
64
  await runHooksCommand(args);
@@ -90,6 +94,21 @@ function normalizeCommand(command) {
90
94
  return "version";
91
95
  throw new Error(`Unknown command: ${command}`);
92
96
  }
97
+ function parseInitCommandArgs(args) {
98
+ let force = false;
99
+ let advanced = false;
100
+ for (const arg of args) {
101
+ if (arg === "--force" && !force)
102
+ force = true;
103
+ else if (arg === "--advanced" && !advanced)
104
+ advanced = true;
105
+ else if (arg === "--force" || arg === "--advanced")
106
+ throw new Error(`${arg} may only be supplied once.`);
107
+ else
108
+ throw new Error(`Unknown init option: ${arg}`);
109
+ }
110
+ return { force, advanced };
111
+ }
93
112
  function parseServeCommandArgs(args) {
94
113
  let allowElevated = false;
95
114
  for (const arg of args) {
@@ -123,7 +142,7 @@ async function ensureConfigured() {
123
142
  "Or provide FORGERELAY_OAUTH_OWNER_TOKEN and FORGERELAY_ALLOWED_ROOTS.",
124
143
  ].join("\n"));
125
144
  }
126
- await runInit({ force: false, version: installedForgeRelayVersion() });
145
+ await runInit({ force: false, advanced: false, version: installedForgeRelayVersion() });
127
146
  }
128
147
  async function serve(runtimePrivilege) {
129
148
  const sqliteStatus = checkSqliteNative();
@@ -384,7 +403,7 @@ async function runDoctor() {
384
403
  console.log(`Agent-managed Language Server install: ${config.allowAgentLanguageServerInstall ? "enabled" : "disabled"}`);
385
404
  console.log(`Allowed roots: ${config.allowedRoots.join(", ")}`);
386
405
  console.log(`Allowed hosts: ${config.allowedHosts.join(", ")}`);
387
- console.log(formatExternalMcpDoctor(inspectExternalMcpStatus(resolveExternalMcpScope({}, {
406
+ console.log(formatExternalMcpDoctor(inspectExternalMcpStatus(await resolveExternalMcpScope({}, {
388
407
  env: process.env,
389
408
  cwd: process.cwd(),
390
409
  }))));
@@ -393,8 +412,16 @@ async function runDoctor() {
393
412
  console.log(`Config status: ${error instanceof Error ? error.message : String(error)}`);
394
413
  }
395
414
  }
396
- function runConfigCommand(args) {
415
+ async function runConfigCommand(args) {
397
416
  const [subcommand, key, ...rest] = args;
417
+ if (subcommand === "migrate") {
418
+ await runConfigMigration(args.slice(1));
419
+ return;
420
+ }
421
+ if (subcommand === "check" || subcommand === "sources" || subcommand === "explain") {
422
+ process.exitCode = await runConfigInspection(args);
423
+ return;
424
+ }
398
425
  const files = loadForgeRelayFiles();
399
426
  if (!subcommand || subcommand === "get") {
400
427
  console.log(JSON.stringify(files.config, null, 2));
@@ -424,10 +451,16 @@ function printHelp() {
424
451
  " forgerelay serve Start the server",
425
452
  " forgerelay serve --allow-elevated",
426
453
  " Explicitly allow this invocation to run with elevated/unknown OS privilege",
427
- " forgerelay init Create or update ~/.forgerelay/config.json and auth.json",
454
+ " forgerelay init Run minimal first-time setup",
455
+ " forgerelay init --advanced Configure common advanced runtime options",
456
+ " forgerelay init --force Update setup-owned fields without migrating legacy config",
428
457
  " forgerelay doctor Show config, runtime, and native dependency status",
429
458
  " forgerelay config get Print persisted config",
430
459
  " forgerelay config set publicBaseUrl <url[,url...]|null>",
460
+ " forgerelay config check [--global|--project <path>] [--json]",
461
+ " forgerelay config sources [--global|--project <path>] [--json]",
462
+ " forgerelay config explain <logical-path> [--global|--project <path>] [--json]",
463
+ " forgerelay config migrate [--dry-run] [--global|--project <path>]",
431
464
  " forgerelay hooks list [--project <path>]",
432
465
  " forgerelay hooks check [--project <path>]",
433
466
  " forgerelay agents ls List subagent sessions",