@neuralnomads/codenomad-dev 0.20.0-dev-20260922-f3834dd2 → 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.
- package/dist/automation/developer-inspection-targets.js +25 -0
- package/dist/automation/developer-inspection-targets.js.map +1 -0
- package/dist/developer-cdp.js +5 -4
- package/dist/developer-cdp.js.map +1 -1
- package/dist/developer-cdp.test.js +18 -0
- package/dist/developer-cdp.test.js.map +1 -1
- package/dist/server/routes/automation-plugin.js +28 -15
- package/dist/server/routes/automation-plugin.js.map +1 -1
- package/dist/server/routes/automation-plugin.test.js +10 -6
- package/dist/server/routes/automation-plugin.test.js.map +1 -1
- package/dist/workspaces/__tests__/git-common-directory.test.js +86 -0
- package/dist/workspaces/__tests__/git-common-directory.test.js.map +1 -0
- package/dist/workspaces/git-common-directory.js +18 -0
- package/dist/workspaces/git-common-directory.js.map +1 -0
- package/dist/workspaces/git-process.js +80 -0
- package/dist/workspaces/git-process.js.map +1 -0
- package/dist/workspaces/git-worktrees.js +3 -9
- package/dist/workspaces/git-worktrees.js.map +1 -1
- package/package.json +1 -1
- package/public/assets/{App-DljM3SwE.js → App-BSkmbNUC.js} +15 -15
- package/public/assets/App-BSkmbNUC.js.map +1 -0
- package/public/assets/{FilesTab-CP3GICgc.js → FilesTab-CznSFRW_.js} +3 -3
- package/public/assets/{FilesTab-CP3GICgc.js.map → FilesTab-CznSFRW_.js.map} +1 -1
- package/public/assets/{GitChangesTab-DC5uIAYI.js → GitChangesTab-DLtyzpX0.js} +3 -3
- package/public/assets/{GitChangesTab-DC5uIAYI.js.map → GitChangesTab-DLtyzpX0.js.map} +1 -1
- package/public/assets/{SplitFilePanel-D4kbA67I.js → SplitFilePanel-BHkyIER4.js} +2 -2
- package/public/assets/{SplitFilePanel-D4kbA67I.js.map → SplitFilePanel-BHkyIER4.js.map} +1 -1
- package/public/assets/{StatusTab-B7-e7Jym.js → StatusTab-BB3L4UEF.js} +2 -2
- package/public/assets/{StatusTab-B7-e7Jym.js.map → StatusTab-BB3L4UEF.js.map} +1 -1
- package/public/assets/{align-justify-t6BvuuGq.js → align-justify-BGcr8Y3G.js} +2 -2
- package/public/assets/{align-justify-t6BvuuGq.js.map → align-justify-BGcr8Y3G.js.map} +1 -1
- package/public/assets/{bundle-full-LA4LRJvC.js → bundle-full-CwHbX2Tz.js} +2 -2
- package/public/assets/{bundle-full-LA4LRJvC.js.map → bundle-full-CwHbX2Tz.js.map} +1 -1
- package/public/assets/{client-state-BELVPVQK.js → client-state-CjVIIkWh.js} +2 -2
- package/public/assets/{client-state-BELVPVQK.js.map → client-state-CjVIIkWh.js.map} +1 -1
- package/public/assets/{diff-viewer-n0toXKZF.js → diff-viewer-FNBCogHl.js} +2 -2
- package/public/assets/{diff-viewer-n0toXKZF.js.map → diff-viewer-FNBCogHl.js.map} +1 -1
- package/public/assets/{index-BT4hx4H9.js → index-AeE49W2c.js} +2 -2
- package/public/assets/{index-BT4hx4H9.js.map → index-AeE49W2c.js.map} +1 -1
- package/public/assets/{index-CyxtFA9R.js → index-B875QQLi.js} +2 -2
- package/public/assets/{index-CyxtFA9R.js.map → index-B875QQLi.js.map} +1 -1
- package/public/assets/{index-CYrgGxN5.js → index-BhInW6hR.js} +3 -3
- package/public/assets/{index-CYrgGxN5.js.map → index-BhInW6hR.js.map} +1 -1
- package/public/assets/index-C2jBtFkI.js +2 -0
- package/public/assets/{index-mSrN15cu.js.map → index-C2jBtFkI.js.map} +1 -1
- package/public/assets/{index-CQPkLT1b.js → index-C8zCyB6Z.js} +2 -2
- package/public/assets/{index-CQPkLT1b.js.map → index-C8zCyB6Z.js.map} +1 -1
- package/public/assets/{index-CjsBXyUr.js → index-C9vJvaFa.js} +2 -2
- package/public/assets/{index-CjsBXyUr.js.map → index-C9vJvaFa.js.map} +1 -1
- package/public/assets/{index-sYGWeU2B.js → index-CLdQSSfU.js} +2 -2
- package/public/assets/{index-sYGWeU2B.js.map → index-CLdQSSfU.js.map} +1 -1
- package/public/assets/{index-mN9OrWRM.js → index-CLgpBTF6.js} +2 -2
- package/public/assets/{index-mN9OrWRM.js.map → index-CLgpBTF6.js.map} +1 -1
- package/public/assets/{index-BRo0m4HH.js → index-CaYFZAN4.js} +2 -2
- package/public/assets/{index-BRo0m4HH.js.map → index-CaYFZAN4.js.map} +1 -1
- package/public/assets/{index-DjwZf3Yh.js → index-ChudAs0f.js} +2 -2
- package/public/assets/{index-DjwZf3Yh.js.map → index-ChudAs0f.js.map} +1 -1
- package/public/assets/{index-DINKO8rw.js → index-D292-iMl.js} +2 -2
- package/public/assets/{index-DINKO8rw.js.map → index-D292-iMl.js.map} +1 -1
- package/public/assets/{index-fkx7BwtE.js → index-Dk3HFhZs.js} +2 -2
- package/public/assets/{index-fkx7BwtE.js.map → index-Dk3HFhZs.js.map} +1 -1
- package/public/assets/{loading-D4az9Vmg.js → loading-D8BcUQaM.js} +2 -2
- package/public/assets/{loading-D4az9Vmg.js.map → loading-D8BcUQaM.js.map} +1 -1
- package/public/assets/main-DRbjgAo8.js +42 -0
- package/public/assets/main-DRbjgAo8.js.map +1 -0
- package/public/assets/{markdown-Dtp2_t2x.js → markdown-CRPEbAqL.js} +4 -4
- package/public/assets/{markdown-Dtp2_t2x.js.map → markdown-CRPEbAqL.js.map} +1 -1
- package/public/assets/{monaco-viewer-ChA_iFmw.js → monaco-viewer-CG0aPZ-r.js} +2 -2
- package/public/assets/{monaco-viewer-ChA_iFmw.js.map → monaco-viewer-CG0aPZ-r.js.map} +1 -1
- package/public/assets/{startup-state-settings-card-CwqMz9iE.js → startup-state-settings-card-CHqS2Pmg.js} +2 -2
- package/public/assets/{startup-state-settings-card-CwqMz9iE.js.map → startup-state-settings-card-CHqS2Pmg.js.map} +1 -1
- package/public/assets/{terminal-square-5uHstmTD.js → terminal-square-DhmNJqNG.js} +2 -2
- package/public/assets/{terminal-square-5uHstmTD.js.map → terminal-square-DhmNJqNG.js.map} +1 -1
- package/public/assets/{todo-rWdKPGRD.js → todo-DXJ5kP_Z.js} +2 -2
- package/public/assets/{todo-rWdKPGRD.js.map → todo-DXJ5kP_Z.js.map} +1 -1
- package/public/assets/{tool-call-B2lbL8ZE.js → tool-call-Bjo9DalZ.js} +4 -4
- package/public/assets/{tool-call-B2lbL8ZE.js.map → tool-call-Bjo9DalZ.js.map} +1 -1
- package/public/assets/{unified-picker-CyMBKInh.js → unified-picker-CxrTM_tY.js} +2 -2
- package/public/assets/{unified-picker-CyMBKInh.js.map → unified-picker-CxrTM_tY.js.map} +1 -1
- package/public/assets/{wrap-text-CSz4eer6.js → wrap-text-BznZeD4J.js} +2 -2
- package/public/assets/{wrap-text-CSz4eer6.js.map → wrap-text-BznZeD4J.js.map} +1 -1
- package/public/index.html +3 -3
- package/public/loading.html +3 -3
- package/public/sw.js +1 -1
- package/public/sw.js.map +1 -1
- package/public/assets/App-DljM3SwE.js.map +0 -1
- package/public/assets/index-mSrN15cu.js +0 -2
- package/public/assets/main-pe2tFxvx.js +0 -42
- package/public/assets/main-pe2tFxvx.js.map +0 -1
|
@@ -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("
|
|
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
|
-
|
|
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,
|
|
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":"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","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,18 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { runWorktreeGit } from "./git-process";
|
|
3
|
+
const pending = new Map();
|
|
4
|
+
/** Share only in-flight negative-preflight reads. Git remains responsible for
|
|
5
|
+
* discovery/configuration; completed identities never become authority caches. */
|
|
6
|
+
export function readGitCommonDirectory(directory) {
|
|
7
|
+
const existing = pending.get(directory);
|
|
8
|
+
if (existing)
|
|
9
|
+
return existing;
|
|
10
|
+
const task = runWorktreeGit(directory, ["rev-parse", "--path-format=absolute", "--git-common-dir"], 10_000)
|
|
11
|
+
.then(directory => realpath(directory)).finally(() => {
|
|
12
|
+
if (pending.get(directory) === task)
|
|
13
|
+
pending.delete(directory);
|
|
14
|
+
});
|
|
15
|
+
pending.set(directory, task);
|
|
16
|
+
return task;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=git-common-directory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-common-directory.js","sourceRoot":"","sources":["../../src/workspaces/git-common-directory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAE9C,MAAM,OAAO,GAAG,IAAI,GAAG,EAA2B,CAAA;AAElD;kFACkF;AAClF,MAAM,UAAU,sBAAsB,CAAC,SAAiB;IACtD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IACvC,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAA;IAC7B,MAAM,IAAI,GAAG,cAAc,CAAC,SAAS,EAAE,CAAC,WAAW,EAAE,wBAAwB,EAAE,kBAAkB,CAAC,EAAE,MAAM,CAAC;SACxG,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE;QACnD,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,IAAI;YAAE,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IAChE,CAAC,CAAC,CAAA;IACJ,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;IAC5B,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["import { realpath } from \"node:fs/promises\"\nimport { runWorktreeGit } from \"./git-process\"\n\nconst pending = new Map<string, Promise<string>>()\n\n/** Share only in-flight negative-preflight reads. Git remains responsible for\n * discovery/configuration; completed identities never become authority caches. */\nexport function readGitCommonDirectory(directory: string): Promise<string> {\n const existing = pending.get(directory)\n if (existing) return existing\n const task = runWorktreeGit(directory, [\"rev-parse\", \"--path-format=absolute\", \"--git-common-dir\"], 10_000)\n .then(directory => realpath(directory)).finally(() => {\n if (pending.get(directory) === task) pending.delete(directory)\n })\n pending.set(directory, task)\n return task\n}\n"]}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
// Even asynchronous execFile spends synchronous time creating a Windows process.
|
|
3
|
+
// Keep that work off the HTTP/SSE thread. The worker is self-contained so the same
|
|
4
|
+
// entry works in compiled desktop resources and TypeScript test runners.
|
|
5
|
+
function workerMain() {
|
|
6
|
+
const { parentPort } = require("node:worker_threads");
|
|
7
|
+
const { execFile } = require("node:child_process");
|
|
8
|
+
const queue = [];
|
|
9
|
+
let running = 0;
|
|
10
|
+
const scheduler = { pump() {
|
|
11
|
+
while (running < 2 && queue.length) {
|
|
12
|
+
const request = queue.shift();
|
|
13
|
+
running += 1;
|
|
14
|
+
execFile("git", ["-C", request.directory, ...request.args], {
|
|
15
|
+
encoding: "utf8", windowsHide: true, maxBuffer: 1024 * 1024,
|
|
16
|
+
env: request.env, timeout: request.timeout,
|
|
17
|
+
}, (error, stdout, stderr) => {
|
|
18
|
+
parentPort.postMessage({ id: request.id, stdout, stderr,
|
|
19
|
+
...(error ? { error: { message: error.message, code: error.code } } : {}),
|
|
20
|
+
});
|
|
21
|
+
running -= 1;
|
|
22
|
+
scheduler.pump();
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
} };
|
|
26
|
+
parentPort.on("message", (request) => { queue.push(request); scheduler.pump(); });
|
|
27
|
+
}
|
|
28
|
+
let worker;
|
|
29
|
+
let sequence = 0;
|
|
30
|
+
const pending = new Map();
|
|
31
|
+
function getWorker() {
|
|
32
|
+
if (worker)
|
|
33
|
+
return worker;
|
|
34
|
+
const created = new Worker(`(${workerMain.toString()})()`, { eval: true, execArgv: [] });
|
|
35
|
+
worker = created;
|
|
36
|
+
const failed = (error) => {
|
|
37
|
+
if (worker !== created)
|
|
38
|
+
return;
|
|
39
|
+
worker = undefined;
|
|
40
|
+
for (const task of pending.values())
|
|
41
|
+
task.reject(error);
|
|
42
|
+
pending.clear();
|
|
43
|
+
};
|
|
44
|
+
created.on("error", failed);
|
|
45
|
+
created.on("exit", code => failed(new Error(`Git process worker exited (${code})`)));
|
|
46
|
+
created.on("message", (response) => {
|
|
47
|
+
const task = pending.get(response.id);
|
|
48
|
+
if (!task)
|
|
49
|
+
return;
|
|
50
|
+
pending.delete(response.id);
|
|
51
|
+
if (response.error)
|
|
52
|
+
task.reject(Object.assign(new Error(response.error.message), {
|
|
53
|
+
code: response.error.code, stdout: response.stdout, stderr: response.stderr,
|
|
54
|
+
}));
|
|
55
|
+
else
|
|
56
|
+
task.resolve(response.stdout.replace(/\r?\n$/, ""));
|
|
57
|
+
if (pending.size === 0)
|
|
58
|
+
created.unref();
|
|
59
|
+
});
|
|
60
|
+
created.unref();
|
|
61
|
+
return created;
|
|
62
|
+
}
|
|
63
|
+
export function runWorktreeGit(directory, args, timeout) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const current = getWorker();
|
|
66
|
+
const id = ++sequence;
|
|
67
|
+
pending.set(id, { resolve, reject });
|
|
68
|
+
current.ref();
|
|
69
|
+
try {
|
|
70
|
+
current.postMessage({ id, directory, args, env: { ...process.env }, timeout });
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
pending.delete(id);
|
|
74
|
+
if (pending.size === 0)
|
|
75
|
+
current.unref();
|
|
76
|
+
reject(error);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=git-process.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-process.js","sourceRoot":"","sources":["../../src/workspaces/git-process.ts"],"names":[],"mappings":"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","sourcesContent":["import { Worker } from \"node:worker_threads\"\n\ninterface GitRequest { id: number; directory: string; args: string[]; env: NodeJS.ProcessEnv; timeout?: number }\ninterface GitResponse { id: number; stdout: string; stderr: string; error?: { message: string; code?: string | number | null } }\n\n// Even asynchronous execFile spends synchronous time creating a Windows process.\n// Keep that work off the HTTP/SSE thread. The worker is self-contained so the same\n// entry works in compiled desktop resources and TypeScript test runners.\nfunction workerMain() {\n const { parentPort } = require(\"node:worker_threads\") as typeof import(\"node:worker_threads\")\n const { execFile } = require(\"node:child_process\") as typeof import(\"node:child_process\")\n const queue: GitRequest[] = []\n let running = 0\n const scheduler = { pump() {\n while (running < 2 && queue.length) {\n const request = queue.shift()!\n running += 1\n execFile(\"git\", [\"-C\", request.directory, ...request.args], {\n encoding: \"utf8\", windowsHide: true, maxBuffer: 1024 * 1024,\n env: request.env, timeout: request.timeout,\n }, (error, stdout, stderr) => {\n parentPort!.postMessage({ id: request.id, stdout, stderr,\n ...(error ? { error: { message: error.message, code: error.code } } : {}),\n } satisfies GitResponse)\n running -= 1\n scheduler.pump()\n })\n }\n } }\n parentPort!.on(\"message\", (request: GitRequest) => { queue.push(request); scheduler.pump() })\n}\n\nlet worker: Worker | undefined\nlet sequence = 0\nconst pending = new Map<number, { resolve: (stdout: string) => void; reject: (error: Error) => void }>()\n\nfunction getWorker(): Worker {\n if (worker) return worker\n const created = new Worker(`(${workerMain.toString()})()`, { eval: true, execArgv: [] })\n worker = created\n const failed = (error: Error) => {\n if (worker !== created) return\n worker = undefined\n for (const task of pending.values()) task.reject(error)\n pending.clear()\n }\n created.on(\"error\", failed)\n created.on(\"exit\", code => failed(new Error(`Git process worker exited (${code})`)))\n created.on(\"message\", (response: GitResponse) => {\n const task = pending.get(response.id)\n if (!task) return\n pending.delete(response.id)\n if (response.error) task.reject(Object.assign(new Error(response.error.message), {\n code: response.error.code, stdout: response.stdout, stderr: response.stderr,\n }))\n else task.resolve(response.stdout.replace(/\\r?\\n$/, \"\"))\n if (pending.size === 0) created.unref()\n })\n created.unref()\n return created\n}\n\nexport function runWorktreeGit(directory: string, args: string[], timeout?: number): Promise<string> {\n return new Promise((resolve, reject) => {\n const current = getWorker()\n const id = ++sequence\n pending.set(id, { resolve, reject })\n current.ref()\n try {\n current.postMessage({ id, directory, args, env: { ...process.env }, timeout } satisfies GitRequest)\n } catch (error) {\n pending.delete(id)\n if (pending.size === 0) current.unref()\n reject(error)\n }\n })\n}\n"]}
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { execFile } from "node:child_process";
|
|
3
|
-
import { promisify } from "node:util";
|
|
4
2
|
import { readFile, realpath, stat } from "node:fs/promises";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const { stdout } = await execute("git", ["-C", directory, ...args], { windowsHide: true, maxBuffer: 1024 * 1024 });
|
|
8
|
-
return stdout.replace(/\r?\n$/, "");
|
|
9
|
-
}
|
|
3
|
+
import { readGitCommonDirectory } from "./git-common-directory";
|
|
4
|
+
import { runWorktreeGit as git } from "./git-process";
|
|
10
5
|
export async function resolveRepoRoot(folder, logger) {
|
|
11
6
|
try {
|
|
12
7
|
return { repoRoot: await git(folder, ["rev-parse", "--show-toplevel"]), isGitRepo: true };
|
|
@@ -25,9 +20,8 @@ export async function isGitAvailable(folder) {
|
|
|
25
20
|
// even when independent clones share OpenCode's project ID. This is only a
|
|
26
21
|
// negative preflight; a match still requires the native worktree inventory.
|
|
27
22
|
export async function sharesGitCommonDirectory(left, right) {
|
|
28
|
-
const common = async (directory) => realpath(await git(directory, ["rev-parse", "--path-format=absolute", "--git-common-dir"]));
|
|
29
23
|
try {
|
|
30
|
-
const [a, b] = await Promise.all([
|
|
24
|
+
const [a, b] = await Promise.all([readGitCommonDirectory(left), readGitCommonDirectory(right)]);
|
|
31
25
|
return a === b;
|
|
32
26
|
}
|
|
33
27
|
catch {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-worktrees.js","sourceRoot":"","sources":["../../src/workspaces/git-worktrees.ts"],"names":[],"mappings":"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","sourcesContent":["import path from \"node:path\"\nimport { execFile } from \"node:child_process\"\nimport { promisify } from \"node:util\"\nimport { readFile, realpath, stat } from \"node:fs/promises\"\n\nexport interface LogLike {\n debug?: (obj: any, msg?: string) => void\n warn?: (obj: any, msg?: string) => void\n}\n\nconst execute = promisify(execFile)\nasync function git(directory: string, args: string[]): Promise<string> {\n const { stdout } = await execute(\"git\", [\"-C\", directory, ...args], { windowsHide: true, maxBuffer: 1024 * 1024 })\n return stdout.replace(/\\r?\\n$/, \"\")\n}\n\nexport async function resolveRepoRoot(folder: string, logger?: LogLike): Promise<{ repoRoot: string; isGitRepo: boolean }> {\n try {\n return { repoRoot: await git(folder, [\"rev-parse\", \"--show-toplevel\"]), isGitRepo: true }\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") throw new Error(\"Git is not installed or not available in PATH\")\n logger?.debug?.({ folder, err: error }, \"Folder is not a Git repository; using workspace folder as root\")\n return { repoRoot: folder, isGitRepo: false }\n }\n}\n\nexport async function isGitAvailable(folder: string): Promise<boolean> {\n return git(folder, [\"--version\"]).then(() => true, error => error.code !== \"ENOENT\")\n}\n\n// A different physical common directory cannot belong to this local repository,\n// even when independent clones share OpenCode's project ID. This is only a\n// negative preflight; a match still requires the native worktree inventory.\nexport async function sharesGitCommonDirectory(left: string, right: string): Promise<boolean> {\n const common = async (directory: string) => realpath(await git(directory, [\"rev-parse\", \"--path-format=absolute\", \"--git-common-dir\"]))\n try {\n const [a, b] = await Promise.all([common(left), common(right)])\n return a === b\n } catch { return false }\n}\n\n// Git annotations and branch policy, not another worktree registry. Native\n// OpenCode owns discovery, creation and removal of the physical checkouts.\nexport async function readCheckout(directory: string) {\n const [paths, head, branch] = await Promise.all([\n git(directory, [\"rev-parse\", \"--show-toplevel\", \"--path-format=absolute\", \"--git-common-dir\", \"--absolute-git-dir\"]),\n git(directory, [\"rev-parse\", \"--verify\", \"HEAD\"]).catch(() => undefined),\n git(directory, [\"symbolic-ref\", \"--quiet\", \"--short\", \"HEAD\"]).catch(() => undefined),\n ])\n const [root, common, gitDirectory] = paths.split(/\\r?\\n/)\n return { root: await realpath(root), common: await realpath(common), gitDirectory: await realpath(gitDirectory), head, branch }\n}\n\n// One Git snapshot supplies HEAD/branch annotations for every local checkout.\n// Native OpenCode remains the inventory authority; callers intersect its entries\n// with these registrations and verify each checkout's physical common directory.\nexport async function readWorktreeAnnotations(directory: string) {\n const output = await git(directory, [\"worktree\", \"list\", \"--porcelain\", \"-z\"])\n return output.split(\"\\0\\0\").filter(Boolean).map(record => {\n const fields = record.split(\"\\0\")\n const root = fields.find(field => field.startsWith(\"worktree \"))?.slice(9)\n if (!root) throw new Error(\"Git returned a worktree without a directory\")\n const head = fields.find(field => field.startsWith(\"HEAD \"))?.slice(5)\n const branch = fields.find(field => field.startsWith(\"branch refs/heads/\"))?.slice(18)\n return { root, head, branch }\n })\n}\n\nexport async function readCheckoutIdentity(directory: string) {\n const entry = path.join(directory, \".git\")\n const info = await stat(entry)\n let gitDirectory = entry\n if (!info.isDirectory()) {\n const pointer = (await readFile(entry, \"utf8\")).replace(/\\r?\\n$/, \"\")\n if (!pointer.startsWith(\"gitdir: \")) throw new Error(\"Invalid worktree Git directory\")\n gitDirectory = path.resolve(directory, pointer.slice(8))\n }\n const common = await readFile(path.join(gitDirectory, \"commondir\"), \"utf8\").catch(error => {\n if (error.code === \"ENOENT\") return \".\"\n throw error\n })\n const resolvedGit = await realpath(gitDirectory)\n const resolvedCommon = await realpath(path.resolve(gitDirectory, common.replace(/\\r?\\n$/, \"\")))\n const backlink = resolvedGit === resolvedCommon ? undefined : (await readFile(path.join(resolvedGit, \"gitdir\"), \"utf8\")).replace(/\\r?\\n$/, \"\")\n return {\n gitDirectory: resolvedGit,\n common: resolvedCommon,\n root: backlink ? await realpath(path.dirname(path.resolve(resolvedGit, backlink))) : undefined,\n }\n}\n\nexport async function createCheckoutRootVerifier(directory: string) {\n // Read shared/system/global configuration once, letting Git expand includes.\n // Conditional includes and config.worktree can differ for each checkout, so\n // their presence requires Git's own resolution even if this checkout is plain.\n const config = await git(directory, [\"config\", \"--null\", \"--list\", \"--includes\"])\n const configuredRoot = config.split(\"\\0\").some(entry => {\n const [key, value] = entry.split(\"\\n\", 2)\n return key === \"core.worktree\" || key === \"extensions.worktreeconfig\" || key.startsWith(\"includeif.\")\n || (key === \"core.bare\" && value !== \"false\")\n }) || [\"GIT_DIR\", \"GIT_COMMON_DIR\", \"GIT_WORK_TREE\", \"GIT_IMPLICIT_WORK_TREE\"].some(key => process.env[key] !== undefined)\n\n return async (checkout: string) => {\n if (!configuredRoot) return\n // Administrative gitdir backlinks describe registration, not necessarily\n // the effective worktree. Keep annotations batched; only exceptional config\n // needs this single extra process (which also rejects bare checkouts).\n const root = await git(checkout, [\"rev-parse\", \"--show-toplevel\"])\n if (await realpath(root) !== await realpath(checkout)) throw new Error(\"Native worktree entry is not a checkout root\")\n }\n}\n\nexport function isValidWorktreeSlug(slug: string): boolean {\n return Boolean(slug.trim() && slug.length <= 200 && !/[\\x00-\\x1F\\x7F]/.test(slug))\n}\n\nexport async function prepareWorktreeBranch(directory: string, branch: string): Promise<{ existing: boolean; revision: string }> {\n await git(directory, [\"check-ref-format\", \"--branch\", branch])\n const revision = await git(directory, [\"rev-parse\", \"--verify\", `refs/heads/${branch}`]).catch(() => undefined)\n return { existing: Boolean(revision), revision: revision ?? await git(directory, [\"rev-parse\", \"--verify\", \"HEAD\"]) }\n}\n\nexport async function attachWorktreeBranch(directory: string, branch: string, existing: boolean): Promise<void> {\n // Never -B/--force: preserve branch refs and Git's already-checked-out guard.\n await git(directory, [\"switch\", ...(existing ? [] : [\"-c\"]), branch])\n}\n\nexport async function gitExcludePath(directory: string): Promise<string> {\n return path.resolve(directory, await git(directory, [\"rev-parse\", \"--git-path\", \"info/exclude\"]))\n}\n"]}
|
|
1
|
+
{"version":3,"file":"git-worktrees.js","sourceRoot":"","sources":["../../src/workspaces/git-worktrees.ts"],"names":[],"mappings":"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","sourcesContent":["import path from \"node:path\"\nimport { readFile, realpath, stat } from \"node:fs/promises\"\nimport { readGitCommonDirectory } from \"./git-common-directory\"\nimport { runWorktreeGit as git } from \"./git-process\"\n\nexport interface LogLike {\n debug?: (obj: any, msg?: string) => void\n warn?: (obj: any, msg?: string) => void\n}\n\nexport async function resolveRepoRoot(folder: string, logger?: LogLike): Promise<{ repoRoot: string; isGitRepo: boolean }> {\n try {\n return { repoRoot: await git(folder, [\"rev-parse\", \"--show-toplevel\"]), isGitRepo: true }\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") throw new Error(\"Git is not installed or not available in PATH\")\n logger?.debug?.({ folder, err: error }, \"Folder is not a Git repository; using workspace folder as root\")\n return { repoRoot: folder, isGitRepo: false }\n }\n}\n\nexport async function isGitAvailable(folder: string): Promise<boolean> {\n return git(folder, [\"--version\"]).then(() => true, error => error.code !== \"ENOENT\")\n}\n\n// A different physical common directory cannot belong to this local repository,\n// even when independent clones share OpenCode's project ID. This is only a\n// negative preflight; a match still requires the native worktree inventory.\nexport async function sharesGitCommonDirectory(left: string, right: string): Promise<boolean> {\n try {\n const [a, b] = await Promise.all([readGitCommonDirectory(left), readGitCommonDirectory(right)])\n return a === b\n } catch { return false }\n}\n\n// Git annotations and branch policy, not another worktree registry. Native\n// OpenCode owns discovery, creation and removal of the physical checkouts.\nexport async function readCheckout(directory: string) {\n const [paths, head, branch] = await Promise.all([\n git(directory, [\"rev-parse\", \"--show-toplevel\", \"--path-format=absolute\", \"--git-common-dir\", \"--absolute-git-dir\"]),\n git(directory, [\"rev-parse\", \"--verify\", \"HEAD\"]).catch(() => undefined),\n git(directory, [\"symbolic-ref\", \"--quiet\", \"--short\", \"HEAD\"]).catch(() => undefined),\n ])\n const [root, common, gitDirectory] = paths.split(/\\r?\\n/)\n return { root: await realpath(root), common: await realpath(common), gitDirectory: await realpath(gitDirectory), head, branch }\n}\n\n// One Git snapshot supplies HEAD/branch annotations for every local checkout.\n// Native OpenCode remains the inventory authority; callers intersect its entries\n// with these registrations and verify each checkout's physical common directory.\nexport async function readWorktreeAnnotations(directory: string) {\n const output = await git(directory, [\"worktree\", \"list\", \"--porcelain\", \"-z\"])\n return output.split(\"\\0\\0\").filter(Boolean).map(record => {\n const fields = record.split(\"\\0\")\n const root = fields.find(field => field.startsWith(\"worktree \"))?.slice(9)\n if (!root) throw new Error(\"Git returned a worktree without a directory\")\n const head = fields.find(field => field.startsWith(\"HEAD \"))?.slice(5)\n const branch = fields.find(field => field.startsWith(\"branch refs/heads/\"))?.slice(18)\n return { root, head, branch }\n })\n}\n\nexport async function readCheckoutIdentity(directory: string) {\n const entry = path.join(directory, \".git\")\n const info = await stat(entry)\n let gitDirectory = entry\n if (!info.isDirectory()) {\n const pointer = (await readFile(entry, \"utf8\")).replace(/\\r?\\n$/, \"\")\n if (!pointer.startsWith(\"gitdir: \")) throw new Error(\"Invalid worktree Git directory\")\n gitDirectory = path.resolve(directory, pointer.slice(8))\n }\n const common = await readFile(path.join(gitDirectory, \"commondir\"), \"utf8\").catch(error => {\n if (error.code === \"ENOENT\") return \".\"\n throw error\n })\n const resolvedGit = await realpath(gitDirectory)\n const resolvedCommon = await realpath(path.resolve(gitDirectory, common.replace(/\\r?\\n$/, \"\")))\n const backlink = resolvedGit === resolvedCommon ? undefined : (await readFile(path.join(resolvedGit, \"gitdir\"), \"utf8\")).replace(/\\r?\\n$/, \"\")\n return {\n gitDirectory: resolvedGit,\n common: resolvedCommon,\n root: backlink ? await realpath(path.dirname(path.resolve(resolvedGit, backlink))) : undefined,\n }\n}\n\nexport async function createCheckoutRootVerifier(directory: string) {\n // Read shared/system/global configuration once, letting Git expand includes.\n // Conditional includes and config.worktree can differ for each checkout, so\n // their presence requires Git's own resolution even if this checkout is plain.\n const config = await git(directory, [\"config\", \"--null\", \"--list\", \"--includes\"])\n const configuredRoot = config.split(\"\\0\").some(entry => {\n const [key, value] = entry.split(\"\\n\", 2)\n return key === \"core.worktree\" || key === \"extensions.worktreeconfig\" || key.startsWith(\"includeif.\")\n || (key === \"core.bare\" && value !== \"false\")\n }) || [\"GIT_DIR\", \"GIT_COMMON_DIR\", \"GIT_WORK_TREE\", \"GIT_IMPLICIT_WORK_TREE\"].some(key => process.env[key] !== undefined)\n\n return async (checkout: string) => {\n if (!configuredRoot) return\n // Administrative gitdir backlinks describe registration, not necessarily\n // the effective worktree. Keep annotations batched; only exceptional config\n // needs this single extra process (which also rejects bare checkouts).\n const root = await git(checkout, [\"rev-parse\", \"--show-toplevel\"])\n if (await realpath(root) !== await realpath(checkout)) throw new Error(\"Native worktree entry is not a checkout root\")\n }\n}\n\nexport function isValidWorktreeSlug(slug: string): boolean {\n return Boolean(slug.trim() && slug.length <= 200 && !/[\\x00-\\x1F\\x7F]/.test(slug))\n}\n\nexport async function prepareWorktreeBranch(directory: string, branch: string): Promise<{ existing: boolean; revision: string }> {\n await git(directory, [\"check-ref-format\", \"--branch\", branch])\n const revision = await git(directory, [\"rev-parse\", \"--verify\", `refs/heads/${branch}`]).catch(() => undefined)\n return { existing: Boolean(revision), revision: revision ?? await git(directory, [\"rev-parse\", \"--verify\", \"HEAD\"]) }\n}\n\nexport async function attachWorktreeBranch(directory: string, branch: string, existing: boolean): Promise<void> {\n // Never -B/--force: preserve branch refs and Git's already-checked-out guard.\n await git(directory, [\"switch\", ...(existing ? [] : [\"-c\"]), branch])\n}\n\nexport async function gitExcludePath(directory: string): Promise<string> {\n return path.resolve(directory, await git(directory, [\"rev-parse\", \"--git-path\", \"info/exclude\"]))\n}\n"]}
|