@akira-tl/forgerelay 0.8.8 → 0.8.10

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 (264) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +10 -18
  3. package/capabilities/code-intelligence/GUIDE.md +7 -3
  4. package/capabilities/host-integration/GUIDE.md +2 -2
  5. package/capabilities/subagents/GUIDE.md +9 -0
  6. package/capabilities/{workspace-tasks → workspace/workspace-tasks}/GUIDE.md +6 -0
  7. package/dist/activity/{audit-store.js → history/audit-store.js} +129 -21
  8. package/dist/activity/history/bash-output-store.js +250 -0
  9. package/dist/activity/{host-turn-store.js → history/host-turn-store.js} +4 -1
  10. package/dist/activity/{query-service.js → history/query-service.js} +13 -7
  11. package/dist/activity/{lifecycle.js → runtime/lifecycle.js} +1 -1
  12. package/dist/activity/{mcp-query-tools.js → runtime/mcp-query-tools.js} +3 -3
  13. package/dist/activity/storage/history.js +12 -0
  14. package/dist/activity/storage/paths.js +56 -0
  15. package/dist/activity/storage/segmented-log.js +149 -0
  16. package/dist/cli/init.js +180 -0
  17. package/dist/cli/setup-support.js +181 -0
  18. package/dist/cli.js +23 -216
  19. package/dist/lsp/code-intelligence.js +18 -4
  20. package/dist/lsp/runtime/diagnostic-snapshots.js +52 -1
  21. package/dist/lsp/runtime/managed-language-servers.js +172 -0
  22. package/dist/lsp/runtime/manager.js +37 -2
  23. package/dist/lsp/runtime/process-launch.js +3 -0
  24. package/dist/lsp/test-support/server-fixture.js +14 -11
  25. package/dist/mcp/filesystem/filesystem-tools.js +245 -0
  26. package/dist/mcp/hooks/command-runner.js +79 -0
  27. package/dist/{hook-cli.js → mcp/hooks/hook-cli.js} +1 -1
  28. package/dist/{hooks.js → mcp/hooks/hooks.js} +3 -80
  29. package/dist/{oauth-provider.js → mcp/oauth/oauth-provider.js} +1 -1
  30. package/dist/{oauth-store.js → mcp/oauth/oauth-store.js} +5 -4
  31. package/dist/{operations → mcp/operations}/native-bulk-mutations.js +2 -2
  32. package/dist/mcp/panel/app.js +143 -0
  33. package/dist/{mcp-app-template.js → mcp/panel/mcp-app-template.js} +2 -38
  34. package/dist/{process-platform.js → mcp/process/process-platform.js} +21 -9
  35. package/dist/{process-sessions.js → mcp/process/process-sessions.js} +3 -3
  36. package/dist/mcp/process/runtime.js +257 -0
  37. package/dist/mcp/process/tools.js +346 -0
  38. package/dist/mcp/server/core/activity-support.js +70 -0
  39. package/dist/{capabilities.js → mcp/server/core/capabilities.js} +5 -3
  40. package/dist/{capability-registry.js → mcp/server/core/capability-registry.js} +9 -4
  41. package/dist/mcp/server/core/capability-support.js +90 -0
  42. package/dist/mcp/server/core/schemas.js +216 -0
  43. package/dist/mcp/server/core/tool-support.js +210 -0
  44. package/dist/mcp/server/core/version.js +9 -0
  45. package/dist/mcp/server/operations/runtime/filesystem-tools.js +455 -0
  46. package/dist/mcp/server/operations/runtime/mutation-diagnostics.js +54 -0
  47. package/dist/mcp/server/operations/runtime/operation-runtime.js +524 -0
  48. package/dist/mcp/server/transport/http-server.js +301 -0
  49. package/dist/mcp/server/transport/http-support.js +43 -0
  50. package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +242 -0
  51. package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +187 -0
  52. package/dist/mcp/server/workspace/runtime/workspace-open.js +555 -0
  53. package/dist/mcp/server/workspace/runtime/workspace-tools.js +601 -0
  54. package/dist/mcp/server-instructions.js +1 -1
  55. package/dist/{config.js → runtime/config/config.js} +4 -3
  56. package/dist/{user-config.js → runtime/config/user-config.js} +14 -63
  57. package/dist/{db → runtime/state/db}/client.js +1 -1
  58. package/dist/{db → runtime/state/db}/migrations.js +34 -3
  59. package/dist/runtime/state/lock/file-lock.js +130 -0
  60. package/dist/runtime/testing/server-fixture.js +190 -0
  61. package/dist/server.js +133 -4189
  62. package/dist/subagents/providers/adapters/claude.js +86 -52
  63. package/dist/subagents/providers/adapters/codex.js +103 -39
  64. package/dist/subagents/providers/adapters/opencode.js +114 -84
  65. package/dist/subagents/providers/adapters/pi.js +59 -8
  66. package/dist/subagents/providers/availability.js +15 -30
  67. package/dist/subagents/providers/runtime/external-command.js +56 -0
  68. package/dist/subagents/sessions/capability.js +1 -1
  69. package/dist/subagents/sessions/execution.js +1 -1
  70. package/dist/subagents/sessions/mcp/activity-audit-test-support.js +30 -0
  71. package/dist/subagents/sessions/store.js +1 -1
  72. package/dist/ui/.vite/manifest.json +342 -410
  73. package/dist/ui/activity-panel-app.html +2 -4
  74. package/dist/ui/assets/activity-panel-app-7Xe0alhm.css +1 -0
  75. package/dist/ui/assets/activity-panel-app-Sl1Jro8v.js +115 -0
  76. package/dist/ui/assets/angular-html-B_yn91wW.js +1 -0
  77. package/dist/ui/assets/{angular-ts-Bxh4-O1-.js → angular-ts-DSgtloKz.js} +1 -1
  78. package/dist/ui/assets/{apl-BmYHKQ6m.js → apl-NatWtF8X.js} +1 -1
  79. package/dist/ui/assets/{astro-C51vqDKq.js → astro-BYFEVBuN.js} +1 -1
  80. package/dist/ui/assets/{blade-CdhVsmwq.js → blade-DoO7yvMR.js} +1 -1
  81. package/dist/ui/assets/c-Rl2hMCcg.js +1 -0
  82. package/dist/ui/assets/{cobol-Cf6azoN_.js → cobol-ByC5mWiB.js} +1 -1
  83. package/dist/ui/assets/{coffee-vVdGzlmt.js → coffee-Dez77Qov.js} +1 -1
  84. package/dist/ui/assets/cpp-DtaAQT9h.js +1 -0
  85. package/dist/ui/assets/{crystal-5UZy6KIX.js → crystal-Bgmbf8ec.js} +1 -1
  86. package/dist/ui/assets/css-C1tjBtVN.js +1 -0
  87. package/dist/ui/assets/{edge-CESgo4ia.js → edge-DeH6u8Wg.js} +1 -1
  88. package/dist/ui/assets/{elixir-9FKfw4cJ.js → elixir-_H3KnmWT.js} +1 -1
  89. package/dist/ui/assets/{elm-Dv7A0aLa.js → elm-CsCe3U28.js} +1 -1
  90. package/dist/ui/assets/{erb-DHFmDvM0.js → erb-CbM-L2g8.js} +1 -1
  91. package/dist/ui/assets/{git-rebase-utbU6CzV.js → git-rebase-D0s5pDh9.js} +1 -1
  92. package/dist/ui/assets/{glimmer-js-t2kiCkyB.js → glimmer-js-BfOToq_A.js} +1 -1
  93. package/dist/ui/assets/{glimmer-ts-F0Jv5okV.js → glimmer-ts-CCUOkL2K.js} +1 -1
  94. package/dist/ui/assets/glsl-DH4ec9dE.js +1 -0
  95. package/dist/ui/assets/graphql-DilAMBoI.js +1 -0
  96. package/dist/ui/assets/{hack-8yjdbaSA.js → hack-BDkNDZlF.js} +1 -1
  97. package/dist/ui/assets/haml-D_QBp6K7.js +1 -0
  98. package/dist/ui/assets/{handlebars-BBFEg4Lo.js → handlebars-DePUrTpC.js} +1 -1
  99. package/dist/ui/assets/heavy-payload-DOwZpO6n.js +290 -0
  100. package/dist/ui/assets/html-By4dNnM4.js +1 -0
  101. package/dist/ui/assets/{html-derivative-DJAQcZyc.js → html-derivative-DDw9Ef4Q.js} +1 -1
  102. package/dist/ui/assets/{http-Dg5LSYRK.js → http-B0QxWc9M.js} +1 -1
  103. package/dist/ui/assets/{hurl-Y5uYzMZR.js → hurl-vMQ4lda1.js} +1 -1
  104. package/dist/ui/assets/java-DYRe2lWG.js +1 -0
  105. package/dist/ui/assets/javascript-CxyGqwcL.js +1 -0
  106. package/dist/ui/assets/{jinja-Bt30h8Ik.js → jinja-JUwV-fZt.js} +1 -1
  107. package/dist/ui/assets/{jison-D5uZ1Nyo.js → jison-7ZqCEvS8.js} +1 -1
  108. package/dist/ui/assets/json-dbltgiiA.js +1 -0
  109. package/dist/ui/assets/jsx-oDopT7YQ.js +1 -0
  110. package/dist/ui/assets/{julia-76hRaaGz.js → julia-CkKiNSav.js} +1 -1
  111. package/dist/ui/assets/{just-CDE7W0vW.js → just-DuqCAKJ3.js} +1 -1
  112. package/dist/ui/assets/{latex-CVO2Byab.js → latex-sudcRDSn.js} +1 -1
  113. package/dist/ui/assets/{liquid-CA-AS3EX.js → liquid-Di-08Js7.js} +1 -1
  114. package/dist/ui/assets/lua-YlqdMoV0.js +1 -0
  115. package/dist/ui/assets/{marko-_BWrPfny.js → marko-CJRNyJDb.js} +1 -1
  116. package/dist/ui/assets/{mdc-PyJv85eS.js → mdc-t12CzZHs.js} +1 -1
  117. package/dist/ui/assets/{nginx-DAWVw8yu.js → nginx-57-b4XwT.js} +1 -1
  118. package/dist/ui/assets/{nim-CKE1dzpU.js → nim-BzrswYQa.js} +1 -1
  119. package/dist/ui/assets/{perl-BBhWva19.js → perl-D9JR_iIh.js} +1 -1
  120. package/dist/ui/assets/{php-ChIeaAoF.js → php-Py2gFHil.js} +1 -1
  121. package/dist/ui/assets/{pug-CMgI5afi.js → pug-PzLLy0h1.js} +1 -1
  122. package/dist/ui/assets/{qml-CjxTkcFH.js → qml-By8fwgV4.js} +1 -1
  123. package/dist/ui/assets/r-CcezTbLq.js +1 -0
  124. package/dist/ui/assets/{razor-i4DbHr8p.js → razor-dG-vdhL_.js} +1 -1
  125. package/dist/ui/assets/regexp-BlsQ-N_8.js +1 -0
  126. package/dist/ui/assets/{rst-oy_97Kl4.js → rst-C2awDCat.js} +1 -1
  127. package/dist/ui/assets/{ruby-C5zYZIyl.js → ruby-zA2W_49d.js} +1 -1
  128. package/dist/ui/assets/{sas-BnMEh2S3.js → sas-D49bhyaN.js} +1 -1
  129. package/dist/ui/assets/scss-BQb-cp01.js +1 -0
  130. package/dist/ui/assets/shellscript-Dga4H_Ni.js +1 -0
  131. package/dist/ui/assets/{shellsession-BpE5o7_3.js → shellsession-n3-5CqcF.js} +1 -1
  132. package/dist/ui/assets/{soy-COesmhTY.js → soy-XYBOf_BA.js} +1 -1
  133. package/dist/ui/assets/sql-ET4u5Cpl.js +1 -0
  134. package/dist/ui/assets/{stata-CGQEuSZq.js → stata-CLHcJHO_.js} +1 -1
  135. package/dist/ui/assets/{surrealql-Bz4JbU-9.js → surrealql-BdjQ60T4.js} +1 -1
  136. package/dist/ui/assets/{svelte-1jM2ejXj.js → svelte-B2RPtKJe.js} +1 -1
  137. package/dist/ui/assets/{templ-C-VwELcp.js → templ-CrByPW_W.js} +1 -1
  138. package/dist/ui/assets/{tex-t1rUiQCm.js → tex-DZBkg363.js} +1 -1
  139. package/dist/ui/assets/{ts-tags-Bewoa6pS.js → ts-tags-DJR3Gphb.js} +1 -1
  140. package/dist/ui/assets/tsx-CajzROyy.js +1 -0
  141. package/dist/ui/assets/{twig-DVg-vELl.js → twig-BZjk5Zda.js} +1 -1
  142. package/dist/ui/assets/typescript-B-EWToLj.js +1 -0
  143. package/dist/ui/assets/{vue-wPQ7UlMy.js → vue-D_rHCCr8.js} +1 -1
  144. package/dist/ui/assets/{vue-html-BHtJ8geX.js → vue-html-CC4WlvVt.js} +1 -1
  145. package/dist/ui/assets/{vue-vine-CNSGAWwc.js → vue-vine-BgNMVgpk.js} +1 -1
  146. package/dist/ui/assets/xml-BI86-WbH.js +1 -0
  147. package/dist/ui/assets/{xsl-3_vJ27aE.js → xsl-DXeLZV4J.js} +1 -1
  148. package/dist/ui/assets/yaml-60_PFlWa.js +1 -0
  149. package/dist/workspaces/bootstrap.js +72 -0
  150. package/dist/workspaces/context.js +337 -0
  151. package/dist/workspaces/conversation-test-support.js +86 -0
  152. package/dist/{git-worktrees.js → workspaces/git/git-worktrees.js} +1 -1
  153. package/dist/workspaces/inventory.js +131 -0
  154. package/dist/workspaces/paths.js +55 -0
  155. package/dist/{remote-auth.js → workspaces/relay/auth/remote-auth.js} +2 -2
  156. package/dist/workspaces/relay/result-support.js +111 -0
  157. package/dist/workspaces/relay/tests/test-support.js +203 -0
  158. package/dist/{remote-mcp-connection-pool.js → workspaces/relay/transport/remote-mcp-connection-pool.js} +1 -1
  159. package/dist/{remote-transport.js → workspaces/relay/transport/remote-transport.js} +30 -3
  160. package/dist/workspaces/relay/types.js +1 -0
  161. package/dist/{remote-workspace-relay.js → workspaces/relay/workspace-relay.js} +23 -172
  162. package/dist/workspaces/resources/resource-monitor.js +377 -0
  163. package/dist/workspaces/resources/skills.js +283 -0
  164. package/dist/{review-checkpoints.js → workspaces/review/review-checkpoints.js} +2 -2
  165. package/dist/workspaces/sessions.js +460 -0
  166. package/dist/{workspace-store.js → workspaces/state/workspace-store.js} +2 -2
  167. package/dist/workspaces.js +77 -993
  168. package/docs/chatgpt-coding-workflow.md +7 -14
  169. package/docs/configuration.md +26 -34
  170. package/docs/gotchas.md +0 -30
  171. package/docs/roadmap.md +6 -10
  172. package/docs/security.md +1 -5
  173. package/docs/setup.md +0 -11
  174. package/package.json +6 -9
  175. package/scripts/ci/architecture.mjs +109 -0
  176. package/scripts/ci/verify.mjs +5 -0
  177. package/scripts/debug/accept/bootstrap.mjs +326 -0
  178. package/scripts/debug/accept/harness.mjs +76 -0
  179. package/scripts/debug/accept/support.mjs +432 -0
  180. package/scripts/debug/accept.mjs +43 -806
  181. package/scripts/debug/relay-accept/support.mjs +135 -0
  182. package/scripts/debug/relay-accept.mjs +26 -115
  183. package/scripts/debug/traffic/support.mjs +167 -0
  184. package/scripts/debug/traffic/traffic-audit.mjs +4 -161
  185. package/scripts/{lsp-interop-support.test.mjs → lsp/interop-support.test.mjs} +1 -1
  186. package/scripts/{lsp-interop.mjs → lsp/interop.mjs} +3 -3
  187. package/scripts/release/publish.mjs +12 -9
  188. package/scripts/release/release-gate.test.mjs +3 -1
  189. package/scripts/release/release-version.test.mjs +13 -1
  190. package/scripts/release-parity.mjs +1 -1
  191. package/scripts/release-proof.mjs +15 -0
  192. package/scripts/release-proof.test.mjs +27 -1
  193. package/scripts/release-version.mjs +16 -1
  194. package/scripts/wiki/sync.mjs +21 -10
  195. package/dist/activity/bash-output-store.js +0 -127
  196. package/dist/pi-tools.js +0 -110
  197. package/dist/skills.js +0 -127
  198. package/dist/ui/assets/activity-panel-app-7AckaxIR.js +0 -2
  199. package/dist/ui/assets/angular-html-BkgDQl-G.js +0 -1
  200. package/dist/ui/assets/c-IBzBpxAO.js +0 -1
  201. package/dist/ui/assets/chunk-EyZ2wyi3.js +0 -1
  202. package/dist/ui/assets/cpp-CVHeO-Jk.js +0 -1
  203. package/dist/ui/assets/css-DEfus9J5.js +0 -1
  204. package/dist/ui/assets/glsl-_ooCjaeB.js +0 -1
  205. package/dist/ui/assets/graphql-B9LqP-cY.js +0 -1
  206. package/dist/ui/assets/haml-BU5EoZBn.js +0 -1
  207. package/dist/ui/assets/heavy-payload-Bol1_mcs.js +0 -4
  208. package/dist/ui/assets/html-D-C6YZk4.js +0 -1
  209. package/dist/ui/assets/java-ppHvycj5.js +0 -1
  210. package/dist/ui/assets/javascript-DDTjKLlQ.js +0 -1
  211. package/dist/ui/assets/json-Cw91r8h1.js +0 -1
  212. package/dist/ui/assets/jsx-04X2FT_1.js +0 -1
  213. package/dist/ui/assets/lua-CiyBp52Y.js +0 -1
  214. package/dist/ui/assets/r-DfFga4si.js +0 -1
  215. package/dist/ui/assets/regexp-iC9_nbSI.js +0 -1
  216. package/dist/ui/assets/review-payload-BqJp0c5e.js +0 -1
  217. package/dist/ui/assets/scrollbar-CvE-I-jG.js +0 -287
  218. package/dist/ui/assets/scss-BfZXsAWQ.js +0 -1
  219. package/dist/ui/assets/shellscript-BC2srNaB.js +0 -1
  220. package/dist/ui/assets/sql-Ccv_yeUm.js +0 -1
  221. package/dist/ui/assets/tsx-CTXzrBkN.js +0 -1
  222. package/dist/ui/assets/typescript--ElHoFkw.js +0 -1
  223. package/dist/ui/assets/workspace-app-BLW7p6IZ.js +0 -114
  224. package/dist/ui/assets/workspace-app-BjNqR0en.js +0 -1
  225. package/dist/ui/assets/workspace-app-CmaYU4DW.js +0 -3
  226. package/dist/ui/assets/workspace-app-D2bJ5fjt.css +0 -1
  227. package/dist/ui/assets/workspace-lifecycle-app-BVbI2Ilf.js +0 -1
  228. package/dist/ui/assets/xml-JnmX6vyS.js +0 -1
  229. package/dist/ui/assets/yaml-OAcZkP-w.js +0 -1
  230. package/dist/ui/workspace-app.html +0 -18
  231. package/dist/ui/workspace-lifecycle-app.html +0 -18
  232. package/docs/agent-profile-schema.md +0 -114
  233. package/docs/artifact-exchange.md +0 -65
  234. package/docs/versioning.md +0 -205
  235. package/skills/subagent-delegation/SKILL.md +0 -132
  236. /package/capabilities/{managed-worktrees → workspace/managed-worktrees}/GUIDE.md +0 -0
  237. /package/dist/activity/{process-output-audit.js → runtime/process-output-audit.js} +0 -0
  238. /package/dist/{artifact-error.js → mcp/artifacts/artifact-error.js} +0 -0
  239. /package/dist/{artifact-tools.js → mcp/artifacts/artifact-tools.js} +0 -0
  240. /package/dist/{incoming-artifacts.js → mcp/artifacts/incoming-artifacts.js} +0 -0
  241. /package/dist/{advertised-files.js → mcp/filesystem/advertised-files.js} +0 -0
  242. /package/dist/{apply-patch.js → mcp/filesystem/apply-patch.js} +0 -0
  243. /package/dist/{file-mutations.js → mcp/filesystem/file-mutations.js} +0 -0
  244. /package/dist/{roots.js → mcp/filesystem/roots.js} +0 -0
  245. /package/dist/{oauth → mcp/oauth}/public-url.js +0 -0
  246. /package/dist/{oauth → mcp/oauth}/router.js +0 -0
  247. /package/dist/{operations → mcp/operations}/batch/executor.js +0 -0
  248. /package/dist/{operations → mcp/operations}/batch/scheduler.js +0 -0
  249. /package/dist/{operations → mcp/operations}/batch/types.js +0 -0
  250. /package/dist/{operations → mcp/operations}/bulk-mutation.js +0 -0
  251. /package/dist/{operations → mcp/operations}/bulk-read.js +0 -0
  252. /package/dist/{operations → mcp/operations}/core-operation-executor.js +0 -0
  253. /package/dist/{request-meta.js → mcp/request-meta.js} +0 -0
  254. /package/dist/{mcp-sessions.js → mcp/server/transport/mcp-sessions.js} +0 -0
  255. /package/dist/{server-shutdown.js → mcp/server/transport/server-shutdown.js} +0 -0
  256. /package/dist/{logger.js → runtime/logging/logger.js} +0 -0
  257. /package/dist/{db → runtime/state/db}/schema.js +0 -0
  258. /package/dist/{composite-activity.js → workspaces/composite/composite-activity.js} +0 -0
  259. /package/dist/{composite-workspaces.js → workspaces/composite/composite-workspaces.js} +0 -0
  260. /package/dist/{git.js → workspaces/git/git.js} +0 -0
  261. /package/dist/{workspace-presentation.js → workspaces/presentation/workspace-presentation.js} +0 -0
  262. /package/dist/{workspace-task-reminders.js → workspaces/tasks/workspace-task-reminders.js} +0 -0
  263. /package/dist/{workspace-tasks.js → workspaces/tasks/workspace-tasks.js} +0 -0
  264. /package/scripts/{lsp-interop-support.mjs → lsp/interop-support.mjs} +0 -0
package/dist/cli.js CHANGED
@@ -8,23 +8,21 @@ import { tmpdir } from "node:os";
8
8
  import { join, resolve } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import * as prompts from "@clack/prompts";
11
- import { getShellConfig } from "@earendil-works/pi-coding-agent";
12
- import { satisfies } from "semver";
13
- import { loadConfig } from "./config.js";
14
- import { runHooksCommand } from "./hook-cli.js";
11
+ import { loadConfig } from "./runtime/config/config.js";
12
+ import { runInit } from "./cli/init.js";
13
+ import { runHooksCommand } from "./mcp/hooks/hook-cli.js";
15
14
  import { executeSubagentSession } from "./subagents/sessions/execution.js";
16
15
  import { SubagentDeliveryMailbox } from "./subagents/sessions/delivery-mailbox.js";
17
16
  import { SubagentSessionManager } from "./subagents/sessions/manager.js";
18
17
  import { formatSubagentProviderAvailabilitySummary } from "./subagents/providers/availability.js";
19
18
  import { parseSubagentRunArgs } from "./subagents/cli-target.js";
20
- import { ensureForgeRelayInstanceId, generateInstanceId, generateOwnerToken, loadForgeRelayFiles, removeForgeRelayRemote, renameForgeRelayRemote, resolveSubagentsFlag, writeForgeRelayAuth, writeForgeRelayConfig, writeForgeRelayRemote, } from "./user-config.js";
21
- import { expandHomePath } from "./roots.js";
22
- import { shutdownHttpServer } from "./server-shutdown.js";
23
- import { publicEndpointUrl } from "./oauth/public-url.js";
24
- import { authenticateRemote, defaultRemoteAlias, isRemoteMcpUnauthorized, normalizeRemoteServiceTarget, refreshRemoteAuthentication, verifyRemoteMcp, } from "./remote-auth.js";
25
- import { defaultSshRouteAlias, parseSshRoute, readRemoteOwnerToken, withRemoteServiceEndpoint, } from "./remote-transport.js";
19
+ import { ensureForgeRelayInstanceId, loadForgeRelayFiles, removeForgeRelayRemote, renameForgeRelayRemote, writeForgeRelayConfig, writeForgeRelayRemote, } from "./runtime/config/user-config.js";
20
+ import { shutdownHttpServer } from "./mcp/server/transport/server-shutdown.js";
21
+ import { publicEndpointUrl } from "./mcp/oauth/public-url.js";
22
+ import { authenticateRemote, defaultRemoteAlias, isRemoteMcpUnauthorized, normalizeRemoteServiceTarget, refreshRemoteAuthentication, verifyRemoteMcp, } from "./workspaces/relay/auth/remote-auth.js";
23
+ import { defaultSshRouteAlias, parseSshRoute, readRemoteOwnerToken, withRemoteServiceEndpoint, } from "./workspaces/relay/transport/remote-transport.js";
24
+ import { assertSupportedNode, checkBashShell, checkGitAvailable, checkSqliteNative, nodeVersionStatus, normalizeOptionalPublicBaseUrl, } from "./cli/setup-support.js";
26
25
  const require = createRequire(import.meta.url);
27
- const SUPPORTED_NODE_RANGE = ">=20.12 <27";
28
26
  async function main(argv) {
29
27
  assertSupportedNode();
30
28
  const [rawCommand, ...args] = argv;
@@ -74,11 +72,11 @@ function normalizeCommand(command) {
74
72
  async function ensureConfigured() {
75
73
  const files = loadForgeRelayFiles();
76
74
  if (files.configExists && files.authExists) {
77
- ensureForgeRelayInstanceId();
75
+ await ensureForgeRelayInstanceId();
78
76
  return;
79
77
  }
80
78
  if (process.env.FORGERELAY_OAUTH_OWNER_TOKEN) {
81
- ensureForgeRelayInstanceId();
79
+ await ensureForgeRelayInstanceId();
82
80
  return;
83
81
  }
84
82
  if (!input.isTTY || !output.isTTY) {
@@ -93,92 +91,6 @@ async function ensureConfigured() {
93
91
  }
94
92
  await runInit({ force: false });
95
93
  }
96
- async function runInit({ force }) {
97
- const files = loadForgeRelayFiles();
98
- if (!force && files.configExists && files.authExists) {
99
- prompts.log.info(`ForgeRelay is already configured at ${files.dir}`);
100
- prompts.log.info("Run `forgerelay init --force` to update it.");
101
- return;
102
- }
103
- try {
104
- prompts.intro("ForgeRelay setup");
105
- const defaultRoots = files.config.allowedRoots?.join(", ") || process.cwd();
106
- const rootsAnswer = await textPrompt({
107
- message: `Where are your projects located? Press Enter to use ${defaultRoots}`,
108
- placeholder: defaultRoots,
109
- defaultValue: defaultRoots,
110
- validate: (value) => value?.trim() ? undefined : "Enter at least one project root.",
111
- });
112
- const allowedRoots = rootsAnswer
113
- .split(",")
114
- .map((root) => resolve(expandHomePath(root.trim())))
115
- .filter(Boolean);
116
- const defaultPort = String(files.config.port ?? 7676);
117
- const portAnswer = await textPrompt({
118
- message: `Which local port should ForgeRelay use? Press Enter to use ${defaultPort}`,
119
- placeholder: defaultPort,
120
- defaultValue: defaultPort,
121
- validate: validatePort,
122
- });
123
- const port = Number(portAnswer);
124
- prompts.note([
125
- "ForgeRelay needs one or more public base URLs so ChatGPT or Claude can reach this MCP server.",
126
- "Create a tunnel or reverse proxy with Cloudflare Tunnel, ngrok, Pinggy, Tailscale Funnel, or your own HTTPS proxy.",
127
- "Each URL may include its own route prefix. Separate multiple URLs with commas; the first is canonical.",
128
- "",
129
- "Example: https://forge.example.com/forgerelay/main, https://alias.example.com/relay",
130
- ].join("\n"), "Public URL required");
131
- const defaultPublicBaseUrls = Array.isArray(files.config.publicBaseUrl)
132
- ? files.config.publicBaseUrl.join(", ")
133
- : files.config.publicBaseUrl ?? "";
134
- const publicBaseUrls = normalizePublicBaseUrlsInput(await textPrompt({
135
- message: defaultPublicBaseUrls
136
- ? `What are the public base URLs? Press Enter to keep ${defaultPublicBaseUrls}`
137
- : "What are the public base URLs?",
138
- placeholder: defaultPublicBaseUrls || "https://your-tunnel-host.example.com",
139
- defaultValue: defaultPublicBaseUrls,
140
- validate: validateRequiredPublicBaseUrls,
141
- }));
142
- const publicBaseUrl = compactPublicBaseUrlConfig(publicBaseUrls);
143
- const config = {
144
- host: files.config.host ?? "127.0.0.1",
145
- port,
146
- allowedRoots,
147
- publicBaseUrl,
148
- allowedHosts: files.config.allowedHosts,
149
- workflowInstructions: files.config.workflowInstructions,
150
- appendInstructions: files.config.appendInstructions,
151
- subagents: resolveSubagentsFlag(files.config),
152
- };
153
- const auth = {
154
- ...files.auth,
155
- ownerToken: files.auth.ownerToken ?? generateOwnerToken(),
156
- instanceId: files.auth.instanceId ?? generateInstanceId(),
157
- };
158
- const configPath = writeForgeRelayConfig(config);
159
- const authPath = writeForgeRelayAuth(auth);
160
- const lines = [
161
- `Config: ${configPath}`,
162
- `Auth: ${authPath}`,
163
- `Local MCP URL: http://${config.host}:${config.port}/mcp`,
164
- ...publicBaseUrls.map((baseUrl, index) => `${index === 0 ? "Public MCP URL" : "Public MCP alias"}: ${publicEndpointUrl(baseUrl, "mcp").toString()}`),
165
- ];
166
- prompts.note(lines.join("\n"), "ForgeRelay configured");
167
- prompts.note([
168
- `Owner password: ${auth.ownerToken}`,
169
- "Use this when ChatGPT or Claude asks you to approve ForgeRelay access.",
170
- `Stored at: ${authPath}`,
171
- ].join("\n"), "Owner password");
172
- prompts.outro("Run `forgerelay serve` to start the MCP server.");
173
- }
174
- catch (error) {
175
- if (error instanceof SetupCancelledError) {
176
- prompts.cancel("Setup cancelled");
177
- return;
178
- }
179
- throw error;
180
- }
181
- }
182
94
  async function serve() {
183
95
  const sqliteStatus = checkSqliteNative();
184
96
  if (sqliteStatus !== "ok") {
@@ -195,7 +107,7 @@ async function serve() {
195
107
  const { app, close, subagentProviders } = createServer(config);
196
108
  const httpServer = app.listen(config.port, config.host, () => {
197
109
  console.log(`forgerelay listening on http://${config.host}:${config.port}/mcp`);
198
- console.log(`public base url: ${config.publicBaseUrl}`);
110
+ console.log(`client-facing base url: ${config.publicBaseUrl}`);
199
111
  console.log(`allowed roots: ${config.allowedRoots.join(", ")}`);
200
112
  console.log(`allowed hosts: ${config.allowedHosts.join(", ")}`);
201
113
  if (config.allowedHosts.includes("*")) {
@@ -316,7 +228,7 @@ async function runAuthCommand(args) {
316
228
  if (!fromAlias || !toAlias || extra.length > 0) {
317
229
  throw new Error("Usage: forgerelay auth rename <old-alias> <new-alias>");
318
230
  }
319
- renameForgeRelayRemote(fromAlias, toAlias);
231
+ await renameForgeRelayRemote(fromAlias, toAlias);
320
232
  console.log(`Renamed remote ${fromAlias} to ${toAlias}.`);
321
233
  return;
322
234
  }
@@ -324,7 +236,7 @@ async function runAuthCommand(args) {
324
236
  const [alias, ...extra] = rest;
325
237
  if (!alias || extra.length > 0)
326
238
  throw new Error("Usage: forgerelay auth remove <alias>");
327
- removeForgeRelayRemote(alias);
239
+ await removeForgeRelayRemote(alias);
328
240
  console.log(`Removed remote ${alias}.`);
329
241
  return;
330
242
  }
@@ -341,7 +253,7 @@ async function runAuthCommand(args) {
341
253
  let refreshed = false;
342
254
  if (remote.accessTokenExpiresAt <= Math.floor(Date.now() / 1000)) {
343
255
  remote = await refreshRemoteAuthentication(remote, endpoint);
344
- writeForgeRelayRemote(alias, remote);
256
+ await writeForgeRelayRemote(alias, remote);
345
257
  refreshed = true;
346
258
  }
347
259
  try {
@@ -351,7 +263,7 @@ async function runAuthCommand(args) {
351
263
  if (refreshed || !isRemoteMcpUnauthorized(error))
352
264
  throw error;
353
265
  remote = await refreshRemoteAuthentication(remote, endpoint);
354
- writeForgeRelayRemote(alias, remote);
266
+ await writeForgeRelayRemote(alias, remote);
355
267
  await verifyRemoteMcp(remote, endpoint);
356
268
  }
357
269
  });
@@ -378,9 +290,9 @@ async function runAuthCommand(args) {
378
290
  : defaultRemoteAlias(remote.target);
379
291
  const alias = parsed.alias?.trim() || existingAlias || defaultAlias;
380
292
  if (!files.auth.instanceId) {
381
- ensureForgeRelayInstanceId();
293
+ await ensureForgeRelayInstanceId();
382
294
  }
383
- writeForgeRelayRemote(alias, remote);
295
+ await writeForgeRelayRemote(alias, remote);
384
296
  console.log(`Authenticated remote ${alias} (${remote.instanceId}).`);
385
297
  }
386
298
  async function runDoctor() {
@@ -396,16 +308,17 @@ async function runDoctor() {
396
308
  console.log(`SQLite native dependency: ${checkSqliteNative()}`);
397
309
  try {
398
310
  const config = loadConfig();
399
- console.log(`Local MCP URL: http://${config.host}:${config.port}/mcp`);
400
- console.log(`Public base URLs: ${config.publicBaseUrls.join(", ")}`);
401
- console.log(`Public base URL: ${config.publicBaseUrl}`);
402
- console.log(`Public MCP URL: ${publicEndpointUrl(config.publicBaseUrl, "mcp").toString()}`);
311
+ console.log(`Bind MCP URL: http://${config.host}:${config.port}/mcp`);
312
+ console.log(`Client-facing base URLs: ${config.publicBaseUrls.join(", ")}`);
313
+ console.log(`Client-facing base URL: ${config.publicBaseUrl}`);
314
+ console.log(`Client-facing MCP URL: ${publicEndpointUrl(config.publicBaseUrl, "mcp").toString()}`);
403
315
  console.log(`Tool mode: ${config.toolMode}`);
404
316
  console.log(`Widgets: ${config.widgets}`);
405
317
  console.log(`Trust proxy: ${config.logging.trustProxy ? "one hop" : "off"}`);
406
318
  console.log(`Artifacts: ${config.artifactsEnabled ? "enabled" : "disabled"}`);
407
319
  console.log(`Subagents: ${config.subagents ? "enabled" : "disabled"}`);
408
320
  console.log(`Skills: ${config.skillsEnabled ? "enabled" : "disabled"}`);
321
+ console.log(`Agent-managed Language Server install: ${config.allowAgentLanguageServerInstall ? "enabled" : "disabled"}`);
409
322
  console.log(`Allowed roots: ${config.allowedRoots.join(", ")}`);
410
323
  console.log(`Allowed hosts: ${config.allowedHosts.join(", ")}`);
411
324
  }
@@ -641,112 +554,6 @@ function printVersion() {
641
554
  }
642
555
  console.log(packageJson.version);
643
556
  }
644
- function isNullConfigValue(value) {
645
- const normalized = value.trim().toLowerCase();
646
- return !normalized || normalized === "null" || normalized === "none";
647
- }
648
- function normalizeOptionalPublicBaseUrl(value) {
649
- const trimmed = value.trim();
650
- if (isNullConfigValue(trimmed))
651
- return null;
652
- return compactPublicBaseUrlConfig(normalizePublicBaseUrlsInput(trimmed));
653
- }
654
- function normalizePublicBaseUrlsInput(value) {
655
- const entries = value
656
- .split(",")
657
- .map((entry) => entry.trim())
658
- .filter(Boolean);
659
- if (entries.length === 0)
660
- throw new Error("Enter at least one public base URL.");
661
- return Array.from(new Set(entries.map((entry) => normalizePublicBaseUrl(entry))));
662
- }
663
- function compactPublicBaseUrlConfig(baseUrls) {
664
- return baseUrls.length === 1 ? baseUrls[0] : baseUrls;
665
- }
666
- function normalizePublicBaseUrl(value) {
667
- const trimmed = value.trim();
668
- const parsed = new URL(trimmed);
669
- parsed.hash = "";
670
- parsed.search = "";
671
- parsed.pathname = parsed.pathname.replace(/\/+$/, "");
672
- return parsed.toString().replace(/\/$/, "");
673
- }
674
- async function textPrompt(options) {
675
- const result = await prompts.text({
676
- ...options,
677
- validate: (value) => options.validate?.(value?.trim() ? value : options.defaultValue),
678
- });
679
- if (prompts.isCancel(result))
680
- throw new SetupCancelledError();
681
- const value = String(result).trim();
682
- return value || options.defaultValue;
683
- }
684
- function validatePort(value) {
685
- const port = Number(value);
686
- return Number.isInteger(port) && port >= 1 && port <= 65535
687
- ? undefined
688
- : "Enter a port between 1 and 65535.";
689
- }
690
- function validateRequiredPublicBaseUrls(value) {
691
- const trimmed = value?.trim() ?? "";
692
- if (!trimmed)
693
- return "Enter at least one public base URL from your tunnel or reverse proxy.";
694
- try {
695
- normalizePublicBaseUrlsInput(trimmed);
696
- return undefined;
697
- }
698
- catch (error) {
699
- return error instanceof Error ? error.message : String(error);
700
- }
701
- }
702
- function assertSupportedNode() {
703
- if (satisfies(process.versions.node, SUPPORTED_NODE_RANGE))
704
- return;
705
- throw new Error([
706
- `ForgeRelay requires Node ${SUPPORTED_NODE_RANGE}.`,
707
- `Current Node: ${process.version}`,
708
- "",
709
- "Install Node 22 LTS or use a version manager such as nvm, fnm, or mise.",
710
- ].join("\n"));
711
- }
712
- function nodeVersionStatus() {
713
- return satisfies(process.versions.node, SUPPORTED_NODE_RANGE)
714
- ? `supported ${SUPPORTED_NODE_RANGE}`
715
- : `unsupported, requires ${SUPPORTED_NODE_RANGE}`;
716
- }
717
- class SetupCancelledError extends Error {
718
- }
719
- function checkSqliteNative() {
720
- try {
721
- const Database = require("better-sqlite3");
722
- const db = new Database(":memory:");
723
- db.close();
724
- return "ok";
725
- }
726
- catch (error) {
727
- return error instanceof Error ? error.message : String(error);
728
- }
729
- }
730
- function checkGitAvailable() {
731
- try {
732
- const { execFileSync } = require("node:child_process");
733
- return execFileSync("git", ["--version"], { encoding: "utf8" }).trim();
734
- }
735
- catch (error) {
736
- const message = error instanceof Error ? error.message : String(error);
737
- return `unavailable (${message})`;
738
- }
739
- }
740
- function checkBashShell() {
741
- try {
742
- const { shell, args } = getShellConfig();
743
- return `${shell} ${args.join(" ")}`;
744
- }
745
- catch (error) {
746
- const message = error instanceof Error ? error.message : String(error);
747
- return `unavailable (${message})`;
748
- }
749
- }
750
557
  main(process.argv.slice(2)).catch((error) => {
751
558
  console.error(error instanceof Error ? error.message : String(error));
752
559
  process.exitCode = 1;
@@ -3,9 +3,9 @@ import { readFile, realpath } from "node:fs/promises";
3
3
  import { basename, resolve } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { createMessageConnection } from "vscode-jsonrpc/node";
6
- import { DefinitionRequest, DocumentDiagnosticReportKind, DocumentDiagnosticRequest, DocumentSymbolRequest, ExitNotification, HoverRequest, InitializeRequest, ReferencesRequest, InitializedNotification, MarkupKind, PositionEncodingKind, PublishDiagnosticsNotification, ShutdownRequest, WorkspaceSymbolRequest, } from "vscode-languageserver-protocol";
6
+ import { DefinitionRequest, DiagnosticTag, DocumentDiagnosticReportKind, DocumentDiagnosticRequest, DocumentSymbolRequest, ExitNotification, HoverRequest, InitializeRequest, ReferencesRequest, InitializedNotification, MarkupKind, PositionEncodingKind, PublishDiagnosticsNotification, ShutdownRequest, WorkspaceSymbolRequest, } from "vscode-languageserver-protocol";
7
7
  import { LanguageServerConfigurationError, } from "./language-server-config.js";
8
- import { terminateProcessTree } from "../process-platform.js";
8
+ import { terminateProcessTree } from "../mcp/process/process-platform.js";
9
9
  import { CodeIntelligenceError } from "./code-intelligence-error.js";
10
10
  import { DEFAULT_CODE_INTELLIGENCE_RESULT_LIMIT } from "./code-intelligence-types.js";
11
11
  import { normalizeHoverContents } from "./normalization/hover.js";
@@ -14,9 +14,11 @@ import { isWithin, locationEntries, normalizeLocations, workspaceDisplayPath, }
14
14
  import { lspPositionFromUser, rangeFromLsp } from "./position-encoding.js";
15
15
  import { DiagnosticSnapshotStore } from "./runtime/diagnostic-snapshots.js";
16
16
  import { DocumentSynchronizer } from "./runtime/document-synchronizer.js";
17
+ import { languageServerCommandRequiresShell } from "./runtime/process-launch.js";
17
18
  import { SemanticRequestCoordinator } from "./runtime/semantic-requests.js";
18
19
  export { CodeIntelligenceError } from "./code-intelligence-error.js";
19
20
  const STDERR_TAIL_BYTES = 64 * 1024;
21
+ const PUSH_DIAGNOSTIC_WAIT_MS = 1_000;
20
22
  export class LanguageService {
21
23
  workspaceRoot;
22
24
  project;
@@ -258,7 +260,10 @@ export class LanguageService {
258
260
  ...this.diagnosticSnapshots.readPull(document, limit),
259
261
  };
260
262
  }
261
- const snapshot = this.diagnosticSnapshots.readPush(document, limit);
263
+ const snapshot = await this.diagnosticSnapshots.waitForFreshPush(document, limit, Math.min(PUSH_DIAGNOSTIC_WAIT_MS, this.policy.requestTimeoutMs), signal);
264
+ if (signal?.aborted) {
265
+ throw new CodeIntelligenceError("code.request_cancelled", `Diagnostic request for ${input.path} was cancelled by the Host.`);
266
+ }
262
267
  if (snapshot.freshness.state === "missing" && !this.diagnosticSnapshots.hasObservedPushDiagnostics()) {
263
268
  throw new CodeIntelligenceError("code.operation_unsupported", `Language server ${this.project.definition.id} does not provide pull diagnostics and has not published push diagnostics.`);
264
269
  }
@@ -336,7 +341,7 @@ export class LanguageService {
336
341
  stdio: "pipe",
337
342
  windowsHide: true,
338
343
  detached,
339
- shell: false,
344
+ shell: languageServerCommandRequiresShell(definition.command),
340
345
  });
341
346
  }
342
347
  catch (error) {
@@ -370,6 +375,9 @@ export class LanguageService {
370
375
  clientInfo: { name: "forgerelay" },
371
376
  rootUri,
372
377
  workspaceFolders: [{ uri: rootUri, name: basename(this.project.projectRoot) }],
378
+ ...(definition.initializationOptions === undefined
379
+ ? {}
380
+ : { initializationOptions: definition.initializationOptions }),
373
381
  capabilities: {
374
382
  general: {
375
383
  positionEncodings: [
@@ -405,6 +413,12 @@ export class LanguageService {
405
413
  dynamicRegistration: false,
406
414
  hierarchicalDocumentSymbolSupport: true,
407
415
  },
416
+ publishDiagnostics: {
417
+ relatedInformation: true,
418
+ tagSupport: {
419
+ valueSet: [DiagnosticTag.Unnecessary, DiagnosticTag.Deprecated],
420
+ },
421
+ },
408
422
  diagnostic: {
409
423
  dynamicRegistration: false,
410
424
  relatedDocumentSupport: false,
@@ -4,6 +4,7 @@ export class DiagnosticSnapshotStore {
4
4
  maxDiagnosticsPerDocument;
5
5
  pushSnapshots = new Map();
6
6
  pullSnapshots = new Map();
7
+ pushWaiters = new Map();
7
8
  pushObserved = false;
8
9
  constructor(maxDocuments, maxDiagnosticsPerDocument) {
9
10
  this.maxDocuments = maxDocuments;
@@ -11,10 +12,13 @@ export class DiagnosticSnapshotStore {
11
12
  }
12
13
  capturePush(params, document, encoding) {
13
14
  this.pushObserved = true;
14
- if (!document)
15
+ if (!document) {
16
+ this.notifyPushWaiters(params.uri, true);
15
17
  return;
18
+ }
16
19
  const snapshot = this.normalizeSnapshot(params.diagnostics, document, encoding, params.version === undefined ? {} : { publishedVersion: params.version });
17
20
  this.setBounded(this.pushSnapshots, params.uri, snapshot);
21
+ this.notifyPushWaiters(params.uri, true);
18
22
  }
19
23
  capturePull(diagnostics, document, encoding, resultId) {
20
24
  const snapshot = this.normalizeSnapshot(diagnostics, document, encoding, resultId === undefined ? {} : { resultId });
@@ -41,12 +45,52 @@ export class DiagnosticSnapshotStore {
41
45
  readPull(document, limit) {
42
46
  return this.read(this.pullSnapshots, document, limit, false);
43
47
  }
48
+ async waitForFreshPush(document, limit, timeoutMs, signal) {
49
+ let snapshot = this.readPush(document, limit);
50
+ const deadline = Date.now() + timeoutMs;
51
+ while (snapshot.freshness.state === "missing" || snapshot.freshness.state === "stale") {
52
+ if (signal?.aborted)
53
+ break;
54
+ const remaining = deadline - Date.now();
55
+ if (remaining <= 0 || !await this.waitForPush(document.uri, remaining, signal))
56
+ break;
57
+ snapshot = this.readPush(document, limit);
58
+ }
59
+ return snapshot;
60
+ }
44
61
  hasObservedPushDiagnostics() {
45
62
  return this.pushObserved;
46
63
  }
64
+ waitForPush(uri, timeoutMs, signal) {
65
+ if (signal?.aborted)
66
+ return Promise.resolve(false);
67
+ return new Promise((resolve) => {
68
+ let settled = false;
69
+ const waiters = this.pushWaiters.get(uri) ?? new Set();
70
+ const finish = (observed) => {
71
+ if (settled)
72
+ return;
73
+ settled = true;
74
+ clearTimeout(timer);
75
+ signal?.removeEventListener("abort", onAbort);
76
+ waiters.delete(finish);
77
+ if (waiters.size === 0)
78
+ this.pushWaiters.delete(uri);
79
+ resolve(observed);
80
+ };
81
+ const onAbort = () => finish(false);
82
+ const timer = setTimeout(() => finish(false), timeoutMs);
83
+ timer.unref();
84
+ signal?.addEventListener("abort", onAbort, { once: true });
85
+ waiters.add(finish);
86
+ this.pushWaiters.set(uri, waiters);
87
+ });
88
+ }
47
89
  clear() {
48
90
  this.pushSnapshots.clear();
49
91
  this.pullSnapshots.clear();
92
+ for (const uri of [...this.pushWaiters.keys()])
93
+ this.notifyPushWaiters(uri, false);
50
94
  this.pushObserved = false;
51
95
  }
52
96
  get size() {
@@ -70,6 +114,13 @@ export class DiagnosticSnapshotStore {
70
114
  ...metadata,
71
115
  };
72
116
  }
117
+ notifyPushWaiters(uri, observed) {
118
+ const waiters = this.pushWaiters.get(uri);
119
+ if (!waiters)
120
+ return;
121
+ for (const waiter of [...waiters])
122
+ waiter(observed);
123
+ }
73
124
  setBounded(snapshots, uri, snapshot) {
74
125
  snapshots.delete(uri);
75
126
  snapshots.set(uri, snapshot);
@@ -0,0 +1,172 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { delimiter, join, resolve } from "node:path";
4
+ const MANAGED_LANGUAGE_SERVERS = {
5
+ typescript: {
6
+ id: "typescript",
7
+ label: "TypeScript / JavaScript",
8
+ executable: "typescript-language-server",
9
+ packages: ["typescript-language-server@6", "typescript@6"],
10
+ },
11
+ pyright: {
12
+ id: "pyright",
13
+ label: "Python (Pyright)",
14
+ executable: "pyright-langserver",
15
+ packages: ["pyright@1"],
16
+ },
17
+ };
18
+ export function managedLanguageServerRoot(configDir) {
19
+ return join(configDir, "language-servers");
20
+ }
21
+ export function managedLanguageServerBinDir(configDir) {
22
+ return join(managedLanguageServerRoot(configDir), "node_modules", ".bin");
23
+ }
24
+ export function managedLanguageServerExecutablePath(configDir, id, platform = process.platform) {
25
+ const bin = managedLanguageServerBinDir(configDir);
26
+ const executable = MANAGED_LANGUAGE_SERVERS[id].executable;
27
+ const extensions = platform === "win32" ? [".cmd", ".exe", ".bat", ""] : [""];
28
+ return extensions
29
+ .map((extension) => join(bin, `${executable}${extension}`))
30
+ .find((path) => existsSync(path));
31
+ }
32
+ export function managedLanguageServerIdForCommand(configDir, command, platform = process.platform) {
33
+ return supportedManagedLanguageServers().find((id) => {
34
+ const executable = managedLanguageServerExecutablePath(configDir, id, platform);
35
+ if (executable === undefined)
36
+ return false;
37
+ const executablePath = resolve(executable);
38
+ const commandPath = resolve(command);
39
+ return platform === "win32"
40
+ ? executablePath.toLowerCase() === commandPath.toLowerCase()
41
+ : executablePath === commandPath;
42
+ });
43
+ }
44
+ export function managedTypeScriptTsserverPath(configDir) {
45
+ const path = join(managedLanguageServerRoot(configDir), "node_modules", "typescript", "lib", "tsserver.js");
46
+ return existsSync(path) ? path : undefined;
47
+ }
48
+ export function managedLanguageServerRuntimeIdentity(configDir, id) {
49
+ const packageNames = id === "typescript"
50
+ ? ["typescript-language-server", "typescript"]
51
+ : ["pyright"];
52
+ try {
53
+ const versions = packageNames.map((name) => {
54
+ const packageJson = JSON.parse(readFileSync(join(managedLanguageServerRoot(configDir), "node_modules", name, "package.json"), "utf8"));
55
+ if (!packageJson.version)
56
+ throw new Error(`Missing version for ${name}`);
57
+ return [name, packageJson.version];
58
+ });
59
+ return JSON.stringify(versions);
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ }
65
+ export function withManagedLanguageServerPath(env, configDir) {
66
+ if (!configDir)
67
+ return { ...env };
68
+ const bin = managedLanguageServerBinDir(configDir);
69
+ return {
70
+ ...env,
71
+ PATH: [bin, env.PATH].filter((value) => Boolean(value)).join(delimiter),
72
+ };
73
+ }
74
+ export function managedLanguageServerOptions() {
75
+ return [
76
+ {
77
+ value: "typescript",
78
+ label: MANAGED_LANGUAGE_SERVERS.typescript.label,
79
+ hint: "Installs typescript-language-server and TypeScript into ForgeRelay's private config directory.",
80
+ },
81
+ {
82
+ value: "pyright",
83
+ label: MANAGED_LANGUAGE_SERVERS.pyright.label,
84
+ hint: "Installs Pyright into ForgeRelay's private config directory.",
85
+ },
86
+ ];
87
+ }
88
+ export function supportedManagedLanguageServers() {
89
+ return Object.keys(MANAGED_LANGUAGE_SERVERS);
90
+ }
91
+ export function installedManagedLanguageServers(configDir) {
92
+ return supportedManagedLanguageServers()
93
+ .filter((id) => managedExecutableExists(configDir, MANAGED_LANGUAGE_SERVERS[id].executable));
94
+ }
95
+ export function managedLanguageServerPackages(ids) {
96
+ const packages = new Set();
97
+ for (const id of ids) {
98
+ for (const packageSpec of MANAGED_LANGUAGE_SERVERS[id].packages)
99
+ packages.add(packageSpec);
100
+ }
101
+ return [...packages];
102
+ }
103
+ export async function installManagedLanguageServers(ids, configDir, runNpm = defaultNpmRunner) {
104
+ const selected = Array.from(new Set(ids));
105
+ const root = managedLanguageServerRoot(configDir);
106
+ const packages = managedLanguageServerPackages(selected);
107
+ if (packages.length === 0)
108
+ return { installed: [], packages: [], root };
109
+ mkdirSync(root, { recursive: true });
110
+ const packageJsonPath = join(root, "package.json");
111
+ if (!existsSync(packageJsonPath)) {
112
+ writeFileSync(packageJsonPath, JSON.stringify({
113
+ name: "forgerelay-managed-language-servers",
114
+ private: true,
115
+ description: "ForgeRelay-managed optional Language Servers. Do not publish.",
116
+ }, null, 2) + "\n");
117
+ }
118
+ await runNpm([
119
+ "install",
120
+ "--prefix",
121
+ root,
122
+ "--save-exact",
123
+ "--no-audit",
124
+ "--no-fund",
125
+ ...packages,
126
+ ]);
127
+ for (const id of selected)
128
+ assertManagedLanguageServerInstall(configDir, id);
129
+ return { installed: selected, packages, root };
130
+ }
131
+ function assertManagedLanguageServerInstall(configDir, id) {
132
+ if (!managedLanguageServerExecutablePath(configDir, id)) {
133
+ throw new Error(`Managed Language Server ${id} installed without its expected executable.`);
134
+ }
135
+ if (id === "typescript" && !managedTypeScriptTsserverPath(configDir)) {
136
+ throw new Error("Managed TypeScript Language Server installed without a compatible TypeScript tsserver.js. " +
137
+ "ForgeRelay requires a tsserver-based TypeScript package for typescript-language-server.");
138
+ }
139
+ }
140
+ function managedExecutableExists(configDir, executable) {
141
+ const bin = managedLanguageServerBinDir(configDir);
142
+ if (process.platform === "win32") {
143
+ return [".cmd", ".exe", ".bat", ""].some((extension) => existsSync(join(bin, `${executable}${extension}`)));
144
+ }
145
+ return existsSync(join(bin, executable));
146
+ }
147
+ function defaultNpmRunner(args) {
148
+ const command = process.platform === "win32" ? "npm.cmd" : "npm";
149
+ return new Promise((resolve, reject) => {
150
+ const child = spawn(command, args, {
151
+ env: process.env,
152
+ stdio: ["ignore", "pipe", "pipe"],
153
+ windowsHide: true,
154
+ shell: process.platform === "win32",
155
+ });
156
+ let stdout = "";
157
+ let stderr = "";
158
+ child.stdout?.setEncoding("utf8");
159
+ child.stderr?.setEncoding("utf8");
160
+ child.stdout?.on("data", (chunk) => { stdout += chunk; });
161
+ child.stderr?.on("data", (chunk) => { stderr += chunk; });
162
+ child.once("error", reject);
163
+ child.once("close", (code) => {
164
+ if (code === 0) {
165
+ resolve();
166
+ return;
167
+ }
168
+ const detail = (stderr || stdout).trim();
169
+ reject(new Error(`npm install for managed Language Servers failed with exit ${code ?? "unknown"}${detail ? `: ${detail}` : ""}`));
170
+ });
171
+ });
172
+ }