@agimon-ai/doompi-git 0.0.1-alpha.3 → 0.0.1-alpha.5
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/adapters/hub/hubClient.cjs +1 -1
- package/dist/adapters/hub/hubClient.cjs.map +1 -1
- package/dist/adapters/hub/hubClient.mjs +1 -1
- package/dist/adapters/hub/hubClient.mjs.map +1 -1
- package/dist/adapters/pi/extensions/runWorktreeTool.cjs +2 -2
- package/dist/adapters/pi/extensions/runWorktreeTool.cjs.map +1 -1
- package/dist/adapters/pi/extensions/runWorktreeTool.mjs +2 -2
- package/dist/adapters/pi/extensions/runWorktreeTool.mjs.map +1 -1
- package/dist/adapters/web/worktreesChannel.cjs +1 -1
- package/dist/adapters/web/worktreesChannel.cjs.map +1 -1
- package/dist/adapters/web/worktreesChannel.d.cts.map +1 -1
- package/dist/adapters/web/worktreesChannel.d.mts.map +1 -1
- package/dist/adapters/web/worktreesChannel.mjs +1 -1
- package/dist/adapters/web/worktreesChannel.mjs.map +1 -1
- package/dist/adapters/worktree/compositionMirror.cjs +2 -0
- package/dist/adapters/worktree/compositionMirror.cjs.map +1 -0
- package/dist/adapters/worktree/compositionMirror.mjs +2 -0
- package/dist/adapters/worktree/compositionMirror.mjs.map +1 -0
- package/dist/adapters/worktree/gitCli.cjs +1 -1
- package/dist/adapters/worktree/gitCli.cjs.map +1 -1
- package/dist/adapters/worktree/gitCli.mjs +1 -1
- package/dist/adapters/worktree/gitCli.mjs.map +1 -1
- package/dist/adapters/worktree/worktreeOperations.cjs +1 -1
- package/dist/adapters/worktree/worktreeOperations.cjs.map +1 -1
- package/dist/adapters/worktree/worktreeOperations.d.cts +8 -2
- package/dist/adapters/worktree/worktreeOperations.d.cts.map +1 -1
- package/dist/adapters/worktree/worktreeOperations.d.mts +8 -2
- package/dist/adapters/worktree/worktreeOperations.d.mts.map +1 -1
- package/dist/adapters/worktree/worktreeOperations.mjs +1 -1
- package/dist/adapters/worktree/worktreeOperations.mjs.map +1 -1
- package/dist/services/support/errors.cjs.map +1 -1
- package/dist/services/support/errors.mjs.map +1 -1
- package/dist/services/worktreeNaming.cjs +1 -1
- package/dist/services/worktreeNaming.cjs.map +1 -1
- package/dist/services/worktreeNaming.d.cts +7 -0
- package/dist/services/worktreeNaming.d.cts.map +1 -1
- package/dist/services/worktreeNaming.d.mts +7 -0
- package/dist/services/worktreeNaming.d.mts.map +1 -1
- package/dist/services/worktreeNaming.mjs +1 -1
- package/dist/services/worktreeNaming.mjs.map +1 -1
- package/package.json +8 -6
- package/src/prompts/doompi-use-git/SKILL.md +5 -2
- package/src/types/worktreeRegistry.ts +9 -0
- package/src/web/components/RunWorktreeToolMessage.tsx +20 -12
- package/src/web/components/WorktreesActivitySection.tsx +25 -10
- package/src/web/components/WorktreesPanel.tsx +94 -45
- package/dist/adapters/worktree/dependencyInstall.cjs +0 -2
- package/dist/adapters/worktree/dependencyInstall.cjs.map +0 -1
- package/dist/adapters/worktree/dependencyInstall.mjs +0 -2
- package/dist/adapters/worktree/dependencyInstall.mjs.map +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:fs");t=e.__toESM(t,1);let n=require("@agimon-ai/doompi-web-contracts");const r=`/api/sessions
|
|
1
|
+
const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:fs");t=e.__toESM(t,1);let n=require("@agimon-ai/doompi-web-contracts");const r=`/api/sessions`,i=6e5;var a=class extends Error{constructor(e){super(e),this.name=`HubUnavailableError`}};function o(e,n){return t.default.existsSync(`${e}/sessions/${n}.json`)}async function s(e,t){let n=c(e);if(n!==void 0&&await l(n.url))try{let e=await fetch(`${n.url}${r}/${encodeURIComponent(t)}`,{method:`DELETE`,signal:AbortSignal.timeout(3e4)});if(!e.ok&&e.status!==404)throw new a((await e.json().catch(()=>({}))).error??`The cockpit could not stop the session (${String(e.status)}).`)}catch(e){throw e instanceof a?e:new a(`Could not reach the cockpit: ${e.message}`)}}function c(e){let r;try{r=t.default.readFileSync((0,n.hubAdvertisementPath)(e),`utf8`)}catch{return}return(0,n.parseHubAdvertisement)(r)}async function l(e){try{let t=await fetch(`${e}/api/health`,{signal:AbortSignal.timeout(1e3)});if(!t.ok)return!1;let n=await t.json();return n.ok===!0&&n.role===`hub`}catch{return!1}}async function u(e){let t=c(e.registryDir);if(t===void 0)throw new a(`No cockpit is running. Worktree sessions are created by the cockpit, so start it and try again.`);if(!await l(t.url))throw new a(`The cockpit that advertised ${t.url} is no longer answering. Start it and try again.`);let n;try{n=await fetch(`${t.url}${r}`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({cwd:e.cwd,name:e.name,parentSessionId:e.parentSessionId,provenance:`worktree`}),signal:e.signal===void 0?AbortSignal.timeout(i):AbortSignal.any([e.signal,AbortSignal.timeout(i)])})}catch(t){throw e.signal?.aborted===!0?t:new a(`Could not reach the cockpit: ${t.message}`)}let o=await n.json().catch(()=>({}));if(!n.ok)throw new a(o.error??`The cockpit refused the session (${String(n.status)}).`);if(typeof o.sessionId!=`string`||o.sessionId===``)throw new a(`The cockpit accepted the session but did not name it.`);return o.sessionId}exports.HubUnavailableError=a,exports.createWorktreeSession=u,exports.readHubAdvertisement=c,exports.sessionIsLive=o,exports.stopWorktreeSession=s;
|
|
2
2
|
//# sourceMappingURL=hubClient.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hubClient.cjs","names":["fs","hubAdvertisementPath","parseHubAdvertisement"],"sources":["../../../src/adapters/hub/hubClient.ts"],"sourcesContent":["import fs from 'node:fs';\nimport { hubAdvertisementPath, parseHubAdvertisement } from '@agimon-ai/doompi-web-contracts';\nimport type { HubAdvertisement } from '@agimon-ai/doompi-web-contracts';\n\nconst HEALTH_ROUTE = '/api/health';\nconst SESSIONS_ROUTE = '/api/sessions';\nconst PROBE_TIMEOUT_MS = 1000;\n/**\n * Creating a session is not a quick call.\n *\n * The hub validates, then runs `ensureSessionSynced`, then spawns. A cockpit\n * that has never synced the composition builds it first, which is minutes on a\n * cold cache
|
|
1
|
+
{"version":3,"file":"hubClient.cjs","names":["fs","hubAdvertisementPath","parseHubAdvertisement"],"sources":["../../../src/adapters/hub/hubClient.ts"],"sourcesContent":["import fs from 'node:fs';\nimport { hubAdvertisementPath, parseHubAdvertisement } from '@agimon-ai/doompi-web-contracts';\nimport type { HubAdvertisement } from '@agimon-ai/doompi-web-contracts';\n\nconst HEALTH_ROUTE = '/api/health';\nconst SESSIONS_ROUTE = '/api/sessions';\nconst PROBE_TIMEOUT_MS = 1000;\n/**\n * Creating a session is not a quick call.\n *\n * The hub validates, then runs `ensureSessionSynced`, then spawns. A cockpit\n * that has never synced the composition builds it first, which is minutes on a\n * cold cache. A 30s timeout was measured aborting a create that the hub then\n * completed anyway, leaving the caller told it failed while a session started\n * behind it.\n */\nconst CREATE_TIMEOUT_MS = 600_000;\n/** Stopping is just a signal and a record removal, so it stays short. */\nconst STOP_TIMEOUT_MS = 30_000;\nconst HUB_ROLE = 'hub';\n\n/** Raised when the cockpit that owns session lifecycle is not reachable. */\nexport class HubUnavailableError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'HubUnavailableError';\n }\n}\n\n/**\n * Whether the hub still has a record for a session.\n *\n * The session server writes its record into the shared registry directory and\n * removes it on exit, so the file's presence is the liveness signal. A pid is\n * not available here: the hub spawns the process, so this package never learns\n * one, and a pid it invented would be a field that always lies.\n */\nexport function sessionIsLive(registryDir: string, sessionId: string): boolean {\n return fs.existsSync(`${registryDir}/sessions/${sessionId}.json`);\n}\n\n/**\n * Asks the hub to stop a session it started.\n *\n * Stopping is the hub's job for the same reason starting is. Signalling the\n * process directly would leave the hub's own record of it behind and race the\n * cleanup it does on the way down.\n *\n * A session the hub does not know about is already stopped, which is the\n * outcome the caller wanted, so a 404 is success.\n */\nexport async function stopWorktreeSession(registryDir: string, sessionId: string): Promise<void> {\n const advertisement = readHubAdvertisement(registryDir);\n if (advertisement === undefined) return;\n if (!(await hubAnswers(advertisement.url))) return;\n try {\n const response = await fetch(`${advertisement.url}${SESSIONS_ROUTE}/${encodeURIComponent(sessionId)}`, {\n method: 'DELETE',\n signal: AbortSignal.timeout(STOP_TIMEOUT_MS),\n });\n if (!response.ok && response.status !== 404) {\n const body = (await response.json().catch(() => ({}))) as { error?: string };\n throw new HubUnavailableError(\n body.error ?? `The cockpit could not stop the session (${String(response.status)}).`,\n );\n }\n } catch (error) {\n if (error instanceof HubUnavailableError) throw error;\n throw new HubUnavailableError(`Could not reach the cockpit: ${(error as Error).message}`);\n }\n}\n\nexport interface CreateWorktreeSessionInput {\n /** The worktree directory; must already exist. */\n cwd: string;\n /** Rail display name for the new session. */\n name: string;\n /** The session asking for this one, so the rail can nest them. */\n parentSessionId: string;\n registryDir: string;\n /** The caller's own abort, composed with the create timeout below. */\n signal?: AbortSignal;\n}\n\n/** Reads the hub's published address, if it published one. */\nexport function readHubAdvertisement(registryDir: string): HubAdvertisement | undefined {\n let raw: string;\n try {\n raw = fs.readFileSync(hubAdvertisementPath(registryDir), 'utf8');\n } catch {\n return undefined;\n }\n return parseHubAdvertisement(raw);\n}\n\n/**\n * Confirms a live cockpit hub answers at the address, rather than a leftover.\n *\n * The advertisement file outlives a crash and a port gets reused, so finding it\n * proves nothing. Only a hub that answers /api/health and calls itself a hub is\n * worth sending a session-create to; anything else is something else's port.\n */\nasync function hubAnswers(url: string): Promise<boolean> {\n try {\n const response = await fetch(`${url}${HEALTH_ROUTE}`, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });\n if (!response.ok) return false;\n const body = (await response.json()) as { ok?: boolean; role?: string };\n return body.ok === true && body.role === HUB_ROLE;\n } catch {\n return false;\n }\n}\n\n/**\n * Asks the cockpit hub to start a session in a worktree.\n *\n * The hub owns session lifecycle: it validates the directory, syncs the\n * composition, spawns the server and records the lineage. This package only\n * asks. Creating the process here instead would put a second spawn path beside\n * the hub's, with its own idea of what a session needs, and a session server is\n * bound to one session anyway so it has no business starting another.\n *\n * The panel and the agent both come through here, so there is one route to keep\n * correct rather than two that drift.\n */\nexport async function createWorktreeSession(input: CreateWorktreeSessionInput): Promise<string> {\n const advertisement = readHubAdvertisement(input.registryDir);\n if (advertisement === undefined) {\n throw new HubUnavailableError(\n 'No cockpit is running. Worktree sessions are created by the cockpit, so start it and try again.',\n );\n }\n if (!(await hubAnswers(advertisement.url))) {\n throw new HubUnavailableError(\n `The cockpit that advertised ${advertisement.url} is no longer answering. Start it and try again.`,\n );\n }\n\n let response: Response;\n try {\n response = await fetch(`${advertisement.url}${SESSIONS_ROUTE}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n cwd: input.cwd,\n name: input.name,\n parentSessionId: input.parentSessionId,\n provenance: 'worktree',\n }),\n signal:\n input.signal === undefined\n ? AbortSignal.timeout(CREATE_TIMEOUT_MS)\n : AbortSignal.any([input.signal, AbortSignal.timeout(CREATE_TIMEOUT_MS)]),\n });\n } catch (error) {\n // A caller that gave up is not a broken cockpit, and saying so would send\n // the reader to check a hub that is fine.\n if (input.signal?.aborted === true) throw error;\n throw new HubUnavailableError(`Could not reach the cockpit: ${(error as Error).message}`);\n }\n\n const body = (await response.json().catch(() => ({}))) as { sessionId?: string; error?: string };\n if (!response.ok) {\n throw new HubUnavailableError(body.error ?? `The cockpit refused the session (${String(response.status)}).`);\n }\n if (typeof body.sessionId !== 'string' || body.sessionId === '') {\n throw new HubUnavailableError('The cockpit accepted the session but did not name it.');\n }\n return body.sessionId;\n}\n"],"mappings":"mJAIA,MACM,EAAiB,gBAWjB,EAAoB,IAM1B,IAAa,EAAb,cAAyC,KAAM,CAC7C,YAAY,EAAiB,CAC3B,MAAM,CAAO,EACb,KAAK,KAAO,qBACd,CACF,EAUA,SAAgB,EAAc,EAAqB,EAA4B,CAC7E,OAAOA,EAAAA,QAAG,WAAW,GAAG,EAAY,YAAY,EAAU,MAAM,CAClE,CAYA,eAAsB,EAAoB,EAAqB,EAAkC,CAC/F,IAAM,EAAgB,EAAqB,CAAW,EAClD,OAAkB,IAAA,IAChB,MAAM,EAAW,EAAc,GAAG,EACxC,GAAI,CACF,IAAM,EAAW,MAAM,MAAM,GAAG,EAAc,MAAM,EAAe,GAAG,mBAAmB,CAAS,IAAK,CACrG,OAAQ,SACR,OAAQ,YAAY,QAAQ,GAAe,CAC7C,CAAC,EACD,GAAI,CAAC,EAAS,IAAM,EAAS,SAAW,IAEtC,MAAM,IAAI,GACR,MAFkB,EAAS,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EAElD,CAAK,OAAS,2CAA2C,OAAO,EAAS,MAAM,EAAE,GACnF,CAEJ,OAAS,EAAO,CAEd,MADI,aAAiB,EAA2B,EAC1C,IAAI,EAAoB,gCAAiC,EAAgB,SAAS,CAC1F,CACF,CAeA,SAAgB,EAAqB,EAAmD,CACtF,IAAI,EACJ,GAAI,CACF,EAAMA,EAAAA,QAAG,cAAA,EAAaC,EAAAA,qBAAAA,CAAqB,CAAW,EAAG,MAAM,CACjE,MAAQ,CACN,MACF,CACA,OAAA,EAAOC,EAAAA,sBAAAA,CAAsB,CAAG,CAClC,CASA,eAAe,EAAW,EAA+B,CACvD,GAAI,CACF,IAAM,EAAW,MAAM,MAAM,GAAG,eAAsB,CAAE,OAAQ,YAAY,QAAQ,GAAgB,CAAE,CAAC,EACvG,GAAI,CAAC,EAAS,GAAI,MAAO,GACzB,IAAM,EAAQ,MAAM,EAAS,KAAK,EAClC,OAAO,EAAK,KAAO,IAAQ,EAAK,OAAS,KAC3C,MAAQ,CACN,MAAO,EACT,CACF,CAcA,eAAsB,EAAsB,EAAoD,CAC9F,IAAM,EAAgB,EAAqB,EAAM,WAAW,EAC5D,GAAI,IAAkB,IAAA,GACpB,MAAM,IAAI,EACR,iGACF,EAEF,GAAI,CAAE,MAAM,EAAW,EAAc,GAAG,EACtC,MAAM,IAAI,EACR,+BAA+B,EAAc,IAAI,iDACnD,EAGF,IAAI,EACJ,GAAI,CACF,EAAW,MAAM,MAAM,GAAG,EAAc,MAAM,IAAkB,CAC9D,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CACnB,IAAK,EAAM,IACX,KAAM,EAAM,KACZ,gBAAiB,EAAM,gBACvB,WAAY,UACd,CAAC,EACD,OACE,EAAM,SAAW,IAAA,GACb,YAAY,QAAQ,CAAiB,EACrC,YAAY,IAAI,CAAC,EAAM,OAAQ,YAAY,QAAQ,CAAiB,CAAC,CAAC,CAC9E,CAAC,CACH,OAAS,EAAO,CAId,MADI,EAAM,QAAQ,UAAY,GAAY,EACpC,IAAI,EAAoB,gCAAiC,EAAgB,SAAS,CAC1F,CAEA,IAAM,EAAQ,MAAM,EAAS,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EACpD,GAAI,CAAC,EAAS,GACZ,MAAM,IAAI,EAAoB,EAAK,OAAS,oCAAoC,OAAO,EAAS,MAAM,EAAE,GAAG,EAE7G,GAAI,OAAO,EAAK,WAAc,UAAY,EAAK,YAAc,GAC3D,MAAM,IAAI,EAAoB,uDAAuD,EAEvF,OAAO,EAAK,SACd"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import e from"node:fs";import{hubAdvertisementPath as t,parseHubAdvertisement as n}from"@agimon-ai/doompi-web-contracts";const r=`/api/sessions
|
|
1
|
+
import e from"node:fs";import{hubAdvertisementPath as t,parseHubAdvertisement as n}from"@agimon-ai/doompi-web-contracts";const r=`/api/sessions`,i=6e5;var a=class extends Error{constructor(e){super(e),this.name=`HubUnavailableError`}};function o(t,n){return e.existsSync(`${t}/sessions/${n}.json`)}async function s(e,t){let n=c(e);if(n!==void 0&&await l(n.url))try{let e=await fetch(`${n.url}${r}/${encodeURIComponent(t)}`,{method:`DELETE`,signal:AbortSignal.timeout(3e4)});if(!e.ok&&e.status!==404)throw new a((await e.json().catch(()=>({}))).error??`The cockpit could not stop the session (${String(e.status)}).`)}catch(e){throw e instanceof a?e:new a(`Could not reach the cockpit: ${e.message}`)}}function c(r){let i;try{i=e.readFileSync(t(r),`utf8`)}catch{return}return n(i)}async function l(e){try{let t=await fetch(`${e}/api/health`,{signal:AbortSignal.timeout(1e3)});if(!t.ok)return!1;let n=await t.json();return n.ok===!0&&n.role===`hub`}catch{return!1}}async function u(e){let t=c(e.registryDir);if(t===void 0)throw new a(`No cockpit is running. Worktree sessions are created by the cockpit, so start it and try again.`);if(!await l(t.url))throw new a(`The cockpit that advertised ${t.url} is no longer answering. Start it and try again.`);let n;try{n=await fetch(`${t.url}${r}`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({cwd:e.cwd,name:e.name,parentSessionId:e.parentSessionId,provenance:`worktree`}),signal:e.signal===void 0?AbortSignal.timeout(i):AbortSignal.any([e.signal,AbortSignal.timeout(i)])})}catch(t){throw e.signal?.aborted===!0?t:new a(`Could not reach the cockpit: ${t.message}`)}let o=await n.json().catch(()=>({}));if(!n.ok)throw new a(o.error??`The cockpit refused the session (${String(n.status)}).`);if(typeof o.sessionId!=`string`||o.sessionId===``)throw new a(`The cockpit accepted the session but did not name it.`);return o.sessionId}export{a as HubUnavailableError,u as createWorktreeSession,c as readHubAdvertisement,o as sessionIsLive,s as stopWorktreeSession};
|
|
2
2
|
//# sourceMappingURL=hubClient.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hubClient.mjs","names":[],"sources":["../../../src/adapters/hub/hubClient.ts"],"sourcesContent":["import fs from 'node:fs';\nimport { hubAdvertisementPath, parseHubAdvertisement } from '@agimon-ai/doompi-web-contracts';\nimport type { HubAdvertisement } from '@agimon-ai/doompi-web-contracts';\n\nconst HEALTH_ROUTE = '/api/health';\nconst SESSIONS_ROUTE = '/api/sessions';\nconst PROBE_TIMEOUT_MS = 1000;\n/**\n * Creating a session is not a quick call.\n *\n * The hub validates, then runs `ensureSessionSynced`, then spawns. A cockpit\n * that has never synced the composition builds it first, which is minutes on a\n * cold cache
|
|
1
|
+
{"version":3,"file":"hubClient.mjs","names":[],"sources":["../../../src/adapters/hub/hubClient.ts"],"sourcesContent":["import fs from 'node:fs';\nimport { hubAdvertisementPath, parseHubAdvertisement } from '@agimon-ai/doompi-web-contracts';\nimport type { HubAdvertisement } from '@agimon-ai/doompi-web-contracts';\n\nconst HEALTH_ROUTE = '/api/health';\nconst SESSIONS_ROUTE = '/api/sessions';\nconst PROBE_TIMEOUT_MS = 1000;\n/**\n * Creating a session is not a quick call.\n *\n * The hub validates, then runs `ensureSessionSynced`, then spawns. A cockpit\n * that has never synced the composition builds it first, which is minutes on a\n * cold cache. A 30s timeout was measured aborting a create that the hub then\n * completed anyway, leaving the caller told it failed while a session started\n * behind it.\n */\nconst CREATE_TIMEOUT_MS = 600_000;\n/** Stopping is just a signal and a record removal, so it stays short. */\nconst STOP_TIMEOUT_MS = 30_000;\nconst HUB_ROLE = 'hub';\n\n/** Raised when the cockpit that owns session lifecycle is not reachable. */\nexport class HubUnavailableError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'HubUnavailableError';\n }\n}\n\n/**\n * Whether the hub still has a record for a session.\n *\n * The session server writes its record into the shared registry directory and\n * removes it on exit, so the file's presence is the liveness signal. A pid is\n * not available here: the hub spawns the process, so this package never learns\n * one, and a pid it invented would be a field that always lies.\n */\nexport function sessionIsLive(registryDir: string, sessionId: string): boolean {\n return fs.existsSync(`${registryDir}/sessions/${sessionId}.json`);\n}\n\n/**\n * Asks the hub to stop a session it started.\n *\n * Stopping is the hub's job for the same reason starting is. Signalling the\n * process directly would leave the hub's own record of it behind and race the\n * cleanup it does on the way down.\n *\n * A session the hub does not know about is already stopped, which is the\n * outcome the caller wanted, so a 404 is success.\n */\nexport async function stopWorktreeSession(registryDir: string, sessionId: string): Promise<void> {\n const advertisement = readHubAdvertisement(registryDir);\n if (advertisement === undefined) return;\n if (!(await hubAnswers(advertisement.url))) return;\n try {\n const response = await fetch(`${advertisement.url}${SESSIONS_ROUTE}/${encodeURIComponent(sessionId)}`, {\n method: 'DELETE',\n signal: AbortSignal.timeout(STOP_TIMEOUT_MS),\n });\n if (!response.ok && response.status !== 404) {\n const body = (await response.json().catch(() => ({}))) as { error?: string };\n throw new HubUnavailableError(\n body.error ?? `The cockpit could not stop the session (${String(response.status)}).`,\n );\n }\n } catch (error) {\n if (error instanceof HubUnavailableError) throw error;\n throw new HubUnavailableError(`Could not reach the cockpit: ${(error as Error).message}`);\n }\n}\n\nexport interface CreateWorktreeSessionInput {\n /** The worktree directory; must already exist. */\n cwd: string;\n /** Rail display name for the new session. */\n name: string;\n /** The session asking for this one, so the rail can nest them. */\n parentSessionId: string;\n registryDir: string;\n /** The caller's own abort, composed with the create timeout below. */\n signal?: AbortSignal;\n}\n\n/** Reads the hub's published address, if it published one. */\nexport function readHubAdvertisement(registryDir: string): HubAdvertisement | undefined {\n let raw: string;\n try {\n raw = fs.readFileSync(hubAdvertisementPath(registryDir), 'utf8');\n } catch {\n return undefined;\n }\n return parseHubAdvertisement(raw);\n}\n\n/**\n * Confirms a live cockpit hub answers at the address, rather than a leftover.\n *\n * The advertisement file outlives a crash and a port gets reused, so finding it\n * proves nothing. Only a hub that answers /api/health and calls itself a hub is\n * worth sending a session-create to; anything else is something else's port.\n */\nasync function hubAnswers(url: string): Promise<boolean> {\n try {\n const response = await fetch(`${url}${HEALTH_ROUTE}`, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });\n if (!response.ok) return false;\n const body = (await response.json()) as { ok?: boolean; role?: string };\n return body.ok === true && body.role === HUB_ROLE;\n } catch {\n return false;\n }\n}\n\n/**\n * Asks the cockpit hub to start a session in a worktree.\n *\n * The hub owns session lifecycle: it validates the directory, syncs the\n * composition, spawns the server and records the lineage. This package only\n * asks. Creating the process here instead would put a second spawn path beside\n * the hub's, with its own idea of what a session needs, and a session server is\n * bound to one session anyway so it has no business starting another.\n *\n * The panel and the agent both come through here, so there is one route to keep\n * correct rather than two that drift.\n */\nexport async function createWorktreeSession(input: CreateWorktreeSessionInput): Promise<string> {\n const advertisement = readHubAdvertisement(input.registryDir);\n if (advertisement === undefined) {\n throw new HubUnavailableError(\n 'No cockpit is running. Worktree sessions are created by the cockpit, so start it and try again.',\n );\n }\n if (!(await hubAnswers(advertisement.url))) {\n throw new HubUnavailableError(\n `The cockpit that advertised ${advertisement.url} is no longer answering. Start it and try again.`,\n );\n }\n\n let response: Response;\n try {\n response = await fetch(`${advertisement.url}${SESSIONS_ROUTE}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n cwd: input.cwd,\n name: input.name,\n parentSessionId: input.parentSessionId,\n provenance: 'worktree',\n }),\n signal:\n input.signal === undefined\n ? AbortSignal.timeout(CREATE_TIMEOUT_MS)\n : AbortSignal.any([input.signal, AbortSignal.timeout(CREATE_TIMEOUT_MS)]),\n });\n } catch (error) {\n // A caller that gave up is not a broken cockpit, and saying so would send\n // the reader to check a hub that is fine.\n if (input.signal?.aborted === true) throw error;\n throw new HubUnavailableError(`Could not reach the cockpit: ${(error as Error).message}`);\n }\n\n const body = (await response.json().catch(() => ({}))) as { sessionId?: string; error?: string };\n if (!response.ok) {\n throw new HubUnavailableError(body.error ?? `The cockpit refused the session (${String(response.status)}).`);\n }\n if (typeof body.sessionId !== 'string' || body.sessionId === '') {\n throw new HubUnavailableError('The cockpit accepted the session but did not name it.');\n }\n return body.sessionId;\n}\n"],"mappings":"yHAIA,MACM,EAAiB,gBAWjB,EAAoB,IAM1B,IAAa,EAAb,cAAyC,KAAM,CAC7C,YAAY,EAAiB,CAC3B,MAAM,CAAO,EACb,KAAK,KAAO,qBACd,CACF,EAUA,SAAgB,EAAc,EAAqB,EAA4B,CAC7E,OAAO,EAAG,WAAW,GAAG,EAAY,YAAY,EAAU,MAAM,CAClE,CAYA,eAAsB,EAAoB,EAAqB,EAAkC,CAC/F,IAAM,EAAgB,EAAqB,CAAW,EAClD,OAAkB,IAAA,IAChB,MAAM,EAAW,EAAc,GAAG,EACxC,GAAI,CACF,IAAM,EAAW,MAAM,MAAM,GAAG,EAAc,MAAM,EAAe,GAAG,mBAAmB,CAAS,IAAK,CACrG,OAAQ,SACR,OAAQ,YAAY,QAAQ,GAAe,CAC7C,CAAC,EACD,GAAI,CAAC,EAAS,IAAM,EAAS,SAAW,IAEtC,MAAM,IAAI,GACR,MAFkB,EAAS,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EAElD,CAAK,OAAS,2CAA2C,OAAO,EAAS,MAAM,EAAE,GACnF,CAEJ,OAAS,EAAO,CAEd,MADI,aAAiB,EAA2B,EAC1C,IAAI,EAAoB,gCAAiC,EAAgB,SAAS,CAC1F,CACF,CAeA,SAAgB,EAAqB,EAAmD,CACtF,IAAI,EACJ,GAAI,CACF,EAAM,EAAG,aAAa,EAAqB,CAAW,EAAG,MAAM,CACjE,MAAQ,CACN,MACF,CACA,OAAO,EAAsB,CAAG,CAClC,CASA,eAAe,EAAW,EAA+B,CACvD,GAAI,CACF,IAAM,EAAW,MAAM,MAAM,GAAG,eAAsB,CAAE,OAAQ,YAAY,QAAQ,GAAgB,CAAE,CAAC,EACvG,GAAI,CAAC,EAAS,GAAI,MAAO,GACzB,IAAM,EAAQ,MAAM,EAAS,KAAK,EAClC,OAAO,EAAK,KAAO,IAAQ,EAAK,OAAS,KAC3C,MAAQ,CACN,MAAO,EACT,CACF,CAcA,eAAsB,EAAsB,EAAoD,CAC9F,IAAM,EAAgB,EAAqB,EAAM,WAAW,EAC5D,GAAI,IAAkB,IAAA,GACpB,MAAM,IAAI,EACR,iGACF,EAEF,GAAI,CAAE,MAAM,EAAW,EAAc,GAAG,EACtC,MAAM,IAAI,EACR,+BAA+B,EAAc,IAAI,iDACnD,EAGF,IAAI,EACJ,GAAI,CACF,EAAW,MAAM,MAAM,GAAG,EAAc,MAAM,IAAkB,CAC9D,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CACnB,IAAK,EAAM,IACX,KAAM,EAAM,KACZ,gBAAiB,EAAM,gBACvB,WAAY,UACd,CAAC,EACD,OACE,EAAM,SAAW,IAAA,GACb,YAAY,QAAQ,CAAiB,EACrC,YAAY,IAAI,CAAC,EAAM,OAAQ,YAAY,QAAQ,CAAiB,CAAC,CAAC,CAC9E,CAAC,CACH,OAAS,EAAO,CAId,MADI,EAAM,QAAQ,UAAY,GAAY,EACpC,IAAI,EAAoB,gCAAiC,EAAgB,SAAS,CAC1F,CAEA,IAAM,EAAQ,MAAM,EAAS,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EACpD,GAAI,CAAC,EAAS,GACZ,MAAM,IAAI,EAAoB,EAAK,OAAS,oCAAoC,OAAO,EAAS,MAAM,EAAE,GAAG,EAE7G,GAAI,OAAO,EAAK,WAAc,UAAY,EAAK,YAAc,GAC3D,MAAM,IAAI,EAAoB,uDAAuD,EAEvF,OAAO,EAAK,SACd"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const e=require("../../../services/support/errors.cjs"),t=require("../../../schemas/runWorktreeTool.cjs"),n=`run_worktree`,r=new Set(Object.values(t.WORKTREE_ACTIONS));function i(n){if(typeof n!=`object`||!n||Array.isArray(n))throw e.invalidRequest(`run_worktree parameters must be an object.`,`Call it with one documented action shape.`);let i=n,a=i.action;if(typeof a!=`string`||!r.has(a))throw e.invalidRequest(`Unknown action ${typeof a==`string`?`'${a}'`:`value`}.`,`Use one of: ${[...r].join(`, `)}.`);let o=t.WORKTREE_ACTION_FIELDS[a],s=Object.keys(i).filter(e=>!o.includes(e));if(s.length>0)throw e.invalidRequest(`action='${a}' does not accept: ${s.join(`, `)}.`,`Fields for '${a}': ${o.join(`, `)}.`);for(let t of[`branch`,`id`])if(o.includes(t)&&t in i){let n=i[t];if(typeof n!=`string`||n.trim()===``)throw e.invalidRequest(`action='${a}' requires a nonblank '${t}'.`,`Pass a ${t}.`)}if(a===t.WORKTREE_ACTIONS.spawn_worktree&&typeof i.branch!=`string`)throw e.invalidRequest(`action='spawn_worktree' requires 'branch'.`,`Name the branch to create.`);if((a===t.WORKTREE_ACTIONS.close_worktree||a===t.WORKTREE_ACTIONS.status||a===t.WORKTREE_ACTIONS.merge)&&typeof i.id!=`string`)throw e.invalidRequest(`action='${a}' requires 'id'.`,`Call list to see the current ids.`);return i}function a(e){return`${e.id} ${e.branch} ${e.status} ${e.path}`}function o(...e){return[{type:`text`,text:e.join(`
|
|
2
|
-
`)}]}async function s(e,n,r){let
|
|
2
|
+
`)}]}async function s(e,n,r,i={}){let s={cwd:r.cwd,sessionId:r.sessionManager.getSessionId()};switch(e.action){case t.WORKTREE_ACTIONS.spawn_worktree:{let t=await n.spawn(s,{branch:e.branch,...e.baseRef===void 0?{}:{baseRef:e.baseRef},...e.name===void 0?{}:{name:e.name}},i);return{content:o(`Worktree ${t.id} created on ${t.branch} from ${t.baseRef}.`,`Path: ${t.path}`,`Session ${t.sessionId} is starting; it appears nested under this one in the rail.`),details:{action:e.action,records:[t]}}}case t.WORKTREE_ACTIONS.close_worktree:{let t=await n.close(s,e.id,e.force??!1);return{content:o(`Worktree ${t.id} (${t.branch}) closed and removed.`),details:{action:e.action,records:[t]}}}case t.WORKTREE_ACTIONS.list:{let t=await n.list(s);return{content:t.length===0?o(`No worktrees for this repository.`):o(`${String(t.length)} worktree(s):`,...t.map(a)),details:{action:e.action,records:t}}}case t.WORKTREE_ACTIONS.status:{let{record:t,dirtyFiles:r}=await n.status(s,e.id);return{content:o(a(t),`Session: ${t.sessionId}`,r.length===0?`Clean.`:`${String(r.length)} uncommitted file(s): ${r.slice(0,20).join(`, `)}`),details:{action:e.action,records:[t]}}}case t.WORKTREE_ACTIONS.merge:{let t=await n.merge(s,e.id,...e.message===void 0?[]:[e.message]);return{content:o(`Merged ${t.branch} into this checkout.`),details:{action:e.action,records:[t]}}}case t.WORKTREE_ACTIONS.send:return await n.send(s,e.id,e.message),{content:o(`Message sent to worktree ${e.id}.`),details:{action:e.action}};case t.WORKTREE_ACTIONS.messages:{let t=await n.messages(s,e.id);return{content:t.length===0?o(`No messages.`):o(`${String(t.length)} message(s):`,...t.map(e=>`[${e.sentAt}] ${e.from}: ${e.text}`)),details:{action:e.action}}}case t.WORKTREE_ACTIONS.prune:{let t=e.dryRun??!1,r=await n.prune(s,t);return{content:o(t?`Prune plan (nothing was changed):`:`Pruned:`,`Remove: ${r.remove.length===0?`none`:r.remove.map(e=>e.id).join(`, `)}`,`Forget: ${r.forget.length===0?`none`:r.forget.map(e=>e.id).join(`, `)}`,`Kept, uncommitted work: ${r.keptDirty.length===0?`none`:r.keptDirty.map(e=>e.id).join(`, `)}`,`Untracked directories, left alone: ${r.untracked.length===0?`none`:r.untracked.join(`, `)}`,...r.keptBranches.length===0?[]:[`Branches kept, they hold commits: ${r.keptBranches.join(`, `)}`]),details:{action:e.action}}}}}function c(e,r){let a={name:n,label:`Worktree`,description:`Create and manage git worktrees, each with its own DoomPi session nested under this one.
|
|
3
3
|
|
|
4
4
|
Actions:
|
|
5
5
|
- spawn_worktree: create a worktree on a new branch and start a session in it. Requires 'branch'. Optional 'baseRef' (defaults to the current branch), 'name' for the rail, 'task' as the first message.
|
|
@@ -11,5 +11,5 @@ Actions:
|
|
|
11
11
|
- send: send a message to the other side of a worktree. Requires 'id' and 'message'.
|
|
12
12
|
- messages: read messages waiting for you from a worktree. Requires 'id'.
|
|
13
13
|
|
|
14
|
-
Worktrees live outside the repository, under ~/.pi/.doom/git/worktrees.`,parameters:t.RunWorktreeParams,prepareArguments:i,execute:async(e,t,n,a,
|
|
14
|
+
Worktrees live outside the repository, under ~/.pi/.doom/git/worktrees.`,parameters:t.RunWorktreeParams,prepareArguments:i,execute:async(e,t,n,a,c)=>{let l=i(t);return await s(l,r,c,{...n===void 0?{}:{signal:n},onProgress:e=>a?.({content:o(e),details:{action:l.action}})})}};e.registerTool(a)}exports.RUN_WORKTREE_TOOL_NAME=n,exports.registerRunWorktreeTool=c,exports.validateParams=i;
|
|
15
15
|
//# sourceMappingURL=runWorktreeTool.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runWorktreeTool.cjs","names":["WORKTREE_ACTIONS","invalidRequest","WORKTREE_ACTION_FIELDS","RunWorktreeParams"],"sources":["../../../../src/adapters/pi/extensions/runWorktreeTool.ts"],"sourcesContent":["import type { AgentToolResult } from '@earendil-works/pi-agent-core';\nimport type { ExtensionAPI, ExtensionContext, ToolDefinition } from '@earendil-works/pi-coding-agent';\nimport {\n RunWorktreeParams,\n WORKTREE_ACTION_FIELDS,\n WORKTREE_ACTIONS,\n type WorktreeAction,\n type RunWorktreeToolParams,\n} from '../../../schemas/runWorktreeTool.ts';\nimport { invalidRequest } from '../../../services/support/errors.ts';\nimport type { WorktreeOperations } from '../../worktree/worktreeOperations.ts';\nimport type { WorktreeRecord } from '../../../types/worktreeRegistry.ts';\n\nexport const RUN_WORKTREE_TOOL_NAME = 'run_worktree';\n\nconst IMPLEMENTED_ACTIONS: ReadonlySet<string> = new Set(Object.values(WORKTREE_ACTIONS));\n\nconst DESCRIPTION = `Create and manage git worktrees, each with its own DoomPi session nested under this one.\n\nActions:\n- spawn_worktree: create a worktree on a new branch and start a session in it. Requires 'branch'. Optional 'baseRef' (defaults to the current branch), 'name' for the rail, 'task' as the first message.\n- close_worktree: stop a worktree's session and delete its directory. Requires 'id'. Refuses a dirty tree unless 'force' is true.\n- list: every worktree for this repository, with status.\n- status: one worktree and its uncommitted files. Requires 'id'.\n- merge: merge a worktree's branch into this checkout. Requires 'id'. Refuses when this checkout is dirty.\n- prune: remove orphaned worktrees. Pass 'dryRun' to see the plan without destroying anything.\n- send: send a message to the other side of a worktree. Requires 'id' and 'message'.\n- messages: read messages waiting for you from a worktree. Requires 'id'.\n\nWorktrees live outside the repository, under ~/.pi/.doom/git/worktrees.`;\n\nexport interface RunWorktreeToolDetails {\n action: WorktreeAction;\n records?: WorktreeRecord[];\n}\n\n/**\n * Rejects a call that is not exactly one documented action shape.\n *\n * Unknown fields are refused rather than ignored. A caller that passes 'branch'\n * to close_worktree has a different operation in mind than the one that would\n * run, and silently dropping the field would carry it out anyway.\n */\nexport function validateParams(input: unknown): RunWorktreeToolParams {\n if (typeof input !== 'object' || input === null || Array.isArray(input)) {\n throw invalidRequest('run_worktree parameters must be an object.', 'Call it with one documented action shape.');\n }\n const record = input as Record<string, unknown>;\n const action = record.action;\n if (typeof action !== 'string' || !IMPLEMENTED_ACTIONS.has(action)) {\n throw invalidRequest(\n `Unknown action ${typeof action === 'string' ? `'${action}'` : 'value'}.`,\n `Use one of: ${[...IMPLEMENTED_ACTIONS].join(', ')}.`,\n );\n }\n const allowed = WORKTREE_ACTION_FIELDS[action as WorktreeAction];\n const unexpected = Object.keys(record).filter((key) => !allowed.includes(key));\n if (unexpected.length > 0) {\n throw invalidRequest(\n `action='${action}' does not accept: ${unexpected.join(', ')}.`,\n `Fields for '${action}': ${allowed.join(', ')}.`,\n );\n }\n for (const field of ['branch', 'id'] as const) {\n if (allowed.includes(field) && field in record) {\n const value = record[field];\n if (typeof value !== 'string' || value.trim() === '') {\n throw invalidRequest(`action='${action}' requires a nonblank '${field}'.`, `Pass a ${field}.`);\n }\n }\n }\n if (action === WORKTREE_ACTIONS.spawn_worktree && typeof record.branch !== 'string') {\n throw invalidRequest(\"action='spawn_worktree' requires 'branch'.\", 'Name the branch to create.');\n }\n if (\n (action === WORKTREE_ACTIONS.close_worktree ||\n action === WORKTREE_ACTIONS.status ||\n action === WORKTREE_ACTIONS.merge) &&\n typeof record.id !== 'string'\n ) {\n throw invalidRequest(`action='${action}' requires 'id'.`, 'Call list to see the current ids.');\n }\n return record as RunWorktreeToolParams;\n}\n\nfunction describe(record: WorktreeRecord): string {\n return `${record.id} ${record.branch} ${record.status} ${record.path}`;\n}\n\n/** A tool result's content, which Pi expects as content blocks rather than a string. */\nfunction text(...lines: readonly string[]): [{ type: 'text'; text: string }] {\n return [{ type: 'text', text: lines.join('\\n') }];\n}\n\nasync function run(\n params: RunWorktreeToolParams,\n operations: WorktreeOperations,\n ctx: ExtensionContext,\n): Promise<AgentToolResult<RunWorktreeToolDetails>> {\n const context = { cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId() };\n switch (params.action) {\n case WORKTREE_ACTIONS.spawn_worktree: {\n const record = await operations.spawn(context, {\n branch: params.branch,\n ...(params.baseRef === undefined ? {} : { baseRef: params.baseRef }),\n ...(params.name === undefined ? {} : { name: params.name }),\n });\n return {\n content: text(\n `Worktree ${record.id} created on ${record.branch} from ${record.baseRef}.`,\n `Path: ${record.path}`,\n `Session ${record.sessionId} is starting; it appears nested under this one in the rail.`,\n ),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.close_worktree: {\n const record = await operations.close(context, params.id, params.force ?? false);\n return {\n content: text(`Worktree ${record.id} (${record.branch}) closed and removed.`),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.list: {\n const records = await operations.list(context);\n return {\n content:\n records.length === 0\n ? text('No worktrees for this repository.')\n : text(`${String(records.length)} worktree(s):`, ...records.map(describe)),\n details: { action: params.action, records },\n };\n }\n case WORKTREE_ACTIONS.status: {\n const { record, dirtyFiles } = await operations.status(context, params.id);\n return {\n content: text(\n describe(record),\n `Session: ${record.sessionId}`,\n dirtyFiles.length === 0\n ? 'Clean.'\n : `${String(dirtyFiles.length)} uncommitted file(s): ${dirtyFiles.slice(0, 20).join(', ')}`,\n ),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.merge: {\n const record = await operations.merge(\n context,\n params.id,\n ...(params.message === undefined ? [] : ([params.message] as const)),\n );\n return {\n content: text(`Merged ${record.branch} into this checkout.`),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.send: {\n await operations.send(context, params.id, params.message);\n return { content: text(`Message sent to worktree ${params.id}.`), details: { action: params.action } };\n }\n case WORKTREE_ACTIONS.messages: {\n const waiting = await operations.messages(context, params.id);\n return {\n content:\n waiting.length === 0\n ? text('No messages.')\n : text(\n `${String(waiting.length)} message(s):`,\n ...waiting.map((entry) => `[${entry.sentAt}] ${entry.from}: ${entry.text}`),\n ),\n details: { action: params.action },\n };\n }\n case WORKTREE_ACTIONS.prune: {\n const dryRun = params.dryRun ?? false;\n const plan = await operations.prune(context, dryRun);\n const lines = [\n dryRun ? 'Prune plan (nothing was changed):' : 'Pruned:',\n `Remove: ${plan.remove.length === 0 ? 'none' : plan.remove.map((record) => record.id).join(', ')}`,\n `Forget: ${plan.forget.length === 0 ? 'none' : plan.forget.map((record) => record.id).join(', ')}`,\n `Kept, uncommitted work: ${plan.keptDirty.length === 0 ? 'none' : plan.keptDirty.map((record) => record.id).join(', ')}`,\n `Untracked directories, left alone: ${plan.untracked.length === 0 ? 'none' : plan.untracked.join(', ')}`,\n ];\n return { content: text(...lines), details: { action: params.action } };\n }\n }\n}\n\n/** Registers `run_worktree` on a Pi host, once. */\nexport function registerRunWorktreeTool(pi: ExtensionAPI, operations: WorktreeOperations): void {\n const tool: ToolDefinition<typeof RunWorktreeParams, RunWorktreeToolDetails> = {\n name: RUN_WORKTREE_TOOL_NAME,\n label: 'Worktree',\n description: DESCRIPTION,\n parameters: RunWorktreeParams,\n prepareArguments: validateParams,\n execute: async (_id, rawParams, _signal, _onUpdate, ctx) => run(validateParams(rawParams), operations, ctx),\n };\n pi.registerTool(tool);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"runWorktreeTool.cjs","names":["WORKTREE_ACTIONS","invalidRequest","WORKTREE_ACTION_FIELDS","RunWorktreeParams"],"sources":["../../../../src/adapters/pi/extensions/runWorktreeTool.ts"],"sourcesContent":["import type { AgentToolResult } from '@earendil-works/pi-agent-core';\nimport type { ExtensionAPI, ExtensionContext, ToolDefinition } from '@earendil-works/pi-coding-agent';\nimport {\n RunWorktreeParams,\n WORKTREE_ACTION_FIELDS,\n WORKTREE_ACTIONS,\n type WorktreeAction,\n type RunWorktreeToolParams,\n} from '../../../schemas/runWorktreeTool.ts';\nimport { invalidRequest } from '../../../services/support/errors.ts';\nimport type { WorktreeOperations } from '../../worktree/worktreeOperations.ts';\nimport type { WorktreeRecord } from '../../../types/worktreeRegistry.ts';\n\nexport const RUN_WORKTREE_TOOL_NAME = 'run_worktree';\n\nconst IMPLEMENTED_ACTIONS: ReadonlySet<string> = new Set(Object.values(WORKTREE_ACTIONS));\n\nconst DESCRIPTION = `Create and manage git worktrees, each with its own DoomPi session nested under this one.\n\nActions:\n- spawn_worktree: create a worktree on a new branch and start a session in it. Requires 'branch'. Optional 'baseRef' (defaults to the current branch), 'name' for the rail, 'task' as the first message.\n- close_worktree: stop a worktree's session and delete its directory. Requires 'id'. Refuses a dirty tree unless 'force' is true.\n- list: every worktree for this repository, with status.\n- status: one worktree and its uncommitted files. Requires 'id'.\n- merge: merge a worktree's branch into this checkout. Requires 'id'. Refuses when this checkout is dirty.\n- prune: remove orphaned worktrees. Pass 'dryRun' to see the plan without destroying anything.\n- send: send a message to the other side of a worktree. Requires 'id' and 'message'.\n- messages: read messages waiting for you from a worktree. Requires 'id'.\n\nWorktrees live outside the repository, under ~/.pi/.doom/git/worktrees.`;\n\nexport interface RunWorktreeToolDetails {\n action: WorktreeAction;\n records?: WorktreeRecord[];\n}\n\n/**\n * Rejects a call that is not exactly one documented action shape.\n *\n * Unknown fields are refused rather than ignored. A caller that passes 'branch'\n * to close_worktree has a different operation in mind than the one that would\n * run, and silently dropping the field would carry it out anyway.\n */\nexport function validateParams(input: unknown): RunWorktreeToolParams {\n if (typeof input !== 'object' || input === null || Array.isArray(input)) {\n throw invalidRequest('run_worktree parameters must be an object.', 'Call it with one documented action shape.');\n }\n const record = input as Record<string, unknown>;\n const action = record.action;\n if (typeof action !== 'string' || !IMPLEMENTED_ACTIONS.has(action)) {\n throw invalidRequest(\n `Unknown action ${typeof action === 'string' ? `'${action}'` : 'value'}.`,\n `Use one of: ${[...IMPLEMENTED_ACTIONS].join(', ')}.`,\n );\n }\n const allowed = WORKTREE_ACTION_FIELDS[action as WorktreeAction];\n const unexpected = Object.keys(record).filter((key) => !allowed.includes(key));\n if (unexpected.length > 0) {\n throw invalidRequest(\n `action='${action}' does not accept: ${unexpected.join(', ')}.`,\n `Fields for '${action}': ${allowed.join(', ')}.`,\n );\n }\n for (const field of ['branch', 'id'] as const) {\n if (allowed.includes(field) && field in record) {\n const value = record[field];\n if (typeof value !== 'string' || value.trim() === '') {\n throw invalidRequest(`action='${action}' requires a nonblank '${field}'.`, `Pass a ${field}.`);\n }\n }\n }\n if (action === WORKTREE_ACTIONS.spawn_worktree && typeof record.branch !== 'string') {\n throw invalidRequest(\"action='spawn_worktree' requires 'branch'.\", 'Name the branch to create.');\n }\n if (\n (action === WORKTREE_ACTIONS.close_worktree ||\n action === WORKTREE_ACTIONS.status ||\n action === WORKTREE_ACTIONS.merge) &&\n typeof record.id !== 'string'\n ) {\n throw invalidRequest(`action='${action}' requires 'id'.`, 'Call list to see the current ids.');\n }\n return record as RunWorktreeToolParams;\n}\n\nfunction describe(record: WorktreeRecord): string {\n return `${record.id} ${record.branch} ${record.status} ${record.path}`;\n}\n\n/** A tool result's content, which Pi expects as content blocks rather than a string. */\nfunction text(...lines: readonly string[]): [{ type: 'text'; text: string }] {\n return [{ type: 'text', text: lines.join('\\n') }];\n}\n\nasync function run(\n params: RunWorktreeToolParams,\n operations: WorktreeOperations,\n ctx: ExtensionContext,\n host: { signal?: AbortSignal; onProgress?: (label: string) => void } = {},\n): Promise<AgentToolResult<RunWorktreeToolDetails>> {\n const context = { cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId() };\n switch (params.action) {\n case WORKTREE_ACTIONS.spawn_worktree: {\n const record = await operations.spawn(\n context,\n {\n branch: params.branch,\n ...(params.baseRef === undefined ? {} : { baseRef: params.baseRef }),\n ...(params.name === undefined ? {} : { name: params.name }),\n },\n host,\n );\n return {\n content: text(\n `Worktree ${record.id} created on ${record.branch} from ${record.baseRef}.`,\n `Path: ${record.path}`,\n `Session ${record.sessionId} is starting; it appears nested under this one in the rail.`,\n ),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.close_worktree: {\n const record = await operations.close(context, params.id, params.force ?? false);\n return {\n content: text(`Worktree ${record.id} (${record.branch}) closed and removed.`),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.list: {\n const records = await operations.list(context);\n return {\n content:\n records.length === 0\n ? text('No worktrees for this repository.')\n : text(`${String(records.length)} worktree(s):`, ...records.map(describe)),\n details: { action: params.action, records },\n };\n }\n case WORKTREE_ACTIONS.status: {\n const { record, dirtyFiles } = await operations.status(context, params.id);\n return {\n content: text(\n describe(record),\n `Session: ${record.sessionId}`,\n dirtyFiles.length === 0\n ? 'Clean.'\n : `${String(dirtyFiles.length)} uncommitted file(s): ${dirtyFiles.slice(0, 20).join(', ')}`,\n ),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.merge: {\n const record = await operations.merge(\n context,\n params.id,\n ...(params.message === undefined ? [] : ([params.message] as const)),\n );\n return {\n content: text(`Merged ${record.branch} into this checkout.`),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.send: {\n await operations.send(context, params.id, params.message);\n return { content: text(`Message sent to worktree ${params.id}.`), details: { action: params.action } };\n }\n case WORKTREE_ACTIONS.messages: {\n const waiting = await operations.messages(context, params.id);\n return {\n content:\n waiting.length === 0\n ? text('No messages.')\n : text(\n `${String(waiting.length)} message(s):`,\n ...waiting.map((entry) => `[${entry.sentAt}] ${entry.from}: ${entry.text}`),\n ),\n details: { action: params.action },\n };\n }\n case WORKTREE_ACTIONS.prune: {\n const dryRun = params.dryRun ?? false;\n const plan = await operations.prune(context, dryRun);\n const lines = [\n dryRun ? 'Prune plan (nothing was changed):' : 'Pruned:',\n `Remove: ${plan.remove.length === 0 ? 'none' : plan.remove.map((record) => record.id).join(', ')}`,\n `Forget: ${plan.forget.length === 0 ? 'none' : plan.forget.map((record) => record.id).join(', ')}`,\n `Kept, uncommitted work: ${plan.keptDirty.length === 0 ? 'none' : plan.keptDirty.map((record) => record.id).join(', ')}`,\n `Untracked directories, left alone: ${plan.untracked.length === 0 ? 'none' : plan.untracked.join(', ')}`,\n ...(plan.keptBranches.length === 0\n ? []\n : [`Branches kept, they hold commits: ${plan.keptBranches.join(', ')}`]),\n ];\n return { content: text(...lines), details: { action: params.action } };\n }\n }\n}\n\n/** Registers `run_worktree` on a Pi host, once. */\nexport function registerRunWorktreeTool(pi: ExtensionAPI, operations: WorktreeOperations): void {\n const tool: ToolDefinition<typeof RunWorktreeParams, RunWorktreeToolDetails> = {\n name: RUN_WORKTREE_TOOL_NAME,\n label: 'Worktree',\n description: DESCRIPTION,\n parameters: RunWorktreeParams,\n prepareArguments: validateParams,\n // The signal and the progress callback are the host's two answers to a\n // call that takes minutes. Dropping them is what let an interrupted spawn\n // run on with nobody waiting for it, and left the caller staring at\n // nothing while it did.\n execute: async (_id, rawParams, signal, onUpdate, ctx) => {\n const params = validateParams(rawParams);\n return await run(params, operations, ctx, {\n ...(signal === undefined ? {} : { signal }),\n onProgress: (label) => onUpdate?.({ content: text(label), details: { action: params.action } }),\n });\n },\n };\n pi.registerTool(tool);\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import{invalidRequest as e}from"../../../services/support/errors.mjs";import{RunWorktreeParams as t,WORKTREE_ACTIONS as n,WORKTREE_ACTION_FIELDS as r}from"../../../schemas/runWorktreeTool.mjs";const i=`run_worktree`,a=new Set(Object.values(n));function o(t){if(typeof t!=`object`||!t||Array.isArray(t))throw e(`run_worktree parameters must be an object.`,`Call it with one documented action shape.`);let i=t,o=i.action;if(typeof o!=`string`||!a.has(o))throw e(`Unknown action ${typeof o==`string`?`'${o}'`:`value`}.`,`Use one of: ${[...a].join(`, `)}.`);let s=r[o],c=Object.keys(i).filter(e=>!s.includes(e));if(c.length>0)throw e(`action='${o}' does not accept: ${c.join(`, `)}.`,`Fields for '${o}': ${s.join(`, `)}.`);for(let t of[`branch`,`id`])if(s.includes(t)&&t in i){let n=i[t];if(typeof n!=`string`||n.trim()===``)throw e(`action='${o}' requires a nonblank '${t}'.`,`Pass a ${t}.`)}if(o===n.spawn_worktree&&typeof i.branch!=`string`)throw e(`action='spawn_worktree' requires 'branch'.`,`Name the branch to create.`);if((o===n.close_worktree||o===n.status||o===n.merge)&&typeof i.id!=`string`)throw e(`action='${o}' requires 'id'.`,`Call list to see the current ids.`);return i}function s(e){return`${e.id} ${e.branch} ${e.status} ${e.path}`}function c(...e){return[{type:`text`,text:e.join(`
|
|
2
|
-
`)}]}async function l(e,t,r){let
|
|
2
|
+
`)}]}async function l(e,t,r,i={}){let a={cwd:r.cwd,sessionId:r.sessionManager.getSessionId()};switch(e.action){case n.spawn_worktree:{let n=await t.spawn(a,{branch:e.branch,...e.baseRef===void 0?{}:{baseRef:e.baseRef},...e.name===void 0?{}:{name:e.name}},i);return{content:c(`Worktree ${n.id} created on ${n.branch} from ${n.baseRef}.`,`Path: ${n.path}`,`Session ${n.sessionId} is starting; it appears nested under this one in the rail.`),details:{action:e.action,records:[n]}}}case n.close_worktree:{let n=await t.close(a,e.id,e.force??!1);return{content:c(`Worktree ${n.id} (${n.branch}) closed and removed.`),details:{action:e.action,records:[n]}}}case n.list:{let n=await t.list(a);return{content:n.length===0?c(`No worktrees for this repository.`):c(`${String(n.length)} worktree(s):`,...n.map(s)),details:{action:e.action,records:n}}}case n.status:{let{record:n,dirtyFiles:r}=await t.status(a,e.id);return{content:c(s(n),`Session: ${n.sessionId}`,r.length===0?`Clean.`:`${String(r.length)} uncommitted file(s): ${r.slice(0,20).join(`, `)}`),details:{action:e.action,records:[n]}}}case n.merge:{let n=await t.merge(a,e.id,...e.message===void 0?[]:[e.message]);return{content:c(`Merged ${n.branch} into this checkout.`),details:{action:e.action,records:[n]}}}case n.send:return await t.send(a,e.id,e.message),{content:c(`Message sent to worktree ${e.id}.`),details:{action:e.action}};case n.messages:{let n=await t.messages(a,e.id);return{content:n.length===0?c(`No messages.`):c(`${String(n.length)} message(s):`,...n.map(e=>`[${e.sentAt}] ${e.from}: ${e.text}`)),details:{action:e.action}}}case n.prune:{let n=e.dryRun??!1,r=await t.prune(a,n);return{content:c(n?`Prune plan (nothing was changed):`:`Pruned:`,`Remove: ${r.remove.length===0?`none`:r.remove.map(e=>e.id).join(`, `)}`,`Forget: ${r.forget.length===0?`none`:r.forget.map(e=>e.id).join(`, `)}`,`Kept, uncommitted work: ${r.keptDirty.length===0?`none`:r.keptDirty.map(e=>e.id).join(`, `)}`,`Untracked directories, left alone: ${r.untracked.length===0?`none`:r.untracked.join(`, `)}`,...r.keptBranches.length===0?[]:[`Branches kept, they hold commits: ${r.keptBranches.join(`, `)}`]),details:{action:e.action}}}}}function u(e,n){let r={name:i,label:`Worktree`,description:`Create and manage git worktrees, each with its own DoomPi session nested under this one.
|
|
3
3
|
|
|
4
4
|
Actions:
|
|
5
5
|
- spawn_worktree: create a worktree on a new branch and start a session in it. Requires 'branch'. Optional 'baseRef' (defaults to the current branch), 'name' for the rail, 'task' as the first message.
|
|
@@ -11,5 +11,5 @@ Actions:
|
|
|
11
11
|
- send: send a message to the other side of a worktree. Requires 'id' and 'message'.
|
|
12
12
|
- messages: read messages waiting for you from a worktree. Requires 'id'.
|
|
13
13
|
|
|
14
|
-
Worktrees live outside the repository, under ~/.pi/.doom/git/worktrees.`,parameters:t,prepareArguments:o,execute:async(e,t,r,i,a)=>
|
|
14
|
+
Worktrees live outside the repository, under ~/.pi/.doom/git/worktrees.`,parameters:t,prepareArguments:o,execute:async(e,t,r,i,a)=>{let s=o(t);return await l(s,n,a,{...r===void 0?{}:{signal:r},onProgress:e=>i?.({content:c(e),details:{action:s.action}})})}};e.registerTool(r)}export{i as RUN_WORKTREE_TOOL_NAME,u as registerRunWorktreeTool,o as validateParams};
|
|
15
15
|
//# sourceMappingURL=runWorktreeTool.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runWorktreeTool.mjs","names":[],"sources":["../../../../src/adapters/pi/extensions/runWorktreeTool.ts"],"sourcesContent":["import type { AgentToolResult } from '@earendil-works/pi-agent-core';\nimport type { ExtensionAPI, ExtensionContext, ToolDefinition } from '@earendil-works/pi-coding-agent';\nimport {\n RunWorktreeParams,\n WORKTREE_ACTION_FIELDS,\n WORKTREE_ACTIONS,\n type WorktreeAction,\n type RunWorktreeToolParams,\n} from '../../../schemas/runWorktreeTool.ts';\nimport { invalidRequest } from '../../../services/support/errors.ts';\nimport type { WorktreeOperations } from '../../worktree/worktreeOperations.ts';\nimport type { WorktreeRecord } from '../../../types/worktreeRegistry.ts';\n\nexport const RUN_WORKTREE_TOOL_NAME = 'run_worktree';\n\nconst IMPLEMENTED_ACTIONS: ReadonlySet<string> = new Set(Object.values(WORKTREE_ACTIONS));\n\nconst DESCRIPTION = `Create and manage git worktrees, each with its own DoomPi session nested under this one.\n\nActions:\n- spawn_worktree: create a worktree on a new branch and start a session in it. Requires 'branch'. Optional 'baseRef' (defaults to the current branch), 'name' for the rail, 'task' as the first message.\n- close_worktree: stop a worktree's session and delete its directory. Requires 'id'. Refuses a dirty tree unless 'force' is true.\n- list: every worktree for this repository, with status.\n- status: one worktree and its uncommitted files. Requires 'id'.\n- merge: merge a worktree's branch into this checkout. Requires 'id'. Refuses when this checkout is dirty.\n- prune: remove orphaned worktrees. Pass 'dryRun' to see the plan without destroying anything.\n- send: send a message to the other side of a worktree. Requires 'id' and 'message'.\n- messages: read messages waiting for you from a worktree. Requires 'id'.\n\nWorktrees live outside the repository, under ~/.pi/.doom/git/worktrees.`;\n\nexport interface RunWorktreeToolDetails {\n action: WorktreeAction;\n records?: WorktreeRecord[];\n}\n\n/**\n * Rejects a call that is not exactly one documented action shape.\n *\n * Unknown fields are refused rather than ignored. A caller that passes 'branch'\n * to close_worktree has a different operation in mind than the one that would\n * run, and silently dropping the field would carry it out anyway.\n */\nexport function validateParams(input: unknown): RunWorktreeToolParams {\n if (typeof input !== 'object' || input === null || Array.isArray(input)) {\n throw invalidRequest('run_worktree parameters must be an object.', 'Call it with one documented action shape.');\n }\n const record = input as Record<string, unknown>;\n const action = record.action;\n if (typeof action !== 'string' || !IMPLEMENTED_ACTIONS.has(action)) {\n throw invalidRequest(\n `Unknown action ${typeof action === 'string' ? `'${action}'` : 'value'}.`,\n `Use one of: ${[...IMPLEMENTED_ACTIONS].join(', ')}.`,\n );\n }\n const allowed = WORKTREE_ACTION_FIELDS[action as WorktreeAction];\n const unexpected = Object.keys(record).filter((key) => !allowed.includes(key));\n if (unexpected.length > 0) {\n throw invalidRequest(\n `action='${action}' does not accept: ${unexpected.join(', ')}.`,\n `Fields for '${action}': ${allowed.join(', ')}.`,\n );\n }\n for (const field of ['branch', 'id'] as const) {\n if (allowed.includes(field) && field in record) {\n const value = record[field];\n if (typeof value !== 'string' || value.trim() === '') {\n throw invalidRequest(`action='${action}' requires a nonblank '${field}'.`, `Pass a ${field}.`);\n }\n }\n }\n if (action === WORKTREE_ACTIONS.spawn_worktree && typeof record.branch !== 'string') {\n throw invalidRequest(\"action='spawn_worktree' requires 'branch'.\", 'Name the branch to create.');\n }\n if (\n (action === WORKTREE_ACTIONS.close_worktree ||\n action === WORKTREE_ACTIONS.status ||\n action === WORKTREE_ACTIONS.merge) &&\n typeof record.id !== 'string'\n ) {\n throw invalidRequest(`action='${action}' requires 'id'.`, 'Call list to see the current ids.');\n }\n return record as RunWorktreeToolParams;\n}\n\nfunction describe(record: WorktreeRecord): string {\n return `${record.id} ${record.branch} ${record.status} ${record.path}`;\n}\n\n/** A tool result's content, which Pi expects as content blocks rather than a string. */\nfunction text(...lines: readonly string[]): [{ type: 'text'; text: string }] {\n return [{ type: 'text', text: lines.join('\\n') }];\n}\n\nasync function run(\n params: RunWorktreeToolParams,\n operations: WorktreeOperations,\n ctx: ExtensionContext,\n): Promise<AgentToolResult<RunWorktreeToolDetails>> {\n const context = { cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId() };\n switch (params.action) {\n case WORKTREE_ACTIONS.spawn_worktree: {\n const record = await operations.spawn(context, {\n branch: params.branch,\n ...(params.baseRef === undefined ? {} : { baseRef: params.baseRef }),\n ...(params.name === undefined ? {} : { name: params.name }),\n });\n return {\n content: text(\n `Worktree ${record.id} created on ${record.branch} from ${record.baseRef}.`,\n `Path: ${record.path}`,\n `Session ${record.sessionId} is starting; it appears nested under this one in the rail.`,\n ),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.close_worktree: {\n const record = await operations.close(context, params.id, params.force ?? false);\n return {\n content: text(`Worktree ${record.id} (${record.branch}) closed and removed.`),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.list: {\n const records = await operations.list(context);\n return {\n content:\n records.length === 0\n ? text('No worktrees for this repository.')\n : text(`${String(records.length)} worktree(s):`, ...records.map(describe)),\n details: { action: params.action, records },\n };\n }\n case WORKTREE_ACTIONS.status: {\n const { record, dirtyFiles } = await operations.status(context, params.id);\n return {\n content: text(\n describe(record),\n `Session: ${record.sessionId}`,\n dirtyFiles.length === 0\n ? 'Clean.'\n : `${String(dirtyFiles.length)} uncommitted file(s): ${dirtyFiles.slice(0, 20).join(', ')}`,\n ),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.merge: {\n const record = await operations.merge(\n context,\n params.id,\n ...(params.message === undefined ? [] : ([params.message] as const)),\n );\n return {\n content: text(`Merged ${record.branch} into this checkout.`),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.send: {\n await operations.send(context, params.id, params.message);\n return { content: text(`Message sent to worktree ${params.id}.`), details: { action: params.action } };\n }\n case WORKTREE_ACTIONS.messages: {\n const waiting = await operations.messages(context, params.id);\n return {\n content:\n waiting.length === 0\n ? text('No messages.')\n : text(\n `${String(waiting.length)} message(s):`,\n ...waiting.map((entry) => `[${entry.sentAt}] ${entry.from}: ${entry.text}`),\n ),\n details: { action: params.action },\n };\n }\n case WORKTREE_ACTIONS.prune: {\n const dryRun = params.dryRun ?? false;\n const plan = await operations.prune(context, dryRun);\n const lines = [\n dryRun ? 'Prune plan (nothing was changed):' : 'Pruned:',\n `Remove: ${plan.remove.length === 0 ? 'none' : plan.remove.map((record) => record.id).join(', ')}`,\n `Forget: ${plan.forget.length === 0 ? 'none' : plan.forget.map((record) => record.id).join(', ')}`,\n `Kept, uncommitted work: ${plan.keptDirty.length === 0 ? 'none' : plan.keptDirty.map((record) => record.id).join(', ')}`,\n `Untracked directories, left alone: ${plan.untracked.length === 0 ? 'none' : plan.untracked.join(', ')}`,\n ];\n return { content: text(...lines), details: { action: params.action } };\n }\n }\n}\n\n/** Registers `run_worktree` on a Pi host, once. */\nexport function registerRunWorktreeTool(pi: ExtensionAPI, operations: WorktreeOperations): void {\n const tool: ToolDefinition<typeof RunWorktreeParams, RunWorktreeToolDetails> = {\n name: RUN_WORKTREE_TOOL_NAME,\n label: 'Worktree',\n description: DESCRIPTION,\n parameters: RunWorktreeParams,\n prepareArguments: validateParams,\n execute: async (_id, rawParams, _signal, _onUpdate, ctx) => run(validateParams(rawParams), operations, ctx),\n };\n pi.registerTool(tool);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"runWorktreeTool.mjs","names":[],"sources":["../../../../src/adapters/pi/extensions/runWorktreeTool.ts"],"sourcesContent":["import type { AgentToolResult } from '@earendil-works/pi-agent-core';\nimport type { ExtensionAPI, ExtensionContext, ToolDefinition } from '@earendil-works/pi-coding-agent';\nimport {\n RunWorktreeParams,\n WORKTREE_ACTION_FIELDS,\n WORKTREE_ACTIONS,\n type WorktreeAction,\n type RunWorktreeToolParams,\n} from '../../../schemas/runWorktreeTool.ts';\nimport { invalidRequest } from '../../../services/support/errors.ts';\nimport type { WorktreeOperations } from '../../worktree/worktreeOperations.ts';\nimport type { WorktreeRecord } from '../../../types/worktreeRegistry.ts';\n\nexport const RUN_WORKTREE_TOOL_NAME = 'run_worktree';\n\nconst IMPLEMENTED_ACTIONS: ReadonlySet<string> = new Set(Object.values(WORKTREE_ACTIONS));\n\nconst DESCRIPTION = `Create and manage git worktrees, each with its own DoomPi session nested under this one.\n\nActions:\n- spawn_worktree: create a worktree on a new branch and start a session in it. Requires 'branch'. Optional 'baseRef' (defaults to the current branch), 'name' for the rail, 'task' as the first message.\n- close_worktree: stop a worktree's session and delete its directory. Requires 'id'. Refuses a dirty tree unless 'force' is true.\n- list: every worktree for this repository, with status.\n- status: one worktree and its uncommitted files. Requires 'id'.\n- merge: merge a worktree's branch into this checkout. Requires 'id'. Refuses when this checkout is dirty.\n- prune: remove orphaned worktrees. Pass 'dryRun' to see the plan without destroying anything.\n- send: send a message to the other side of a worktree. Requires 'id' and 'message'.\n- messages: read messages waiting for you from a worktree. Requires 'id'.\n\nWorktrees live outside the repository, under ~/.pi/.doom/git/worktrees.`;\n\nexport interface RunWorktreeToolDetails {\n action: WorktreeAction;\n records?: WorktreeRecord[];\n}\n\n/**\n * Rejects a call that is not exactly one documented action shape.\n *\n * Unknown fields are refused rather than ignored. A caller that passes 'branch'\n * to close_worktree has a different operation in mind than the one that would\n * run, and silently dropping the field would carry it out anyway.\n */\nexport function validateParams(input: unknown): RunWorktreeToolParams {\n if (typeof input !== 'object' || input === null || Array.isArray(input)) {\n throw invalidRequest('run_worktree parameters must be an object.', 'Call it with one documented action shape.');\n }\n const record = input as Record<string, unknown>;\n const action = record.action;\n if (typeof action !== 'string' || !IMPLEMENTED_ACTIONS.has(action)) {\n throw invalidRequest(\n `Unknown action ${typeof action === 'string' ? `'${action}'` : 'value'}.`,\n `Use one of: ${[...IMPLEMENTED_ACTIONS].join(', ')}.`,\n );\n }\n const allowed = WORKTREE_ACTION_FIELDS[action as WorktreeAction];\n const unexpected = Object.keys(record).filter((key) => !allowed.includes(key));\n if (unexpected.length > 0) {\n throw invalidRequest(\n `action='${action}' does not accept: ${unexpected.join(', ')}.`,\n `Fields for '${action}': ${allowed.join(', ')}.`,\n );\n }\n for (const field of ['branch', 'id'] as const) {\n if (allowed.includes(field) && field in record) {\n const value = record[field];\n if (typeof value !== 'string' || value.trim() === '') {\n throw invalidRequest(`action='${action}' requires a nonblank '${field}'.`, `Pass a ${field}.`);\n }\n }\n }\n if (action === WORKTREE_ACTIONS.spawn_worktree && typeof record.branch !== 'string') {\n throw invalidRequest(\"action='spawn_worktree' requires 'branch'.\", 'Name the branch to create.');\n }\n if (\n (action === WORKTREE_ACTIONS.close_worktree ||\n action === WORKTREE_ACTIONS.status ||\n action === WORKTREE_ACTIONS.merge) &&\n typeof record.id !== 'string'\n ) {\n throw invalidRequest(`action='${action}' requires 'id'.`, 'Call list to see the current ids.');\n }\n return record as RunWorktreeToolParams;\n}\n\nfunction describe(record: WorktreeRecord): string {\n return `${record.id} ${record.branch} ${record.status} ${record.path}`;\n}\n\n/** A tool result's content, which Pi expects as content blocks rather than a string. */\nfunction text(...lines: readonly string[]): [{ type: 'text'; text: string }] {\n return [{ type: 'text', text: lines.join('\\n') }];\n}\n\nasync function run(\n params: RunWorktreeToolParams,\n operations: WorktreeOperations,\n ctx: ExtensionContext,\n host: { signal?: AbortSignal; onProgress?: (label: string) => void } = {},\n): Promise<AgentToolResult<RunWorktreeToolDetails>> {\n const context = { cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId() };\n switch (params.action) {\n case WORKTREE_ACTIONS.spawn_worktree: {\n const record = await operations.spawn(\n context,\n {\n branch: params.branch,\n ...(params.baseRef === undefined ? {} : { baseRef: params.baseRef }),\n ...(params.name === undefined ? {} : { name: params.name }),\n },\n host,\n );\n return {\n content: text(\n `Worktree ${record.id} created on ${record.branch} from ${record.baseRef}.`,\n `Path: ${record.path}`,\n `Session ${record.sessionId} is starting; it appears nested under this one in the rail.`,\n ),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.close_worktree: {\n const record = await operations.close(context, params.id, params.force ?? false);\n return {\n content: text(`Worktree ${record.id} (${record.branch}) closed and removed.`),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.list: {\n const records = await operations.list(context);\n return {\n content:\n records.length === 0\n ? text('No worktrees for this repository.')\n : text(`${String(records.length)} worktree(s):`, ...records.map(describe)),\n details: { action: params.action, records },\n };\n }\n case WORKTREE_ACTIONS.status: {\n const { record, dirtyFiles } = await operations.status(context, params.id);\n return {\n content: text(\n describe(record),\n `Session: ${record.sessionId}`,\n dirtyFiles.length === 0\n ? 'Clean.'\n : `${String(dirtyFiles.length)} uncommitted file(s): ${dirtyFiles.slice(0, 20).join(', ')}`,\n ),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.merge: {\n const record = await operations.merge(\n context,\n params.id,\n ...(params.message === undefined ? [] : ([params.message] as const)),\n );\n return {\n content: text(`Merged ${record.branch} into this checkout.`),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.send: {\n await operations.send(context, params.id, params.message);\n return { content: text(`Message sent to worktree ${params.id}.`), details: { action: params.action } };\n }\n case WORKTREE_ACTIONS.messages: {\n const waiting = await operations.messages(context, params.id);\n return {\n content:\n waiting.length === 0\n ? text('No messages.')\n : text(\n `${String(waiting.length)} message(s):`,\n ...waiting.map((entry) => `[${entry.sentAt}] ${entry.from}: ${entry.text}`),\n ),\n details: { action: params.action },\n };\n }\n case WORKTREE_ACTIONS.prune: {\n const dryRun = params.dryRun ?? false;\n const plan = await operations.prune(context, dryRun);\n const lines = [\n dryRun ? 'Prune plan (nothing was changed):' : 'Pruned:',\n `Remove: ${plan.remove.length === 0 ? 'none' : plan.remove.map((record) => record.id).join(', ')}`,\n `Forget: ${plan.forget.length === 0 ? 'none' : plan.forget.map((record) => record.id).join(', ')}`,\n `Kept, uncommitted work: ${plan.keptDirty.length === 0 ? 'none' : plan.keptDirty.map((record) => record.id).join(', ')}`,\n `Untracked directories, left alone: ${plan.untracked.length === 0 ? 'none' : plan.untracked.join(', ')}`,\n ...(plan.keptBranches.length === 0\n ? []\n : [`Branches kept, they hold commits: ${plan.keptBranches.join(', ')}`]),\n ];\n return { content: text(...lines), details: { action: params.action } };\n }\n }\n}\n\n/** Registers `run_worktree` on a Pi host, once. */\nexport function registerRunWorktreeTool(pi: ExtensionAPI, operations: WorktreeOperations): void {\n const tool: ToolDefinition<typeof RunWorktreeParams, RunWorktreeToolDetails> = {\n name: RUN_WORKTREE_TOOL_NAME,\n label: 'Worktree',\n description: DESCRIPTION,\n parameters: RunWorktreeParams,\n prepareArguments: validateParams,\n // The signal and the progress callback are the host's two answers to a\n // call that takes minutes. Dropping them is what let an interrupted spawn\n // run on with nobody waiting for it, and left the caller staring at\n // nothing while it did.\n execute: async (_id, rawParams, signal, onUpdate, ctx) => {\n const params = validateParams(rawParams);\n return await run(params, operations, ctx, {\n ...(signal === undefined ? {} : { signal }),\n onProgress: (label) => onUpdate?.({ content: text(label), details: { action: params.action } }),\n });\n },\n };\n pi.registerTool(tool);\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../worktree/gitCli.cjs"),t=require("../hub/hubClient.cjs"),n=require("../filesystem/paths.cjs"),r=require("../worktree/worktreeRegistry.cjs"),i=require("../../services/support/errors.cjs"),a=require("../worktree/worktreeOperations.cjs"),o=require("../../types/webWorktrees.cjs");function s(e){return{worktrees:e.worktrees,...e.pending===void 0?{}:{pending:e.pending},...e.error===void 0?{}:{error:e.error}}}function c(e,t){try{return r.createWorktreeRegistry(n.registryFile(e.cwd,t.homeDir)).list().flatMap(n=>{let r=n.parentSessionId===e.sessionId,i=!r&&!t.isSessionLive(t.registryDir,n.parentSessionId);return!r&&!i?[]:[{id:n.id,branch:n.branch,path:n.path,sessionId:n.status===`orphaned`?null:n.sessionId,orphaned:n.status===`orphaned`,unowned:i}]})}catch{return[]}}function l(e,t){return e.length===t.length&&e.every((e,n)=>{let r=t[n];return r!==void 0&&e.id===r.id&&e.branch===r.branch&&e.sessionId===r.sessionId&&e.orphaned===r.orphaned&&e.unowned===r.unowned})}function u(e){if(typeof e!=`object`||!e)return;let t=e;if(t.action===`create`)return typeof t.branch!=`string`||t.branch.trim()===``?void 0:{action:`create`,branch:t.branch.trim(),...typeof t.baseRef==`string`&&t.baseRef.trim()!==``?{baseRef:t.baseRef.trim()}:{}};if(t.action===`close`)return typeof t.id!=`string`||t.id===``?void 0:{action:`close`,id:t.id,...t.force===!0?{force:!0}:{}}}function d(e){return e instanceof i.DoomGitExpectedError?e.message:`The worktree operation failed.`}function f(r={}){let i=r.intervalMs??4e3,f;return{frameType:o.GIT_WORKTREES_TYPE,start(o){let p=r.operations??a.createWorktreeOperations({git:e.createWorktreeGit()}),m={registryDir:r.registryDir??n.hubRegistryDir(),homeDir:r.homeDir,isSessionLive:r.isSessionLive??t.sessionIsLive},h=new Map,g=new Map,_=new Map,v=new Set,y=(e,t)=>{let n=h.get(e.sessionId),r={worktrees:c(e,m),pending:n?.pending,error:n?.error};(t||n===void 0||!l(n.worktrees,r.worktrees)||n.pending!==r.pending||n.error!==r.error)&&(h.set(e.sessionId,r),o.publish(e.sessionId,s(r)))},b=(e,t,n)=>{let r=h.get(e.sessionId);h.set(e.sessionId,{worktrees:r?.worktrees??c(e,m),pending:t,error:n}),y(e,!0)},x=async(e,t)=>{let n={cwd:e.cwd,sessionId:e.sessionId};if(t.action===`create`){b(e,`creating ${t.branch}\u2026`,void 0),await p.spawn(n,{branch:t.branch,...t.baseRef===void 0?{}:{baseRef:t.baseRef}});return}b(e,`closing ${t.id}\u2026`,void 0),await p.close(n,t.id,t.force??!1)};return f=(e,t)=>{let n=u(t);n!==void 0&&(v.has(e.sessionId)||(v.add(e.sessionId),x(e,n).then(()=>{b(e,void 0,void 0)}).catch(t=>{o.onNotice(`worktree command failed for ${e.sessionId} (${d(t)})`),b(e,void 0,d(t))}).finally(()=>{v.delete(e.sessionId)})))},{payloadFor(e){let t=h.get(e.sessionId);return t===void 0?void 0:s(t)},sessionAdded(e){g.set(e.sessionId,e),y(e,!1);let t=setInterval(()=>{y(e,!1)},i);t.unref?.(),_.set(e.sessionId,t)},sessionRemoved(e){let t=_.get(e);t!==void 0&&clearInterval(t),_.delete(e),h.delete(e),g.delete(e),v.delete(e)},close(){for(let e of _.values())clearInterval(e);_.clear(),h.clear(),g.clear(),v.clear()}}},receive(e,t){f?.(e,t)}}}const p=[f()];exports.createWorktreesChannel=f,exports.webHubChannels=p;
|
|
1
|
+
const e=require("../worktree/gitCli.cjs"),t=require("../hub/hubClient.cjs"),n=require("../filesystem/paths.cjs"),r=require("../worktree/worktreeRegistry.cjs"),i=require("../../services/support/errors.cjs"),a=require("../worktree/worktreeOperations.cjs"),o=require("../../types/webWorktrees.cjs");function s(e){return{worktrees:e.worktrees,...e.pending===void 0?{}:{pending:e.pending},...e.error===void 0?{}:{error:e.error}}}function c(e,t){try{return r.createWorktreeRegistry(n.registryFile(e.cwd,t.homeDir)).list().flatMap(n=>{let r=n.parentSessionId===e.sessionId,i=!r&&!t.isSessionLive(t.registryDir,n.parentSessionId);return!r&&!i?[]:[{id:n.id,branch:n.branch,path:n.path,sessionId:n.status===`orphaned`?null:n.sessionId,orphaned:n.status===`orphaned`,unowned:i}]})}catch{return[]}}function l(e,t){return e.length===t.length&&e.every((e,n)=>{let r=t[n];return r!==void 0&&e.id===r.id&&e.branch===r.branch&&e.sessionId===r.sessionId&&e.orphaned===r.orphaned&&e.unowned===r.unowned})}function u(e){if(typeof e!=`object`||!e)return;let t=e;if(t.action===`create`)return typeof t.branch!=`string`||t.branch.trim()===``?void 0:{action:`create`,branch:t.branch.trim(),...typeof t.baseRef==`string`&&t.baseRef.trim()!==``?{baseRef:t.baseRef.trim()}:{}};if(t.action===`close`)return typeof t.id!=`string`||t.id===``?void 0:{action:`close`,id:t.id,...t.force===!0?{force:!0}:{}}}function d(e){return e instanceof i.DoomGitExpectedError?e.message:`The worktree operation failed.`}function f(r={}){let i=r.intervalMs??4e3,f;return{frameType:o.GIT_WORKTREES_TYPE,start(o){let p=r.operations??a.createWorktreeOperations({git:e.createWorktreeGit()}),m={registryDir:r.registryDir??n.hubRegistryDir(),homeDir:r.homeDir,isSessionLive:r.isSessionLive??t.sessionIsLive},h=new Map,g=new Map,_=new Map,v=new Set,y=(e,t)=>{let n=h.get(e.sessionId),r={worktrees:c(e,m),pending:n?.pending,error:n?.error};(t||n===void 0||!l(n.worktrees,r.worktrees)||n.pending!==r.pending||n.error!==r.error)&&(h.set(e.sessionId,r),o.publish(e.sessionId,s(r)))},b=(e,t,n)=>{let r=h.get(e.sessionId);h.set(e.sessionId,{worktrees:r?.worktrees??c(e,m),pending:t,error:n}),y(e,!0)},x=async(e,t)=>{let n={cwd:e.cwd,sessionId:e.sessionId};if(t.action===`create`){b(e,`creating ${t.branch}\u2026`,void 0),await p.spawn(n,{branch:t.branch,...t.baseRef===void 0?{}:{baseRef:t.baseRef}},{onProgress:t=>b(e,t,void 0)});return}b(e,`closing ${t.id}\u2026`,void 0),await p.close(n,t.id,t.force??!1)};return f=(e,t)=>{let n=u(t);n!==void 0&&(v.has(e.sessionId)||(v.add(e.sessionId),x(e,n).then(()=>{b(e,void 0,void 0)}).catch(t=>{o.onNotice(`worktree command failed for ${e.sessionId} (${d(t)})`),b(e,void 0,d(t))}).finally(()=>{v.delete(e.sessionId)})))},{payloadFor(e){let t=h.get(e.sessionId);return t===void 0?void 0:s(t)},sessionAdded(e){g.set(e.sessionId,e),y(e,!1);let t=setInterval(()=>{y(e,!1)},i);t.unref?.(),_.set(e.sessionId,t)},sessionRemoved(e){let t=_.get(e);t!==void 0&&clearInterval(t),_.delete(e),h.delete(e),g.delete(e),v.delete(e)},close(){for(let e of _.values())clearInterval(e);_.clear(),h.clear(),g.clear(),v.clear()}}},receive(e,t){f?.(e,t)}}}const p=[f()];exports.createWorktreesChannel=f,exports.webHubChannels=p;
|
|
2
2
|
//# sourceMappingURL=worktreesChannel.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktreesChannel.cjs","names":["createWorktreeRegistry","registryFile","DoomGitExpectedError","GIT_WORKTREES_TYPE","createWorktreeOperations","createWorktreeGit","hubRegistryDir","sessionIsLive"],"sources":["../../../src/adapters/web/worktreesChannel.ts"],"sourcesContent":["/**\n * The worktrees data channel, and the commands the dock sends back through it.\n *\n * DESIGN PATTERNS:\n * - Hub-scoped: it reads this machine's registry files, which a browser cannot.\n * - Scoped to the session that owns the worktree, not to the repository. Two\n * sessions in one checkout are two pieces of work; showing each the other's\n * worktrees offered a close button for something the reader never made.\n * A worktree whose parent session is gone is the exception: it is shown to\n * everyone in the repository, marked unowned, or nobody could ever close it.\n * - Poll, do not watch. The registry is a single small JSON file written by\n * this package alone, and a watcher on a path that may not exist yet costs\n * more than re-reading a few hundred bytes.\n * - Commands run the same `worktreeOperations` the tool runs, so the panel and\n * the agent cannot drift into two ideas of what creating a worktree means.\n * - Publish only what the dock renders. Paths and provenance stay in the\n * registry; the page gets a view.\n *\n * AVOID:\n * - Throwing out of a poll or a command. A registry that cannot be read is an\n * empty list, and a failed command is an error string on the next frame:\n * the dock degrades to what it can tell the reader, never to a dead channel.\n */\nimport type { HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { GIT_WORKTREES_TYPE, type GitWorktreesCommand, type WorktreeView } from '../../types/webWorktrees.ts';\nimport { DoomGitExpectedError } from '../../services/support/errors.ts';\nimport { hubRegistryDir, registryFile } from '../filesystem/paths.ts';\nimport { sessionIsLive } from '../hub/hubClient.ts';\nimport { createWorktreeGit } from '../worktree/gitCli.ts';\nimport { createWorktreeOperations, type WorktreeOperations } from '../worktree/worktreeOperations.ts';\nimport { createWorktreeRegistry } from '../worktree/worktreeRegistry.ts';\n\n/** How often a session's registry is re-read. Worktrees change by hand, not by the second. */\nconst POLL_MS = 4000;\n\ninterface SessionState {\n worktrees: WorktreeView[];\n pending: string | undefined;\n error: string | undefined;\n}\n\nfunction payloadOf(state: SessionState): Record<string, unknown> {\n return {\n worktrees: state.worktrees,\n ...(state.pending === undefined ? {} : { pending: state.pending }),\n ...(state.error === undefined ? {} : { error: state.error }),\n };\n}\n\n/** What the channel needs from its surroundings, injected so tests stay off this machine. */\nexport interface WorktreesChannelOptions {\n intervalMs?: number;\n operations?: WorktreeOperations;\n homeDir?: string;\n registryDir?: string;\n isSessionLive?: (registryDir: string, sessionId: string) => boolean;\n}\n\ninterface ViewDeps {\n registryDir: string;\n homeDir: string | undefined;\n isSessionLive: (registryDir: string, sessionId: string) => boolean;\n}\n\n/**\n * The worktrees one session may see: its own, plus any whose parent session\n * has gone. Liveness is the registry record's presence, the same signal the\n * rail uses to decide a session exists at all.\n */\nfunction viewsFor(scope: HubSessionScope, deps: ViewDeps): WorktreeView[] {\n try {\n return createWorktreeRegistry(registryFile(scope.cwd, deps.homeDir))\n .list()\n .flatMap((record) => {\n const owned = record.parentSessionId === scope.sessionId;\n const unowned = !owned && !deps.isSessionLive(deps.registryDir, record.parentSessionId);\n if (!owned && !unowned) return [];\n return [\n {\n id: record.id,\n branch: record.branch,\n path: record.path,\n sessionId: record.status === 'orphaned' ? null : record.sessionId,\n orphaned: record.status === 'orphaned',\n unowned,\n },\n ];\n });\n } catch {\n return [];\n }\n}\n\nfunction sameList(left: readonly WorktreeView[], right: readonly WorktreeView[]): boolean {\n if (left.length !== right.length) return false;\n return left.every((entry, index) => {\n const other = right[index];\n return (\n other !== undefined &&\n entry.id === other.id &&\n entry.branch === other.branch &&\n entry.sessionId === other.sessionId &&\n entry.orphaned === other.orphaned &&\n entry.unowned === other.unowned\n );\n });\n}\n\n/** A command the page sent, or undefined when the frame was not one. */\nfunction parseCommand(payload: unknown): GitWorktreesCommand | undefined {\n if (typeof payload !== 'object' || payload === null) return undefined;\n const value = payload as Record<string, unknown>;\n if (value.action === 'create') {\n if (typeof value.branch !== 'string' || value.branch.trim() === '') return undefined;\n return {\n action: 'create',\n branch: value.branch.trim(),\n ...(typeof value.baseRef === 'string' && value.baseRef.trim() !== '' ? { baseRef: value.baseRef.trim() } : {}),\n };\n }\n if (value.action === 'close') {\n if (typeof value.id !== 'string' || value.id === '') return undefined;\n return { action: 'close', id: value.id, ...(value.force === true ? { force: true } : {}) };\n }\n return undefined;\n}\n\n/**\n * What the reader is told when a command fails.\n *\n * An expected failure explains itself. Anything else is summarised, never\n * echoed: a git error can carry a remote URL with a token in it.\n */\nfunction failureText(error: unknown): string {\n return error instanceof DoomGitExpectedError ? error.message : 'The worktree operation failed.';\n}\n\n/**\n * One poller per managed session, publishing only when the list actually\n * changed so an idle cockpit stays quiet.\n */\nexport function createWorktreesChannel(options: WorktreesChannelOptions = {}): WebHubChannel {\n const intervalMs = options.intervalMs ?? POLL_MS;\n // Assigned by start, because receive is reachable only while the channel is\n // running and the command needs the state the poller keeps.\n let receiveCommand: ((scope: HubSessionScope, payload: unknown) => void) | undefined;\n\n return {\n frameType: GIT_WORKTREES_TYPE,\n start(host) {\n const worktrees = options.operations ?? createWorktreeOperations({ git: createWorktreeGit() });\n const views: ViewDeps = {\n registryDir: options.registryDir ?? hubRegistryDir(),\n homeDir: options.homeDir,\n isSessionLive: options.isSessionLive ?? sessionIsLive,\n };\n const latest = new Map<string, SessionState>();\n const scopes = new Map<string, HubSessionScope>();\n const timers = new Map<string, ReturnType<typeof setInterval>>();\n const busy = new Set<string>();\n\n /** Republishes a session, skipping a frame that would say nothing new. */\n const publish = (scope: HubSessionScope, force: boolean): void => {\n const previous = latest.get(scope.sessionId);\n const next: SessionState = {\n worktrees: viewsFor(scope, views),\n pending: previous?.pending,\n error: previous?.error,\n };\n if (\n !force &&\n previous !== undefined &&\n sameList(previous.worktrees, next.worktrees) &&\n previous.pending === next.pending &&\n previous.error === next.error\n ) {\n return;\n }\n latest.set(scope.sessionId, next);\n host.publish(scope.sessionId, payloadOf(next));\n };\n\n /** Sets the operation label and clears the last failure, then publishes. */\n const mark = (scope: HubSessionScope, pending: string | undefined, error: string | undefined): void => {\n const previous = latest.get(scope.sessionId);\n latest.set(scope.sessionId, {\n worktrees: previous?.worktrees ?? viewsFor(scope, views),\n pending,\n error,\n });\n publish(scope, true);\n };\n\n const run = async (scope: HubSessionScope, command: GitWorktreesCommand): Promise<void> => {\n const context = { cwd: scope.cwd, sessionId: scope.sessionId };\n if (command.action === 'create') {\n mark(scope, `creating ${command.branch}\\u2026`, undefined);\n await worktrees.spawn(context, {\n branch: command.branch,\n ...(command.baseRef === undefined ? {} : { baseRef: command.baseRef }),\n });\n return;\n }\n mark(scope, `closing ${command.id}\\u2026`, undefined);\n await worktrees.close(context, command.id, command.force ?? false);\n };\n\n const source: HubChannelSource = {\n payloadFor(scope) {\n const state = latest.get(scope.sessionId);\n return state === undefined ? undefined : payloadOf(state);\n },\n sessionAdded(scope) {\n scopes.set(scope.sessionId, scope);\n publish(scope, false);\n const timer = setInterval(() => {\n publish(scope, false);\n }, intervalMs);\n timer.unref?.();\n timers.set(scope.sessionId, timer);\n },\n sessionRemoved(sessionId) {\n const timer = timers.get(sessionId);\n if (timer !== undefined) clearInterval(timer);\n timers.delete(sessionId);\n latest.delete(sessionId);\n scopes.delete(sessionId);\n busy.delete(sessionId);\n },\n close() {\n for (const timer of timers.values()) clearInterval(timer);\n timers.clear();\n latest.clear();\n scopes.clear();\n busy.clear();\n },\n };\n receiveCommand = (scope, payload) => {\n const command = parseCommand(payload);\n if (command === undefined) return;\n // One operation per session. A create runs for minutes, and a second\n // click must not start a second worktree behind the first.\n if (busy.has(scope.sessionId)) return;\n busy.add(scope.sessionId);\n void run(scope, command)\n .then(() => {\n mark(scope, undefined, undefined);\n })\n .catch((error: unknown) => {\n host.onNotice(`worktree command failed for ${scope.sessionId} (${failureText(error)})`);\n mark(scope, undefined, failureText(error));\n })\n .finally(() => {\n busy.delete(scope.sessionId);\n });\n };\n\n return source;\n },\n receive(scope, payload) {\n receiveCommand?.(scope, payload);\n },\n };\n}\n\n/** The named export the generated hub channel registry imports. */\nexport const webHubChannels: readonly WebHubChannel[] = [createWorktreesChannel()];\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"worktreesChannel.cjs","names":["createWorktreeRegistry","registryFile","DoomGitExpectedError","GIT_WORKTREES_TYPE","createWorktreeOperations","createWorktreeGit","hubRegistryDir","sessionIsLive"],"sources":["../../../src/adapters/web/worktreesChannel.ts"],"sourcesContent":["/**\n * The worktrees data channel, and the commands the dock sends back through it.\n *\n * DESIGN PATTERNS:\n * - Hub-scoped: it reads this machine's registry files, which a browser cannot.\n * - Scoped to the session that owns the worktree, not to the repository. Two\n * sessions in one checkout are two pieces of work; showing each the other's\n * worktrees offered a close button for something the reader never made.\n * A worktree whose parent session is gone is the exception: it is shown to\n * everyone in the repository, marked unowned, or nobody could ever close it.\n * - Poll, do not watch. The registry is a single small JSON file written by\n * this package alone, and a watcher on a path that may not exist yet costs\n * more than re-reading a few hundred bytes.\n * - Commands run the same `worktreeOperations` the tool runs, so the panel and\n * the agent cannot drift into two ideas of what creating a worktree means.\n * - Publish only what the dock renders. Paths and provenance stay in the\n * registry; the page gets a view.\n *\n * AVOID:\n * - Throwing out of a poll or a command. A registry that cannot be read is an\n * empty list, and a failed command is an error string on the next frame:\n * the dock degrades to what it can tell the reader, never to a dead channel.\n */\nimport type { HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { GIT_WORKTREES_TYPE, type GitWorktreesCommand, type WorktreeView } from '../../types/webWorktrees.ts';\nimport { DoomGitExpectedError } from '../../services/support/errors.ts';\nimport { hubRegistryDir, registryFile } from '../filesystem/paths.ts';\nimport { sessionIsLive } from '../hub/hubClient.ts';\nimport { createWorktreeGit } from '../worktree/gitCli.ts';\nimport { createWorktreeOperations, type WorktreeOperations } from '../worktree/worktreeOperations.ts';\nimport { createWorktreeRegistry } from '../worktree/worktreeRegistry.ts';\n\n/** How often a session's registry is re-read. Worktrees change by hand, not by the second. */\nconst POLL_MS = 4000;\n\ninterface SessionState {\n worktrees: WorktreeView[];\n pending: string | undefined;\n error: string | undefined;\n}\n\nfunction payloadOf(state: SessionState): Record<string, unknown> {\n return {\n worktrees: state.worktrees,\n ...(state.pending === undefined ? {} : { pending: state.pending }),\n ...(state.error === undefined ? {} : { error: state.error }),\n };\n}\n\n/** What the channel needs from its surroundings, injected so tests stay off this machine. */\nexport interface WorktreesChannelOptions {\n intervalMs?: number;\n operations?: WorktreeOperations;\n homeDir?: string;\n registryDir?: string;\n isSessionLive?: (registryDir: string, sessionId: string) => boolean;\n}\n\ninterface ViewDeps {\n registryDir: string;\n homeDir: string | undefined;\n isSessionLive: (registryDir: string, sessionId: string) => boolean;\n}\n\n/**\n * The worktrees one session may see: its own, plus any whose parent session\n * has gone. Liveness is the registry record's presence, the same signal the\n * rail uses to decide a session exists at all.\n */\nfunction viewsFor(scope: HubSessionScope, deps: ViewDeps): WorktreeView[] {\n try {\n return createWorktreeRegistry(registryFile(scope.cwd, deps.homeDir))\n .list()\n .flatMap((record) => {\n const owned = record.parentSessionId === scope.sessionId;\n const unowned = !owned && !deps.isSessionLive(deps.registryDir, record.parentSessionId);\n if (!owned && !unowned) return [];\n return [\n {\n id: record.id,\n branch: record.branch,\n path: record.path,\n sessionId: record.status === 'orphaned' ? null : record.sessionId,\n orphaned: record.status === 'orphaned',\n unowned,\n },\n ];\n });\n } catch {\n return [];\n }\n}\n\nfunction sameList(left: readonly WorktreeView[], right: readonly WorktreeView[]): boolean {\n if (left.length !== right.length) return false;\n return left.every((entry, index) => {\n const other = right[index];\n return (\n other !== undefined &&\n entry.id === other.id &&\n entry.branch === other.branch &&\n entry.sessionId === other.sessionId &&\n entry.orphaned === other.orphaned &&\n entry.unowned === other.unowned\n );\n });\n}\n\n/** A command the page sent, or undefined when the frame was not one. */\nfunction parseCommand(payload: unknown): GitWorktreesCommand | undefined {\n if (typeof payload !== 'object' || payload === null) return undefined;\n const value = payload as Record<string, unknown>;\n if (value.action === 'create') {\n if (typeof value.branch !== 'string' || value.branch.trim() === '') return undefined;\n return {\n action: 'create',\n branch: value.branch.trim(),\n ...(typeof value.baseRef === 'string' && value.baseRef.trim() !== '' ? { baseRef: value.baseRef.trim() } : {}),\n };\n }\n if (value.action === 'close') {\n if (typeof value.id !== 'string' || value.id === '') return undefined;\n return { action: 'close', id: value.id, ...(value.force === true ? { force: true } : {}) };\n }\n return undefined;\n}\n\n/**\n * What the reader is told when a command fails.\n *\n * An expected failure explains itself. Anything else is summarised, never\n * echoed: a git error can carry a remote URL with a token in it.\n */\nfunction failureText(error: unknown): string {\n return error instanceof DoomGitExpectedError ? error.message : 'The worktree operation failed.';\n}\n\n/**\n * One poller per managed session, publishing only when the list actually\n * changed so an idle cockpit stays quiet.\n */\nexport function createWorktreesChannel(options: WorktreesChannelOptions = {}): WebHubChannel {\n const intervalMs = options.intervalMs ?? POLL_MS;\n // Assigned by start, because receive is reachable only while the channel is\n // running and the command needs the state the poller keeps.\n let receiveCommand: ((scope: HubSessionScope, payload: unknown) => void) | undefined;\n\n return {\n frameType: GIT_WORKTREES_TYPE,\n start(host) {\n const worktrees = options.operations ?? createWorktreeOperations({ git: createWorktreeGit() });\n const views: ViewDeps = {\n registryDir: options.registryDir ?? hubRegistryDir(),\n homeDir: options.homeDir,\n isSessionLive: options.isSessionLive ?? sessionIsLive,\n };\n const latest = new Map<string, SessionState>();\n const scopes = new Map<string, HubSessionScope>();\n const timers = new Map<string, ReturnType<typeof setInterval>>();\n const busy = new Set<string>();\n\n /** Republishes a session, skipping a frame that would say nothing new. */\n const publish = (scope: HubSessionScope, force: boolean): void => {\n const previous = latest.get(scope.sessionId);\n const next: SessionState = {\n worktrees: viewsFor(scope, views),\n pending: previous?.pending,\n error: previous?.error,\n };\n if (\n !force &&\n previous !== undefined &&\n sameList(previous.worktrees, next.worktrees) &&\n previous.pending === next.pending &&\n previous.error === next.error\n ) {\n return;\n }\n latest.set(scope.sessionId, next);\n host.publish(scope.sessionId, payloadOf(next));\n };\n\n /** Sets the operation label and clears the last failure, then publishes. */\n const mark = (scope: HubSessionScope, pending: string | undefined, error: string | undefined): void => {\n const previous = latest.get(scope.sessionId);\n latest.set(scope.sessionId, {\n worktrees: previous?.worktrees ?? viewsFor(scope, views),\n pending,\n error,\n });\n publish(scope, true);\n };\n\n const run = async (scope: HubSessionScope, command: GitWorktreesCommand): Promise<void> => {\n const context = { cwd: scope.cwd, sessionId: scope.sessionId };\n if (command.action === 'create') {\n mark(scope, `creating ${command.branch}\\u2026`, undefined);\n await worktrees.spawn(\n context,\n {\n branch: command.branch,\n ...(command.baseRef === undefined ? {} : { baseRef: command.baseRef }),\n },\n // Each phase replaces the label, so the panel says what is\n // happening instead of holding one line for the whole wait.\n { onProgress: (label) => mark(scope, label, undefined) },\n );\n return;\n }\n mark(scope, `closing ${command.id}\\u2026`, undefined);\n await worktrees.close(context, command.id, command.force ?? false);\n };\n\n const source: HubChannelSource = {\n payloadFor(scope) {\n const state = latest.get(scope.sessionId);\n return state === undefined ? undefined : payloadOf(state);\n },\n sessionAdded(scope) {\n scopes.set(scope.sessionId, scope);\n publish(scope, false);\n const timer = setInterval(() => {\n publish(scope, false);\n }, intervalMs);\n timer.unref?.();\n timers.set(scope.sessionId, timer);\n },\n sessionRemoved(sessionId) {\n const timer = timers.get(sessionId);\n if (timer !== undefined) clearInterval(timer);\n timers.delete(sessionId);\n latest.delete(sessionId);\n scopes.delete(sessionId);\n busy.delete(sessionId);\n },\n close() {\n for (const timer of timers.values()) clearInterval(timer);\n timers.clear();\n latest.clear();\n scopes.clear();\n busy.clear();\n },\n };\n receiveCommand = (scope, payload) => {\n const command = parseCommand(payload);\n if (command === undefined) return;\n // One operation per session. A create runs for minutes, and a second\n // click must not start a second worktree behind the first.\n if (busy.has(scope.sessionId)) return;\n busy.add(scope.sessionId);\n void run(scope, command)\n .then(() => {\n mark(scope, undefined, undefined);\n })\n .catch((error: unknown) => {\n host.onNotice(`worktree command failed for ${scope.sessionId} (${failureText(error)})`);\n mark(scope, undefined, failureText(error));\n })\n .finally(() => {\n busy.delete(scope.sessionId);\n });\n };\n\n return source;\n },\n receive(scope, payload) {\n receiveCommand?.(scope, payload);\n },\n };\n}\n\n/** The named export the generated hub channel registry imports. */\nexport const webHubChannels: readonly WebHubChannel[] = [createWorktreesChannel()];\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktreesChannel.d.cts","names":[],"sources":["../../../src/adapters/web/worktreesChannel.ts"],"mappings":";;;;UAkDiB;EACf;EACA,aAAa;EACb;EACA;EACA,iBAAiB,qBAAqB;;;;;;iBAsFxB,uBAAuB,UAAS,0BAA+B;;
|
|
1
|
+
{"version":3,"file":"worktreesChannel.d.cts","names":[],"sources":["../../../src/adapters/web/worktreesChannel.ts"],"mappings":";;;;UAkDiB;EACf;EACA,aAAa;EACb;EACA;EACA,iBAAiB,qBAAqB;;;;;;iBAsFxB,uBAAuB,UAAS,0BAA+B;;cAmIlE,yBAAyB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktreesChannel.d.mts","names":[],"sources":["../../../src/adapters/web/worktreesChannel.ts"],"mappings":";;;;UAkDiB;EACf;EACA,aAAa;EACb;EACA;EACA,iBAAiB,qBAAqB;;;;;;iBAsFxB,uBAAuB,UAAS,0BAA+B;;
|
|
1
|
+
{"version":3,"file":"worktreesChannel.d.mts","names":[],"sources":["../../../src/adapters/web/worktreesChannel.ts"],"mappings":";;;;UAkDiB;EACf;EACA,aAAa;EACb;EACA;EACA,iBAAiB,qBAAqB;;;;;;iBAsFxB,uBAAuB,UAAS,0BAA+B;;cAmIlE,yBAAyB"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{createWorktreeGit as e}from"../worktree/gitCli.mjs";import{sessionIsLive as t}from"../hub/hubClient.mjs";import{hubRegistryDir as n,registryFile as r}from"../filesystem/paths.mjs";import{createWorktreeRegistry as i}from"../worktree/worktreeRegistry.mjs";import{DoomGitExpectedError as a}from"../../services/support/errors.mjs";import{createWorktreeOperations as o}from"../worktree/worktreeOperations.mjs";import{GIT_WORKTREES_TYPE as s}from"../../types/webWorktrees.mjs";function c(e){return{worktrees:e.worktrees,...e.pending===void 0?{}:{pending:e.pending},...e.error===void 0?{}:{error:e.error}}}function l(e,t){try{return i(r(e.cwd,t.homeDir)).list().flatMap(n=>{let r=n.parentSessionId===e.sessionId,i=!r&&!t.isSessionLive(t.registryDir,n.parentSessionId);return!r&&!i?[]:[{id:n.id,branch:n.branch,path:n.path,sessionId:n.status===`orphaned`?null:n.sessionId,orphaned:n.status===`orphaned`,unowned:i}]})}catch{return[]}}function u(e,t){return e.length===t.length&&e.every((e,n)=>{let r=t[n];return r!==void 0&&e.id===r.id&&e.branch===r.branch&&e.sessionId===r.sessionId&&e.orphaned===r.orphaned&&e.unowned===r.unowned})}function d(e){if(typeof e!=`object`||!e)return;let t=e;if(t.action===`create`)return typeof t.branch!=`string`||t.branch.trim()===``?void 0:{action:`create`,branch:t.branch.trim(),...typeof t.baseRef==`string`&&t.baseRef.trim()!==``?{baseRef:t.baseRef.trim()}:{}};if(t.action===`close`)return typeof t.id!=`string`||t.id===``?void 0:{action:`close`,id:t.id,...t.force===!0?{force:!0}:{}}}function f(e){return e instanceof a?e.message:`The worktree operation failed.`}function p(r={}){let i=r.intervalMs??4e3,a;return{frameType:s,start(s){let p=r.operations??o({git:e()}),m={registryDir:r.registryDir??n(),homeDir:r.homeDir,isSessionLive:r.isSessionLive??t},h=new Map,g=new Map,_=new Map,v=new Set,y=(e,t)=>{let n=h.get(e.sessionId),r={worktrees:l(e,m),pending:n?.pending,error:n?.error};(t||n===void 0||!u(n.worktrees,r.worktrees)||n.pending!==r.pending||n.error!==r.error)&&(h.set(e.sessionId,r),s.publish(e.sessionId,c(r)))},b=(e,t,n)=>{let r=h.get(e.sessionId);h.set(e.sessionId,{worktrees:r?.worktrees??l(e,m),pending:t,error:n}),y(e,!0)},x=async(e,t)=>{let n={cwd:e.cwd,sessionId:e.sessionId};if(t.action===`create`){b(e,`creating ${t.branch}\u2026`,void 0),await p.spawn(n,{branch:t.branch,...t.baseRef===void 0?{}:{baseRef:t.baseRef}});return}b(e,`closing ${t.id}\u2026`,void 0),await p.close(n,t.id,t.force??!1)};return a=(e,t)=>{let n=d(t);n!==void 0&&(v.has(e.sessionId)||(v.add(e.sessionId),x(e,n).then(()=>{b(e,void 0,void 0)}).catch(t=>{s.onNotice(`worktree command failed for ${e.sessionId} (${f(t)})`),b(e,void 0,f(t))}).finally(()=>{v.delete(e.sessionId)})))},{payloadFor(e){let t=h.get(e.sessionId);return t===void 0?void 0:c(t)},sessionAdded(e){g.set(e.sessionId,e),y(e,!1);let t=setInterval(()=>{y(e,!1)},i);t.unref?.(),_.set(e.sessionId,t)},sessionRemoved(e){let t=_.get(e);t!==void 0&&clearInterval(t),_.delete(e),h.delete(e),g.delete(e),v.delete(e)},close(){for(let e of _.values())clearInterval(e);_.clear(),h.clear(),g.clear(),v.clear()}}},receive(e,t){a?.(e,t)}}}const m=[p()];export{p as createWorktreesChannel,m as webHubChannels};
|
|
1
|
+
import{createWorktreeGit as e}from"../worktree/gitCli.mjs";import{sessionIsLive as t}from"../hub/hubClient.mjs";import{hubRegistryDir as n,registryFile as r}from"../filesystem/paths.mjs";import{createWorktreeRegistry as i}from"../worktree/worktreeRegistry.mjs";import{DoomGitExpectedError as a}from"../../services/support/errors.mjs";import{createWorktreeOperations as o}from"../worktree/worktreeOperations.mjs";import{GIT_WORKTREES_TYPE as s}from"../../types/webWorktrees.mjs";function c(e){return{worktrees:e.worktrees,...e.pending===void 0?{}:{pending:e.pending},...e.error===void 0?{}:{error:e.error}}}function l(e,t){try{return i(r(e.cwd,t.homeDir)).list().flatMap(n=>{let r=n.parentSessionId===e.sessionId,i=!r&&!t.isSessionLive(t.registryDir,n.parentSessionId);return!r&&!i?[]:[{id:n.id,branch:n.branch,path:n.path,sessionId:n.status===`orphaned`?null:n.sessionId,orphaned:n.status===`orphaned`,unowned:i}]})}catch{return[]}}function u(e,t){return e.length===t.length&&e.every((e,n)=>{let r=t[n];return r!==void 0&&e.id===r.id&&e.branch===r.branch&&e.sessionId===r.sessionId&&e.orphaned===r.orphaned&&e.unowned===r.unowned})}function d(e){if(typeof e!=`object`||!e)return;let t=e;if(t.action===`create`)return typeof t.branch!=`string`||t.branch.trim()===``?void 0:{action:`create`,branch:t.branch.trim(),...typeof t.baseRef==`string`&&t.baseRef.trim()!==``?{baseRef:t.baseRef.trim()}:{}};if(t.action===`close`)return typeof t.id!=`string`||t.id===``?void 0:{action:`close`,id:t.id,...t.force===!0?{force:!0}:{}}}function f(e){return e instanceof a?e.message:`The worktree operation failed.`}function p(r={}){let i=r.intervalMs??4e3,a;return{frameType:s,start(s){let p=r.operations??o({git:e()}),m={registryDir:r.registryDir??n(),homeDir:r.homeDir,isSessionLive:r.isSessionLive??t},h=new Map,g=new Map,_=new Map,v=new Set,y=(e,t)=>{let n=h.get(e.sessionId),r={worktrees:l(e,m),pending:n?.pending,error:n?.error};(t||n===void 0||!u(n.worktrees,r.worktrees)||n.pending!==r.pending||n.error!==r.error)&&(h.set(e.sessionId,r),s.publish(e.sessionId,c(r)))},b=(e,t,n)=>{let r=h.get(e.sessionId);h.set(e.sessionId,{worktrees:r?.worktrees??l(e,m),pending:t,error:n}),y(e,!0)},x=async(e,t)=>{let n={cwd:e.cwd,sessionId:e.sessionId};if(t.action===`create`){b(e,`creating ${t.branch}\u2026`,void 0),await p.spawn(n,{branch:t.branch,...t.baseRef===void 0?{}:{baseRef:t.baseRef}},{onProgress:t=>b(e,t,void 0)});return}b(e,`closing ${t.id}\u2026`,void 0),await p.close(n,t.id,t.force??!1)};return a=(e,t)=>{let n=d(t);n!==void 0&&(v.has(e.sessionId)||(v.add(e.sessionId),x(e,n).then(()=>{b(e,void 0,void 0)}).catch(t=>{s.onNotice(`worktree command failed for ${e.sessionId} (${f(t)})`),b(e,void 0,f(t))}).finally(()=>{v.delete(e.sessionId)})))},{payloadFor(e){let t=h.get(e.sessionId);return t===void 0?void 0:c(t)},sessionAdded(e){g.set(e.sessionId,e),y(e,!1);let t=setInterval(()=>{y(e,!1)},i);t.unref?.(),_.set(e.sessionId,t)},sessionRemoved(e){let t=_.get(e);t!==void 0&&clearInterval(t),_.delete(e),h.delete(e),g.delete(e),v.delete(e)},close(){for(let e of _.values())clearInterval(e);_.clear(),h.clear(),g.clear(),v.clear()}}},receive(e,t){a?.(e,t)}}}const m=[p()];export{p as createWorktreesChannel,m as webHubChannels};
|
|
2
2
|
//# sourceMappingURL=worktreesChannel.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktreesChannel.mjs","names":[],"sources":["../../../src/adapters/web/worktreesChannel.ts"],"sourcesContent":["/**\n * The worktrees data channel, and the commands the dock sends back through it.\n *\n * DESIGN PATTERNS:\n * - Hub-scoped: it reads this machine's registry files, which a browser cannot.\n * - Scoped to the session that owns the worktree, not to the repository. Two\n * sessions in one checkout are two pieces of work; showing each the other's\n * worktrees offered a close button for something the reader never made.\n * A worktree whose parent session is gone is the exception: it is shown to\n * everyone in the repository, marked unowned, or nobody could ever close it.\n * - Poll, do not watch. The registry is a single small JSON file written by\n * this package alone, and a watcher on a path that may not exist yet costs\n * more than re-reading a few hundred bytes.\n * - Commands run the same `worktreeOperations` the tool runs, so the panel and\n * the agent cannot drift into two ideas of what creating a worktree means.\n * - Publish only what the dock renders. Paths and provenance stay in the\n * registry; the page gets a view.\n *\n * AVOID:\n * - Throwing out of a poll or a command. A registry that cannot be read is an\n * empty list, and a failed command is an error string on the next frame:\n * the dock degrades to what it can tell the reader, never to a dead channel.\n */\nimport type { HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { GIT_WORKTREES_TYPE, type GitWorktreesCommand, type WorktreeView } from '../../types/webWorktrees.ts';\nimport { DoomGitExpectedError } from '../../services/support/errors.ts';\nimport { hubRegistryDir, registryFile } from '../filesystem/paths.ts';\nimport { sessionIsLive } from '../hub/hubClient.ts';\nimport { createWorktreeGit } from '../worktree/gitCli.ts';\nimport { createWorktreeOperations, type WorktreeOperations } from '../worktree/worktreeOperations.ts';\nimport { createWorktreeRegistry } from '../worktree/worktreeRegistry.ts';\n\n/** How often a session's registry is re-read. Worktrees change by hand, not by the second. */\nconst POLL_MS = 4000;\n\ninterface SessionState {\n worktrees: WorktreeView[];\n pending: string | undefined;\n error: string | undefined;\n}\n\nfunction payloadOf(state: SessionState): Record<string, unknown> {\n return {\n worktrees: state.worktrees,\n ...(state.pending === undefined ? {} : { pending: state.pending }),\n ...(state.error === undefined ? {} : { error: state.error }),\n };\n}\n\n/** What the channel needs from its surroundings, injected so tests stay off this machine. */\nexport interface WorktreesChannelOptions {\n intervalMs?: number;\n operations?: WorktreeOperations;\n homeDir?: string;\n registryDir?: string;\n isSessionLive?: (registryDir: string, sessionId: string) => boolean;\n}\n\ninterface ViewDeps {\n registryDir: string;\n homeDir: string | undefined;\n isSessionLive: (registryDir: string, sessionId: string) => boolean;\n}\n\n/**\n * The worktrees one session may see: its own, plus any whose parent session\n * has gone. Liveness is the registry record's presence, the same signal the\n * rail uses to decide a session exists at all.\n */\nfunction viewsFor(scope: HubSessionScope, deps: ViewDeps): WorktreeView[] {\n try {\n return createWorktreeRegistry(registryFile(scope.cwd, deps.homeDir))\n .list()\n .flatMap((record) => {\n const owned = record.parentSessionId === scope.sessionId;\n const unowned = !owned && !deps.isSessionLive(deps.registryDir, record.parentSessionId);\n if (!owned && !unowned) return [];\n return [\n {\n id: record.id,\n branch: record.branch,\n path: record.path,\n sessionId: record.status === 'orphaned' ? null : record.sessionId,\n orphaned: record.status === 'orphaned',\n unowned,\n },\n ];\n });\n } catch {\n return [];\n }\n}\n\nfunction sameList(left: readonly WorktreeView[], right: readonly WorktreeView[]): boolean {\n if (left.length !== right.length) return false;\n return left.every((entry, index) => {\n const other = right[index];\n return (\n other !== undefined &&\n entry.id === other.id &&\n entry.branch === other.branch &&\n entry.sessionId === other.sessionId &&\n entry.orphaned === other.orphaned &&\n entry.unowned === other.unowned\n );\n });\n}\n\n/** A command the page sent, or undefined when the frame was not one. */\nfunction parseCommand(payload: unknown): GitWorktreesCommand | undefined {\n if (typeof payload !== 'object' || payload === null) return undefined;\n const value = payload as Record<string, unknown>;\n if (value.action === 'create') {\n if (typeof value.branch !== 'string' || value.branch.trim() === '') return undefined;\n return {\n action: 'create',\n branch: value.branch.trim(),\n ...(typeof value.baseRef === 'string' && value.baseRef.trim() !== '' ? { baseRef: value.baseRef.trim() } : {}),\n };\n }\n if (value.action === 'close') {\n if (typeof value.id !== 'string' || value.id === '') return undefined;\n return { action: 'close', id: value.id, ...(value.force === true ? { force: true } : {}) };\n }\n return undefined;\n}\n\n/**\n * What the reader is told when a command fails.\n *\n * An expected failure explains itself. Anything else is summarised, never\n * echoed: a git error can carry a remote URL with a token in it.\n */\nfunction failureText(error: unknown): string {\n return error instanceof DoomGitExpectedError ? error.message : 'The worktree operation failed.';\n}\n\n/**\n * One poller per managed session, publishing only when the list actually\n * changed so an idle cockpit stays quiet.\n */\nexport function createWorktreesChannel(options: WorktreesChannelOptions = {}): WebHubChannel {\n const intervalMs = options.intervalMs ?? POLL_MS;\n // Assigned by start, because receive is reachable only while the channel is\n // running and the command needs the state the poller keeps.\n let receiveCommand: ((scope: HubSessionScope, payload: unknown) => void) | undefined;\n\n return {\n frameType: GIT_WORKTREES_TYPE,\n start(host) {\n const worktrees = options.operations ?? createWorktreeOperations({ git: createWorktreeGit() });\n const views: ViewDeps = {\n registryDir: options.registryDir ?? hubRegistryDir(),\n homeDir: options.homeDir,\n isSessionLive: options.isSessionLive ?? sessionIsLive,\n };\n const latest = new Map<string, SessionState>();\n const scopes = new Map<string, HubSessionScope>();\n const timers = new Map<string, ReturnType<typeof setInterval>>();\n const busy = new Set<string>();\n\n /** Republishes a session, skipping a frame that would say nothing new. */\n const publish = (scope: HubSessionScope, force: boolean): void => {\n const previous = latest.get(scope.sessionId);\n const next: SessionState = {\n worktrees: viewsFor(scope, views),\n pending: previous?.pending,\n error: previous?.error,\n };\n if (\n !force &&\n previous !== undefined &&\n sameList(previous.worktrees, next.worktrees) &&\n previous.pending === next.pending &&\n previous.error === next.error\n ) {\n return;\n }\n latest.set(scope.sessionId, next);\n host.publish(scope.sessionId, payloadOf(next));\n };\n\n /** Sets the operation label and clears the last failure, then publishes. */\n const mark = (scope: HubSessionScope, pending: string | undefined, error: string | undefined): void => {\n const previous = latest.get(scope.sessionId);\n latest.set(scope.sessionId, {\n worktrees: previous?.worktrees ?? viewsFor(scope, views),\n pending,\n error,\n });\n publish(scope, true);\n };\n\n const run = async (scope: HubSessionScope, command: GitWorktreesCommand): Promise<void> => {\n const context = { cwd: scope.cwd, sessionId: scope.sessionId };\n if (command.action === 'create') {\n mark(scope, `creating ${command.branch}\\u2026`, undefined);\n await worktrees.spawn(context, {\n branch: command.branch,\n ...(command.baseRef === undefined ? {} : { baseRef: command.baseRef }),\n });\n return;\n }\n mark(scope, `closing ${command.id}\\u2026`, undefined);\n await worktrees.close(context, command.id, command.force ?? false);\n };\n\n const source: HubChannelSource = {\n payloadFor(scope) {\n const state = latest.get(scope.sessionId);\n return state === undefined ? undefined : payloadOf(state);\n },\n sessionAdded(scope) {\n scopes.set(scope.sessionId, scope);\n publish(scope, false);\n const timer = setInterval(() => {\n publish(scope, false);\n }, intervalMs);\n timer.unref?.();\n timers.set(scope.sessionId, timer);\n },\n sessionRemoved(sessionId) {\n const timer = timers.get(sessionId);\n if (timer !== undefined) clearInterval(timer);\n timers.delete(sessionId);\n latest.delete(sessionId);\n scopes.delete(sessionId);\n busy.delete(sessionId);\n },\n close() {\n for (const timer of timers.values()) clearInterval(timer);\n timers.clear();\n latest.clear();\n scopes.clear();\n busy.clear();\n },\n };\n receiveCommand = (scope, payload) => {\n const command = parseCommand(payload);\n if (command === undefined) return;\n // One operation per session. A create runs for minutes, and a second\n // click must not start a second worktree behind the first.\n if (busy.has(scope.sessionId)) return;\n busy.add(scope.sessionId);\n void run(scope, command)\n .then(() => {\n mark(scope, undefined, undefined);\n })\n .catch((error: unknown) => {\n host.onNotice(`worktree command failed for ${scope.sessionId} (${failureText(error)})`);\n mark(scope, undefined, failureText(error));\n })\n .finally(() => {\n busy.delete(scope.sessionId);\n });\n };\n\n return source;\n },\n receive(scope, payload) {\n receiveCommand?.(scope, payload);\n },\n };\n}\n\n/** The named export the generated hub channel registry imports. */\nexport const webHubChannels: readonly WebHubChannel[] = [createWorktreesChannel()];\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"worktreesChannel.mjs","names":[],"sources":["../../../src/adapters/web/worktreesChannel.ts"],"sourcesContent":["/**\n * The worktrees data channel, and the commands the dock sends back through it.\n *\n * DESIGN PATTERNS:\n * - Hub-scoped: it reads this machine's registry files, which a browser cannot.\n * - Scoped to the session that owns the worktree, not to the repository. Two\n * sessions in one checkout are two pieces of work; showing each the other's\n * worktrees offered a close button for something the reader never made.\n * A worktree whose parent session is gone is the exception: it is shown to\n * everyone in the repository, marked unowned, or nobody could ever close it.\n * - Poll, do not watch. The registry is a single small JSON file written by\n * this package alone, and a watcher on a path that may not exist yet costs\n * more than re-reading a few hundred bytes.\n * - Commands run the same `worktreeOperations` the tool runs, so the panel and\n * the agent cannot drift into two ideas of what creating a worktree means.\n * - Publish only what the dock renders. Paths and provenance stay in the\n * registry; the page gets a view.\n *\n * AVOID:\n * - Throwing out of a poll or a command. A registry that cannot be read is an\n * empty list, and a failed command is an error string on the next frame:\n * the dock degrades to what it can tell the reader, never to a dead channel.\n */\nimport type { HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { GIT_WORKTREES_TYPE, type GitWorktreesCommand, type WorktreeView } from '../../types/webWorktrees.ts';\nimport { DoomGitExpectedError } from '../../services/support/errors.ts';\nimport { hubRegistryDir, registryFile } from '../filesystem/paths.ts';\nimport { sessionIsLive } from '../hub/hubClient.ts';\nimport { createWorktreeGit } from '../worktree/gitCli.ts';\nimport { createWorktreeOperations, type WorktreeOperations } from '../worktree/worktreeOperations.ts';\nimport { createWorktreeRegistry } from '../worktree/worktreeRegistry.ts';\n\n/** How often a session's registry is re-read. Worktrees change by hand, not by the second. */\nconst POLL_MS = 4000;\n\ninterface SessionState {\n worktrees: WorktreeView[];\n pending: string | undefined;\n error: string | undefined;\n}\n\nfunction payloadOf(state: SessionState): Record<string, unknown> {\n return {\n worktrees: state.worktrees,\n ...(state.pending === undefined ? {} : { pending: state.pending }),\n ...(state.error === undefined ? {} : { error: state.error }),\n };\n}\n\n/** What the channel needs from its surroundings, injected so tests stay off this machine. */\nexport interface WorktreesChannelOptions {\n intervalMs?: number;\n operations?: WorktreeOperations;\n homeDir?: string;\n registryDir?: string;\n isSessionLive?: (registryDir: string, sessionId: string) => boolean;\n}\n\ninterface ViewDeps {\n registryDir: string;\n homeDir: string | undefined;\n isSessionLive: (registryDir: string, sessionId: string) => boolean;\n}\n\n/**\n * The worktrees one session may see: its own, plus any whose parent session\n * has gone. Liveness is the registry record's presence, the same signal the\n * rail uses to decide a session exists at all.\n */\nfunction viewsFor(scope: HubSessionScope, deps: ViewDeps): WorktreeView[] {\n try {\n return createWorktreeRegistry(registryFile(scope.cwd, deps.homeDir))\n .list()\n .flatMap((record) => {\n const owned = record.parentSessionId === scope.sessionId;\n const unowned = !owned && !deps.isSessionLive(deps.registryDir, record.parentSessionId);\n if (!owned && !unowned) return [];\n return [\n {\n id: record.id,\n branch: record.branch,\n path: record.path,\n sessionId: record.status === 'orphaned' ? null : record.sessionId,\n orphaned: record.status === 'orphaned',\n unowned,\n },\n ];\n });\n } catch {\n return [];\n }\n}\n\nfunction sameList(left: readonly WorktreeView[], right: readonly WorktreeView[]): boolean {\n if (left.length !== right.length) return false;\n return left.every((entry, index) => {\n const other = right[index];\n return (\n other !== undefined &&\n entry.id === other.id &&\n entry.branch === other.branch &&\n entry.sessionId === other.sessionId &&\n entry.orphaned === other.orphaned &&\n entry.unowned === other.unowned\n );\n });\n}\n\n/** A command the page sent, or undefined when the frame was not one. */\nfunction parseCommand(payload: unknown): GitWorktreesCommand | undefined {\n if (typeof payload !== 'object' || payload === null) return undefined;\n const value = payload as Record<string, unknown>;\n if (value.action === 'create') {\n if (typeof value.branch !== 'string' || value.branch.trim() === '') return undefined;\n return {\n action: 'create',\n branch: value.branch.trim(),\n ...(typeof value.baseRef === 'string' && value.baseRef.trim() !== '' ? { baseRef: value.baseRef.trim() } : {}),\n };\n }\n if (value.action === 'close') {\n if (typeof value.id !== 'string' || value.id === '') return undefined;\n return { action: 'close', id: value.id, ...(value.force === true ? { force: true } : {}) };\n }\n return undefined;\n}\n\n/**\n * What the reader is told when a command fails.\n *\n * An expected failure explains itself. Anything else is summarised, never\n * echoed: a git error can carry a remote URL with a token in it.\n */\nfunction failureText(error: unknown): string {\n return error instanceof DoomGitExpectedError ? error.message : 'The worktree operation failed.';\n}\n\n/**\n * One poller per managed session, publishing only when the list actually\n * changed so an idle cockpit stays quiet.\n */\nexport function createWorktreesChannel(options: WorktreesChannelOptions = {}): WebHubChannel {\n const intervalMs = options.intervalMs ?? POLL_MS;\n // Assigned by start, because receive is reachable only while the channel is\n // running and the command needs the state the poller keeps.\n let receiveCommand: ((scope: HubSessionScope, payload: unknown) => void) | undefined;\n\n return {\n frameType: GIT_WORKTREES_TYPE,\n start(host) {\n const worktrees = options.operations ?? createWorktreeOperations({ git: createWorktreeGit() });\n const views: ViewDeps = {\n registryDir: options.registryDir ?? hubRegistryDir(),\n homeDir: options.homeDir,\n isSessionLive: options.isSessionLive ?? sessionIsLive,\n };\n const latest = new Map<string, SessionState>();\n const scopes = new Map<string, HubSessionScope>();\n const timers = new Map<string, ReturnType<typeof setInterval>>();\n const busy = new Set<string>();\n\n /** Republishes a session, skipping a frame that would say nothing new. */\n const publish = (scope: HubSessionScope, force: boolean): void => {\n const previous = latest.get(scope.sessionId);\n const next: SessionState = {\n worktrees: viewsFor(scope, views),\n pending: previous?.pending,\n error: previous?.error,\n };\n if (\n !force &&\n previous !== undefined &&\n sameList(previous.worktrees, next.worktrees) &&\n previous.pending === next.pending &&\n previous.error === next.error\n ) {\n return;\n }\n latest.set(scope.sessionId, next);\n host.publish(scope.sessionId, payloadOf(next));\n };\n\n /** Sets the operation label and clears the last failure, then publishes. */\n const mark = (scope: HubSessionScope, pending: string | undefined, error: string | undefined): void => {\n const previous = latest.get(scope.sessionId);\n latest.set(scope.sessionId, {\n worktrees: previous?.worktrees ?? viewsFor(scope, views),\n pending,\n error,\n });\n publish(scope, true);\n };\n\n const run = async (scope: HubSessionScope, command: GitWorktreesCommand): Promise<void> => {\n const context = { cwd: scope.cwd, sessionId: scope.sessionId };\n if (command.action === 'create') {\n mark(scope, `creating ${command.branch}\\u2026`, undefined);\n await worktrees.spawn(\n context,\n {\n branch: command.branch,\n ...(command.baseRef === undefined ? {} : { baseRef: command.baseRef }),\n },\n // Each phase replaces the label, so the panel says what is\n // happening instead of holding one line for the whole wait.\n { onProgress: (label) => mark(scope, label, undefined) },\n );\n return;\n }\n mark(scope, `closing ${command.id}\\u2026`, undefined);\n await worktrees.close(context, command.id, command.force ?? false);\n };\n\n const source: HubChannelSource = {\n payloadFor(scope) {\n const state = latest.get(scope.sessionId);\n return state === undefined ? undefined : payloadOf(state);\n },\n sessionAdded(scope) {\n scopes.set(scope.sessionId, scope);\n publish(scope, false);\n const timer = setInterval(() => {\n publish(scope, false);\n }, intervalMs);\n timer.unref?.();\n timers.set(scope.sessionId, timer);\n },\n sessionRemoved(sessionId) {\n const timer = timers.get(sessionId);\n if (timer !== undefined) clearInterval(timer);\n timers.delete(sessionId);\n latest.delete(sessionId);\n scopes.delete(sessionId);\n busy.delete(sessionId);\n },\n close() {\n for (const timer of timers.values()) clearInterval(timer);\n timers.clear();\n latest.clear();\n scopes.clear();\n busy.clear();\n },\n };\n receiveCommand = (scope, payload) => {\n const command = parseCommand(payload);\n if (command === undefined) return;\n // One operation per session. A create runs for minutes, and a second\n // click must not start a second worktree behind the first.\n if (busy.has(scope.sessionId)) return;\n busy.add(scope.sessionId);\n void run(scope, command)\n .then(() => {\n mark(scope, undefined, undefined);\n })\n .catch((error: unknown) => {\n host.onNotice(`worktree command failed for ${scope.sessionId} (${failureText(error)})`);\n mark(scope, undefined, failureText(error));\n })\n .finally(() => {\n busy.delete(scope.sessionId);\n });\n };\n\n return source;\n },\n receive(scope, payload) {\n receiveCommand?.(scope, payload);\n },\n };\n}\n\n/** The named export the generated hub channel registry imports. */\nexport const webHubChannels: readonly WebHubChannel[] = [createWorktreesChannel()];\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:fs");t=e.__toESM(t,1);let n=require("node:path");n=e.__toESM(n,1);const r=`node_modules`;function i(e){let i={outputs:[],modules:[]},a=(o,s)=>{let c;try{c=t.default.readdirSync(n.default.join(e,o),{withFileTypes:!0})}catch{return}for(let e of c){if(!e.isDirectory())continue;let t=o===``?e.name:`${o}/${e.name}`;if(e.name!==`.git`){if(e.name===r){i.modules.push(t);continue}if(e.name===`dist`){i.outputs.push(t);continue}s<4&&a(t,s+1)}}};return a(``,1),i}function a(e,a){if(!t.default.existsSync(n.default.join(e,r)))return{kind:`skipped`,reason:`no-modules`};let o=i(e),s=0,c=0;for(let r of o.modules){let i=n.default.join(a,r);if(!t.default.existsSync(i))try{t.default.mkdirSync(n.default.dirname(i),{recursive:!0}),t.default.symlinkSync(n.default.join(e,r),i,`dir`),c+=1}catch{}}for(let r of o.outputs){let i=n.default.join(a,r);if(!t.default.existsSync(i))try{t.default.mkdirSync(n.default.dirname(i),{recursive:!0}),t.default.cpSync(n.default.join(e,r),i,{recursive:!0}),s+=1}catch{}}return{kind:`mirrored`,copied:s,linked:c}}exports.mirrorComposition=a;
|
|
2
|
+
//# sourceMappingURL=compositionMirror.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compositionMirror.cjs","names":["fs","path"],"sources":["../../../src/adapters/worktree/compositionMirror.ts"],"sourcesContent":["/**\n * Giving a new worktree the build output its parent checkout already has.\n *\n * DESIGN PATTERNS:\n * - Mirrors, never builds. Everything here is a copy or a symlink of something\n * the parent checkout already produced, so it costs seconds and cannot fail\n * the way a package manager or a compiler can.\n * - `node_modules` is linked, `dist` is copied. The module tree is gigabytes\n * and read-only in practice, so a link is right; build output is small and a\n * link would send a build run inside the worktree straight into the parent's\n * directory.\n * - Absent parent output means nothing to do. A repository with no\n * `node_modules` at its root is either not a JS project or not installed, and\n * either way this has no opinion about it, so a Rust, Go or Python worktree\n * pays nothing.\n *\n * WHY THIS EXISTS:\n * A DoomPi session composes the extensions its repository's config names, and\n * this repository names its own workspace packages by relative path. Those\n * packages declare `pi.extensions` under `dist/`, which git does not track, so\n * a fresh worktree resolves none of them and the session dies on start. The\n * parent checkout has already built exactly what the child needs.\n *\n * AVOID:\n * - Recursing into `node_modules`. The scan is bounded and skips it; without\n * that this walks a gigabyte to find nothing.\n * - Treating a mirror failure as fatal. The worktree is good either way; only\n * the session's composition is poorer for it.\n */\nimport fs from 'node:fs';\nimport path from 'node:path';\n\n/** Directories that are build output worth copying into a new worktree. */\nconst OUTPUT_DIRECTORY = 'dist';\nconst MODULES_DIRECTORY = 'node_modules';\nconst GIT_DIRECTORY = '.git';\n/**\n * How deep the scan goes.\n *\n * Four levels reaches `layers/<group>/<package>/dist` in this repository's\n * layout, and a workspace that buries a package deeper than that gets a\n * partial mirror rather than a slow one.\n */\nconst MAX_DEPTH = 4;\n\nexport type MirrorOutcome =\n | { kind: 'skipped'; reason: 'no-modules' }\n | { kind: 'mirrored'; copied: number; linked: number };\n\ninterface Found {\n /** Paths relative to the source root. */\n outputs: string[];\n modules: string[];\n}\n\nfunction scan(root: string): Found {\n const found: Found = { outputs: [], modules: [] };\n const walk = (relative: string, depth: number): void => {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(path.join(root, relative), { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const child = relative === '' ? entry.name : `${relative}/${entry.name}`;\n if (entry.name === GIT_DIRECTORY) continue;\n // Both of these are destinations, not places to look inside.\n if (entry.name === MODULES_DIRECTORY) {\n found.modules.push(child);\n continue;\n }\n if (entry.name === OUTPUT_DIRECTORY) {\n found.outputs.push(child);\n continue;\n }\n if (depth < MAX_DEPTH) walk(child, depth + 1);\n }\n };\n walk('', 1);\n return found;\n}\n\n/**\n * Copies the parent's build output into a worktree and links its modules.\n *\n * Individual failures are counted out rather than thrown: a mirror that lands\n * partly is still better than none, and the caller has a worktree either way.\n */\nexport function mirrorComposition(source: string, target: string): MirrorOutcome {\n if (!fs.existsSync(path.join(source, MODULES_DIRECTORY))) return { kind: 'skipped', reason: 'no-modules' };\n\n const found = scan(source);\n let copied = 0;\n let linked = 0;\n\n for (const relative of found.modules) {\n const destination = path.join(target, relative);\n if (fs.existsSync(destination)) continue;\n try {\n fs.mkdirSync(path.dirname(destination), { recursive: true });\n fs.symlinkSync(path.join(source, relative), destination, 'dir');\n linked += 1;\n } catch {\n // A link the filesystem refuses costs this package one resolution, not\n // the worktree.\n }\n }\n\n for (const relative of found.outputs) {\n const destination = path.join(target, relative);\n if (fs.existsSync(destination)) continue;\n try {\n fs.mkdirSync(path.dirname(destination), { recursive: true });\n fs.cpSync(path.join(source, relative), destination, { recursive: true });\n copied += 1;\n } catch {\n // Same reasoning as a failed link.\n }\n }\n\n return { kind: 'mirrored', copied, linked };\n}\n"],"mappings":"8IAiCA,MACM,EAAoB,eAqB1B,SAAS,EAAK,EAAqB,CACjC,IAAM,EAAe,CAAE,QAAS,CAAC,EAAG,QAAS,CAAC,CAAE,EAC1C,GAAQ,EAAkB,IAAwB,CACtD,IAAI,EACJ,GAAI,CACF,EAAUA,EAAAA,QAAG,YAAYC,EAAAA,QAAK,KAAK,EAAM,CAAQ,EAAG,CAAE,cAAe,EAAK,CAAC,CAC7E,MAAQ,CACN,MACF,CACA,IAAK,IAAM,KAAS,EAAS,CAC3B,GAAI,CAAC,EAAM,YAAY,EAAG,SAC1B,IAAM,EAAQ,IAAa,GAAK,EAAM,KAAO,GAAG,EAAS,GAAG,EAAM,OAC9D,KAAM,OAAS,OAEnB,IAAI,EAAM,OAAS,EAAmB,CACpC,EAAM,QAAQ,KAAK,CAAK,EACxB,QACF,CACA,GAAI,EAAM,OAAS,OAAkB,CACnC,EAAM,QAAQ,KAAK,CAAK,EACxB,QACF,CACI,EAAQ,GAAW,EAAK,EAAO,EAAQ,CAAC,CAL5C,CAMF,CACF,EAEA,OADA,EAAK,GAAI,CAAC,EACH,CACT,CAQA,SAAgB,EAAkB,EAAgB,EAA+B,CAC/E,GAAI,CAACD,EAAAA,QAAG,WAAWC,EAAAA,QAAK,KAAK,EAAQ,CAAiB,CAAC,EAAG,MAAO,CAAE,KAAM,UAAW,OAAQ,YAAa,EAEzG,IAAM,EAAQ,EAAK,CAAM,EACrB,EAAS,EACT,EAAS,EAEb,IAAK,IAAM,KAAY,EAAM,QAAS,CACpC,IAAM,EAAcA,EAAAA,QAAK,KAAK,EAAQ,CAAQ,EAC1CD,MAAAA,QAAG,WAAW,CAAW,EAC7B,GAAI,CACF,EAAA,QAAG,UAAUC,EAAAA,QAAK,QAAQ,CAAW,EAAG,CAAE,UAAW,EAAK,CAAC,EAC3D,EAAA,QAAG,YAAYA,EAAAA,QAAK,KAAK,EAAQ,CAAQ,EAAG,EAAa,KAAK,EAC9D,GAAU,CACZ,MAAQ,CAGR,CACF,CAEA,IAAK,IAAM,KAAY,EAAM,QAAS,CACpC,IAAM,EAAcA,EAAAA,QAAK,KAAK,EAAQ,CAAQ,EAC1CD,MAAAA,QAAG,WAAW,CAAW,EAC7B,GAAI,CACF,EAAA,QAAG,UAAUC,EAAAA,QAAK,QAAQ,CAAW,EAAG,CAAE,UAAW,EAAK,CAAC,EAC3D,EAAA,QAAG,OAAOA,EAAAA,QAAK,KAAK,EAAQ,CAAQ,EAAG,EAAa,CAAE,UAAW,EAAK,CAAC,EACvE,GAAU,CACZ,MAAQ,CAER,CACF,CAEA,MAAO,CAAE,KAAM,WAAY,SAAQ,QAAO,CAC5C"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import e from"node:fs";import t from"node:path";const n=`node_modules`;function r(r){let i={outputs:[],modules:[]},a=(o,s)=>{let c;try{c=e.readdirSync(t.join(r,o),{withFileTypes:!0})}catch{return}for(let e of c){if(!e.isDirectory())continue;let t=o===``?e.name:`${o}/${e.name}`;if(e.name!==`.git`){if(e.name===n){i.modules.push(t);continue}if(e.name===`dist`){i.outputs.push(t);continue}s<4&&a(t,s+1)}}};return a(``,1),i}function i(i,a){if(!e.existsSync(t.join(i,n)))return{kind:`skipped`,reason:`no-modules`};let o=r(i),s=0,c=0;for(let n of o.modules){let r=t.join(a,n);if(!e.existsSync(r))try{e.mkdirSync(t.dirname(r),{recursive:!0}),e.symlinkSync(t.join(i,n),r,`dir`),c+=1}catch{}}for(let n of o.outputs){let r=t.join(a,n);if(!e.existsSync(r))try{e.mkdirSync(t.dirname(r),{recursive:!0}),e.cpSync(t.join(i,n),r,{recursive:!0}),s+=1}catch{}}return{kind:`mirrored`,copied:s,linked:c}}export{i as mirrorComposition};
|
|
2
|
+
//# sourceMappingURL=compositionMirror.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compositionMirror.mjs","names":[],"sources":["../../../src/adapters/worktree/compositionMirror.ts"],"sourcesContent":["/**\n * Giving a new worktree the build output its parent checkout already has.\n *\n * DESIGN PATTERNS:\n * - Mirrors, never builds. Everything here is a copy or a symlink of something\n * the parent checkout already produced, so it costs seconds and cannot fail\n * the way a package manager or a compiler can.\n * - `node_modules` is linked, `dist` is copied. The module tree is gigabytes\n * and read-only in practice, so a link is right; build output is small and a\n * link would send a build run inside the worktree straight into the parent's\n * directory.\n * - Absent parent output means nothing to do. A repository with no\n * `node_modules` at its root is either not a JS project or not installed, and\n * either way this has no opinion about it, so a Rust, Go or Python worktree\n * pays nothing.\n *\n * WHY THIS EXISTS:\n * A DoomPi session composes the extensions its repository's config names, and\n * this repository names its own workspace packages by relative path. Those\n * packages declare `pi.extensions` under `dist/`, which git does not track, so\n * a fresh worktree resolves none of them and the session dies on start. The\n * parent checkout has already built exactly what the child needs.\n *\n * AVOID:\n * - Recursing into `node_modules`. The scan is bounded and skips it; without\n * that this walks a gigabyte to find nothing.\n * - Treating a mirror failure as fatal. The worktree is good either way; only\n * the session's composition is poorer for it.\n */\nimport fs from 'node:fs';\nimport path from 'node:path';\n\n/** Directories that are build output worth copying into a new worktree. */\nconst OUTPUT_DIRECTORY = 'dist';\nconst MODULES_DIRECTORY = 'node_modules';\nconst GIT_DIRECTORY = '.git';\n/**\n * How deep the scan goes.\n *\n * Four levels reaches `layers/<group>/<package>/dist` in this repository's\n * layout, and a workspace that buries a package deeper than that gets a\n * partial mirror rather than a slow one.\n */\nconst MAX_DEPTH = 4;\n\nexport type MirrorOutcome =\n | { kind: 'skipped'; reason: 'no-modules' }\n | { kind: 'mirrored'; copied: number; linked: number };\n\ninterface Found {\n /** Paths relative to the source root. */\n outputs: string[];\n modules: string[];\n}\n\nfunction scan(root: string): Found {\n const found: Found = { outputs: [], modules: [] };\n const walk = (relative: string, depth: number): void => {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(path.join(root, relative), { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const child = relative === '' ? entry.name : `${relative}/${entry.name}`;\n if (entry.name === GIT_DIRECTORY) continue;\n // Both of these are destinations, not places to look inside.\n if (entry.name === MODULES_DIRECTORY) {\n found.modules.push(child);\n continue;\n }\n if (entry.name === OUTPUT_DIRECTORY) {\n found.outputs.push(child);\n continue;\n }\n if (depth < MAX_DEPTH) walk(child, depth + 1);\n }\n };\n walk('', 1);\n return found;\n}\n\n/**\n * Copies the parent's build output into a worktree and links its modules.\n *\n * Individual failures are counted out rather than thrown: a mirror that lands\n * partly is still better than none, and the caller has a worktree either way.\n */\nexport function mirrorComposition(source: string, target: string): MirrorOutcome {\n if (!fs.existsSync(path.join(source, MODULES_DIRECTORY))) return { kind: 'skipped', reason: 'no-modules' };\n\n const found = scan(source);\n let copied = 0;\n let linked = 0;\n\n for (const relative of found.modules) {\n const destination = path.join(target, relative);\n if (fs.existsSync(destination)) continue;\n try {\n fs.mkdirSync(path.dirname(destination), { recursive: true });\n fs.symlinkSync(path.join(source, relative), destination, 'dir');\n linked += 1;\n } catch {\n // A link the filesystem refuses costs this package one resolution, not\n // the worktree.\n }\n }\n\n for (const relative of found.outputs) {\n const destination = path.join(target, relative);\n if (fs.existsSync(destination)) continue;\n try {\n fs.mkdirSync(path.dirname(destination), { recursive: true });\n fs.cpSync(path.join(source, relative), destination, { recursive: true });\n copied += 1;\n } catch {\n // Same reasoning as a failed link.\n }\n }\n\n return { kind: 'mirrored', copied, linked };\n}\n"],"mappings":"gDAiCA,MACM,EAAoB,eAqB1B,SAAS,EAAK,EAAqB,CACjC,IAAM,EAAe,CAAE,QAAS,CAAC,EAAG,QAAS,CAAC,CAAE,EAC1C,GAAQ,EAAkB,IAAwB,CACtD,IAAI,EACJ,GAAI,CACF,EAAU,EAAG,YAAY,EAAK,KAAK,EAAM,CAAQ,EAAG,CAAE,cAAe,EAAK,CAAC,CAC7E,MAAQ,CACN,MACF,CACA,IAAK,IAAM,KAAS,EAAS,CAC3B,GAAI,CAAC,EAAM,YAAY,EAAG,SAC1B,IAAM,EAAQ,IAAa,GAAK,EAAM,KAAO,GAAG,EAAS,GAAG,EAAM,OAC9D,KAAM,OAAS,OAEnB,IAAI,EAAM,OAAS,EAAmB,CACpC,EAAM,QAAQ,KAAK,CAAK,EACxB,QACF,CACA,GAAI,EAAM,OAAS,OAAkB,CACnC,EAAM,QAAQ,KAAK,CAAK,EACxB,QACF,CACI,EAAQ,GAAW,EAAK,EAAO,EAAQ,CAAC,CAL5C,CAMF,CACF,EAEA,OADA,EAAK,GAAI,CAAC,EACH,CACT,CAQA,SAAgB,EAAkB,EAAgB,EAA+B,CAC/E,GAAI,CAAC,EAAG,WAAW,EAAK,KAAK,EAAQ,CAAiB,CAAC,EAAG,MAAO,CAAE,KAAM,UAAW,OAAQ,YAAa,EAEzG,IAAM,EAAQ,EAAK,CAAM,EACrB,EAAS,EACT,EAAS,EAEb,IAAK,IAAM,KAAY,EAAM,QAAS,CACpC,IAAM,EAAc,EAAK,KAAK,EAAQ,CAAQ,EAC1C,MAAG,WAAW,CAAW,EAC7B,GAAI,CACF,EAAG,UAAU,EAAK,QAAQ,CAAW,EAAG,CAAE,UAAW,EAAK,CAAC,EAC3D,EAAG,YAAY,EAAK,KAAK,EAAQ,CAAQ,EAAG,EAAa,KAAK,EAC9D,GAAU,CACZ,MAAQ,CAGR,CACF,CAEA,IAAK,IAAM,KAAY,EAAM,QAAS,CACpC,IAAM,EAAc,EAAK,KAAK,EAAQ,CAAQ,EAC1C,MAAG,WAAW,CAAW,EAC7B,GAAI,CACF,EAAG,UAAU,EAAK,QAAQ,CAAW,EAAG,CAAE,UAAW,EAAK,CAAC,EAC3D,EAAG,OAAO,EAAK,KAAK,EAAQ,CAAQ,EAAG,EAAa,CAAE,UAAW,EAAK,CAAC,EACvE,GAAU,CACZ,MAAQ,CAER,CACF,CAEA,MAAO,CAAE,KAAM,WAAY,SAAQ,QAAO,CAC5C"}
|