@neuralnomads/codenomad-dev 0.20.0-dev-20260921-63bbdbb1 → 0.20.0-dev-20260922-e68ac524

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 (134) hide show
  1. package/dist/automation/developer-inspection-targets.js +25 -0
  2. package/dist/automation/developer-inspection-targets.js.map +1 -0
  3. package/dist/developer-cdp.js +5 -4
  4. package/dist/developer-cdp.js.map +1 -1
  5. package/dist/developer-cdp.test.js +18 -0
  6. package/dist/developer-cdp.test.js.map +1 -1
  7. package/dist/opencode/session-pruning/navigation-contract.js +1 -0
  8. package/dist/opencode/session-pruning/navigation-contract.js.map +1 -1
  9. package/dist/opencode/session-pruning/navigation.test.js +12 -1
  10. package/dist/opencode/session-pruning/navigation.test.js.map +1 -1
  11. package/dist/opencode/session-pruning/outline-index.js +14 -1
  12. package/dist/opencode/session-pruning/outline-index.js.map +1 -1
  13. package/dist/plugins/session-pruning/plugin.mjs +16 -3
  14. package/dist/server/__tests__/instance-proxy.test.js +32 -0
  15. package/dist/server/__tests__/instance-proxy.test.js.map +1 -1
  16. package/dist/server/http-server.js +1 -1
  17. package/dist/server/http-server.js.map +1 -1
  18. package/dist/server/routes/automation-plugin.js +28 -15
  19. package/dist/server/routes/automation-plugin.js.map +1 -1
  20. package/dist/server/routes/automation-plugin.test.js +10 -6
  21. package/dist/server/routes/automation-plugin.test.js.map +1 -1
  22. package/dist/workspaces/__tests__/git-common-directory.test.js +86 -0
  23. package/dist/workspaces/__tests__/git-common-directory.test.js.map +1 -0
  24. package/dist/workspaces/__tests__/git-worktree-config.test.js +130 -0
  25. package/dist/workspaces/__tests__/git-worktree-config.test.js.map +1 -0
  26. package/dist/workspaces/__tests__/git-worktrees.test.js +49 -1
  27. package/dist/workspaces/__tests__/git-worktrees.test.js.map +1 -1
  28. package/dist/workspaces/__tests__/native-worktree-fixture.js +2 -2
  29. package/dist/workspaces/__tests__/native-worktree-fixture.js.map +1 -1
  30. package/dist/workspaces/git-common-directory.js +18 -0
  31. package/dist/workspaces/git-common-directory.js.map +1 -0
  32. package/dist/workspaces/git-process.js +80 -0
  33. package/dist/workspaces/git-process.js.map +1 -0
  34. package/dist/workspaces/git-worktrees.js +64 -10
  35. package/dist/workspaces/git-worktrees.js.map +1 -1
  36. package/dist/workspaces/native-worktrees.js +24 -9
  37. package/dist/workspaces/native-worktrees.js.map +1 -1
  38. package/package.json +1 -1
  39. package/public/assets/App-BSkmbNUC.js +51 -0
  40. package/public/assets/App-BSkmbNUC.js.map +1 -0
  41. package/public/assets/FilesTab-CznSFRW_.js +3 -0
  42. package/public/assets/{FilesTab-DOOT4Own.js.map → FilesTab-CznSFRW_.js.map} +1 -1
  43. package/public/assets/{GitChangesTab-DqS17ZG_.js → GitChangesTab-DLtyzpX0.js} +3 -3
  44. package/public/assets/{GitChangesTab-DqS17ZG_.js.map → GitChangesTab-DLtyzpX0.js.map} +1 -1
  45. package/public/assets/{SplitFilePanel-DpXPYY4L.js → SplitFilePanel-BHkyIER4.js} +2 -2
  46. package/public/assets/{SplitFilePanel-DpXPYY4L.js.map → SplitFilePanel-BHkyIER4.js.map} +1 -1
  47. package/public/assets/{StatusTab-CynqW2pr.js → StatusTab-BB3L4UEF.js} +2 -2
  48. package/public/assets/{StatusTab-CynqW2pr.js.map → StatusTab-BB3L4UEF.js.map} +1 -1
  49. package/public/assets/{align-justify-C_MrQdWv.js → align-justify-BGcr8Y3G.js} +2 -2
  50. package/public/assets/{align-justify-C_MrQdWv.js.map → align-justify-BGcr8Y3G.js.map} +1 -1
  51. package/public/assets/{bundle-full-CVPoVh_h.js → bundle-full-CwHbX2Tz.js} +2 -2
  52. package/public/assets/{bundle-full-CVPoVh_h.js.map → bundle-full-CwHbX2Tz.js.map} +1 -1
  53. package/public/assets/client-state-CjVIIkWh.js +2 -0
  54. package/public/assets/client-state-CjVIIkWh.js.map +1 -0
  55. package/public/assets/{diff-viewer-BzgxNlWD.js → diff-viewer-FNBCogHl.js} +2 -2
  56. package/public/assets/{diff-viewer-BzgxNlWD.js.map → diff-viewer-FNBCogHl.js.map} +1 -1
  57. package/public/assets/index-AeE49W2c.js +2 -0
  58. package/public/assets/index-AeE49W2c.js.map +1 -0
  59. package/public/assets/index-B875QQLi.js +2 -0
  60. package/public/assets/index-B875QQLi.js.map +1 -0
  61. package/public/assets/index-BhInW6hR.js +3 -0
  62. package/public/assets/index-BhInW6hR.js.map +1 -0
  63. package/public/assets/index-C2jBtFkI.js +2 -0
  64. package/public/assets/{index-CrjSY1y2.js.map → index-C2jBtFkI.js.map} +1 -1
  65. package/public/assets/index-C8zCyB6Z.js +2 -0
  66. package/public/assets/index-C8zCyB6Z.js.map +1 -0
  67. package/public/assets/index-C9vJvaFa.js +2 -0
  68. package/public/assets/index-C9vJvaFa.js.map +1 -0
  69. package/public/assets/index-CLdQSSfU.js +2 -0
  70. package/public/assets/index-CLdQSSfU.js.map +1 -0
  71. package/public/assets/{index-Ce-SrKvP.js → index-CLgpBTF6.js} +2 -2
  72. package/public/assets/{index-Ce-SrKvP.js.map → index-CLgpBTF6.js.map} +1 -1
  73. package/public/assets/index-CaYFZAN4.js +2 -0
  74. package/public/assets/index-CaYFZAN4.js.map +1 -0
  75. package/public/assets/index-ChudAs0f.js +2 -0
  76. package/public/assets/index-ChudAs0f.js.map +1 -0
  77. package/public/assets/index-D292-iMl.js +2 -0
  78. package/public/assets/index-D292-iMl.js.map +1 -0
  79. package/public/assets/index-DYBqUC9X.css +1 -0
  80. package/public/assets/index-Dk3HFhZs.js +2 -0
  81. package/public/assets/index-Dk3HFhZs.js.map +1 -0
  82. package/public/assets/{loading-BWjkEyUD.js → loading-D8BcUQaM.js} +2 -2
  83. package/public/assets/{loading-BWjkEyUD.js.map → loading-D8BcUQaM.js.map} +1 -1
  84. package/public/assets/main-DRbjgAo8.js +42 -0
  85. package/public/assets/main-DRbjgAo8.js.map +1 -0
  86. package/public/assets/{markdown-Bkm0F3oO.js → markdown-CRPEbAqL.js} +4 -4
  87. package/public/assets/{markdown-Bkm0F3oO.js.map → markdown-CRPEbAqL.js.map} +1 -1
  88. package/public/assets/{monaco-viewer-C10fFymA.js → monaco-viewer-CG0aPZ-r.js} +2 -2
  89. package/public/assets/{monaco-viewer-C10fFymA.js.map → monaco-viewer-CG0aPZ-r.js.map} +1 -1
  90. package/public/assets/{startup-state-settings-card-BTfYhHov.js → startup-state-settings-card-CHqS2Pmg.js} +2 -2
  91. package/public/assets/{startup-state-settings-card-BTfYhHov.js.map → startup-state-settings-card-CHqS2Pmg.js.map} +1 -1
  92. package/public/assets/{terminal-square-Dhdgf3hj.js → terminal-square-DhmNJqNG.js} +2 -2
  93. package/public/assets/{terminal-square-Dhdgf3hj.js.map → terminal-square-DhmNJqNG.js.map} +1 -1
  94. package/public/assets/{todo-pfT9OVP8.js → todo-DXJ5kP_Z.js} +2 -2
  95. package/public/assets/{todo-pfT9OVP8.js.map → todo-DXJ5kP_Z.js.map} +1 -1
  96. package/public/assets/{tool-call-ja_vg-Y6.js → tool-call-Bjo9DalZ.js} +5 -5
  97. package/public/assets/{tool-call-ja_vg-Y6.js.map → tool-call-Bjo9DalZ.js.map} +1 -1
  98. package/public/assets/{unified-picker-Bi-fpJBA.js → unified-picker-CxrTM_tY.js} +2 -2
  99. package/public/assets/{unified-picker-Bi-fpJBA.js.map → unified-picker-CxrTM_tY.js.map} +1 -1
  100. package/public/assets/{wrap-text-DJHeAvj4.js → wrap-text-BznZeD4J.js} +2 -2
  101. package/public/assets/{wrap-text-DJHeAvj4.js.map → wrap-text-BznZeD4J.js.map} +1 -1
  102. package/public/index.html +4 -4
  103. package/public/loading.html +4 -4
  104. package/public/sw.js +1 -1
  105. package/public/sw.js.map +1 -1
  106. package/public/assets/App-bGIUQ-dD.js +0 -49
  107. package/public/assets/App-bGIUQ-dD.js.map +0 -1
  108. package/public/assets/FilesTab-DOOT4Own.js +0 -3
  109. package/public/assets/client-state-DHHjx28k.js +0 -2
  110. package/public/assets/client-state-DHHjx28k.js.map +0 -1
  111. package/public/assets/index-B5KIA1QZ.js +0 -2
  112. package/public/assets/index-B5KIA1QZ.js.map +0 -1
  113. package/public/assets/index-BO3LYyge.js +0 -2
  114. package/public/assets/index-BO3LYyge.js.map +0 -1
  115. package/public/assets/index-Bzngra1L.js +0 -3
  116. package/public/assets/index-Bzngra1L.js.map +0 -1
  117. package/public/assets/index-C6-rb6B7.css +0 -1
  118. package/public/assets/index-CI6ZE9xs.js +0 -2
  119. package/public/assets/index-CI6ZE9xs.js.map +0 -1
  120. package/public/assets/index-CSUBS-o9.js +0 -2
  121. package/public/assets/index-CSUBS-o9.js.map +0 -1
  122. package/public/assets/index-Cl4PCYg4.js +0 -2
  123. package/public/assets/index-Cl4PCYg4.js.map +0 -1
  124. package/public/assets/index-CrjSY1y2.js +0 -2
  125. package/public/assets/index-D7SuJgSm.js +0 -2
  126. package/public/assets/index-D7SuJgSm.js.map +0 -1
  127. package/public/assets/index-Daf72P6o.js +0 -2
  128. package/public/assets/index-Daf72P6o.js.map +0 -1
  129. package/public/assets/index-WIaMCSWq.js +0 -2
  130. package/public/assets/index-WIaMCSWq.js.map +0 -1
  131. package/public/assets/index-WsSXkxQ_.js +0 -2
  132. package/public/assets/index-WsSXkxQ_.js.map +0 -1
  133. package/public/assets/main-Hn7UJb2f.js +0 -42
  134. package/public/assets/main-Hn7UJb2f.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  import { timingSafeEqual } from "node:crypto";
2
2
  import { AUTOMATION_BRIDGE_PATH, parseBrowserAction, parseDeveloperAction } from "../../opencode/automation-plugin";
3
+ import { DeveloperInspectionTargets } from "../../automation/developer-inspection-targets";
3
4
  export function isAutomationPluginRequest(request, deps) {
4
5
  if (request.method !== "POST" || request.url.split("?")[0] !== AUTOMATION_BRIDGE_PATH)
5
6
  return false;
@@ -13,6 +14,7 @@ export function isAutomationPluginRequest(request, deps) {
13
14
  return actual.length === expected.length && timingSafeEqual(actual, expected);
14
15
  }
15
16
  export function registerAutomationPluginRoute(app, deps) {
17
+ const inspectedTargets = new DeveloperInspectionTargets();
16
18
  app.post(AUTOMATION_BRIDGE_PATH, { bodyLimit: 32 * 1024 }, async (request, reply) => {
17
19
  if (!isAutomationPluginRequest(request, deps))
18
20
  return reply.code(401).send({ error: "Unauthorized automation bridge" });
@@ -77,37 +79,48 @@ export function registerAutomationPluginRoute(app, deps) {
77
79
  endpoint: status.cdpUrl,
78
80
  runId: status.runId,
79
81
  windowId: status.windowId,
80
- sessionId: body.sessionID,
81
82
  };
82
- let context;
83
+ let command;
84
+ if (body.mode !== "developer-probe") {
85
+ try {
86
+ command = parseDeveloperAction(body.command);
87
+ }
88
+ catch (error) {
89
+ return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
90
+ }
91
+ }
92
+ let identity;
83
93
  try {
84
- context = await deps.developerCdp.context(selection);
94
+ if (body.mode === "developer-probe" || command?.action === "inspect") {
95
+ await deps.developerCdp.context(selection);
96
+ }
97
+ else {
98
+ identity = inspectedTargets.get(body.sessionID, selection);
99
+ await deps.developerCdp.context(identity);
100
+ }
85
101
  }
86
102
  catch (error) {
103
+ if (body.mode !== "developer-probe")
104
+ inspectedTargets.forget(body.sessionID);
87
105
  return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) });
88
106
  }
89
- if (!await deps.workspaceManager.ownsLocation(context.instanceId, location)) {
90
- return reply.code(404).send({ error: "The visible CodeNomad workspace does not own this OpenCode session" });
91
- }
92
- const identity = { ...selection, instanceId: context.instanceId };
107
+ identity ??= selection;
93
108
  if (body.mode === "developer-probe") {
94
109
  return reply.send({ result: { available: true, nativeIdentity: status.nativeIdentity, runId: status.runId } });
95
110
  }
96
- let command;
97
- try {
98
- command = parseDeveloperAction(body.command);
99
- }
100
- catch (error) {
101
- return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
102
- }
111
+ if (!command)
112
+ return reply.code(400).send({ error: "Missing developer command" });
103
113
  try {
104
114
  if (command.action === "restart") {
115
+ inspectedTargets.forget(body.sessionID);
105
116
  const result = await deps.nativeParent.request("developer.restart", {});
106
117
  deps.developerCdp.close(status.runId);
107
118
  return reply.send({ result });
108
119
  }
109
120
  if (command.action === "inspect") {
110
- return reply.send({ result: await deps.developerCdp.inspect(identity) });
121
+ const result = await deps.developerCdp.inspect(identity);
122
+ inspectedTargets.remember(body.sessionID, identity);
123
+ return reply.send({ result });
111
124
  }
112
125
  if (command.action === "screenshot") {
113
126
  const image = await deps.developerCdp.screenshot(identity);
@@ -1 +1 @@
1
- {"version":3,"file":"automation-plugin.js","sourceRoot":"","sources":["../../../src/server/routes/automation-plugin.ts"],"names":[],"mappings":"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","sourcesContent":["import { timingSafeEqual } from \"node:crypto\"\nimport type { FastifyInstance, FastifyRequest } from \"fastify\"\nimport type { AuthManager } from \"../../auth/manager\"\nimport type { DeveloperCdp } from \"../../developer-cdp\"\nimport type { DeveloperCdpIdentity, DeveloperCdpSelection } from \"../../developer-cdp\"\nimport type { NativeParent } from \"../../native-parent\"\nimport { AUTOMATION_BRIDGE_PATH, parseBrowserAction, parseDeveloperAction } from \"../../opencode/automation-plugin\"\nimport type { WorkspaceManager } from \"../../workspaces/manager\"\n\ninterface AutomationPluginRouteDeps {\n authManager: AuthManager\n bridgeToken: string\n nativeParent: NativeParent\n workspaceManager: WorkspaceManager\n developerCdp: DeveloperCdp\n}\n\ninterface DeveloperNativeStatus {\n status: {\n state: string\n runId?: string\n nativeIdentity?: string\n cdpUrl?: string\n windowId?: string\n }\n logs?: unknown[]\n}\n\nexport function isAutomationPluginRequest(\n request: FastifyRequest,\n deps: Pick<AutomationPluginRouteDeps, \"authManager\" | \"bridgeToken\">,\n): boolean {\n if (request.method !== \"POST\" || request.url.split(\"?\")[0] !== AUTOMATION_BRIDGE_PATH) return false\n if (!deps.authManager.isLoopbackRequest(request)) return false\n const supplied = request.headers[\"x-codenomad-automation-token\"]\n if (typeof supplied !== \"string\") return false\n const actual = Buffer.from(supplied)\n const expected = Buffer.from(deps.bridgeToken)\n return actual.length === expected.length && timingSafeEqual(actual, expected)\n}\n\nexport function registerAutomationPluginRoute(app: FastifyInstance, deps: AutomationPluginRouteDeps): void {\n app.post(AUTOMATION_BRIDGE_PATH, { bodyLimit: 32 * 1024 }, async (request, reply) => {\n if (!isAutomationPluginRequest(request, deps)) return reply.code(401).send({ error: \"Unauthorized automation bridge\" })\n const body = request.body as { mode?: unknown; sessionID?: unknown; command?: unknown } | undefined\n if (!body || ![\"developer-probe\", \"developer-execute\", \"browser-claim\", \"browser-probe\", \"browser-execute\"].includes(String(body.mode))\n || typeof body.sessionID !== \"string\" || body.sessionID.length > 256) {\n return reply.code(400).send({ error: \"Invalid automation bridge request\" })\n }\n\n let location\n try {\n location = (await (await deps.workspaceManager.getSharedServiceClient()).session.get({ sessionID: body.sessionID })).location\n } catch {\n return reply.code(404).send({ error: \"Session not found\" })\n }\n const owned = (await Promise.all(deps.workspaceManager.list().map((workspace) =>\n deps.workspaceManager.ownsLocation(workspace.id, location).catch(() => false),\n ))).some(Boolean)\n if (!owned) return reply.code(404).send({ error: \"Session is not owned by this CodeNomad instance\" })\n\n if (body.mode === \"browser-claim\") return reply.send({ result: { available: true } })\n if (body.mode === \"browser-probe\") {\n try {\n const result = await deps.nativeParent.request<{ available: boolean }>(\"browser.probe\", { sessionID: body.sessionID })\n return result.available ? reply.send({ result }) : reply.code(404).send({ error: \"No visible browser target\" })\n } catch (error) {\n return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) })\n }\n }\n if (body.mode === \"browser-execute\") {\n let command\n try {\n command = parseBrowserAction(body.command)\n } catch (error) {\n return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) })\n }\n try {\n const result = await deps.nativeParent.request(\"browser.execute\", { sessionID: body.sessionID, command })\n return reply.send({ result })\n } catch (error) {\n return reply.code(409).send({ error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n let native: DeveloperNativeStatus\n try {\n native = await deps.nativeParent.request<DeveloperNativeStatus>(\"developer.status\", {})\n } catch (error) {\n return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) })\n }\n const status = native.status\n const available = status?.state === \"ready\" && typeof status.runId === \"string\"\n && typeof status.nativeIdentity === \"string\" && typeof status.cdpUrl === \"string\" && typeof status.windowId === \"string\"\n if (!available) {\n if (typeof status?.runId === \"string\") deps.developerCdp.close(status.runId)\n return reply.code(404).send({ error: \"Native automation has no active CodeNomad session\" })\n }\n\n const selection: DeveloperCdpSelection = {\n endpoint: status.cdpUrl!,\n runId: status.runId!,\n windowId: status.windowId!,\n sessionId: body.sessionID,\n }\n let context\n try {\n context = await deps.developerCdp.context(selection)\n } catch (error) {\n return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) })\n }\n if (!await deps.workspaceManager.ownsLocation(context.instanceId, location)) {\n return reply.code(404).send({ error: \"The visible CodeNomad workspace does not own this OpenCode session\" })\n }\n const identity: DeveloperCdpIdentity = { ...selection, instanceId: context.instanceId }\n if (body.mode === \"developer-probe\") {\n return reply.send({ result: { available: true, nativeIdentity: status.nativeIdentity, runId: status.runId } })\n }\n\n let command\n try {\n command = parseDeveloperAction(body.command)\n } catch (error) {\n return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) })\n }\n try {\n if (command.action === \"restart\") {\n const result = await deps.nativeParent.request<DeveloperNativeStatus[\"status\"]>(\"developer.restart\", {})\n deps.developerCdp.close(status.runId)\n return reply.send({ result })\n }\n if (command.action === \"inspect\") {\n return reply.send({ result: await deps.developerCdp.inspect(identity) })\n }\n if (command.action === \"screenshot\") {\n const image = await deps.developerCdp.screenshot(identity)\n return reply.send({ result: { image: { data: image.data, mime: image.mediaType } } })\n }\n await deps.developerCdp.act(command.action === \"type\"\n ? { ...identity, kind: \"type\", ref: command.ref, text: command.text }\n : { ...identity, kind: \"click\", ref: command.ref })\n return reply.send({ result: { ok: true } })\n } catch (error) {\n return reply.code(409).send({ error: error instanceof Error ? error.message : String(error) })\n }\n })\n}\n"]}
1
+ {"version":3,"file":"automation-plugin.js","sourceRoot":"","sources":["../../../src/server/routes/automation-plugin.ts"],"names":[],"mappings":"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","sourcesContent":["import { timingSafeEqual } from \"node:crypto\"\nimport type { FastifyInstance, FastifyRequest } from \"fastify\"\nimport type { AuthManager } from \"../../auth/manager\"\nimport type { DeveloperCdp } from \"../../developer-cdp\"\nimport type { DeveloperCdpIdentity, DeveloperCdpSelection } from \"../../developer-cdp\"\nimport type { NativeParent } from \"../../native-parent\"\nimport { AUTOMATION_BRIDGE_PATH, parseBrowserAction, parseDeveloperAction } from \"../../opencode/automation-plugin\"\nimport type { WorkspaceManager } from \"../../workspaces/manager\"\nimport { DeveloperInspectionTargets } from \"../../automation/developer-inspection-targets\"\n\ninterface AutomationPluginRouteDeps {\n authManager: AuthManager\n bridgeToken: string\n nativeParent: NativeParent\n workspaceManager: WorkspaceManager\n developerCdp: DeveloperCdp\n}\n\ninterface DeveloperNativeStatus {\n status: {\n state: string\n runId?: string\n nativeIdentity?: string\n cdpUrl?: string\n windowId?: string\n }\n logs?: unknown[]\n}\n\nexport function isAutomationPluginRequest(\n request: FastifyRequest,\n deps: Pick<AutomationPluginRouteDeps, \"authManager\" | \"bridgeToken\">,\n): boolean {\n if (request.method !== \"POST\" || request.url.split(\"?\")[0] !== AUTOMATION_BRIDGE_PATH) return false\n if (!deps.authManager.isLoopbackRequest(request)) return false\n const supplied = request.headers[\"x-codenomad-automation-token\"]\n if (typeof supplied !== \"string\") return false\n const actual = Buffer.from(supplied)\n const expected = Buffer.from(deps.bridgeToken)\n return actual.length === expected.length && timingSafeEqual(actual, expected)\n}\n\nexport function registerAutomationPluginRoute(app: FastifyInstance, deps: AutomationPluginRouteDeps): void {\n const inspectedTargets = new DeveloperInspectionTargets()\n app.post(AUTOMATION_BRIDGE_PATH, { bodyLimit: 32 * 1024 }, async (request, reply) => {\n if (!isAutomationPluginRequest(request, deps)) return reply.code(401).send({ error: \"Unauthorized automation bridge\" })\n const body = request.body as { mode?: unknown; sessionID?: unknown; command?: unknown } | undefined\n if (!body || ![\"developer-probe\", \"developer-execute\", \"browser-claim\", \"browser-probe\", \"browser-execute\"].includes(String(body.mode))\n || typeof body.sessionID !== \"string\" || body.sessionID.length > 256) {\n return reply.code(400).send({ error: \"Invalid automation bridge request\" })\n }\n\n let location\n try {\n location = (await (await deps.workspaceManager.getSharedServiceClient()).session.get({ sessionID: body.sessionID })).location\n } catch {\n return reply.code(404).send({ error: \"Session not found\" })\n }\n const owned = (await Promise.all(deps.workspaceManager.list().map((workspace) =>\n deps.workspaceManager.ownsLocation(workspace.id, location).catch(() => false),\n ))).some(Boolean)\n if (!owned) return reply.code(404).send({ error: \"Session is not owned by this CodeNomad instance\" })\n\n if (body.mode === \"browser-claim\") return reply.send({ result: { available: true } })\n if (body.mode === \"browser-probe\") {\n try {\n const result = await deps.nativeParent.request<{ available: boolean }>(\"browser.probe\", { sessionID: body.sessionID })\n return result.available ? reply.send({ result }) : reply.code(404).send({ error: \"No visible browser target\" })\n } catch (error) {\n return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) })\n }\n }\n if (body.mode === \"browser-execute\") {\n let command\n try {\n command = parseBrowserAction(body.command)\n } catch (error) {\n return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) })\n }\n try {\n const result = await deps.nativeParent.request(\"browser.execute\", { sessionID: body.sessionID, command })\n return reply.send({ result })\n } catch (error) {\n return reply.code(409).send({ error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n let native: DeveloperNativeStatus\n try {\n native = await deps.nativeParent.request<DeveloperNativeStatus>(\"developer.status\", {})\n } catch (error) {\n return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) })\n }\n const status = native.status\n const available = status?.state === \"ready\" && typeof status.runId === \"string\"\n && typeof status.nativeIdentity === \"string\" && typeof status.cdpUrl === \"string\" && typeof status.windowId === \"string\"\n if (!available) {\n if (typeof status?.runId === \"string\") deps.developerCdp.close(status.runId)\n return reply.code(404).send({ error: \"Native automation has no active CodeNomad session\" })\n }\n\n const selection: DeveloperCdpSelection = {\n endpoint: status.cdpUrl!,\n runId: status.runId!,\n windowId: status.windowId!,\n }\n let command\n if (body.mode !== \"developer-probe\") {\n try {\n command = parseDeveloperAction(body.command)\n } catch (error) {\n return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) })\n }\n }\n let identity: DeveloperCdpIdentity | undefined\n try {\n if (body.mode === \"developer-probe\" || command?.action === \"inspect\") {\n await deps.developerCdp.context(selection)\n } else {\n identity = inspectedTargets.get(body.sessionID, selection)\n await deps.developerCdp.context(identity)\n }\n } catch (error) {\n if (body.mode !== \"developer-probe\") inspectedTargets.forget(body.sessionID)\n return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) })\n }\n identity ??= selection\n if (body.mode === \"developer-probe\") {\n return reply.send({ result: { available: true, nativeIdentity: status.nativeIdentity, runId: status.runId } })\n }\n\n if (!command) return reply.code(400).send({ error: \"Missing developer command\" })\n try {\n if (command.action === \"restart\") {\n inspectedTargets.forget(body.sessionID)\n const result = await deps.nativeParent.request<DeveloperNativeStatus[\"status\"]>(\"developer.restart\", {})\n deps.developerCdp.close(status.runId)\n return reply.send({ result })\n }\n if (command.action === \"inspect\") {\n const result = await deps.developerCdp.inspect(identity)\n inspectedTargets.remember(body.sessionID, identity)\n return reply.send({ result })\n }\n if (command.action === \"screenshot\") {\n const image = await deps.developerCdp.screenshot(identity)\n return reply.send({ result: { image: { data: image.data, mime: image.mediaType } } })\n }\n await deps.developerCdp.act(command.action === \"type\"\n ? { ...identity, kind: \"type\", ref: command.ref, text: command.text }\n : { ...identity, kind: \"click\", ref: command.ref })\n return reply.send({ result: { ok: true } })\n } catch (error) {\n return reply.code(409).send({ error: error instanceof Error ? error.message : String(error) })\n }\n })\n}\n"]}
@@ -3,7 +3,7 @@ import test from "node:test";
3
3
  import Fastify from "fastify";
4
4
  import { AUTOMATION_BRIDGE_PATH } from "../../opencode/automation-plugin";
5
5
  import { registerAutomationPluginRoute } from "./automation-plugin";
6
- test("fences Developer Mode by the visible owned session and forwards CDP actions", async () => {
6
+ test("targets the inspected window independently of the visible conversation", async () => {
7
7
  const app = Fastify({ logger: false });
8
8
  const nativeCalls = [];
9
9
  let state = "ready";
@@ -34,8 +34,7 @@ test("fences Developer Mode by the visible owned session and forwards CDP action
34
34
  },
35
35
  developerCdp: {
36
36
  context: async (identity) => {
37
- if (identity.sessionId !== visibleSession)
38
- throw new Error("active session mismatch");
37
+ assert.equal(identity.sessionId, undefined);
39
38
  return { windowId: "window-1", instanceId: "workspace-1", sessionId: visibleSession };
40
39
  },
41
40
  inspect: async (identity) => {
@@ -48,6 +47,10 @@ test("fences Developer Mode by the visible owned session and forwards CDP action
48
47
  };
49
48
  },
50
49
  close: () => undefined,
50
+ act: async (identity) => {
51
+ assert.equal(identity.sessionId, undefined);
52
+ assert.equal(identity.instanceId, undefined);
53
+ },
51
54
  },
52
55
  workspaceManager: {
53
56
  getSharedServiceClient: async () => ({ session: { get: async () => ({ location: { directory: "D:\\project" } }) } }),
@@ -68,17 +71,18 @@ test("fences Developer Mode by the visible owned session and forwards CDP action
68
71
  endpoint: "http://127.0.0.1:9222",
69
72
  runId: "run-1",
70
73
  windowId: "window-1",
71
- sessionId: "session-1",
72
- instanceId: "workspace-1",
73
74
  });
74
75
  assert.equal(inspect.json().result.context.sessionId, "session-1");
76
+ visibleSession = "session-2";
77
+ assert.equal((await request({ mode: "developer-execute", sessionID: "session-1", command: { action: "click", ref: "ax1" } })).statusCode, 200);
75
78
  const restart = await request({ mode: "developer-execute", sessionID: "session-1", command: { action: "restart" } });
76
79
  assert.equal(restart.statusCode, 200);
77
80
  assert.deepEqual(nativeCalls.slice(-2), [
78
81
  { method: "developer.status", params: {} },
79
82
  { method: "developer.restart", params: {} },
80
83
  ]);
81
- assert.equal((await request({ mode: "developer-probe", sessionID: "session-2" })).statusCode, 404);
84
+ assert.equal((await request({ mode: "developer-probe", sessionID: "session-2" })).statusCode, 200);
85
+ assert.equal((await request({ mode: "developer-execute", sessionID: "session-1", command: { action: "restart" } })).statusCode, 404);
82
86
  state = "stopped";
83
87
  assert.equal((await request({ mode: "developer-probe", sessionID: "session-2" })).statusCode, 404);
84
88
  state = "ready";
@@ -1 +1 @@
1
- {"version":3,"file":"automation-plugin.test.js","sourceRoot":"","sources":["../../../src/server/routes/automation-plugin.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 { AUTOMATION_BRIDGE_PATH } from \"../../opencode/automation-plugin\"\nimport { registerAutomationPluginRoute } from \"./automation-plugin\"\n\ntest(\"fences Developer Mode by the visible owned session and forwards CDP actions\", async () => {\n const app = Fastify({ logger: false })\n const nativeCalls: Array<{ method: string; params: unknown }> = []\n let state = \"ready\"\n let runId = \"run-1\"\n let visibleSession = \"session-1\"\n let inspectedIdentity: unknown\n registerAutomationPluginRoute(app, {\n authManager: { isLoopbackRequest: () => true },\n bridgeToken: \"secret\",\n nativeParent: {\n request: async (method: string, params: unknown) => {\n nativeCalls.push({ method, params })\n if (method === \"developer.restart\") {\n runId = \"run-2\"\n return { state: \"starting\", runId }\n }\n return {\n status: {\n state,\n runId,\n nativeIdentity: \"electron:test\",\n cdpUrl: \"http://127.0.0.1:9222\",\n windowId: \"window-1\",\n },\n logs: [],\n }\n },\n },\n developerCdp: {\n context: async (identity: { sessionId: string }) => {\n if (identity.sessionId !== visibleSession) throw new Error(\"active session mismatch\")\n return { windowId: \"window-1\", instanceId: \"workspace-1\", sessionId: visibleSession }\n },\n inspect: async (identity: unknown) => {\n inspectedIdentity = identity\n return {\n target: { id: \"page-1\", title: \"CodeNomad\", url: \"http://app.test/\" },\n context: { windowId: \"window-1\", instanceId: \"workspace-1\", sessionId: visibleSession },\n nodes: [],\n diagnostics: [],\n }\n },\n close: () => undefined,\n },\n workspaceManager: {\n getSharedServiceClient: async () => ({ session: { get: async () => ({ location: { directory: \"D:\\\\project\" } }) } }),\n list: () => [{ id: \"workspace-1\" }],\n ownsLocation: async () => true,\n },\n } as never)\n\n const request = async (body: Record<string, unknown>) => app.inject({\n method: \"POST\",\n url: AUTOMATION_BRIDGE_PATH,\n headers: { \"x-codenomad-automation-token\": \"secret\" },\n payload: body,\n })\n\n assert.equal((await request({ mode: \"developer-probe\", sessionID: \"session-1\" })).statusCode, 200)\n const inspect = await request({ mode: \"developer-execute\", sessionID: \"session-1\", command: { action: \"inspect\" } })\n assert.equal(inspect.statusCode, 200)\n assert.deepEqual(inspectedIdentity, {\n endpoint: \"http://127.0.0.1:9222\",\n runId: \"run-1\",\n windowId: \"window-1\",\n sessionId: \"session-1\",\n instanceId: \"workspace-1\",\n })\n assert.equal(inspect.json().result.context.sessionId, \"session-1\")\n\n const restart = await request({ mode: \"developer-execute\", sessionID: \"session-1\", command: { action: \"restart\" } })\n assert.equal(restart.statusCode, 200)\n assert.deepEqual(nativeCalls.slice(-2), [\n { method: \"developer.status\", params: {} },\n { method: \"developer.restart\", params: {} },\n ])\n assert.equal((await request({ mode: \"developer-probe\", sessionID: \"session-2\" })).statusCode, 404)\n\n state = \"stopped\"\n assert.equal((await request({ mode: \"developer-probe\", sessionID: \"session-2\" })).statusCode, 404)\n state = \"ready\"\n runId = \"run-3\"\n visibleSession = \"session-2\"\n assert.equal((await request({ mode: \"developer-probe\", sessionID: \"session-2\" })).statusCode, 200)\n\n const unauthorized = await app.inject({ method: \"POST\", url: AUTOMATION_BRIDGE_PATH, payload: { mode: \"developer-probe\", sessionID: \"session-1\" } })\n assert.equal(unauthorized.statusCode, 401)\n await app.close()\n})\n\ntest(\"browser works with an unavailable developer target and unrelated inventory failure\", async () => {\n const app = Fastify({ logger: false })\n const nativeCalls: Array<{ method: string; params: unknown }> = []\n registerAutomationPluginRoute(app, {\n authManager: { isLoopbackRequest: () => true },\n bridgeToken: \"secret\",\n nativeParent: {\n request: async (method: string, params: unknown) => {\n nativeCalls.push({ method, params })\n return method === \"developer.status\" ? { status: { state: \"stopped\" } }\n : method === \"browser.probe\" ? { available: true } : { url: \"https://example.com/\" }\n },\n },\n workspaceManager: {\n getSharedServiceClient: async () => ({ session: { get: async () => ({ location: { directory: \"D:\\\\project\" } }) } }),\n list: () => [{ id: \"unrelated-worktree\" }, { id: \"workspace-1\" }],\n ownsLocation: async (id: string) => {\n if (id === \"unrelated-worktree\") throw new Error(\"Native worktree inventory is unavailable\")\n return true\n },\n },\n } as never)\n\n const request = async (body: Record<string, unknown>) => app.inject({\n method: \"POST\",\n url: AUTOMATION_BRIDGE_PATH,\n headers: { \"x-codenomad-automation-token\": \"secret\" },\n payload: body,\n })\n\n assert.equal((await request({ mode: \"browser-claim\", sessionID: \"session-1\" })).statusCode, 200)\n assert.equal(nativeCalls.length, 0)\n assert.equal((await request({ mode: \"browser-probe\", sessionID: \"session-1\" })).statusCode, 200)\n const open = await request({ mode: \"browser-execute\", sessionID: \"session-1\", command: { action: \"open\", url: \"https://example.com\" } })\n assert.equal(open.statusCode, 200)\n assert.deepEqual(nativeCalls, [\n { method: \"browser.probe\", params: { sessionID: \"session-1\" } },\n {\n method: \"browser.execute\",\n params: { sessionID: \"session-1\", command: { action: \"open\", url: \"https://example.com\" } },\n },\n ])\n const developer = await request({ mode: \"developer-execute\", sessionID: \"session-1\", command: { action: \"inspect\" } })\n assert.equal(developer.statusCode, 404)\n assert.match(developer.json().error, /Native automation has no active/)\n await app.close()\n})\n"]}
1
+ {"version":3,"file":"automation-plugin.test.js","sourceRoot":"","sources":["../../../src/server/routes/automation-plugin.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 { AUTOMATION_BRIDGE_PATH } from \"../../opencode/automation-plugin\"\nimport { registerAutomationPluginRoute } from \"./automation-plugin\"\n\ntest(\"targets the inspected window independently of the visible conversation\", async () => {\n const app = Fastify({ logger: false })\n const nativeCalls: Array<{ method: string; params: unknown }> = []\n let state = \"ready\"\n let runId = \"run-1\"\n let visibleSession = \"session-1\"\n let inspectedIdentity: unknown\n registerAutomationPluginRoute(app, {\n authManager: { isLoopbackRequest: () => true },\n bridgeToken: \"secret\",\n nativeParent: {\n request: async (method: string, params: unknown) => {\n nativeCalls.push({ method, params })\n if (method === \"developer.restart\") {\n runId = \"run-2\"\n return { state: \"starting\", runId }\n }\n return {\n status: {\n state,\n runId,\n nativeIdentity: \"electron:test\",\n cdpUrl: \"http://127.0.0.1:9222\",\n windowId: \"window-1\",\n },\n logs: [],\n }\n },\n },\n developerCdp: {\n context: async (identity: { sessionId?: string }) => {\n assert.equal(identity.sessionId, undefined)\n return { windowId: \"window-1\", instanceId: \"workspace-1\", sessionId: visibleSession }\n },\n inspect: async (identity: unknown) => {\n inspectedIdentity = identity\n return {\n target: { id: \"page-1\", title: \"CodeNomad\", url: \"http://app.test/\" },\n context: { windowId: \"window-1\", instanceId: \"workspace-1\", sessionId: visibleSession },\n nodes: [],\n diagnostics: [],\n }\n },\n close: () => undefined,\n act: async (identity: { sessionId?: string; instanceId?: string }) => {\n assert.equal(identity.sessionId, undefined)\n assert.equal(identity.instanceId, undefined)\n },\n },\n workspaceManager: {\n getSharedServiceClient: async () => ({ session: { get: async () => ({ location: { directory: \"D:\\\\project\" } }) } }),\n list: () => [{ id: \"workspace-1\" }],\n ownsLocation: async () => true,\n },\n } as never)\n\n const request = async (body: Record<string, unknown>) => app.inject({\n method: \"POST\",\n url: AUTOMATION_BRIDGE_PATH,\n headers: { \"x-codenomad-automation-token\": \"secret\" },\n payload: body,\n })\n\n assert.equal((await request({ mode: \"developer-probe\", sessionID: \"session-1\" })).statusCode, 200)\n const inspect = await request({ mode: \"developer-execute\", sessionID: \"session-1\", command: { action: \"inspect\" } })\n assert.equal(inspect.statusCode, 200)\n assert.deepEqual(inspectedIdentity, {\n endpoint: \"http://127.0.0.1:9222\",\n runId: \"run-1\",\n windowId: \"window-1\",\n })\n assert.equal(inspect.json().result.context.sessionId, \"session-1\")\n\n visibleSession = \"session-2\"\n assert.equal((await request({ mode: \"developer-execute\", sessionID: \"session-1\", command: { action: \"click\", ref: \"ax1\" } })).statusCode, 200)\n\n const restart = await request({ mode: \"developer-execute\", sessionID: \"session-1\", command: { action: \"restart\" } })\n assert.equal(restart.statusCode, 200)\n assert.deepEqual(nativeCalls.slice(-2), [\n { method: \"developer.status\", params: {} },\n { method: \"developer.restart\", params: {} },\n ])\n assert.equal((await request({ mode: \"developer-probe\", sessionID: \"session-2\" })).statusCode, 200)\n assert.equal((await request({ mode: \"developer-execute\", sessionID: \"session-1\", command: { action: \"restart\" } })).statusCode, 404)\n\n state = \"stopped\"\n assert.equal((await request({ mode: \"developer-probe\", sessionID: \"session-2\" })).statusCode, 404)\n state = \"ready\"\n runId = \"run-3\"\n visibleSession = \"session-2\"\n assert.equal((await request({ mode: \"developer-probe\", sessionID: \"session-2\" })).statusCode, 200)\n\n const unauthorized = await app.inject({ method: \"POST\", url: AUTOMATION_BRIDGE_PATH, payload: { mode: \"developer-probe\", sessionID: \"session-1\" } })\n assert.equal(unauthorized.statusCode, 401)\n await app.close()\n})\n\ntest(\"browser works with an unavailable developer target and unrelated inventory failure\", async () => {\n const app = Fastify({ logger: false })\n const nativeCalls: Array<{ method: string; params: unknown }> = []\n registerAutomationPluginRoute(app, {\n authManager: { isLoopbackRequest: () => true },\n bridgeToken: \"secret\",\n nativeParent: {\n request: async (method: string, params: unknown) => {\n nativeCalls.push({ method, params })\n return method === \"developer.status\" ? { status: { state: \"stopped\" } }\n : method === \"browser.probe\" ? { available: true } : { url: \"https://example.com/\" }\n },\n },\n workspaceManager: {\n getSharedServiceClient: async () => ({ session: { get: async () => ({ location: { directory: \"D:\\\\project\" } }) } }),\n list: () => [{ id: \"unrelated-worktree\" }, { id: \"workspace-1\" }],\n ownsLocation: async (id: string) => {\n if (id === \"unrelated-worktree\") throw new Error(\"Native worktree inventory is unavailable\")\n return true\n },\n },\n } as never)\n\n const request = async (body: Record<string, unknown>) => app.inject({\n method: \"POST\",\n url: AUTOMATION_BRIDGE_PATH,\n headers: { \"x-codenomad-automation-token\": \"secret\" },\n payload: body,\n })\n\n assert.equal((await request({ mode: \"browser-claim\", sessionID: \"session-1\" })).statusCode, 200)\n assert.equal(nativeCalls.length, 0)\n assert.equal((await request({ mode: \"browser-probe\", sessionID: \"session-1\" })).statusCode, 200)\n const open = await request({ mode: \"browser-execute\", sessionID: \"session-1\", command: { action: \"open\", url: \"https://example.com\" } })\n assert.equal(open.statusCode, 200)\n assert.deepEqual(nativeCalls, [\n { method: \"browser.probe\", params: { sessionID: \"session-1\" } },\n {\n method: \"browser.execute\",\n params: { sessionID: \"session-1\", command: { action: \"open\", url: \"https://example.com\" } },\n },\n ])\n const developer = await request({ mode: \"developer-execute\", sessionID: \"session-1\", command: { action: \"inspect\" } })\n assert.equal(developer.statusCode, 404)\n assert.match(developer.json().error, /Native automation has no active/)\n await app.close()\n})\n"]}
@@ -0,0 +1,86 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { afterEach, beforeEach, describe, it } from "node:test";
7
+ import { readGitCommonDirectory } from "../git-common-directory";
8
+ import { sharesGitCommonDirectory } from "../git-worktrees";
9
+ const git = (directory, ...args) => execFileSync("git", ["-C", directory, ...args], {
10
+ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true,
11
+ }).trim();
12
+ describe("Git ownership preflight admission", () => {
13
+ let temp;
14
+ let repo;
15
+ let linked;
16
+ let trace;
17
+ let environment;
18
+ beforeEach(() => {
19
+ environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.startsWith("GIT_")));
20
+ for (const key of Object.keys(environment))
21
+ delete process.env[key];
22
+ temp = mkdtempSync(path.join(tmpdir(), "codenomad-git-preflight-"));
23
+ process.env.GIT_CONFIG_NOSYSTEM = "1";
24
+ process.env.GIT_CONFIG_GLOBAL = path.join(temp, "empty-global-config");
25
+ writeFileSync(process.env.GIT_CONFIG_GLOBAL, "");
26
+ repo = path.join(temp, "main repo");
27
+ linked = path.join(temp, "linked checkout");
28
+ git(temp, "init", "--initial-branch=main", repo);
29
+ git(repo, "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "--allow-empty", "-m", "initial");
30
+ git(repo, "worktree", "add", "-b", "feature", linked);
31
+ trace = path.join(temp, "trace.json");
32
+ process.env.GIT_TRACE2_EVENT = trace.replaceAll("\\", "/");
33
+ });
34
+ afterEach(() => {
35
+ for (const key of Object.keys(process.env))
36
+ if (key.startsWith("GIT_"))
37
+ delete process.env[key];
38
+ Object.assign(process.env, environment);
39
+ rmSync(temp, { recursive: true, force: true });
40
+ });
41
+ const events = (file) => readFileSync(file, "utf8").trim().split("\n").map(line => JSON.parse(line));
42
+ it("coalesces a concurrent session/project ownership fan into one command per directory", async () => {
43
+ const reads = Array.from({ length: 48 }, () => sharesGitCommonDirectory(repo, linked));
44
+ assert.deepEqual(await Promise.all(reads), Array(48).fill(true));
45
+ const starts = events(trace).filter(event => event.event === "start");
46
+ assert.equal(starts.length, 2);
47
+ assert.deepEqual(starts.map(event => event.argv[2]).sort(), [repo, linked].sort());
48
+ });
49
+ it("bounds distinct Git processes without holding the calling event loop", async () => {
50
+ const directories = Array.from({ length: 10 }, (_, index) => path.join(temp, `repo-${index}`));
51
+ for (const directory of directories)
52
+ git(temp, "init", directory);
53
+ writeFileSync(trace, "");
54
+ const reads = directories.map(readGitCommonDirectory);
55
+ let settled = false;
56
+ void Promise.all(reads).then(() => { settled = true; });
57
+ await new Promise(resolve => setImmediate(resolve));
58
+ assert.equal(settled, false);
59
+ assert.deepEqual(await Promise.all(reads), directories.map(directory => realpathSync(path.join(directory, ".git"))));
60
+ const timeline = events(trace).filter(event => event.event === "start" || event.event === "exit")
61
+ .sort((a, b) => a.time.localeCompare(b.time));
62
+ let running = 0;
63
+ let starts = 0;
64
+ for (const event of timeline) {
65
+ running += event.event === "start" ? 1 : -1;
66
+ if (event.event === "start")
67
+ starts += 1;
68
+ assert.ok(running >= 0 && running <= 2, `concurrent Git processes: ${running}`);
69
+ }
70
+ assert.equal(starts, directories.length);
71
+ assert.equal(running, 0);
72
+ });
73
+ it("rechecks changed Git identity and does not retain failed reads", async () => {
74
+ assert.equal(await sharesGitCommonDirectory(repo, linked), true);
75
+ const other = path.join(temp, "independent clone");
76
+ git(temp, "clone", repo, other);
77
+ rmSync(path.join(linked, ".git"));
78
+ writeFileSync(path.join(linked, ".git"), `gitdir: ${path.join(other, ".git").replaceAll("\\", "/")}\n`);
79
+ assert.equal(await sharesGitCommonDirectory(repo, linked), false);
80
+ const missing = path.join(temp, "not-yet-created");
81
+ await assert.rejects(readGitCommonDirectory(missing));
82
+ git(temp, "init", missing);
83
+ assert.equal(await readGitCommonDirectory(missing), realpathSync(path.join(missing, ".git")));
84
+ });
85
+ });
86
+ //# sourceMappingURL=git-common-directory.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-common-directory.test.js","sourceRoot":"","sources":["../../../src/workspaces/__tests__/git-common-directory.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAA;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACxF,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAA;AAE3D,MAAM,GAAG,GAAG,CAAC,SAAiB,EAAE,GAAG,IAAc,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,EAAE;IACpG,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,EAAE,IAAI;CACvE,CAAC,CAAC,IAAI,EAAE,CAAA;AAET,QAAQ,CAAC,mCAAmC,EAAE,GAAG,EAAE;IACjD,IAAI,IAAY,CAAA;IAChB,IAAI,IAAY,CAAA;IAChB,IAAI,MAAc,CAAA;IAClB,IAAI,KAAa,CAAA;IACjB,IAAI,WAA+C,CAAA;IAEnD,UAAU,CAAC,GAAG,EAAE;QACd,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;QACvG,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC;YAAE,OAAO,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACnE,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,0BAA0B,CAAC,CAAC,CAAA;QACnE,OAAO,CAAC,GAAG,CAAC,mBAAmB,GAAG,GAAG,CAAA;QACrC,OAAO,CAAC,GAAG,CAAC,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAA;QACtE,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAA;QAChD,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAA;QACnC,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAA;QAC3C,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,uBAAuB,EAAE,IAAI,CAAC,CAAA;QAChD,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,6BAA6B,EAAE,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,SAAS,CAAC,CAAA;QAClH,GAAG,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,CAAA;QACrD,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;QACrC,OAAO,CAAC,GAAG,CAAC,gBAAgB,GAAG,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IAC5D,CAAC,CAAC,CAAA;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC;YAAE,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC;gBAAE,OAAO,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC/F,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,WAAW,CAAC,CAAA;QACvC,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IAChD,CAAC,CAAC,CAAA;IAEF,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;IAE5G,EAAE,CAAC,qFAAqF,EAAE,KAAK,IAAI,EAAE;QACnG,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,wBAAwB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAA;QACtF,MAAM,CAAC,SAAS,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;QAChE,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,OAAO,CAAC,CAAA;QACrE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;QAC9B,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;IACpF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sEAAsE,EAAE,KAAK,IAAI,EAAE;QACpF,MAAM,WAAW,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,KAAK,EAAE,CAAC,CAAC,CAAA;QAC9F,KAAK,MAAM,SAAS,IAAI,WAAW;YAAE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,CAAA;QACjE,aAAa,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;QACxB,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAA;QACrD,IAAI,OAAO,GAAG,KAAK,CAAA;QACnB,KAAK,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,GAAG,IAAI,CAAA,CAAC,CAAC,CAAC,CAAA;QACtD,MAAM,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAA;QACzD,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;QAC5B,MAAM,CAAC,SAAS,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;QACpH,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,OAAO,IAAI,KAAK,CAAC,KAAK,KAAK,MAAM,CAAC;aAC9F,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;QAC/C,IAAI,OAAO,GAAG,CAAC,CAAA;QACf,IAAI,MAAM,GAAG,CAAC,CAAA;QACd,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,OAAO,IAAI,KAAK,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;YAC3C,IAAI,KAAK,CAAC,KAAK,KAAK,OAAO;gBAAE,MAAM,IAAI,CAAC,CAAA;YACxC,MAAM,CAAC,EAAE,CAAC,OAAO,IAAI,CAAC,IAAI,OAAO,IAAI,CAAC,EAAE,6BAA6B,OAAO,EAAE,CAAC,CAAA;QACjF,CAAC;QACD,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;QACxC,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;IAC1B,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gEAAgE,EAAE,KAAK,IAAI,EAAE;QAC9E,MAAM,CAAC,KAAK,CAAC,MAAM,wBAAwB,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,CAAA;QAChE,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAA;QAClD,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAA;QAC/B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;QACjC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,CAAA;QACvG,MAAM,CAAC,KAAK,CAAC,MAAM,wBAAwB,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,CAAA;QACjE,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAA;QAClD,MAAM,MAAM,CAAC,OAAO,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC,CAAA;QACrD,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;QAC1B,MAAM,CAAC,KAAK,CAAC,MAAM,sBAAsB,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;IAC/F,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import assert from \"node:assert/strict\"\nimport { execFileSync } from \"node:child_process\"\nimport { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport path from \"node:path\"\nimport { afterEach, beforeEach, describe, it } from \"node:test\"\nimport { readGitCommonDirectory } from \"../git-common-directory\"\nimport { sharesGitCommonDirectory } from \"../git-worktrees\"\n\nconst git = (directory: string, ...args: string[]) => execFileSync(\"git\", [\"-C\", directory, ...args], {\n encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"pipe\"], windowsHide: true,\n}).trim()\n\ndescribe(\"Git ownership preflight admission\", () => {\n let temp: string\n let repo: string\n let linked: string\n let trace: string\n let environment: Record<string, string | undefined>\n\n beforeEach(() => {\n environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.startsWith(\"GIT_\")))\n for (const key of Object.keys(environment)) delete process.env[key]\n temp = mkdtempSync(path.join(tmpdir(), \"codenomad-git-preflight-\"))\n process.env.GIT_CONFIG_NOSYSTEM = \"1\"\n process.env.GIT_CONFIG_GLOBAL = path.join(temp, \"empty-global-config\")\n writeFileSync(process.env.GIT_CONFIG_GLOBAL, \"\")\n repo = path.join(temp, \"main repo\")\n linked = path.join(temp, \"linked checkout\")\n git(temp, \"init\", \"--initial-branch=main\", repo)\n git(repo, \"-c\", \"user.name=Test\", \"-c\", \"user.email=test@example.com\", \"commit\", \"--allow-empty\", \"-m\", \"initial\")\n git(repo, \"worktree\", \"add\", \"-b\", \"feature\", linked)\n trace = path.join(temp, \"trace.json\")\n process.env.GIT_TRACE2_EVENT = trace.replaceAll(\"\\\\\", \"/\")\n })\n\n afterEach(() => {\n for (const key of Object.keys(process.env)) if (key.startsWith(\"GIT_\")) delete process.env[key]\n Object.assign(process.env, environment)\n rmSync(temp, { recursive: true, force: true })\n })\n\n const events = (file: string) => readFileSync(file, \"utf8\").trim().split(\"\\n\").map(line => JSON.parse(line))\n\n it(\"coalesces a concurrent session/project ownership fan into one command per directory\", async () => {\n const reads = Array.from({ length: 48 }, () => sharesGitCommonDirectory(repo, linked))\n assert.deepEqual(await Promise.all(reads), Array(48).fill(true))\n const starts = events(trace).filter(event => event.event === \"start\")\n assert.equal(starts.length, 2)\n assert.deepEqual(starts.map(event => event.argv[2]).sort(), [repo, linked].sort())\n })\n\n it(\"bounds distinct Git processes without holding the calling event loop\", async () => {\n const directories = Array.from({ length: 10 }, (_, index) => path.join(temp, `repo-${index}`))\n for (const directory of directories) git(temp, \"init\", directory)\n writeFileSync(trace, \"\")\n const reads = directories.map(readGitCommonDirectory)\n let settled = false\n void Promise.all(reads).then(() => { settled = true })\n await new Promise<void>(resolve => setImmediate(resolve))\n assert.equal(settled, false)\n assert.deepEqual(await Promise.all(reads), directories.map(directory => realpathSync(path.join(directory, \".git\"))))\n const timeline = events(trace).filter(event => event.event === \"start\" || event.event === \"exit\")\n .sort((a, b) => a.time.localeCompare(b.time))\n let running = 0\n let starts = 0\n for (const event of timeline) {\n running += event.event === \"start\" ? 1 : -1\n if (event.event === \"start\") starts += 1\n assert.ok(running >= 0 && running <= 2, `concurrent Git processes: ${running}`)\n }\n assert.equal(starts, directories.length)\n assert.equal(running, 0)\n })\n\n it(\"rechecks changed Git identity and does not retain failed reads\", async () => {\n assert.equal(await sharesGitCommonDirectory(repo, linked), true)\n const other = path.join(temp, \"independent clone\")\n git(temp, \"clone\", repo, other)\n rmSync(path.join(linked, \".git\"))\n writeFileSync(path.join(linked, \".git\"), `gitdir: ${path.join(other, \".git\").replaceAll(\"\\\\\", \"/\")}\\n`)\n assert.equal(await sharesGitCommonDirectory(repo, linked), false)\n const missing = path.join(temp, \"not-yet-created\")\n await assert.rejects(readGitCommonDirectory(missing))\n git(temp, \"init\", missing)\n assert.equal(await readGitCommonDirectory(missing), realpathSync(path.join(missing, \".git\")))\n })\n})\n"]}
@@ -0,0 +1,130 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { afterEach, beforeEach, describe, it } from "node:test";
7
+ import { fixtureCatalogue } from "./native-worktree-fixture";
8
+ const git = (directory, ...args) => execFileSync("git", ["-C", directory, ...args], {
9
+ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
10
+ }).trim();
11
+ const gitPath = (directory) => directory.replaceAll("\\", "/");
12
+ describe("configured Git checkout roots", () => {
13
+ let temp;
14
+ let repo;
15
+ let linked;
16
+ let redirected;
17
+ let environment;
18
+ beforeEach(() => {
19
+ environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.startsWith("GIT_")));
20
+ for (const key of Object.keys(environment))
21
+ delete process.env[key];
22
+ temp = mkdtempSync(path.join(tmpdir(), "codenomad-checkout-config-"));
23
+ process.env.GIT_CONFIG_NOSYSTEM = "1";
24
+ process.env.GIT_CONFIG_GLOBAL = path.join(temp, "empty-global-config");
25
+ writeFileSync(process.env.GIT_CONFIG_GLOBAL, "");
26
+ repo = path.join(temp, "main repo");
27
+ linked = path.join(temp, "linked checkout");
28
+ redirected = path.join(temp, "effective root");
29
+ git(temp, "init", "--initial-branch=main", repo);
30
+ git(repo, "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "--allow-empty", "-m", "initial");
31
+ git(repo, "worktree", "add", "-b", "feature", linked);
32
+ mkdirSync(redirected);
33
+ });
34
+ afterEach(() => {
35
+ for (const key of Object.keys(process.env))
36
+ if (key.startsWith("GIT_"))
37
+ delete process.env[key];
38
+ Object.assign(process.env, environment);
39
+ rmSync(temp, { recursive: true, force: true });
40
+ });
41
+ for (const mode of ["absolute", "relative"]) {
42
+ it(`rejects an administrative backlink redirected by ${mode} configuration`, async () => {
43
+ mkdirSync(path.join(repo, "nested"));
44
+ mkdirSync(path.join(linked, "nested"));
45
+ const workspace = path.join(repo, "nested");
46
+ const before = await fixtureCatalogue(workspace);
47
+ assert.equal(before.worktrees.length, 2);
48
+ assert.equal(before.worktrees.find(entry => entry.kind === "worktree")?.directory, path.join(realpathSync(linked), "nested"));
49
+ const admin = git(linked, "rev-parse", "--absolute-git-dir");
50
+ const backlink = readFileSync(path.join(admin, "gitdir"), "utf8");
51
+ git(repo, "config", "extensions.worktreeConfig", "true");
52
+ git(linked, "config", "--worktree", "core.worktree", mode === "absolute" ? redirected : gitPath(path.relative(admin, redirected)));
53
+ assert.equal(realpathSync(git(linked, "rev-parse", "--show-toplevel")), realpathSync(redirected));
54
+ assert.equal(readFileSync(path.join(admin, "gitdir"), "utf8"), backlink);
55
+ await assert.rejects(fixtureCatalogue(workspace), /not a checkout root/);
56
+ });
57
+ }
58
+ it("rejects a linked checkout made bare through worktree config", async () => {
59
+ git(repo, "config", "extensions.worktreeConfig", "true");
60
+ git(linked, "config", "--worktree", "core.bare", "true");
61
+ assert.equal(git(linked, "rev-parse", "--is-bare-repository"), "true");
62
+ await assert.rejects(fixtureCatalogue(repo), /must be run in a work tree/);
63
+ });
64
+ it("checks a redirected main checkout when opened from an ordinary linked checkout", async () => {
65
+ git(repo, "config", "core.worktree", redirected);
66
+ assert.equal(realpathSync(git(repo, "rev-parse", "--show-toplevel")), realpathSync(redirected));
67
+ assert.equal(realpathSync(git(linked, "rev-parse", "--show-toplevel")), realpathSync(linked));
68
+ await assert.rejects(fixtureCatalogue(linked), /not a checkout root/);
69
+ });
70
+ for (const mode of ["worktree", "conditional shared", "conditional global"]) {
71
+ it(`uses Git's effective root semantics for ${mode} includes`, async () => {
72
+ const admin = git(linked, "rev-parse", "--absolute-git-dir");
73
+ const included = gitPath(path.join(temp, "root-config"));
74
+ writeFileSync(included, `[core]\n\tworktree = "${gitPath(redirected)}"\n`);
75
+ if (mode === "worktree") {
76
+ git(repo, "config", "extensions.worktreeConfig", "true");
77
+ git(linked, "config", "--worktree", "include.path", included);
78
+ }
79
+ else {
80
+ git(repo, "config", mode === "conditional global" ? "--global" : "--local", `includeIf.gitdir:${gitPath(admin)}.path`, included);
81
+ // The main checkout does not activate this condition.
82
+ assert.throws(() => git(repo, "config", "--get", "core.worktree"));
83
+ }
84
+ assert.equal(git(linked, "config", "--includes", "--get", "core.worktree"), gitPath(redirected));
85
+ // Git's early repository setup can ignore included core.worktree even
86
+ // though `config --get` reports it. Do not emulate that setup in JS.
87
+ const effectiveRoot = realpathSync(git(linked, "rev-parse", "--show-toplevel"));
88
+ if (effectiveRoot === realpathSync(linked)) {
89
+ assert.equal((await fixtureCatalogue(repo)).worktrees.length, 2);
90
+ }
91
+ else {
92
+ await assert.rejects(fixtureCatalogue(repo), /not a checkout root/);
93
+ }
94
+ });
95
+ }
96
+ it("accepts unredirected worktree configuration and still rejects substituted backlinks", async () => {
97
+ git(repo, "config", "extensions.worktreeConfig", "true");
98
+ git(linked, "config", "--worktree", "core.worktree", linked);
99
+ assert.equal((await fixtureCatalogue(repo)).worktrees.length, 2);
100
+ const other = path.join(temp, "other checkout");
101
+ git(repo, "worktree", "add", "--detach", other);
102
+ rmSync(path.join(linked, ".git"));
103
+ writeFileSync(path.join(linked, ".git"), readFileSync(path.join(other, ".git")));
104
+ await assert.rejects(fixtureCatalogue(repo), /not a checkout root/);
105
+ });
106
+ it("keeps the ordinary scan process count constant with benign includes and more checkouts", async () => {
107
+ const included = path.join(temp, "benign-config");
108
+ writeFileSync(included, "[core]\n\tbare = false\n[user]\n\tname = Test\n");
109
+ git(repo, "config", "include.path", gitPath(included));
110
+ const other = path.join(temp, "detached checkout");
111
+ git(repo, "worktree", "add", "--detach", other);
112
+ const trace = path.join(temp, "git-trace.json");
113
+ process.env.GIT_TRACE2_EVENT = gitPath(trace);
114
+ const scan = async (directories) => {
115
+ writeFileSync(trace, "");
116
+ const result = await fixtureCatalogue(repo, directories);
117
+ const starts = readFileSync(trace, "utf8").trim().split("\n").map(line => JSON.parse(line))
118
+ .filter(event => event.event === "start" && event.argv.some((arg) => arg.startsWith(temp)));
119
+ assert.equal(starts.filter(event => event.argv.includes("--porcelain")).length, 1);
120
+ assert.equal(starts.filter(event => event.argv.includes("--includes")).length, 1);
121
+ return { result, commands: starts.map(event => event.argv.slice(3)) };
122
+ };
123
+ const one = await scan([repo]);
124
+ const many = await scan([repo, linked, other]);
125
+ assert.equal(many.result.worktrees.length, 3);
126
+ assert.deepEqual(many.commands.sort(), one.commands.sort());
127
+ assert.equal(many.result.worktrees.find(entry => entry.branch === "feature")?.removable, true);
128
+ });
129
+ });
130
+ //# sourceMappingURL=git-worktree-config.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-worktree-config.test.js","sourceRoot":"","sources":["../../../src/workspaces/__tests__/git-worktree-config.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { execFileSync } from \"node:child_process\"\nimport { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport path from \"node:path\"\nimport { afterEach, beforeEach, describe, it } from \"node:test\"\nimport { fixtureCatalogue } from \"./native-worktree-fixture\"\n\nconst git = (directory: string, ...args: string[]) => execFileSync(\"git\", [\"-C\", directory, ...args], {\n encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"pipe\"],\n}).trim()\nconst gitPath = (directory: string) => directory.replaceAll(\"\\\\\", \"/\")\n\ndescribe(\"configured Git checkout roots\", () => {\n let temp: string\n let repo: string\n let linked: string\n let redirected: string\n let environment: Record<string, string | undefined>\n\n beforeEach(() => {\n environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.startsWith(\"GIT_\")))\n for (const key of Object.keys(environment)) delete process.env[key]\n temp = mkdtempSync(path.join(tmpdir(), \"codenomad-checkout-config-\"))\n process.env.GIT_CONFIG_NOSYSTEM = \"1\"\n process.env.GIT_CONFIG_GLOBAL = path.join(temp, \"empty-global-config\")\n writeFileSync(process.env.GIT_CONFIG_GLOBAL, \"\")\n repo = path.join(temp, \"main repo\")\n linked = path.join(temp, \"linked checkout\")\n redirected = path.join(temp, \"effective root\")\n git(temp, \"init\", \"--initial-branch=main\", repo)\n git(repo, \"-c\", \"user.name=Test\", \"-c\", \"user.email=test@example.com\", \"commit\", \"--allow-empty\", \"-m\", \"initial\")\n git(repo, \"worktree\", \"add\", \"-b\", \"feature\", linked)\n mkdirSync(redirected)\n })\n\n afterEach(() => {\n for (const key of Object.keys(process.env)) if (key.startsWith(\"GIT_\")) delete process.env[key]\n Object.assign(process.env, environment)\n rmSync(temp, { recursive: true, force: true })\n })\n\n for (const mode of [\"absolute\", \"relative\"] as const) {\n it(`rejects an administrative backlink redirected by ${mode} configuration`, async () => {\n mkdirSync(path.join(repo, \"nested\"))\n mkdirSync(path.join(linked, \"nested\"))\n const workspace = path.join(repo, \"nested\")\n const before = await fixtureCatalogue(workspace)\n assert.equal(before.worktrees.length, 2)\n assert.equal(before.worktrees.find(entry => entry.kind === \"worktree\")?.directory, path.join(realpathSync(linked), \"nested\"))\n const admin = git(linked, \"rev-parse\", \"--absolute-git-dir\")\n const backlink = readFileSync(path.join(admin, \"gitdir\"), \"utf8\")\n git(repo, \"config\", \"extensions.worktreeConfig\", \"true\")\n git(linked, \"config\", \"--worktree\", \"core.worktree\",\n mode === \"absolute\" ? redirected : gitPath(path.relative(admin, redirected)))\n assert.equal(realpathSync(git(linked, \"rev-parse\", \"--show-toplevel\")), realpathSync(redirected))\n assert.equal(readFileSync(path.join(admin, \"gitdir\"), \"utf8\"), backlink)\n await assert.rejects(fixtureCatalogue(workspace), /not a checkout root/)\n })\n }\n\n it(\"rejects a linked checkout made bare through worktree config\", async () => {\n git(repo, \"config\", \"extensions.worktreeConfig\", \"true\")\n git(linked, \"config\", \"--worktree\", \"core.bare\", \"true\")\n assert.equal(git(linked, \"rev-parse\", \"--is-bare-repository\"), \"true\")\n await assert.rejects(fixtureCatalogue(repo), /must be run in a work tree/)\n })\n\n it(\"checks a redirected main checkout when opened from an ordinary linked checkout\", async () => {\n git(repo, \"config\", \"core.worktree\", redirected)\n assert.equal(realpathSync(git(repo, \"rev-parse\", \"--show-toplevel\")), realpathSync(redirected))\n assert.equal(realpathSync(git(linked, \"rev-parse\", \"--show-toplevel\")), realpathSync(linked))\n await assert.rejects(fixtureCatalogue(linked), /not a checkout root/)\n })\n\n for (const mode of [\"worktree\", \"conditional shared\", \"conditional global\"] as const) {\n it(`uses Git's effective root semantics for ${mode} includes`, async () => {\n const admin = git(linked, \"rev-parse\", \"--absolute-git-dir\")\n const included = gitPath(path.join(temp, \"root-config\"))\n writeFileSync(included, `[core]\\n\\tworktree = \"${gitPath(redirected)}\"\\n`)\n if (mode === \"worktree\") {\n git(repo, \"config\", \"extensions.worktreeConfig\", \"true\")\n git(linked, \"config\", \"--worktree\", \"include.path\", included)\n } else {\n git(repo, \"config\", mode === \"conditional global\" ? \"--global\" : \"--local\",\n `includeIf.gitdir:${gitPath(admin)}.path`, included)\n // The main checkout does not activate this condition.\n assert.throws(() => git(repo, \"config\", \"--get\", \"core.worktree\"))\n }\n assert.equal(git(linked, \"config\", \"--includes\", \"--get\", \"core.worktree\"), gitPath(redirected))\n // Git's early repository setup can ignore included core.worktree even\n // though `config --get` reports it. Do not emulate that setup in JS.\n const effectiveRoot = realpathSync(git(linked, \"rev-parse\", \"--show-toplevel\"))\n if (effectiveRoot === realpathSync(linked)) {\n assert.equal((await fixtureCatalogue(repo)).worktrees.length, 2)\n } else {\n await assert.rejects(fixtureCatalogue(repo), /not a checkout root/)\n }\n })\n }\n\n it(\"accepts unredirected worktree configuration and still rejects substituted backlinks\", async () => {\n git(repo, \"config\", \"extensions.worktreeConfig\", \"true\")\n git(linked, \"config\", \"--worktree\", \"core.worktree\", linked)\n assert.equal((await fixtureCatalogue(repo)).worktrees.length, 2)\n const other = path.join(temp, \"other checkout\")\n git(repo, \"worktree\", \"add\", \"--detach\", other)\n rmSync(path.join(linked, \".git\"))\n writeFileSync(path.join(linked, \".git\"), readFileSync(path.join(other, \".git\")))\n await assert.rejects(fixtureCatalogue(repo), /not a checkout root/)\n })\n\n it(\"keeps the ordinary scan process count constant with benign includes and more checkouts\", async () => {\n const included = path.join(temp, \"benign-config\")\n writeFileSync(included, \"[core]\\n\\tbare = false\\n[user]\\n\\tname = Test\\n\")\n git(repo, \"config\", \"include.path\", gitPath(included))\n const other = path.join(temp, \"detached checkout\")\n git(repo, \"worktree\", \"add\", \"--detach\", other)\n const trace = path.join(temp, \"git-trace.json\")\n process.env.GIT_TRACE2_EVENT = gitPath(trace)\n const scan = async (directories: string[]) => {\n writeFileSync(trace, \"\")\n const result = await fixtureCatalogue(repo, directories)\n const starts = readFileSync(trace, \"utf8\").trim().split(\"\\n\").map(line => JSON.parse(line))\n .filter(event => event.event === \"start\" && event.argv.some((arg: string) => arg.startsWith(temp)))\n assert.equal(starts.filter(event => event.argv.includes(\"--porcelain\")).length, 1)\n assert.equal(starts.filter(event => event.argv.includes(\"--includes\")).length, 1)\n return { result, commands: starts.map(event => event.argv.slice(3)) }\n }\n const one = await scan([repo])\n const many = await scan([repo, linked, other])\n assert.equal(many.result.worktrees.length, 3)\n assert.deepEqual(many.commands.sort(), one.commands.sort())\n assert.equal(many.result.worktrees.find(entry => entry.branch === \"feature\")?.removable, true)\n })\n})\n"]}
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { execFileSync } from "node:child_process";
3
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { mkdirSync, mkdtempSync, readFileSync, 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";
@@ -27,5 +27,53 @@ describe("listWorktrees", () => {
27
27
  rmSync(temp, { recursive: true, force: true });
28
28
  }
29
29
  });
30
+ it("batches named and detached annotations while retaining native membership and physical repository ownership", async () => {
31
+ const temp = mkdtempSync(path.join(tmpdir(), "codenomad-worktree-annotations-"));
32
+ const repo = path.join(temp, "main repo");
33
+ const named = path.join(temp, "named checkout");
34
+ const detached = path.join(temp, "detached checkout");
35
+ const gitOnly = path.join(temp, "not in native inventory");
36
+ const foreign = path.join(temp, "independent clone");
37
+ const git = (...args) => execFileSync("git", args, { encoding: "utf8" }).trim();
38
+ try {
39
+ git("init", "--initial-branch=main", repo);
40
+ git("-C", repo, "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "--allow-empty", "-m", "initial");
41
+ const head = git("-C", repo, "rev-parse", "HEAD");
42
+ git("-C", repo, "worktree", "add", "-b", "feature/named", named);
43
+ git("-C", repo, "worktree", "add", "--detach", detached);
44
+ git("-C", repo, "worktree", "add", "--detach", gitOnly);
45
+ git("clone", "--local", repo, foreign);
46
+ const directories = [repo, named, detached, foreign];
47
+ const catalogue = await fixtureCatalogue(repo, directories);
48
+ assert.equal(catalogue.worktrees.length, 3);
49
+ const entry = catalogue.worktrees.find(item => item.directory === realpathSync(named));
50
+ assert.equal(entry.branch, "feature/named");
51
+ assert.equal(entry.head, head);
52
+ assert.equal(entry.removable, true);
53
+ const detachedEntry = catalogue.worktrees.find(item => item.directory === realpathSync(detached));
54
+ assert.equal(detachedEntry.branch, undefined);
55
+ assert.equal(detachedEntry.head, head);
56
+ assert.match(detachedEntry.label, /detached checkout @ /);
57
+ assert.equal(catalogue.worktrees[0].removable, false);
58
+ const pointer = readFileSync(path.join(named, ".git"), "utf8");
59
+ try {
60
+ // A registered path can be replaced. Its current Git identity must still
61
+ // belong to this repository; a stale registration alone is insufficient.
62
+ rmSync(path.join(named, ".git"));
63
+ writeFileSync(path.join(named, ".git"), `gitdir: ${path.join(foreign, ".git")}\n`);
64
+ const replaced = await fixtureCatalogue(repo, directories);
65
+ assert.equal(replaced.worktrees.some(item => item.directory === realpathSync(named)), false);
66
+ writeFileSync(path.join(named, ".git"), readFileSync(path.join(detached, ".git")));
67
+ await assert.rejects(fixtureCatalogue(repo, directories), /not a checkout root/);
68
+ }
69
+ finally {
70
+ rmSync(path.join(named, ".git"), { force: true });
71
+ writeFileSync(path.join(named, ".git"), pointer);
72
+ }
73
+ }
74
+ finally {
75
+ rmSync(temp, { recursive: true, force: true });
76
+ }
77
+ });
30
78
  });
31
79
  //# sourceMappingURL=git-worktrees.test.js.map