@neuralnomads/codenomad-dev 0.20.0-dev-20260911-ea37f97b → 0.20.0-dev-20260917-aa51cbb9

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 (191) hide show
  1. package/README.md +2 -2
  2. package/dist/api-types.js.map +1 -1
  3. package/dist/opencode/automation-plugin.test.js +32 -38
  4. package/dist/opencode/automation-plugin.test.js.map +1 -1
  5. package/dist/opencode/compatibility/contract-fixture.js +15 -0
  6. package/dist/opencode/compatibility/contract-fixture.js.map +1 -0
  7. package/dist/opencode/compatibility/events.js +9 -0
  8. package/dist/opencode/compatibility/events.js.map +1 -0
  9. package/dist/opencode/compatibility/location.js +89 -0
  10. package/dist/opencode/compatibility/location.js.map +1 -0
  11. package/dist/opencode/compatibility/location.test.js +92 -0
  12. package/dist/opencode/compatibility/location.test.js.map +1 -0
  13. package/dist/opencode/compatibility/negotiate.js +73 -0
  14. package/dist/opencode/compatibility/negotiate.js.map +1 -0
  15. package/dist/opencode/compatibility/negotiate.test.js +61 -0
  16. package/dist/opencode/compatibility/negotiate.test.js.map +1 -0
  17. package/dist/opencode/compatibility/proxy-locations.js +137 -0
  18. package/dist/opencode/compatibility/proxy-locations.js.map +1 -0
  19. package/dist/opencode/compatibility/proxy-locations.test.js +42 -0
  20. package/dist/opencode/compatibility/proxy-locations.test.js.map +1 -0
  21. package/dist/opencode/compatibility/proxy.js +60 -0
  22. package/dist/opencode/compatibility/proxy.js.map +1 -0
  23. package/dist/opencode/compatibility/proxy.test.js +47 -0
  24. package/dist/opencode/compatibility/proxy.test.js.map +1 -0
  25. package/dist/opencode/compatibility/requests.js +54 -0
  26. package/dist/opencode/compatibility/requests.js.map +1 -0
  27. package/dist/opencode/compatibility/runtime.js +26 -0
  28. package/dist/opencode/compatibility/runtime.js.map +1 -0
  29. package/dist/opencode/compatibility/transport.js +112 -0
  30. package/dist/opencode/compatibility/transport.js.map +1 -0
  31. package/dist/opencode/compatibility/transport.test.js +83 -0
  32. package/dist/opencode/compatibility/transport.test.js.map +1 -0
  33. package/dist/opencode/session-pruning/location.js +16 -0
  34. package/dist/opencode/session-pruning/location.js.map +1 -0
  35. package/dist/opencode/session-pruning/package.test.js +37 -0
  36. package/dist/opencode/session-pruning/package.test.js.map +1 -0
  37. package/dist/opencode/session-pruning/plugin.js +5 -2
  38. package/dist/opencode/session-pruning/plugin.js.map +1 -1
  39. package/dist/opencode/session-pruning/preview-store.js +3 -2
  40. package/dist/opencode/session-pruning/preview-store.js.map +1 -1
  41. package/dist/opencode/session-pruning/pruning.test.js +7 -5
  42. package/dist/opencode/session-pruning/pruning.test.js.map +1 -1
  43. package/dist/opencode/session-pruning/service.js +5 -4
  44. package/dist/opencode/session-pruning/service.js.map +1 -1
  45. package/dist/opencode/session-pruning/service.test.js +65 -53
  46. package/dist/opencode/session-pruning/service.test.js.map +1 -1
  47. package/dist/permissions/opencode-replier.js +2 -2
  48. package/dist/permissions/opencode-replier.js.map +1 -1
  49. package/dist/permissions/opencode-replier.test.js +1 -1
  50. package/dist/permissions/opencode-replier.test.js.map +1 -1
  51. package/dist/permissions/opencode-yolo-metadata.js.map +1 -1
  52. package/dist/permissions/opencode-yolo-metadata.test.js.map +1 -1
  53. package/dist/plugins/session-pruning/plugin.mjs +422 -271
  54. package/dist/server/__tests__/instance-proxy-legacy.test.js +111 -0
  55. package/dist/server/__tests__/instance-proxy-legacy.test.js.map +1 -0
  56. package/dist/server/__tests__/instance-proxy.test.js +95 -34
  57. package/dist/server/__tests__/instance-proxy.test.js.map +1 -1
  58. package/dist/server/http-server.js +147 -237
  59. package/dist/server/http-server.js.map +1 -1
  60. package/dist/server/routes/session-pruning.js +4 -2
  61. package/dist/server/routes/session-pruning.js.map +1 -1
  62. package/dist/server/routes/session-pruning.test.js +16 -2
  63. package/dist/server/routes/session-pruning.test.js.map +1 -1
  64. package/dist/server/routes/worktrees.js +1 -0
  65. package/dist/server/routes/worktrees.js.map +1 -1
  66. package/dist/server/routes/worktrees.test.js +54 -1
  67. package/dist/server/routes/worktrees.test.js.map +1 -1
  68. package/dist/workspaces/host-opencode-service.test.js +56 -7
  69. package/dist/workspaces/host-opencode-service.test.js.map +1 -1
  70. package/dist/workspaces/instance-client.js.map +1 -1
  71. package/dist/workspaces/instance-events.js +71 -57
  72. package/dist/workspaces/instance-events.js.map +1 -1
  73. package/dist/workspaces/instance-events.test.js +122 -9
  74. package/dist/workspaces/instance-events.test.js.map +1 -1
  75. package/dist/workspaces/manager.js +24 -39
  76. package/dist/workspaces/manager.js.map +1 -1
  77. package/dist/workspaces/manager.test.js +43 -7
  78. package/dist/workspaces/manager.test.js.map +1 -1
  79. package/dist/workspaces/opencode-cli-service.js +56 -29
  80. package/dist/workspaces/opencode-cli-service.js.map +1 -1
  81. package/dist/workspaces/opencode-connection-generation.test.js +57 -0
  82. package/dist/workspaces/opencode-connection-generation.test.js.map +1 -0
  83. package/dist/workspaces/opencode-service.js +70 -16
  84. package/dist/workspaces/opencode-service.js.map +1 -1
  85. package/dist/workspaces/opencode-service.test.js +3 -4
  86. package/dist/workspaces/opencode-service.test.js.map +1 -1
  87. package/dist/workspaces/worktree-session-evacuation.js +31 -11
  88. package/dist/workspaces/worktree-session-evacuation.js.map +1 -1
  89. package/dist/workspaces/worktree-session-evacuation.test.js +211 -0
  90. package/dist/workspaces/worktree-session-evacuation.test.js.map +1 -1
  91. package/dist/workspaces/wsl-opencode-service.test.js +64 -24
  92. package/dist/workspaces/wsl-opencode-service.test.js.map +1 -1
  93. package/package.json +3 -3
  94. package/public/assets/App-BROhwPo9.js +54 -0
  95. package/public/assets/App-BROhwPo9.js.map +1 -0
  96. package/public/assets/{FilesTab-CRpGDs1B.js → FilesTab-DY3Vr-3r.js} +3 -3
  97. package/public/assets/{FilesTab-CRpGDs1B.js.map → FilesTab-DY3Vr-3r.js.map} +1 -1
  98. package/public/assets/{GitChangesTab-DxFebTiq.js → GitChangesTab-BD6CJngE.js} +3 -3
  99. package/public/assets/{GitChangesTab-DxFebTiq.js.map → GitChangesTab-BD6CJngE.js.map} +1 -1
  100. package/public/assets/{SplitFilePanel-DgTMrrO-.js → SplitFilePanel-BdY6d_6m.js} +2 -2
  101. package/public/assets/{SplitFilePanel-DgTMrrO-.js.map → SplitFilePanel-BdY6d_6m.js.map} +1 -1
  102. package/public/assets/StatusTab-C-_2yaPa.js +2 -0
  103. package/public/assets/StatusTab-C-_2yaPa.js.map +1 -0
  104. package/public/assets/{align-justify-v_yPXSys.js → align-justify-DI8VTZ4G.js} +2 -2
  105. package/public/assets/{align-justify-v_yPXSys.js.map → align-justify-DI8VTZ4G.js.map} +1 -1
  106. package/public/assets/{bundle-full-HzL_Jtqk.js → bundle-full-D_M65Eb_.js} +2 -2
  107. package/public/assets/{bundle-full-HzL_Jtqk.js.map → bundle-full-D_M65Eb_.js.map} +1 -1
  108. package/public/assets/{client-state-CF-PJ0GG.js → client-state-CWCzCOz6.js} +2 -2
  109. package/public/assets/{client-state-CF-PJ0GG.js.map → client-state-CWCzCOz6.js.map} +1 -1
  110. package/public/assets/{diff-viewer-BFDQBDVy.js → diff-viewer-CGPsYEr3.js} +2 -2
  111. package/public/assets/{diff-viewer-BFDQBDVy.js.map → diff-viewer-CGPsYEr3.js.map} +1 -1
  112. package/public/assets/index-B8-lAqjr.js +2 -0
  113. package/public/assets/index-B8-lAqjr.js.map +1 -0
  114. package/public/assets/index-BeUewQym.js +2 -0
  115. package/public/assets/index-BeUewQym.js.map +1 -0
  116. package/public/assets/index-BxRFHpJd.js +2 -0
  117. package/public/assets/{index-D2UHSOG5.js.map → index-BxRFHpJd.js.map} +1 -1
  118. package/public/assets/index-Bzv1kU-S.js +2 -0
  119. package/public/assets/{index-CTKYD_VJ.js.map → index-Bzv1kU-S.js.map} +1 -1
  120. package/public/assets/index-C9tI4CRG.js +2 -0
  121. package/public/assets/index-C9tI4CRG.js.map +1 -0
  122. package/public/assets/index-CGYHFZ6F.js +2 -0
  123. package/public/assets/index-CGYHFZ6F.js.map +1 -0
  124. package/public/assets/{index-CHYmKl10.js → index-CMEAjBFp.js} +2 -2
  125. package/public/assets/index-CMEAjBFp.js.map +1 -0
  126. package/public/assets/index-Cazypvyc.js +2 -0
  127. package/public/assets/index-Cazypvyc.js.map +1 -0
  128. package/public/assets/index-CwqzofWP.js +2 -0
  129. package/public/assets/index-CwqzofWP.js.map +1 -0
  130. package/public/assets/index-Da9yOa6q.js +2 -0
  131. package/public/assets/index-Da9yOa6q.js.map +1 -0
  132. package/public/assets/{index-RK9gf0uk.js → index-Ddjkirek.js} +2 -2
  133. package/public/assets/{index-RK9gf0uk.js.map → index-Ddjkirek.js.map} +1 -1
  134. package/public/assets/index-HHgvEOAH.js +3 -0
  135. package/public/assets/index-HHgvEOAH.js.map +1 -0
  136. package/public/assets/index-MeFffHqb.css +1 -0
  137. package/public/assets/loading-CdhHiSED.css +1 -0
  138. package/public/assets/{loading-BSB82snY.js → loading-Gqh6NPUr.js} +2 -2
  139. package/public/assets/{loading-BSB82snY.js.map → loading-Gqh6NPUr.js.map} +1 -1
  140. package/public/assets/main-Cbx7KdTr.js +38 -0
  141. package/public/assets/main-Cbx7KdTr.js.map +1 -0
  142. package/public/assets/{markdown-DtidoL2Z.js → markdown-BWnjyvnX.js} +4 -4
  143. package/public/assets/{markdown-DtidoL2Z.js.map → markdown-BWnjyvnX.js.map} +1 -1
  144. package/public/assets/monaco-viewer-BEJIiZWL.js +27 -0
  145. package/public/assets/monaco-viewer-BEJIiZWL.js.map +1 -0
  146. package/public/assets/{startup-state-settings-card-BY741Rt0.js → startup-state-settings-card-C4E2Itop.js} +2 -2
  147. package/public/assets/{startup-state-settings-card-BY741Rt0.js.map → startup-state-settings-card-C4E2Itop.js.map} +1 -1
  148. package/public/assets/{terminal-square-BdC_jwm3.js → terminal-square-C3wutdV0.js} +2 -2
  149. package/public/assets/{terminal-square-BdC_jwm3.js.map → terminal-square-C3wutdV0.js.map} +1 -1
  150. package/public/assets/tool-call-DNsAQ_m4.js +60 -0
  151. package/public/assets/tool-call-DNsAQ_m4.js.map +1 -0
  152. package/public/assets/{unified-picker-C0eo1chJ.js → unified-picker-tB7y17Qz.js} +2 -2
  153. package/public/assets/unified-picker-tB7y17Qz.js.map +1 -0
  154. package/public/assets/{wrap-text-BuMtdvMx.js → wrap-text-CkBxzt04.js} +2 -2
  155. package/public/assets/{wrap-text-BuMtdvMx.js.map → wrap-text-CkBxzt04.js.map} +1 -1
  156. package/public/index.html +4 -4
  157. package/public/loading.html +5 -5
  158. package/public/sw.js +1 -1
  159. package/public/sw.js.map +1 -1
  160. package/public/assets/App-0-FcHzPS.js +0 -44
  161. package/public/assets/App-0-FcHzPS.js.map +0 -1
  162. package/public/assets/StatusTab-LKUA9cbH.js +0 -2
  163. package/public/assets/StatusTab-LKUA9cbH.js.map +0 -1
  164. package/public/assets/index-8iPlm2r0.js +0 -2
  165. package/public/assets/index-8iPlm2r0.js.map +0 -1
  166. package/public/assets/index-B185mC7m.js +0 -2
  167. package/public/assets/index-B185mC7m.js.map +0 -1
  168. package/public/assets/index-Bbj1xF8i.js +0 -2
  169. package/public/assets/index-Bbj1xF8i.js.map +0 -1
  170. package/public/assets/index-BsmFN3k1.js +0 -2
  171. package/public/assets/index-BsmFN3k1.js.map +0 -1
  172. package/public/assets/index-Bz_OQJYA.css +0 -1
  173. package/public/assets/index-C52CfqK3.js +0 -2
  174. package/public/assets/index-C52CfqK3.js.map +0 -1
  175. package/public/assets/index-CHYmKl10.js.map +0 -1
  176. package/public/assets/index-CTKYD_VJ.js +0 -2
  177. package/public/assets/index-D2UHSOG5.js +0 -2
  178. package/public/assets/index-DDoB_Z36.js +0 -2
  179. package/public/assets/index-DDoB_Z36.js.map +0 -1
  180. package/public/assets/index-DcwAtZw-.js +0 -3
  181. package/public/assets/index-DcwAtZw-.js.map +0 -1
  182. package/public/assets/index-DxUD3RYr.js +0 -2
  183. package/public/assets/index-DxUD3RYr.js.map +0 -1
  184. package/public/assets/loading-CmEVQgyj.css +0 -1
  185. package/public/assets/main-CZztwrfv.js +0 -48
  186. package/public/assets/main-CZztwrfv.js.map +0 -1
  187. package/public/assets/monaco-viewer-CpxDfB-6.js +0 -27
  188. package/public/assets/monaco-viewer-CpxDfB-6.js.map +0 -1
  189. package/public/assets/tool-call-DbtCsJ4x.js +0 -60
  190. package/public/assets/tool-call-DbtCsJ4x.js.map +0 -1
  191. package/public/assets/unified-picker-C0eo1chJ.js.map +0 -1
@@ -1,4 +1,5 @@
1
1
  import { messageTargetSchema, PRUNING_RPC_ID, prunePreviewSchema, pruneRequestSchema, pruneResultSchema } from "../../opencode/session-pruning/contract";
2
+ import { locationRequestOptions, readLocationRef } from "../../opencode/compatibility/location";
2
3
  export function registerSessionPruningRoutes(app, deps) {
3
4
  for (const method of ["preview", "prune"]) {
4
5
  app.post(`/api/workspaces/:id/session-pruning/${method}`, { bodyLimit: 1024 * 1024 }, async (request, reply) => {
@@ -7,7 +8,8 @@ export function registerSessionPruningRoutes(app, deps) {
7
8
  return reply.code(400).send({ error: "Invalid pruning request" });
8
9
  const client = await deps.workspaceManager.getSharedServiceClient();
9
10
  const session = await client.session.get({ sessionID: input.data.sessionID });
10
- if (!await deps.workspaceManager.ownsLocation(request.params.id, session.location)) {
11
+ const location = readLocationRef(session.location);
12
+ if (!await deps.workspaceManager.ownsLocation(request.params.id, session.location, client)) {
11
13
  return reply.code(403).send({ error: "Session does not belong to workspace" });
12
14
  }
13
15
  const identity = await deps.workspaceManager.getWorktreeIdentityForPath(request.params.id, session.location.directory);
@@ -21,7 +23,7 @@ export function registerSessionPruningRoutes(app, deps) {
21
23
  // database filename, SQL, or replacement content. Generic RPC stays blocked.
22
24
  const result = await client.rpc.call({
23
25
  rpcID: PRUNING_RPC_ID, method, input: input.data, location: { directory: session.location.directory },
24
- }, { signal: AbortSignal.timeout(15_000) });
26
+ }, { ...locationRequestOptions(location), signal: AbortSignal.timeout(15_000) });
25
27
  const output = (method === "prune" ? pruneResultSchema : prunePreviewSchema).safeParse(result.output);
26
28
  if (!output.success)
27
29
  return { status: "blocked", reason: "unavailable" };
@@ -1 +1 @@
1
- {"version":3,"file":"session-pruning.js","sourceRoot":"","sources":["../../../src/server/routes/session-pruning.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FastifyInstance } from \"fastify\"\nimport { messageTargetSchema, PRUNING_RPC_ID, prunePreviewSchema, pruneRequestSchema, pruneResultSchema } from \"../../opencode/session-pruning/contract\"\nimport type { WorkspaceManager } from \"../../workspaces/manager\"\nimport type { WorktreeDeletionFence } from \"../../workspaces/worktree-session-evacuation\"\n\ninterface RouteDeps {\n workspaceManager: Pick<WorkspaceManager, \"getSharedServiceClient\" | \"ownsLocation\" | \"getWorktreeIdentityForPath\">\n worktreeDeletionFence: WorktreeDeletionFence\n}\n\nexport function registerSessionPruningRoutes(app: FastifyInstance, deps: RouteDeps): void {\n for (const method of [\"preview\", \"prune\"] as const) {\n app.post<{ Params: { id: string } }>(`/api/workspaces/:id/session-pruning/${method}`, { bodyLimit: 1024 * 1024 }, async (request, reply) => {\n const input = (method === \"prune\" ? pruneRequestSchema : messageTargetSchema).safeParse(request.body)\n if (!input.success) return reply.code(400).send({ error: \"Invalid pruning request\" })\n const client = await deps.workspaceManager.getSharedServiceClient()\n const session = await client.session.get({ sessionID: input.data.sessionID })\n if (!await deps.workspaceManager.ownsLocation(request.params.id, session.location)) {\n return reply.code(403).send({ error: \"Session does not belong to workspace\" })\n }\n const identity = await deps.workspaceManager.getWorktreeIdentityForPath(request.params.id, session.location.directory)\n if (!identity) return reply.code(403).send({ error: \"Session does not belong to workspace\" })\n const release = deps.worktreeDeletionFence.enter([identity])\n if (!release) return reply.code(409).send({ error: \"Worktree deletion is in progress\" })\n try {\n // Narrow broker only: callers cannot choose RPC ID, method, location,\n // database filename, SQL, or replacement content. Generic RPC stays blocked.\n const result = await client.rpc.call({\n rpcID: PRUNING_RPC_ID, method, input: input.data, location: { directory: session.location.directory },\n }, { signal: AbortSignal.timeout(15_000) })\n const output = (method === \"prune\" ? pruneResultSchema : prunePreviewSchema).safeParse(result.output)\n if (!output.success) return { status: \"blocked\", reason: \"unavailable\" }\n if (output.data.status === \"pruned\" && (output.data.messageID !== input.data.messageID\n || !(\"indexes\" in input.data) || !Array.isArray(input.data.indexes) || output.data.removedCount !== input.data.indexes.length)) {\n return { status: \"blocked\", reason: \"unavailable\" }\n }\n return output.data\n } catch {\n // Missing plugin, incompatible runtime, timeout: no legacy API fallback\n // and no local-only deletion presented as a successful database write.\n return { status: \"blocked\", reason: \"unavailable\" }\n } finally { release() }\n })\n }\n}\n"]}
1
+ {"version":3,"file":"session-pruning.js","sourceRoot":"","sources":["../../../src/server/routes/session-pruning.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FastifyInstance } from \"fastify\"\nimport { messageTargetSchema, PRUNING_RPC_ID, prunePreviewSchema, pruneRequestSchema, pruneResultSchema } from \"../../opencode/session-pruning/contract\"\nimport type { WorkspaceManager } from \"../../workspaces/manager\"\nimport type { WorktreeDeletionFence } from \"../../workspaces/worktree-session-evacuation\"\nimport { locationRequestOptions, readLocationRef } from \"../../opencode/compatibility/location\"\n\ninterface RouteDeps {\n workspaceManager: Pick<WorkspaceManager, \"getSharedServiceClient\" | \"ownsLocation\" | \"getWorktreeIdentityForPath\">\n worktreeDeletionFence: WorktreeDeletionFence\n}\n\nexport function registerSessionPruningRoutes(app: FastifyInstance, deps: RouteDeps): void {\n for (const method of [\"preview\", \"prune\"] as const) {\n app.post<{ Params: { id: string } }>(`/api/workspaces/:id/session-pruning/${method}`, { bodyLimit: 1024 * 1024 }, async (request, reply) => {\n const input = (method === \"prune\" ? pruneRequestSchema : messageTargetSchema).safeParse(request.body)\n if (!input.success) return reply.code(400).send({ error: \"Invalid pruning request\" })\n const client = await deps.workspaceManager.getSharedServiceClient()\n const session = await client.session.get({ sessionID: input.data.sessionID })\n const location = readLocationRef(session.location)\n if (!await deps.workspaceManager.ownsLocation(request.params.id, session.location, client)) {\n return reply.code(403).send({ error: \"Session does not belong to workspace\" })\n }\n const identity = await deps.workspaceManager.getWorktreeIdentityForPath(request.params.id, session.location.directory)\n if (!identity) return reply.code(403).send({ error: \"Session does not belong to workspace\" })\n const release = deps.worktreeDeletionFence.enter([identity])\n if (!release) return reply.code(409).send({ error: \"Worktree deletion is in progress\" })\n try {\n // Narrow broker only: callers cannot choose RPC ID, method, location,\n // database filename, SQL, or replacement content. Generic RPC stays blocked.\n const result = await client.rpc.call({\n rpcID: PRUNING_RPC_ID, method, input: input.data, location: { directory: session.location.directory },\n }, { ...locationRequestOptions(location), signal: AbortSignal.timeout(15_000) })\n const output = (method === \"prune\" ? pruneResultSchema : prunePreviewSchema).safeParse(result.output)\n if (!output.success) return { status: \"blocked\", reason: \"unavailable\" }\n if (output.data.status === \"pruned\" && (output.data.messageID !== input.data.messageID\n || !(\"indexes\" in input.data) || !Array.isArray(input.data.indexes) || output.data.removedCount !== input.data.indexes.length)) {\n return { status: \"blocked\", reason: \"unavailable\" }\n }\n return output.data\n } catch {\n // Missing plugin, incompatible runtime, timeout: no legacy API fallback\n // and no local-only deletion presented as a successful database write.\n return { status: \"blocked\", reason: \"unavailable\" }\n } finally { release() }\n })\n }\n}\n"]}
@@ -7,14 +7,16 @@ const payload = { sessionID: "s", messageID: "m", revision: "a".repeat(64), inde
7
7
  const url = "/api/workspaces/w/session-pruning/prune";
8
8
  function fixture(options = {}) {
9
9
  const calls = [];
10
+ const requestOptions = [];
10
11
  const app = Fastify();
11
12
  const fence = new WorktreeDeletionFence();
12
13
  registerSessionPruningRoutes(app, { worktreeDeletionFence: fence, workspaceManager: {
13
14
  getSharedServiceClient: async () => ({
14
- session: { get: async () => ({ location: { directory: "/owned/worktree" } }) },
15
+ session: { get: async () => ({ location: { directory: "/owned/worktree", workspaceID: options.workspaceID } }) },
15
16
  rpc: { call: async (input, opts) => {
16
17
  assert(opts.signal instanceof AbortSignal);
17
18
  calls.push(input);
19
+ requestOptions.push(opts);
18
20
  if (options.fail)
19
21
  throw new Error("plugin unavailable");
20
22
  return { output: options.output ?? { status: "blocked", reason: "maintenance_required" } };
@@ -23,7 +25,7 @@ function fixture(options = {}) {
23
25
  ownsLocation: async () => options.owned !== false,
24
26
  getWorktreeIdentityForPath: async () => "/owned/worktree",
25
27
  } });
26
- return { app, calls, fence };
28
+ return { app, calls, fence, requestOptions };
27
29
  }
28
30
  test("broker pins RPC, method and native session location", async () => {
29
31
  const { app, calls } = fixture();
@@ -47,6 +49,18 @@ test("rejects unowned sessions before any RPC", async () => {
47
49
  await app.close();
48
50
  }
49
51
  });
52
+ test("broker preserves the authorized native identity through generated request options", async () => {
53
+ const { app, requestOptions } = fixture({ workspaceID: "native-worktree" });
54
+ try {
55
+ assert.equal((await app.inject({ method: "POST", url, payload })).statusCode, 200);
56
+ assert.deepEqual(JSON.parse(decodeURIComponent(requestOptions[0].headers["x-codenomad-location"])), {
57
+ directory: "/owned/worktree", workspaceID: "native-worktree",
58
+ });
59
+ }
60
+ finally {
61
+ await app.close();
62
+ }
63
+ });
50
64
  test("rejects caller-selected location, RPC, database and replacement content", async () => {
51
65
  const { app, calls } = fixture();
52
66
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"session-pruning.test.js","sourceRoot":"","sources":["../../../src/server/routes/session-pruning.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { test } from \"node:test\"\nimport Fastify from \"fastify\"\nimport { registerSessionPruningRoutes } from \"./session-pruning\"\nimport { WorktreeDeletionFence } from \"../../workspaces/worktree-session-evacuation\"\n\nconst payload = { sessionID: \"s\", messageID: \"m\", revision: \"a\".repeat(64), indexes: [0] }\nconst url = \"/api/workspaces/w/session-pruning/prune\"\nfunction fixture(options: { owned?: boolean; output?: unknown; fail?: boolean } = {}) {\n const calls: any[] = []\n const app = Fastify()\n const fence = new WorktreeDeletionFence()\n registerSessionPruningRoutes(app, { worktreeDeletionFence: fence, workspaceManager: {\n getSharedServiceClient: async () => ({\n session: { get: async () => ({ location: { directory: \"/owned/worktree\" } }) },\n rpc: { call: async (input: unknown, opts: any) => {\n assert(opts.signal instanceof AbortSignal)\n calls.push(input)\n if (options.fail) throw new Error(\"plugin unavailable\")\n return { output: options.output ?? { status: \"blocked\", reason: \"maintenance_required\" } }\n } },\n }) as any,\n ownsLocation: async () => options.owned !== false,\n getWorktreeIdentityForPath: async () => \"/owned/worktree\",\n } })\n return { app, calls, fence }\n}\n\ntest(\"broker pins RPC, method and native session location\", async () => {\n const { app, calls } = fixture()\n try {\n const response = await app.inject({ method: \"POST\", url, payload })\n assert.equal(response.statusCode, 200)\n assert.deepEqual(response.json(), { status: \"blocked\", reason: \"maintenance_required\" })\n assert.deepEqual(calls, [{ rpcID: \"codenomad.session-pruning\", method: \"prune\", input: payload, location: { directory: \"/owned/worktree\" } }])\n } finally { await app.close() }\n})\n\ntest(\"rejects unowned sessions before any RPC\", async () => {\n const { app, calls } = fixture({ owned: false })\n try {\n assert.equal((await app.inject({ method: \"POST\", url, payload })).statusCode, 403)\n assert.equal(calls.length, 0)\n } finally { await app.close() }\n})\n\ntest(\"rejects caller-selected location, RPC, database and replacement content\", async () => {\n const { app, calls } = fixture()\n try {\n for (const extra of [{ location: { directory: \"/other\" } }, { rpcID: \"other\" }, { databasePath: \"live.db\" }, { content: [] }]) {\n assert.equal((await app.inject({ method: \"POST\", url, payload: { ...payload, ...extra } })).statusCode, 400)\n }\n assert.equal(calls.length, 0)\n } finally { await app.close() }\n})\n\ntest(\"does not bypass worktree deletion fence\", async () => {\n const { app, calls, fence } = fixture()\n try {\n await fence.run(\"/owned/worktree\", [\"/owned/worktree\"], async () => {\n assert.equal((await app.inject({ method: \"POST\", url, payload })).statusCode, 409)\n })\n assert.equal(calls.length, 0)\n } finally { await app.close() }\n})\n\ntest(\"missing plugin, malformed response, wrong message or count never report success\", async () => {\n for (const options of [{ fail: true }, { output: {} }, { output: { status: \"pruned\", messageID: \"other\", revision: \"a\".repeat(64), removedCount: 1 } }, { output: { status: \"pruned\", messageID: \"m\", revision: \"a\".repeat(64), removedCount: 2 } }]) {\n const { app, fence } = fixture(options)\n try {\n const response = await app.inject({ method: \"POST\", url, payload })\n assert.deepEqual(response.json(), { status: \"blocked\", reason: \"unavailable\" })\n await fence.run(\"/owned/worktree\", [\"/owned/worktree\"], async () => {})\n } finally { await app.close() }\n }\n})\n\ntest(\"preview accepts IDs only and uses a distinct fixed RPC method\", async () => {\n const { app, calls } = fixture({ output: { status: \"preview\", revision: \"a\".repeat(64), liveMutation: false, parts: [] } })\n try {\n const response = await app.inject({ method: \"POST\", url: url.replace(/prune$/, \"preview\"), payload: { sessionID: \"s\", messageID: \"m\" } })\n assert.equal(response.json().status, \"preview\")\n assert.equal(calls[0].method, \"preview\")\n } finally { await app.close() }\n})\n"]}
1
+ {"version":3,"file":"session-pruning.test.js","sourceRoot":"","sources":["../../../src/server/routes/session-pruning.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { test } from \"node:test\"\nimport Fastify from \"fastify\"\nimport { registerSessionPruningRoutes } from \"./session-pruning\"\nimport { WorktreeDeletionFence } from \"../../workspaces/worktree-session-evacuation\"\n\nconst payload = { sessionID: \"s\", messageID: \"m\", revision: \"a\".repeat(64), indexes: [0] }\nconst url = \"/api/workspaces/w/session-pruning/prune\"\nfunction fixture(options: { owned?: boolean; output?: unknown; fail?: boolean; workspaceID?: string } = {}) {\n const calls: any[] = []\n const requestOptions: any[] = []\n const app = Fastify()\n const fence = new WorktreeDeletionFence()\n registerSessionPruningRoutes(app, { worktreeDeletionFence: fence, workspaceManager: {\n getSharedServiceClient: async () => ({\n session: { get: async () => ({ location: { directory: \"/owned/worktree\", workspaceID: options.workspaceID } }) },\n rpc: { call: async (input: unknown, opts: any) => {\n assert(opts.signal instanceof AbortSignal)\n calls.push(input)\n requestOptions.push(opts)\n if (options.fail) throw new Error(\"plugin unavailable\")\n return { output: options.output ?? { status: \"blocked\", reason: \"maintenance_required\" } }\n } },\n }) as any,\n ownsLocation: async () => options.owned !== false,\n getWorktreeIdentityForPath: async () => \"/owned/worktree\",\n } })\n return { app, calls, fence, requestOptions }\n}\n\ntest(\"broker pins RPC, method and native session location\", async () => {\n const { app, calls } = fixture()\n try {\n const response = await app.inject({ method: \"POST\", url, payload })\n assert.equal(response.statusCode, 200)\n assert.deepEqual(response.json(), { status: \"blocked\", reason: \"maintenance_required\" })\n assert.deepEqual(calls, [{ rpcID: \"codenomad.session-pruning\", method: \"prune\", input: payload, location: { directory: \"/owned/worktree\" } }])\n } finally { await app.close() }\n})\n\ntest(\"rejects unowned sessions before any RPC\", async () => {\n const { app, calls } = fixture({ owned: false })\n try {\n assert.equal((await app.inject({ method: \"POST\", url, payload })).statusCode, 403)\n assert.equal(calls.length, 0)\n } finally { await app.close() }\n})\n\ntest(\"broker preserves the authorized native identity through generated request options\", async () => {\n const { app, requestOptions } = fixture({ workspaceID: \"native-worktree\" })\n try {\n assert.equal((await app.inject({ method: \"POST\", url, payload })).statusCode, 200)\n assert.deepEqual(JSON.parse(decodeURIComponent(requestOptions[0].headers[\"x-codenomad-location\"])), {\n directory: \"/owned/worktree\", workspaceID: \"native-worktree\",\n })\n } finally { await app.close() }\n})\n\ntest(\"rejects caller-selected location, RPC, database and replacement content\", async () => {\n const { app, calls } = fixture()\n try {\n for (const extra of [{ location: { directory: \"/other\" } }, { rpcID: \"other\" }, { databasePath: \"live.db\" }, { content: [] }]) {\n assert.equal((await app.inject({ method: \"POST\", url, payload: { ...payload, ...extra } })).statusCode, 400)\n }\n assert.equal(calls.length, 0)\n } finally { await app.close() }\n})\n\ntest(\"does not bypass worktree deletion fence\", async () => {\n const { app, calls, fence } = fixture()\n try {\n await fence.run(\"/owned/worktree\", [\"/owned/worktree\"], async () => {\n assert.equal((await app.inject({ method: \"POST\", url, payload })).statusCode, 409)\n })\n assert.equal(calls.length, 0)\n } finally { await app.close() }\n})\n\ntest(\"missing plugin, malformed response, wrong message or count never report success\", async () => {\n for (const options of [{ fail: true }, { output: {} }, { output: { status: \"pruned\", messageID: \"other\", revision: \"a\".repeat(64), removedCount: 1 } }, { output: { status: \"pruned\", messageID: \"m\", revision: \"a\".repeat(64), removedCount: 2 } }]) {\n const { app, fence } = fixture(options)\n try {\n const response = await app.inject({ method: \"POST\", url, payload })\n assert.deepEqual(response.json(), { status: \"blocked\", reason: \"unavailable\" })\n await fence.run(\"/owned/worktree\", [\"/owned/worktree\"], async () => {})\n } finally { await app.close() }\n }\n})\n\ntest(\"preview accepts IDs only and uses a distinct fixed RPC method\", async () => {\n const { app, calls } = fixture({ output: { status: \"preview\", revision: \"a\".repeat(64), liveMutation: false, parts: [] } })\n try {\n const response = await app.inject({ method: \"POST\", url: url.replace(/prune$/, \"preview\"), payload: { sessionID: \"s\", messageID: \"m\" } })\n assert.equal(response.json().status, \"preview\")\n assert.equal(calls[0].method, \"preview\")\n } finally { await app.close() }\n})\n"]}
@@ -104,6 +104,7 @@ export function registerWorktreeRoutes(app, deps) {
104
104
  targetDirectory,
105
105
  rootDirectory: projectDirectory,
106
106
  resolveDirectoryIdentity: (directory) => deps.workspaceManager.getWorktreeIdentityForPath(workspace.id, directory),
107
+ resolveExactDirectory: (directory) => deps.workspaceManager.getServiceDirectoryForPath(workspace.id, directory),
107
108
  remove: () => removeWorktree({ workspaceFolder: workspace.path, directory: match.directory, force, logger: request.log }),
108
109
  })));
109
110
  invalidateWorktreeCache(workspace.id);
@@ -1 +1 @@
1
- {"version":3,"file":"worktrees.js","sourceRoot":"","sources":["../../../src/server/routes/worktrees.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FastifyInstance, FastifyReply } from \"fastify\"\nimport { z } from \"zod\"\nimport { WorkspaceManager } from \"../../workspaces/manager\"\nimport {\n resolveRepoRoot,\n listWorktrees,\n isValidWorktreeSlug,\n createManagedWorktree,\n removeWorktree,\n} from \"../../workspaces/git-worktrees\"\nimport { invalidateWorktreeCache } from \"../../workspaces/worktree-directory\"\nimport type { WorktreeListResponse } from \"../../api-types\"\nimport { ensureCodenomadGitExclude } from \"../../workspaces/worktree-map\"\nimport { createInstanceClient } from \"../../workspaces/instance-client\"\nimport { evacuateWorktreeSessions, type WorktreeDeletionFence } from \"../../workspaces/worktree-session-evacuation\"\n\ninterface RouteDeps {\n workspaceManager: WorkspaceManager\n worktreeDeletionFence: WorktreeDeletionFence\n}\n\nconst WorktreeCreateSchema = z.object({\n slug: z.string().trim().min(1),\n branch: z.string().trim().min(1).optional(),\n})\n\nexport function registerWorktreeRoutes(app: FastifyInstance, deps: RouteDeps) {\n app.get<{ Params: { id: string } }>(\"/api/workspaces/:id/worktrees\", async (request, reply) => {\n const workspace = deps.workspaceManager.get(request.params.id)\n if (!workspace) {\n reply.code(404)\n return { error: \"Workspace not found\" }\n }\n\n const { repoRoot, isGitRepo } = await resolveRepoRoot(workspace.path, request.log)\n const worktrees = await listWorktrees({ repoRoot, workspaceFolder: workspace.path, logger: request.log })\n const response: WorktreeListResponse = { worktrees, isGitRepo }\n return response\n })\n\n app.post<{ Params: { id: string } }>(\"/api/workspaces/:id/worktrees\", async (request, reply) => {\n const workspace = deps.workspaceManager.get(request.params.id)\n if (!workspace) {\n reply.code(404)\n return { error: \"Workspace not found\" }\n }\n\n try {\n const body = WorktreeCreateSchema.parse(request.body ?? {})\n const slug = body.slug\n if (!isValidWorktreeSlug(slug) || slug === \"root\") {\n reply.code(400)\n return { error: \"Invalid worktree slug\" }\n }\n if (body.branch) {\n if (!isValidWorktreeSlug(body.branch) || body.branch === \"root\") {\n reply.code(400)\n return { error: \"Invalid worktree branch\" }\n }\n if (body.branch !== slug) {\n reply.code(400)\n return { error: \"Branch must match slug\" }\n }\n }\n\n const { repoRoot, isGitRepo } = await resolveRepoRoot(workspace.path, request.log)\n if (!isGitRepo) {\n reply.code(400)\n return { error: \"Workspace is not a Git repository\" }\n }\n\n await ensureCodenomadGitExclude(workspace.path, request.log).catch(() => undefined)\n\n const created = await createManagedWorktree({\n repoRoot,\n workspaceFolder: workspace.path,\n slug,\n logger: request.log,\n })\n invalidateWorktreeCache(workspace.id)\n\n reply.code(201)\n return created\n } catch (error) {\n return handleError(error, reply)\n }\n })\n\n app.delete<{ Params: { id: string; slug: string }; Querystring: { force?: string } }>(\n \"/api/workspaces/:id/worktrees/:slug\",\n async (request, reply) => {\n const workspace = deps.workspaceManager.get(request.params.id)\n if (!workspace) {\n reply.code(404)\n return { error: \"Workspace not found\" }\n }\n\n const slug = (request.params.slug ?? \"\").trim()\n if (!isValidWorktreeSlug(slug) || slug === \"root\") {\n reply.code(400)\n return { error: \"Invalid worktree slug\" }\n }\n\n const { repoRoot, isGitRepo } = await resolveRepoRoot(workspace.path, request.log)\n if (!isGitRepo) {\n reply.code(400)\n return { error: \"Workspace is not a Git repository\" }\n }\n\n const force = (request.query?.force ?? \"\").toString().toLowerCase() === \"true\"\n\n try {\n const worktrees = await listWorktrees({ repoRoot, workspaceFolder: workspace.path, logger: request.log })\n const match = worktrees.find((wt) => wt.slug === slug)\n if (!match || match.kind === \"root\") {\n reply.code(404)\n return { error: \"Worktree not found\" }\n }\n\n const [client, targetDirectory] = await Promise.all([\n createInstanceClient(deps.workspaceManager, workspace.id),\n deps.workspaceManager.getServiceDirectoryForPath(workspace.id, match.directory),\n ])\n const projectDirectory = deps.workspaceManager.getServiceDirectory(workspace.id)\n if (!client || !projectDirectory || !targetDirectory) {\n throw new Error(\"Unable to inventory sessions before deleting worktree\")\n }\n const worktreeIdentity = await deps.workspaceManager.getWorktreeIdentityForPath(workspace.id, match.directory)\n if (!worktreeIdentity) throw new Error(\"Unable to identify worktree before deletion\")\n await deps.worktreeDeletionFence.run(worktreeIdentity, [worktreeIdentity], () => (\n evacuateWorktreeSessions({\n client,\n projectDirectory,\n targetDirectory,\n rootDirectory: projectDirectory,\n resolveDirectoryIdentity: (directory) => deps.workspaceManager.getWorktreeIdentityForPath(workspace.id, directory),\n remove: () => removeWorktree({ workspaceFolder: workspace.path, directory: match.directory, force, logger: request.log }),\n })\n ))\n invalidateWorktreeCache(workspace.id)\n\n reply.code(204)\n } catch (error) {\n return handleError(error, reply)\n }\n },\n )\n}\n\nfunction handleError(error: unknown, reply: FastifyReply) {\n reply.code(400)\n return { error: error instanceof Error ? error.message : \"Unable to fulfill request\" }\n}\n"]}
1
+ {"version":3,"file":"worktrees.js","sourceRoot":"","sources":["../../../src/server/routes/worktrees.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FastifyInstance, FastifyReply } from \"fastify\"\nimport { z } from \"zod\"\nimport { WorkspaceManager } from \"../../workspaces/manager\"\nimport {\n resolveRepoRoot,\n listWorktrees,\n isValidWorktreeSlug,\n createManagedWorktree,\n removeWorktree,\n} from \"../../workspaces/git-worktrees\"\nimport { invalidateWorktreeCache } from \"../../workspaces/worktree-directory\"\nimport type { WorktreeListResponse } from \"../../api-types\"\nimport { ensureCodenomadGitExclude } from \"../../workspaces/worktree-map\"\nimport { createInstanceClient } from \"../../workspaces/instance-client\"\nimport { evacuateWorktreeSessions, type WorktreeDeletionFence } from \"../../workspaces/worktree-session-evacuation\"\n\ninterface RouteDeps {\n workspaceManager: WorkspaceManager\n worktreeDeletionFence: WorktreeDeletionFence\n}\n\nconst WorktreeCreateSchema = z.object({\n slug: z.string().trim().min(1),\n branch: z.string().trim().min(1).optional(),\n})\n\nexport function registerWorktreeRoutes(app: FastifyInstance, deps: RouteDeps) {\n app.get<{ Params: { id: string } }>(\"/api/workspaces/:id/worktrees\", async (request, reply) => {\n const workspace = deps.workspaceManager.get(request.params.id)\n if (!workspace) {\n reply.code(404)\n return { error: \"Workspace not found\" }\n }\n\n const { repoRoot, isGitRepo } = await resolveRepoRoot(workspace.path, request.log)\n const worktrees = await listWorktrees({ repoRoot, workspaceFolder: workspace.path, logger: request.log })\n const response: WorktreeListResponse = { worktrees, isGitRepo }\n return response\n })\n\n app.post<{ Params: { id: string } }>(\"/api/workspaces/:id/worktrees\", async (request, reply) => {\n const workspace = deps.workspaceManager.get(request.params.id)\n if (!workspace) {\n reply.code(404)\n return { error: \"Workspace not found\" }\n }\n\n try {\n const body = WorktreeCreateSchema.parse(request.body ?? {})\n const slug = body.slug\n if (!isValidWorktreeSlug(slug) || slug === \"root\") {\n reply.code(400)\n return { error: \"Invalid worktree slug\" }\n }\n if (body.branch) {\n if (!isValidWorktreeSlug(body.branch) || body.branch === \"root\") {\n reply.code(400)\n return { error: \"Invalid worktree branch\" }\n }\n if (body.branch !== slug) {\n reply.code(400)\n return { error: \"Branch must match slug\" }\n }\n }\n\n const { repoRoot, isGitRepo } = await resolveRepoRoot(workspace.path, request.log)\n if (!isGitRepo) {\n reply.code(400)\n return { error: \"Workspace is not a Git repository\" }\n }\n\n await ensureCodenomadGitExclude(workspace.path, request.log).catch(() => undefined)\n\n const created = await createManagedWorktree({\n repoRoot,\n workspaceFolder: workspace.path,\n slug,\n logger: request.log,\n })\n invalidateWorktreeCache(workspace.id)\n\n reply.code(201)\n return created\n } catch (error) {\n return handleError(error, reply)\n }\n })\n\n app.delete<{ Params: { id: string; slug: string }; Querystring: { force?: string } }>(\n \"/api/workspaces/:id/worktrees/:slug\",\n async (request, reply) => {\n const workspace = deps.workspaceManager.get(request.params.id)\n if (!workspace) {\n reply.code(404)\n return { error: \"Workspace not found\" }\n }\n\n const slug = (request.params.slug ?? \"\").trim()\n if (!isValidWorktreeSlug(slug) || slug === \"root\") {\n reply.code(400)\n return { error: \"Invalid worktree slug\" }\n }\n\n const { repoRoot, isGitRepo } = await resolveRepoRoot(workspace.path, request.log)\n if (!isGitRepo) {\n reply.code(400)\n return { error: \"Workspace is not a Git repository\" }\n }\n\n const force = (request.query?.force ?? \"\").toString().toLowerCase() === \"true\"\n\n try {\n const worktrees = await listWorktrees({ repoRoot, workspaceFolder: workspace.path, logger: request.log })\n const match = worktrees.find((wt) => wt.slug === slug)\n if (!match || match.kind === \"root\") {\n reply.code(404)\n return { error: \"Worktree not found\" }\n }\n\n const [client, targetDirectory] = await Promise.all([\n createInstanceClient(deps.workspaceManager, workspace.id),\n deps.workspaceManager.getServiceDirectoryForPath(workspace.id, match.directory),\n ])\n const projectDirectory = deps.workspaceManager.getServiceDirectory(workspace.id)\n if (!client || !projectDirectory || !targetDirectory) {\n throw new Error(\"Unable to inventory sessions before deleting worktree\")\n }\n const worktreeIdentity = await deps.workspaceManager.getWorktreeIdentityForPath(workspace.id, match.directory)\n if (!worktreeIdentity) throw new Error(\"Unable to identify worktree before deletion\")\n await deps.worktreeDeletionFence.run(worktreeIdentity, [worktreeIdentity], () => (\n evacuateWorktreeSessions({\n client,\n projectDirectory,\n targetDirectory,\n rootDirectory: projectDirectory,\n resolveDirectoryIdentity: (directory) => deps.workspaceManager.getWorktreeIdentityForPath(workspace.id, directory),\n resolveExactDirectory: (directory) => deps.workspaceManager.getServiceDirectoryForPath(workspace.id, directory),\n remove: () => removeWorktree({ workspaceFolder: workspace.path, directory: match.directory, force, logger: request.log }),\n })\n ))\n invalidateWorktreeCache(workspace.id)\n\n reply.code(204)\n } catch (error) {\n return handleError(error, reply)\n }\n },\n )\n}\n\nfunction handleError(error: unknown, reply: FastifyReply) {\n reply.code(400)\n return { error: error instanceof Error ? error.message : \"Unable to fulfill request\" }\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { execFileSync } from "node:child_process";
3
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import path from "node:path";
6
6
  import { describe, it } from "node:test";
@@ -8,6 +8,58 @@ import Fastify from "fastify";
8
8
  import { registerWorktreeRoutes } from "./worktrees";
9
9
  import { WorktreeDeletionFence } from "../../workspaces/worktree-session-evacuation";
10
10
  describe("worktree routes", () => {
11
+ it("uses exact project paths despite a nested project sharing the root worktree identity", async () => {
12
+ const temp = mkdtempSync(path.join(tmpdir(), "codenomad-nested-project-"));
13
+ const target = path.join(temp, "doomed");
14
+ const nested = path.join(temp, "nested");
15
+ const app = Fastify({ logger: false });
16
+ try {
17
+ execFileSync("git", ["init", "--initial-branch=main", temp]);
18
+ writeFileSync(path.join(temp, "README.md"), "test\n");
19
+ execFileSync("git", ["-C", temp, "add", "README.md"]);
20
+ execFileSync("git", ["-C", temp, "-c", "user.name=CodeNomad Test", "-c", "user.email=test@codenomad.local", "commit", "-m", "test"]);
21
+ execFileSync("git", ["-C", temp, "worktree", "add", "-b", "doomed", target]);
22
+ mkdirSync(nested);
23
+ const projects = [];
24
+ const exactPaths = [];
25
+ const client = {
26
+ location: { get: async () => ({ directory: temp }) },
27
+ project: { list: async () => [
28
+ { id: "nested", canonical: nested, sandboxes: [] },
29
+ { id: "project", canonical: realpathSync(temp), sandboxes: [target] },
30
+ ] },
31
+ session: {
32
+ list: async ({ project }) => {
33
+ projects.push(project);
34
+ return { data: project === "project" ? [{ id: "active", location: { directory: target } }] : [], cursor: {} };
35
+ },
36
+ active: async () => ({ active: { type: "running" } }),
37
+ move: async () => { assert.fail("Active sessions must not move"); },
38
+ },
39
+ };
40
+ const manager = {
41
+ get: () => ({ id: "workspace", path: temp, status: "ready" }),
42
+ getSharedServiceClient: async () => client,
43
+ getServiceDirectory: () => temp,
44
+ getServiceDirectoryForPath: async (_id, directory) => {
45
+ exactPaths.push(directory);
46
+ return realpathSync(directory);
47
+ },
48
+ getWorktreeIdentityForPath: async (_id, directory) => path.resolve(directory) === path.resolve(target) ? "workspace:doomed" : "workspace:root",
49
+ };
50
+ registerWorktreeRoutes(app, { workspaceManager: manager, worktreeDeletionFence: new WorktreeDeletionFence() });
51
+ const response = await app.inject({ method: "DELETE", url: "/api/workspaces/workspace/worktrees/doomed" });
52
+ assert.equal(response.statusCode, 400);
53
+ assert.match(response.json().error, /Active sessions block worktree deletion: active/);
54
+ assert.deepEqual(projects, ["project"]);
55
+ assert.ok(exactPaths.includes(nested), "Project candidates must use the exact-path resolver");
56
+ assert.match(execFileSync("git", ["-C", temp, "worktree", "list", "--porcelain"], { encoding: "utf8" }), /doomed/);
57
+ }
58
+ finally {
59
+ await app.close();
60
+ rmSync(temp, { recursive: true, force: true });
61
+ }
62
+ });
11
63
  it("fails a direct delete call closed when session evacuation fails", async () => {
12
64
  const temp = mkdtempSync(path.join(tmpdir(), "codenomad-delete-worktree-"));
13
65
  const target = path.join(temp, "doomed");
@@ -21,6 +73,7 @@ describe("worktree routes", () => {
21
73
  const workspace = { id: "workspace", path: temp, status: "ready" };
22
74
  const nativeSession = { id: "unloaded", projectID: "project", location: { directory: target }, cost: 0, tokens: {}, time: { created: 1, updated: 1 } };
23
75
  const client = {
76
+ location: { get: async ({ location }) => location },
24
77
  project: {
25
78
  list: async () => [{ id: "project", canonical: temp, sandboxes: [target], time: { created: 1, updated: 1 } }],
26
79
  },
@@ -1 +1 @@
1
- {"version":3,"file":"worktrees.test.js","sourceRoot":"","sources":["../../../src/server/routes/worktrees.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { execFileSync } from \"node:child_process\"\nimport { mkdtempSync, rmSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport path from \"node:path\"\nimport { describe, it } from \"node:test\"\nimport Fastify from \"fastify\"\nimport type { OpenCodeClient, SessionInfo } from \"@opencode-ai/client\"\nimport type { WorkspaceDescriptor } from \"../../api-types\"\nimport type { WorkspaceManager } from \"../../workspaces/manager\"\nimport { registerWorktreeRoutes } from \"./worktrees\"\nimport { WorktreeDeletionFence } from \"../../workspaces/worktree-session-evacuation\"\n\ndescribe(\"worktree routes\", () => {\n it(\"fails a direct delete call closed when session evacuation fails\", async () => {\n const temp = mkdtempSync(path.join(tmpdir(), \"codenomad-delete-worktree-\"))\n const target = path.join(temp, \"doomed\")\n const app = Fastify({ logger: false })\n try {\n execFileSync(\"git\", [\"init\", \"--initial-branch=main\", temp])\n writeFileSync(path.join(temp, \"README.md\"), \"test\\n\")\n execFileSync(\"git\", [\"-C\", temp, \"add\", \"README.md\"])\n execFileSync(\"git\", [\"-C\", temp, \"-c\", \"user.name=CodeNomad Test\", \"-c\", \"user.email=test@codenomad.local\", \"commit\", \"-m\", \"test\"])\n execFileSync(\"git\", [\"-C\", temp, \"worktree\", \"add\", \"-b\", \"doomed\", target])\n\n const workspace = { id: \"workspace\", path: temp, status: \"ready\" } as WorkspaceDescriptor\n const nativeSession = { id: \"unloaded\", projectID: \"project\", location: { directory: target }, cost: 0, tokens: {}, time: { created: 1, updated: 1 } } as SessionInfo\n const client = {\n project: {\n list: async () => [{ id: \"project\", canonical: temp, sandboxes: [target], time: { created: 1, updated: 1 } }],\n },\n session: {\n list: async () => ({ data: [nativeSession], cursor: {} }),\n active: async () => ({}),\n move: async (input: { directory: string }) => {\n if (input.directory === temp) throw new Error(\"native move failed\")\n },\n },\n } as unknown as OpenCodeClient\n const manager = {\n get: () => workspace,\n getSharedServiceClient: async () => client,\n getServiceDirectory: () => temp,\n getServiceDirectoryForPath: async (_id: string, directory: string) => directory,\n getWorktreeIdentityForPath: async () => \"workspace:doomed\",\n } as unknown as WorkspaceManager\n registerWorktreeRoutes(app, { workspaceManager: manager, worktreeDeletionFence: new WorktreeDeletionFence() })\n\n const response = await app.inject({ method: \"DELETE\", url: \"/api/workspaces/workspace/worktrees/doomed\" })\n\n assert.equal(response.statusCode, 400)\n assert.match(response.json().error, /native move failed/)\n assert.match(execFileSync(\"git\", [\"-C\", temp, \"worktree\", \"list\", \"--porcelain\"], { encoding: \"utf8\" }), /doomed/)\n } finally {\n await app.close()\n rmSync(temp, { recursive: true, force: true })\n }\n })\n})\n"]}
1
+ {"version":3,"file":"worktrees.test.js","sourceRoot":"","sources":["../../../src/server/routes/worktrees.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { execFileSync } from \"node:child_process\"\nimport { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport path from \"node:path\"\nimport { describe, it } from \"node:test\"\nimport Fastify from \"fastify\"\nimport type { OpenCodeClient, SessionInfo } from \"@opencode/client\"\nimport type { WorkspaceDescriptor } from \"../../api-types\"\nimport type { WorkspaceManager } from \"../../workspaces/manager\"\nimport { registerWorktreeRoutes } from \"./worktrees\"\nimport { WorktreeDeletionFence } from \"../../workspaces/worktree-session-evacuation\"\n\ndescribe(\"worktree routes\", () => {\n it(\"uses exact project paths despite a nested project sharing the root worktree identity\", async () => {\n const temp = mkdtempSync(path.join(tmpdir(), \"codenomad-nested-project-\"))\n const target = path.join(temp, \"doomed\")\n const nested = path.join(temp, \"nested\")\n const app = Fastify({ logger: false })\n try {\n execFileSync(\"git\", [\"init\", \"--initial-branch=main\", temp])\n writeFileSync(path.join(temp, \"README.md\"), \"test\\n\")\n execFileSync(\"git\", [\"-C\", temp, \"add\", \"README.md\"])\n execFileSync(\"git\", [\"-C\", temp, \"-c\", \"user.name=CodeNomad Test\", \"-c\", \"user.email=test@codenomad.local\", \"commit\", \"-m\", \"test\"])\n execFileSync(\"git\", [\"-C\", temp, \"worktree\", \"add\", \"-b\", \"doomed\", target])\n mkdirSync(nested)\n const projects: string[] = []\n const exactPaths: string[] = []\n const client = {\n location: { get: async () => ({ directory: temp }) },\n project: { list: async () => [\n { id: \"nested\", canonical: nested, sandboxes: [] },\n { id: \"project\", canonical: realpathSync(temp), sandboxes: [target] },\n ] },\n session: {\n list: async ({ project }: { project: string }) => {\n projects.push(project)\n return { data: project === \"project\" ? [{ id: \"active\", location: { directory: target } }] : [], cursor: {} }\n },\n active: async () => ({ active: { type: \"running\" } }),\n move: async () => { assert.fail(\"Active sessions must not move\") },\n },\n } as unknown as OpenCodeClient\n const manager = {\n get: () => ({ id: \"workspace\", path: temp, status: \"ready\" }),\n getSharedServiceClient: async () => client,\n getServiceDirectory: () => temp,\n getServiceDirectoryForPath: async (_id: string, directory: string) => {\n exactPaths.push(directory)\n return realpathSync(directory)\n },\n getWorktreeIdentityForPath: async (_id: string, directory: string) => path.resolve(directory) === path.resolve(target) ? \"workspace:doomed\" : \"workspace:root\",\n } as unknown as WorkspaceManager\n registerWorktreeRoutes(app, { workspaceManager: manager, worktreeDeletionFence: new WorktreeDeletionFence() })\n const response = await app.inject({ method: \"DELETE\", url: \"/api/workspaces/workspace/worktrees/doomed\" })\n assert.equal(response.statusCode, 400)\n assert.match(response.json().error, /Active sessions block worktree deletion: active/)\n assert.deepEqual(projects, [\"project\"])\n assert.ok(exactPaths.includes(nested), \"Project candidates must use the exact-path resolver\")\n assert.match(execFileSync(\"git\", [\"-C\", temp, \"worktree\", \"list\", \"--porcelain\"], { encoding: \"utf8\" }), /doomed/)\n } finally {\n await app.close()\n rmSync(temp, { recursive: true, force: true })\n }\n })\n\n it(\"fails a direct delete call closed when session evacuation fails\", async () => {\n const temp = mkdtempSync(path.join(tmpdir(), \"codenomad-delete-worktree-\"))\n const target = path.join(temp, \"doomed\")\n const app = Fastify({ logger: false })\n try {\n execFileSync(\"git\", [\"init\", \"--initial-branch=main\", temp])\n writeFileSync(path.join(temp, \"README.md\"), \"test\\n\")\n execFileSync(\"git\", [\"-C\", temp, \"add\", \"README.md\"])\n execFileSync(\"git\", [\"-C\", temp, \"-c\", \"user.name=CodeNomad Test\", \"-c\", \"user.email=test@codenomad.local\", \"commit\", \"-m\", \"test\"])\n execFileSync(\"git\", [\"-C\", temp, \"worktree\", \"add\", \"-b\", \"doomed\", target])\n\n const workspace = { id: \"workspace\", path: temp, status: \"ready\" } as WorkspaceDescriptor\n const nativeSession = { id: \"unloaded\", projectID: \"project\", location: { directory: target }, cost: 0, tokens: {}, time: { created: 1, updated: 1 } } as SessionInfo\n const client = {\n location: { get: async ({ location }: { location: { directory: string } }) => location },\n project: {\n list: async () => [{ id: \"project\", canonical: temp, sandboxes: [target], time: { created: 1, updated: 1 } }],\n },\n session: {\n list: async () => ({ data: [nativeSession], cursor: {} }),\n active: async () => ({}),\n move: async (input: { directory: string }) => {\n if (input.directory === temp) throw new Error(\"native move failed\")\n },\n },\n } as unknown as OpenCodeClient\n const manager = {\n get: () => workspace,\n getSharedServiceClient: async () => client,\n getServiceDirectory: () => temp,\n getServiceDirectoryForPath: async (_id: string, directory: string) => directory,\n getWorktreeIdentityForPath: async () => \"workspace:doomed\",\n } as unknown as WorkspaceManager\n registerWorktreeRoutes(app, { workspaceManager: manager, worktreeDeletionFence: new WorktreeDeletionFence() })\n\n const response = await app.inject({ method: \"DELETE\", url: \"/api/workspaces/workspace/worktrees/doomed\" })\n\n assert.equal(response.statusCode, 400)\n assert.match(response.json().error, /native move failed/)\n assert.match(execFileSync(\"git\", [\"-C\", temp, \"worktree\", \"list\", \"--porcelain\"], { encoding: \"utf8\" }), /doomed/)\n } finally {\n await app.close()\n rmSync(temp, { recursive: true, force: true })\n }\n })\n})\n"]}
@@ -23,13 +23,13 @@ describe("HostOpenCodeService", () => {
23
23
  assert.equal(calls[1]?.options.env?.PROVIDER_TOKEN, "secret");
24
24
  assert.equal(calls[1]?.options.env?.NODE_EXTRA_CA_CERTS, "/ca.pem");
25
25
  });
26
- it("authenticates strict bounded health and rejects malformed output", async () => {
26
+ it("authenticates strict bounded status and rejects malformed output", async () => {
27
27
  let authorization = null;
28
28
  const calls = [];
29
29
  const service = createService(calls, {}, {
30
30
  fetch: async (_input, init) => {
31
31
  authorization = new Headers(init?.headers).get("authorization");
32
- return Response.json({ healthy: true, version: "2.0.0", pid: 1 });
32
+ return Response.json({ version: "2.0.4", pid: 1, urls: [url] });
33
33
  },
34
34
  execFile: async (file, args, options) => {
35
35
  calls.push({ file, args, options });
@@ -44,19 +44,68 @@ describe("HostOpenCodeService", () => {
44
44
  await assert.rejects(malformed.discover(), /multiline/);
45
45
  });
46
46
  it("connects to wildcard services through loopback", async () => {
47
- let healthUrl = "";
47
+ let statusUrl = "";
48
48
  const service = createService([], {}, {
49
49
  execFile: async (_file, args) => ({
50
50
  stdout: args[args.length - 1] === "password" ? "password\n" : "http://0.0.0.0:4321\n",
51
51
  stderr: "",
52
52
  }),
53
53
  fetch: async (input) => {
54
- healthUrl = String(input);
55
- return Response.json({ healthy: true, version: "2.0.0", pid: 1 });
54
+ statusUrl = String(input);
55
+ return Response.json({ version: "2.0.4", pid: 1, urls: [url] });
56
56
  },
57
57
  });
58
58
  assert.equal((await service.discover())?.url, "http://127.0.0.1:4321/");
59
- assert.equal(healthUrl, "http://127.0.0.1:4321/api/health");
59
+ assert.equal(statusUrl, "http://127.0.0.1:4321/api/status");
60
+ });
61
+ it("falls back on status 404 to authenticated V2 health for discovery and startup", async () => {
62
+ for (const operation of ["discover", "ensure"]) {
63
+ const requests = [];
64
+ let cancelled = false;
65
+ const service = createService([], {}, {
66
+ execFile: async (_file, args) => ({ stdout: args.at(-1) === "password" ? "password\n" : `${url}\n`, stderr: "" }),
67
+ fetch: async (input, init) => {
68
+ requests.push(String(input));
69
+ assert.equal(new Headers(init?.headers).get("authorization"), `Basic ${Buffer.from("opencode:password").toString("base64")}`);
70
+ if (requests.length === 1)
71
+ return new Response(new ReadableStream({ cancel() { cancelled = true; } }), { status: 404 });
72
+ return Response.json({ healthy: true, version: "2.0.3", pid: 123 });
73
+ },
74
+ });
75
+ assert.equal((await service[operation]())?.url, url);
76
+ assert.deepEqual(requests, [`${url}/api/status`, `${url}/api/health`]);
77
+ assert.equal(cancelled, true);
78
+ }
79
+ });
80
+ it("does not downgrade on authentication, server, transport or malformed status failures", async () => {
81
+ for (const response of [
82
+ () => new Response(null, { status: 401 }),
83
+ () => new Response(null, { status: 403 }),
84
+ () => new Response(null, { status: 503 }),
85
+ () => new Response("invalid JSON"),
86
+ () => Response.json({ healthy: true, version: "2.0.3", pid: 123 }),
87
+ () => { throw new Error("ECONNREFUSED"); },
88
+ ]) {
89
+ const requests = [];
90
+ const service = createService([], {}, {
91
+ fetch: async (input) => { requests.push(String(input)); return response(); },
92
+ });
93
+ await assert.rejects(service.ensure());
94
+ assert.deepEqual(requests, [`${url}/api/status`]);
95
+ }
96
+ });
97
+ it("does not give the fallback a fresh deadline", async (context) => {
98
+ let now = 1000;
99
+ context.mock.method(Date, "now", () => now);
100
+ const requests = [];
101
+ const service = createService([], {}, {
102
+ fetch: async (input) => {
103
+ requests.push(String(input));
104
+ return new Response(new ReadableStream({ cancel() { now = 1500; } }), { status: 404 });
105
+ },
106
+ });
107
+ await assert.rejects(service.ensure(1500), /timed out/);
108
+ assert.deepEqual(requests, [`${url}/api/status`]);
60
109
  });
61
110
  it("redacts startup environment values from failures and hashes identity", async () => {
62
111
  const secret = "DO_NOT_LEAK";
@@ -120,7 +169,7 @@ function createService(calls, startupEnvironment, overrides = {}) {
120
169
  return { stdout: `${url}\n`, stderr: "" };
121
170
  return { stdout: "password\n", stderr: "" };
122
171
  },
123
- fetch: async () => Response.json({ healthy: true, version: "2.0.0", pid: 123 }),
172
+ fetch: async () => Response.json({ version: "2.0.4", pid: 123, urls: [url] }),
124
173
  ...overrides,
125
174
  });
126
175
  }
@@ -1 +1 @@
1
- {"version":3,"file":"host-opencode-service.test.js","sourceRoot":"","sources":["../../src/workspaces/host-opencode-service.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { describe, it } from \"node:test\"\n\nimport { HostOpenCodeService, hostOpenCodeServiceIdentity } from \"./host-opencode-service\"\nimport type { OpenCodeCliServiceDependencies, ServiceExecOptions } from \"./opencode-cli-service\"\nimport { OPENCODE_V2_REQUIRED_ERROR_CODE } from \"../api-types\"\n\nconst url = \"http://127.0.0.1:4321\"\n\ndescribe(\"HostOpenCodeService\", () => {\n it(\"uses status, start, and password through buildSpawnSpec without a shell\", async () => {\n const calls: Array<{ file: string; args: string[]; options: ServiceExecOptions }> = []\n const service = createService(calls, { PROVIDER_TOKEN: \"secret\", NODE_EXTRA_CA_CERTS: \"/ca.pem\" })\n\n assert.equal(await service.discover(), undefined)\n assert.deepEqual(await service.ensure(), {\n url,\n auth: { type: \"basic\", username: \"opencode\", password: \"password\" },\n })\n\n assert.deepEqual(calls.map(({ args }) => args), [\n [\"service\", \"status\"],\n [\"service\", \"start\"],\n [\"service\", \"get\", \"password\"],\n ])\n assert.equal(calls.every(({ options }) => options.shell === false), true)\n assert.equal(calls[0]?.options.env, undefined)\n assert.equal(calls[2]?.options.env, undefined)\n assert.equal(calls[1]?.options.env?.PROVIDER_TOKEN, \"secret\")\n assert.equal(calls[1]?.options.env?.NODE_EXTRA_CA_CERTS, \"/ca.pem\")\n })\n\n it(\"authenticates strict bounded health and rejects malformed output\", async () => {\n let authorization: string | null = null\n const calls: Array<{ file: string; args: string[]; options: ServiceExecOptions }> = []\n const service = createService(calls, {}, {\n fetch: async (_input, init) => {\n authorization = new Headers(init?.headers).get(\"authorization\")\n return Response.json({ healthy: true, version: \"2.0.0\", pid: 1 })\n },\n execFile: async (file, args, options) => {\n calls.push({ file, args, options })\n return { stdout: args[args.length - 1] === \"password\" ? \"password\\n\" : `${url}\\n`, stderr: \"\" }\n },\n })\n await service.discover()\n assert.equal(authorization, `Basic ${Buffer.from(\"opencode:password\").toString(\"base64\")}`)\n\n const malformed = createService([], {}, {\n execFile: async () => ({ stdout: `${url}\\nhttp://127.0.0.1:4322\\n`, stderr: \"\" }),\n })\n await assert.rejects(malformed.discover(), /multiline/)\n })\n\n it(\"connects to wildcard services through loopback\", async () => {\n let healthUrl = \"\"\n const service = createService([], {}, {\n execFile: async (_file, args) => ({\n stdout: args[args.length - 1] === \"password\" ? \"password\\n\" : \"http://0.0.0.0:4321\\n\",\n stderr: \"\",\n }),\n fetch: async (input) => {\n healthUrl = String(input)\n return Response.json({ healthy: true, version: \"2.0.0\", pid: 1 })\n },\n })\n\n assert.equal((await service.discover())?.url, \"http://127.0.0.1:4321/\")\n assert.equal(healthUrl, \"http://127.0.0.1:4321/api/health\")\n })\n\n it(\"redacts startup environment values from failures and hashes identity\", async () => {\n const secret = \"DO_NOT_LEAK\"\n const service = createService([], { TOKEN: secret }, {\n execFile: async (_file, args) => {\n if (args[args.length - 1] === \"status\") return { stdout: \"stopped\\n\", stderr: \"\" }\n throw Object.assign(new Error(secret), { code: 7, stdout: secret, stderr: secret })\n },\n })\n await service.discover()\n await assert.rejects(service.ensure(), (error: Error) => {\n assert.match(error.message, /start failed \\(exit code 7\\)/)\n assert.equal(error.message.includes(secret), false)\n return true\n })\n\n const identity = hostOpenCodeServiceIdentity({\n binary: process.platform === \"win32\" ? String.raw`C:\\tools\\..\\opencode.exe` : \"/opt/../opencode\",\n startupEnvironment: { TOKEN: secret },\n })\n assert.match(identity, /:env:[a-f0-9]{64}$/)\n assert.equal(identity.includes(secret), false)\n })\n\n it(\"reports an actionable compatibility error for an OpenCode V1 binary\", async () => {\n const service = createService([], {}, {\n execFile: async () => {\n throw Object.assign(new Error(\"Command failed\"), {\n code: 1,\n stdout: \"\",\n stderr: `Commands:\\n opencode completion\\n opencode [project] start opencode tui [default]\\n`,\n })\n },\n })\n\n await assert.rejects(service.discover(), (error: Error) => {\n assert.match(error.message, new RegExp(OPENCODE_V2_REQUIRED_ERROR_CODE))\n assert.doesNotMatch(error.message, /Commands:/)\n return true\n })\n })\n\n it(\"recognizes legacy help even when a wrapper exits zero on stdout or stderr\", async () => {\n for (const stream of [\"stdout\", \"stderr\"]) {\n const service = createService([], {}, {\n execFile: async () => ({ stdout: \"\", stderr: \"\", [stream]: \"Commands:\\n opencode completion\\n opencode [project]\\n\" }),\n })\n await assert.rejects(service.discover(), new RegExp(`^Error: ${OPENCODE_V2_REQUIRED_ERROR_CODE}:`))\n }\n })\n})\n\nfunction createService(\n calls: Array<{ file: string; args: string[]; options: ServiceExecOptions }>,\n startupEnvironment: NodeJS.ProcessEnv,\n overrides: Partial<OpenCodeCliServiceDependencies> = {},\n) {\n return new HostOpenCodeService({\n binary: process.execPath,\n startupEnvironment,\n timeoutMs: 500,\n }, {\n execFile: async (file, args, options) => {\n calls.push({ file, args, options })\n const command = args.join(\" \")\n if (command === \"service status\") return { stdout: \"stopped\\n\", stderr: \"\" }\n if (command === \"service start\") return { stdout: `${url}\\n`, stderr: \"\" }\n return { stdout: \"password\\n\", stderr: \"\" }\n },\n fetch: async () => Response.json({ healthy: true, version: \"2.0.0\", pid: 123 }),\n ...overrides,\n })\n}\n"]}
1
+ {"version":3,"file":"host-opencode-service.test.js","sourceRoot":"","sources":["../../src/workspaces/host-opencode-service.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { describe, it } from \"node:test\"\n\nimport { HostOpenCodeService, hostOpenCodeServiceIdentity } from \"./host-opencode-service\"\nimport type { OpenCodeCliServiceDependencies, ServiceExecOptions } from \"./opencode-cli-service\"\nimport { OPENCODE_V2_REQUIRED_ERROR_CODE } from \"../api-types\"\n\nconst url = \"http://127.0.0.1:4321\"\n\ndescribe(\"HostOpenCodeService\", () => {\n it(\"uses status, start, and password through buildSpawnSpec without a shell\", async () => {\n const calls: Array<{ file: string; args: string[]; options: ServiceExecOptions }> = []\n const service = createService(calls, { PROVIDER_TOKEN: \"secret\", NODE_EXTRA_CA_CERTS: \"/ca.pem\" })\n\n assert.equal(await service.discover(), undefined)\n assert.deepEqual(await service.ensure(), {\n url,\n auth: { type: \"basic\", username: \"opencode\", password: \"password\" },\n })\n\n assert.deepEqual(calls.map(({ args }) => args), [\n [\"service\", \"status\"],\n [\"service\", \"start\"],\n [\"service\", \"get\", \"password\"],\n ])\n assert.equal(calls.every(({ options }) => options.shell === false), true)\n assert.equal(calls[0]?.options.env, undefined)\n assert.equal(calls[2]?.options.env, undefined)\n assert.equal(calls[1]?.options.env?.PROVIDER_TOKEN, \"secret\")\n assert.equal(calls[1]?.options.env?.NODE_EXTRA_CA_CERTS, \"/ca.pem\")\n })\n\n it(\"authenticates strict bounded status and rejects malformed output\", async () => {\n let authorization: string | null = null\n const calls: Array<{ file: string; args: string[]; options: ServiceExecOptions }> = []\n const service = createService(calls, {}, {\n fetch: async (_input, init) => {\n authorization = new Headers(init?.headers).get(\"authorization\")\n return Response.json({ version: \"2.0.4\", pid: 1, urls: [url] })\n },\n execFile: async (file, args, options) => {\n calls.push({ file, args, options })\n return { stdout: args[args.length - 1] === \"password\" ? \"password\\n\" : `${url}\\n`, stderr: \"\" }\n },\n })\n await service.discover()\n assert.equal(authorization, `Basic ${Buffer.from(\"opencode:password\").toString(\"base64\")}`)\n\n const malformed = createService([], {}, {\n execFile: async () => ({ stdout: `${url}\\nhttp://127.0.0.1:4322\\n`, stderr: \"\" }),\n })\n await assert.rejects(malformed.discover(), /multiline/)\n })\n\n it(\"connects to wildcard services through loopback\", async () => {\n let statusUrl = \"\"\n const service = createService([], {}, {\n execFile: async (_file, args) => ({\n stdout: args[args.length - 1] === \"password\" ? \"password\\n\" : \"http://0.0.0.0:4321\\n\",\n stderr: \"\",\n }),\n fetch: async (input) => {\n statusUrl = String(input)\n return Response.json({ version: \"2.0.4\", pid: 1, urls: [url] })\n },\n })\n\n assert.equal((await service.discover())?.url, \"http://127.0.0.1:4321/\")\n assert.equal(statusUrl, \"http://127.0.0.1:4321/api/status\")\n })\n\n it(\"falls back on status 404 to authenticated V2 health for discovery and startup\", async () => {\n for (const operation of [\"discover\", \"ensure\"] as const) {\n const requests: string[] = []\n let cancelled = false\n const service = createService([], {}, {\n execFile: async (_file, args) => ({ stdout: args.at(-1) === \"password\" ? \"password\\n\" : `${url}\\n`, stderr: \"\" }),\n fetch: async (input, init) => {\n requests.push(String(input))\n assert.equal(new Headers(init?.headers).get(\"authorization\"), `Basic ${Buffer.from(\"opencode:password\").toString(\"base64\")}`)\n if (requests.length === 1) return new Response(new ReadableStream({ cancel() { cancelled = true } }), { status: 404 })\n return Response.json({ healthy: true, version: \"2.0.3\", pid: 123 })\n },\n })\n assert.equal((await service[operation]())?.url, url)\n assert.deepEqual(requests, [`${url}/api/status`, `${url}/api/health`])\n assert.equal(cancelled, true)\n }\n })\n\n it(\"does not downgrade on authentication, server, transport or malformed status failures\", async () => {\n for (const response of [\n () => new Response(null, { status: 401 }),\n () => new Response(null, { status: 403 }),\n () => new Response(null, { status: 503 }),\n () => new Response(\"invalid JSON\"),\n () => Response.json({ healthy: true, version: \"2.0.3\", pid: 123 }),\n () => { throw new Error(\"ECONNREFUSED\") },\n ]) {\n const requests: string[] = []\n const service = createService([], {}, {\n fetch: async (input) => { requests.push(String(input)); return response() },\n })\n await assert.rejects(service.ensure())\n assert.deepEqual(requests, [`${url}/api/status`])\n }\n })\n\n it(\"does not give the fallback a fresh deadline\", async (context) => {\n let now = 1000\n context.mock.method(Date, \"now\", () => now)\n const requests: string[] = []\n const service = createService([], {}, {\n fetch: async (input) => {\n requests.push(String(input))\n return new Response(new ReadableStream({ cancel() { now = 1500 } }), { status: 404 })\n },\n })\n await assert.rejects(service.ensure(1500), /timed out/)\n assert.deepEqual(requests, [`${url}/api/status`])\n })\n\n it(\"redacts startup environment values from failures and hashes identity\", async () => {\n const secret = \"DO_NOT_LEAK\"\n const service = createService([], { TOKEN: secret }, {\n execFile: async (_file, args) => {\n if (args[args.length - 1] === \"status\") return { stdout: \"stopped\\n\", stderr: \"\" }\n throw Object.assign(new Error(secret), { code: 7, stdout: secret, stderr: secret })\n },\n })\n await service.discover()\n await assert.rejects(service.ensure(), (error: Error) => {\n assert.match(error.message, /start failed \\(exit code 7\\)/)\n assert.equal(error.message.includes(secret), false)\n return true\n })\n\n const identity = hostOpenCodeServiceIdentity({\n binary: process.platform === \"win32\" ? String.raw`C:\\tools\\..\\opencode.exe` : \"/opt/../opencode\",\n startupEnvironment: { TOKEN: secret },\n })\n assert.match(identity, /:env:[a-f0-9]{64}$/)\n assert.equal(identity.includes(secret), false)\n })\n\n it(\"reports an actionable compatibility error for an OpenCode V1 binary\", async () => {\n const service = createService([], {}, {\n execFile: async () => {\n throw Object.assign(new Error(\"Command failed\"), {\n code: 1,\n stdout: \"\",\n stderr: `Commands:\\n opencode completion\\n opencode [project] start opencode tui [default]\\n`,\n })\n },\n })\n\n await assert.rejects(service.discover(), (error: Error) => {\n assert.match(error.message, new RegExp(OPENCODE_V2_REQUIRED_ERROR_CODE))\n assert.doesNotMatch(error.message, /Commands:/)\n return true\n })\n })\n\n it(\"recognizes legacy help even when a wrapper exits zero on stdout or stderr\", async () => {\n for (const stream of [\"stdout\", \"stderr\"]) {\n const service = createService([], {}, {\n execFile: async () => ({ stdout: \"\", stderr: \"\", [stream]: \"Commands:\\n opencode completion\\n opencode [project]\\n\" }),\n })\n await assert.rejects(service.discover(), new RegExp(`^Error: ${OPENCODE_V2_REQUIRED_ERROR_CODE}:`))\n }\n })\n})\n\nfunction createService(\n calls: Array<{ file: string; args: string[]; options: ServiceExecOptions }>,\n startupEnvironment: NodeJS.ProcessEnv,\n overrides: Partial<OpenCodeCliServiceDependencies> = {},\n) {\n return new HostOpenCodeService({\n binary: process.execPath,\n startupEnvironment,\n timeoutMs: 500,\n }, {\n execFile: async (file, args, options) => {\n calls.push({ file, args, options })\n const command = args.join(\" \")\n if (command === \"service status\") return { stdout: \"stopped\\n\", stderr: \"\" }\n if (command === \"service start\") return { stdout: `${url}\\n`, stderr: \"\" }\n return { stdout: \"password\\n\", stderr: \"\" }\n },\n fetch: async () => Response.json({ version: \"2.0.4\", pid: 123, urls: [url] }),\n ...overrides,\n })\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"instance-client.js","sourceRoot":"","sources":["../../src/workspaces/instance-client.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,gBAAkC,EAClC,UAAkB;IAElB,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAA;IAClD,OAAO,gBAAgB,CAAC,sBAAsB,EAAE,CAAA;AAClD,CAAC","sourcesContent":["import type { OpenCodeClient } from \"@opencode-ai/client\"\nimport type { WorkspaceManager } from \"./manager\"\n\n/**\n * Returns the shared native OpenCode client when the logical workspace is ready.\n * Session APIs resolve their location from the session itself.\n */\nexport async function createInstanceClient(\n workspaceManager: WorkspaceManager,\n instanceId: string,\n): Promise<OpenCodeClient | null> {\n if (!workspaceManager.get(instanceId)) return null\n return workspaceManager.getSharedServiceClient()\n}\n"]}
1
+ {"version":3,"file":"instance-client.js","sourceRoot":"","sources":["../../src/workspaces/instance-client.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,gBAAkC,EAClC,UAAkB;IAElB,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAA;IAClD,OAAO,gBAAgB,CAAC,sBAAsB,EAAE,CAAA;AAClD,CAAC","sourcesContent":["import type { OpenCodeClient } from \"@opencode/client\"\nimport type { WorkspaceManager } from \"./manager\"\n\n/**\n * Returns the shared native OpenCode client when the logical workspace is ready.\n * Session APIs resolve their location from the session itself.\n */\nexport async function createInstanceClient(\n workspaceManager: WorkspaceManager,\n instanceId: string,\n): Promise<OpenCodeClient | null> {\n if (!workspaceManager.get(instanceId)) return null\n return workspaceManager.getSharedServiceClient()\n}\n"]}