@agimon-ai/doompi-git 0.0.1-alpha.6 → 0.0.1-alpha.60
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/README.md +32 -2
- package/dist/apiContracts.cjs +387 -0
- package/dist/apiContracts.cjs.map +1 -0
- package/dist/{schemas/apiContracts.d.mts → apiContracts.d.mts} +2 -2
- package/dist/{schemas/apiContracts.d.mts.map → apiContracts.d.mts.map} +1 -1
- package/dist/apiContracts.mjs +382 -0
- package/dist/apiContracts.mjs.map +1 -0
- package/dist/extensions/pi.cjs +104 -1
- package/dist/extensions/pi.cjs.map +1 -1
- package/dist/extensions/pi.d.mts +4 -4
- package/dist/extensions/pi.d.mts.map +1 -1
- package/dist/extensions/pi.mjs +99 -1
- package/dist/extensions/pi.mjs.map +1 -1
- package/dist/extensions/server.cjs +1827 -1
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.d.mts +3 -3
- package/dist/extensions/server.d.mts.map +1 -1
- package/dist/extensions/server.mjs +1819 -1
- package/dist/extensions/server.mjs.map +1 -1
- package/dist/extensions/web.mjs +2639 -0
- package/dist/extensions/web.mjs.map +1 -0
- package/dist/gitDependencies-BEjuOTAE.cjs +33 -0
- package/dist/gitDependencies-BEjuOTAE.cjs.map +1 -0
- package/dist/gitDependencies-DF0fs9OC.mjs +22 -0
- package/dist/gitDependencies-DF0fs9OC.mjs.map +1 -0
- package/dist/index-D79MSCee.d.mts +145 -0
- package/dist/index-D79MSCee.d.mts.map +1 -0
- package/dist/index.cjs +4 -1
- package/dist/index.d.mts +1 -3
- package/dist/index.mjs +2 -1
- package/dist/runWorktree-CKpYGt7u.mjs +303 -0
- package/dist/runWorktree-CKpYGt7u.mjs.map +1 -0
- package/dist/runWorktree-CN3pSKvT.cjs +374 -0
- package/dist/runWorktree-CN3pSKvT.cjs.map +1 -0
- package/dist/worktreeOperations-D86X4CtV.mjs +1078 -0
- package/dist/worktreeOperations-D86X4CtV.mjs.map +1 -0
- package/dist/worktreeOperations-DlAT1sfY.cjs +1234 -0
- package/dist/worktreeOperations-DlAT1sfY.cjs.map +1 -0
- package/llms.txt +4 -0
- package/package.json +43 -34
- package/src/prompts/doompi-use-git/SKILL.md +21 -1
- package/dist/_virtual/_rolldown/runtime.cjs +0 -1
- package/dist/api-contracts.cjs +0 -1
- package/dist/api-contracts.d.cts +0 -2
- package/dist/api-contracts.d.mts +0 -2
- package/dist/api-contracts.mjs +0 -1
- package/dist/constants/git.cjs +0 -2
- package/dist/constants/git.cjs.map +0 -1
- package/dist/constants/git.mjs +0 -2
- package/dist/constants/git.mjs.map +0 -1
- package/dist/controllers/gitCommand.cjs +0 -2
- package/dist/controllers/gitCommand.cjs.map +0 -1
- package/dist/controllers/gitCommand.mjs +0 -2
- package/dist/controllers/gitCommand.mjs.map +0 -1
- package/dist/controllers/gitSession.cjs +0 -2
- package/dist/controllers/gitSession.cjs.map +0 -1
- package/dist/controllers/gitSession.mjs +0 -2
- package/dist/controllers/gitSession.mjs.map +0 -1
- package/dist/controllers/hubApi.cjs +0 -2
- package/dist/controllers/hubApi.cjs.map +0 -1
- package/dist/controllers/hubApi.mjs +0 -2
- package/dist/controllers/hubApi.mjs.map +0 -1
- package/dist/controllers/worktreesChannel.cjs +0 -2
- package/dist/controllers/worktreesChannel.cjs.map +0 -1
- package/dist/controllers/worktreesChannel.mjs +0 -2
- package/dist/controllers/worktreesChannel.mjs.map +0 -1
- package/dist/extensions/pi.d.cts +0 -6
- package/dist/extensions/pi.d.cts.map +0 -1
- package/dist/extensions/server.d.cts +0 -5
- package/dist/extensions/server.d.cts.map +0 -1
- package/dist/index.d.cts +0 -4
- package/dist/schemas/apiContracts.cjs +0 -2
- package/dist/schemas/apiContracts.cjs.map +0 -1
- package/dist/schemas/apiContracts.d.cts +0 -53
- package/dist/schemas/apiContracts.d.cts.map +0 -1
- package/dist/schemas/apiContracts.mjs +0 -2
- package/dist/schemas/apiContracts.mjs.map +0 -1
- package/dist/schemas/runWorktreeTool.cjs +0 -2
- package/dist/schemas/runWorktreeTool.cjs.map +0 -1
- package/dist/schemas/runWorktreeTool.mjs +0 -2
- package/dist/schemas/runWorktreeTool.mjs.map +0 -1
- package/dist/services/atomicJson/index.cjs +0 -2
- package/dist/services/atomicJson/index.cjs.map +0 -1
- package/dist/services/atomicJson/index.mjs +0 -2
- package/dist/services/atomicJson/index.mjs.map +0 -1
- package/dist/services/compositionMirror/index.cjs +0 -2
- package/dist/services/compositionMirror/index.cjs.map +0 -1
- package/dist/services/compositionMirror/index.mjs +0 -2
- package/dist/services/compositionMirror/index.mjs.map +0 -1
- package/dist/services/errors/index.cjs +0 -2
- package/dist/services/errors/index.cjs.map +0 -1
- package/dist/services/errors/index.mjs +0 -2
- package/dist/services/errors/index.mjs.map +0 -1
- package/dist/services/extensionService/index.cjs +0 -2
- package/dist/services/extensionService/index.cjs.map +0 -1
- package/dist/services/extensionService/index.d.cts +0 -8
- package/dist/services/extensionService/index.d.cts.map +0 -1
- package/dist/services/extensionService/index.d.mts +0 -8
- package/dist/services/extensionService/index.d.mts.map +0 -1
- package/dist/services/extensionService/index.mjs +0 -2
- package/dist/services/extensionService/index.mjs.map +0 -1
- package/dist/services/gitCli/index.cjs +0 -4
- package/dist/services/gitCli/index.cjs.map +0 -1
- package/dist/services/gitCli/index.mjs +0 -4
- package/dist/services/gitCli/index.mjs.map +0 -1
- package/dist/services/gitDependencies/index.cjs +0 -2
- package/dist/services/gitDependencies/index.cjs.map +0 -1
- package/dist/services/gitDependencies/index.d.cts +0 -6
- package/dist/services/gitDependencies/index.d.cts.map +0 -1
- package/dist/services/gitDependencies/index.d.mts +0 -6
- package/dist/services/gitDependencies/index.d.mts.map +0 -1
- package/dist/services/gitDependencies/index.mjs +0 -2
- package/dist/services/gitDependencies/index.mjs.map +0 -1
- package/dist/services/paths/index.cjs +0 -2
- package/dist/services/paths/index.cjs.map +0 -1
- package/dist/services/paths/index.mjs +0 -2
- package/dist/services/paths/index.mjs.map +0 -1
- package/dist/services/repositoryIdentity/index.cjs +0 -2
- package/dist/services/repositoryIdentity/index.cjs.map +0 -1
- package/dist/services/repositoryIdentity/index.mjs +0 -2
- package/dist/services/repositoryIdentity/index.mjs.map +0 -1
- package/dist/services/runWorktree/index.cjs +0 -15
- package/dist/services/runWorktree/index.cjs.map +0 -1
- package/dist/services/runWorktree/index.mjs +0 -15
- package/dist/services/runWorktree/index.mjs.map +0 -1
- package/dist/services/worktreeEvents/index.cjs +0 -2
- package/dist/services/worktreeEvents/index.cjs.map +0 -1
- package/dist/services/worktreeEvents/index.d.cts +0 -15
- package/dist/services/worktreeEvents/index.d.cts.map +0 -1
- package/dist/services/worktreeEvents/index.d.mts +0 -15
- package/dist/services/worktreeEvents/index.d.mts.map +0 -1
- package/dist/services/worktreeEvents/index.mjs +0 -2
- package/dist/services/worktreeEvents/index.mjs.map +0 -1
- package/dist/services/worktreeNaming/index.cjs +0 -2
- package/dist/services/worktreeNaming/index.cjs.map +0 -1
- package/dist/services/worktreeNaming/index.d.cts +0 -34
- package/dist/services/worktreeNaming/index.d.cts.map +0 -1
- package/dist/services/worktreeNaming/index.d.mts +0 -34
- package/dist/services/worktreeNaming/index.d.mts.map +0 -1
- package/dist/services/worktreeNaming/index.mjs +0 -2
- package/dist/services/worktreeNaming/index.mjs.map +0 -1
- package/dist/services/worktreeOperations/index.cjs +0 -2
- package/dist/services/worktreeOperations/index.cjs.map +0 -1
- package/dist/services/worktreeOperations/index.d.cts +0 -38
- package/dist/services/worktreeOperations/index.d.cts.map +0 -1
- package/dist/services/worktreeOperations/index.d.mts +0 -38
- package/dist/services/worktreeOperations/index.d.mts.map +0 -1
- package/dist/services/worktreeOperations/index.mjs +0 -2
- package/dist/services/worktreeOperations/index.mjs.map +0 -1
- package/dist/services/worktreeRegistry/index.cjs +0 -2
- package/dist/services/worktreeRegistry/index.cjs.map +0 -1
- package/dist/services/worktreeRegistry/index.mjs +0 -2
- package/dist/services/worktreeRegistry/index.mjs.map +0 -1
- package/dist/tools/runWorktree.cjs +0 -2
- package/dist/tools/runWorktree.cjs.map +0 -1
- package/dist/tools/runWorktree.mjs +0 -2
- package/dist/tools/runWorktree.mjs.map +0 -1
- package/dist/types/extension.d.cts +0 -17
- package/dist/types/extension.d.cts.map +0 -1
- package/dist/types/extension.d.mts +0 -17
- package/dist/types/extension.d.mts.map +0 -1
- package/dist/types/webWorktrees.cjs +0 -2
- package/dist/types/webWorktrees.cjs.map +0 -1
- package/dist/types/webWorktrees.mjs +0 -2
- package/dist/types/webWorktrees.mjs.map +0 -1
- package/dist/types/worktreeRegistry.d.cts +0 -41
- package/dist/types/worktreeRegistry.d.cts.map +0 -1
- package/dist/types/worktreeRegistry.d.mts +0 -41
- package/dist/types/worktreeRegistry.d.mts.map +0 -1
- package/src/extensions/web.ts +0 -33
- package/src/types/webWorktrees.ts +0 -55
- package/src/types/worktreeRegistry.ts +0 -78
- package/src/web/components/RunWorktreeToolMessage.tsx +0 -45
- package/src/web/components/WorktreesActivitySection.tsx +0 -80
- package/src/web/components/WorktreesPanel.tsx +0 -175
- package/src/web/components/toolRenderers.ts +0 -8
- package/src/web/stores/worktreesActivityStore.ts +0 -125
- package/src/web/tsconfig.json +0 -22
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import e from"node:fs";import{createHash as t}from"node:crypto";import n from"node:path";function r(t){let r=n.join(t,`.git`),i;try{i=e.statSync(r)}catch{return}if(i.isDirectory())return r;let a;try{a=e.readFileSync(r,`utf8`).trim()}catch{return}if(!a.startsWith(`gitdir:`))return;let o=n.resolve(t,a.slice(7).trim());try{let t=e.readFileSync(n.join(o,`commondir`),`utf8`).trim();return n.resolve(o,t)}catch{return o}}function i(e){let t=n.basename(e).toLowerCase().replace(/[^a-z0-9]+/gu,`-`).replace(/^-+|-+$/gu,``).slice(0,32);return t===``?`repository`:t}function a(e){let i=r(e),a=i===void 0?`root:${n.resolve(e)}`:`git:${i}`;return t(`sha256`).update(a).digest(`hex`).slice(0,12)}function o(e){return t(`sha256`).update(e).digest(`hex`).slice(0,8)}export{r as gitCommonDirectory,a as repositoryId,i as repositoryLabel,o as shortId};
|
|
2
|
-
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/repositoryIdentity/index.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nconst ID_LENGTH = 12;\nconst MAX_LABEL_LENGTH = 32;\n\n/**\n * The git directory shared by a repository and all of its worktrees.\n *\n * A normal checkout has a `.git` directory. A linked worktree has a `.git`\n * FILE pointing at `<common>/worktrees/<name>`, whose `commondir` points back\n * at the shared directory. Following that chain is what makes every worktree of\n * one repository agree on the repository's identity, which matters here because\n * the registry is per repository and must be found from inside a worktree too.\n */\nexport function gitCommonDirectory(root: string): string | undefined {\n const marker = path.join(root, '.git');\n let stat: fs.Stats;\n try {\n stat = fs.statSync(marker);\n } catch {\n return undefined;\n }\n if (stat.isDirectory()) return marker;\n let pointer: string;\n try {\n pointer = fs.readFileSync(marker, 'utf8').trim();\n } catch {\n return undefined;\n }\n if (!pointer.startsWith('gitdir:')) return undefined;\n const gitDir = path.resolve(root, pointer.slice('gitdir:'.length).trim());\n try {\n const common = fs.readFileSync(path.join(gitDir, 'commondir'), 'utf8').trim();\n return path.resolve(gitDir, common);\n } catch {\n // A submodule's .git file has no commondir; its gitdir is already common.\n return gitDir;\n }\n}\n\n/** A directory name that is recognisable to a person and safe on a filesystem. */\nexport function repositoryLabel(root: string): string {\n const label = path\n .basename(root)\n .toLowerCase()\n .replace(/[^a-z0-9]+/gu, '-')\n .replace(/^-+|-+$/gu, '')\n .slice(0, MAX_LABEL_LENGTH);\n return label === '' ? 'repository' : label;\n}\n\n/**\n * A stable id for the repository a directory belongs to.\n *\n * Keyed on the common git directory rather than the given path, so the main\n * checkout and every worktree of it resolve to one id and therefore to one\n * registry. Falling back to the path keeps a non-repository directory usable\n * instead of failing, though nothing here should reach that case.\n */\nexport function repositoryId(root: string): string {\n const common = gitCommonDirectory(root);\n const token = common === undefined ? `root:${path.resolve(root)}` : `git:${common}`;\n return createHash('sha256').update(token).digest('hex').slice(0, ID_LENGTH);\n}\n\n/** A short, collision-resistant suffix for one worktree directory. */\nexport function shortId(seed: string): string {\n return createHash('sha256').update(seed).digest('hex').slice(0, 8);\n}\n"],"mappings":"yFAgBA,SAAgB,EAAmB,EAAkC,CACnE,IAAM,EAAS,EAAK,KAAK,EAAM,MAAM,EACjC,EACJ,GAAI,CACF,EAAO,EAAG,SAAS,CAAM,CAC3B,MAAQ,CACN,MACF,CACA,GAAI,EAAK,YAAY,EAAG,OAAO,EAC/B,IAAI,EACJ,GAAI,CACF,EAAU,EAAG,aAAa,EAAQ,MAAM,CAAC,CAAC,KAAK,CACjD,MAAQ,CACN,MACF,CACA,GAAI,CAAC,EAAQ,WAAW,SAAS,EAAG,OACpC,IAAM,EAAS,EAAK,QAAQ,EAAM,EAAQ,MAAM,CAAgB,CAAC,CAAC,KAAK,CAAC,EACxE,GAAI,CACF,IAAM,EAAS,EAAG,aAAa,EAAK,KAAK,EAAQ,WAAW,EAAG,MAAM,CAAC,CAAC,KAAK,EAC5E,OAAO,EAAK,QAAQ,EAAQ,CAAM,CACpC,MAAQ,CAEN,OAAO,CACT,CACF,CAGA,SAAgB,EAAgB,EAAsB,CACpD,IAAM,EAAQ,EACX,SAAS,CAAI,CAAC,CACd,YAAY,CAAC,CACb,QAAQ,eAAgB,GAAG,CAAC,CAC5B,QAAQ,YAAa,EAAE,CAAC,CACxB,MAAM,EAAG,EAAgB,EAC5B,OAAO,IAAU,GAAK,aAAe,CACvC,CAUA,SAAgB,EAAa,EAAsB,CACjD,IAAM,EAAS,EAAmB,CAAI,EAChC,EAAQ,IAAW,IAAA,GAAY,QAAQ,EAAK,QAAQ,CAAI,IAAM,OAAO,IAC3E,OAAO,EAAW,QAAQ,CAAC,CAAC,OAAO,CAAK,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,EAAG,EAAS,CAC5E,CAGA,SAAgB,EAAQ,EAAsB,CAC5C,OAAO,EAAW,QAAQ,CAAC,CAAC,OAAO,CAAI,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,EAAG,CAAC,CACnE"}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
const e=require("../errors/index.cjs"),t=require("../../schemas/runWorktreeTool.cjs"),n=new Set(Object.values(t.WORKTREE_ACTIONS));function r(r){if(typeof r!=`object`||!r||Array.isArray(r))throw e.invalidRequest(`run_worktree parameters must be an object.`,`Call it with one documented action shape.`);let i=r,a=i.action;if(typeof a!=`string`||!n.has(a))throw e.invalidRequest(`Unknown action ${typeof a==`string`?`'${a}'`:`value`}.`,`Use one of: ${[...n].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}.`)}for(let t of[`baseRef`,`name`,`task`,`message`])if(t in i&&(typeof i[t]!=`string`||i[t].trim()===``))throw e.invalidRequest(`action='${a}' requires a nonblank '${t}'.`,`Pass a ${t}.`);for(let t of[`force`,`dryRun`])if(t in i&&typeof i[t]!=`boolean`)throw e.invalidRequest(`action='${a}' requires boolean '${t}'.`,`Pass true or false for ${t}.`);if(a===t.WORKTREE_ACTIONS.send&&typeof i.message!=`string`)throw e.invalidRequest(`action='send' requires 'message'.`,`Pass the message to send.`);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||a===t.WORKTREE_ACTIONS.send||a===t.WORKTREE_ACTIONS.messages)&&typeof i.id!=`string`)throw e.invalidRequest(`action='${a}' requires 'id'.`,`Call list to see the current ids.`);return i}function i(e){return`${e.id} ${e.branch} ${e.status} ${e.path}`}function a(...e){return[{type:`text`,text:e.join(`
|
|
2
|
-
`)}]}async function o(e,n,r,o={}){switch(e.action){case t.WORKTREE_ACTIONS.spawn_worktree:{let t=await n.spawn(r,{branch:e.branch,...e.baseRef===void 0?{}:{baseRef:e.baseRef},...e.name===void 0?{}:{name:e.name}},o);return{content:a(`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(r,e.id,e.force??!1);return{content:a(`Worktree ${t.id} (${t.branch}) closed and removed.`),details:{action:e.action,records:[t]}}}case t.WORKTREE_ACTIONS.list:{let t=await n.list(r);return{content:t.length===0?a(`No worktrees for this repository.`):a(`${String(t.length)} worktree(s):`,...t.map(i)),details:{action:e.action,records:t}}}case t.WORKTREE_ACTIONS.status:{let{record:t,dirtyFiles:o}=await n.status(r,e.id);return{content:a(i(t),`Session: ${t.sessionId}`,o.length===0?`Clean.`:`${String(o.length)} uncommitted file(s): ${o.slice(0,20).join(`, `)}`),details:{action:e.action,records:[t]}}}case t.WORKTREE_ACTIONS.merge:{let t=await n.merge(r,e.id,...e.message===void 0?[]:[e.message]);return{content:a(`Merged ${t.branch} into this checkout.`),details:{action:e.action,records:[t]}}}case t.WORKTREE_ACTIONS.send:return await n.send(r,e.id,e.message),{content:a(`Message sent to worktree ${e.id}.`),details:{action:e.action}};case t.WORKTREE_ACTIONS.messages:{let t=await n.messages(r,e.id);return{content:t.length===0?a(`No messages.`):a(`${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,i=await n.prune(r,t);return{content:a(t?`Prune plan (nothing was changed):`:`Pruned:`,`Remove: ${i.remove.length===0?`none`:i.remove.map(e=>e.id).join(`, `)}`,`Forget: ${i.forget.length===0?`none`:i.forget.map(e=>e.id).join(`, `)}`,`Kept, uncommitted work: ${i.keptDirty.length===0?`none`:i.keptDirty.map(e=>e.id).join(`, `)}`,`Untracked directories, left alone: ${i.untracked.length===0?`none`:i.untracked.join(`, `)}`,...i.keptBranches.length===0?[]:[`Branches kept, they hold commits: ${i.keptBranches.join(`, `)}`]),details:{action:e.action}}}}}exports.RUN_WORKTREE_DESCRIPTION=`Create and manage git worktrees, each with its own DoomPi session nested under this one.
|
|
3
|
-
|
|
4
|
-
Actions:
|
|
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.
|
|
6
|
-
- close_worktree: stop a worktree's session and delete its directory. Requires 'id'. Refuses a dirty tree unless 'force' is true.
|
|
7
|
-
- list: every worktree for this repository, with status.
|
|
8
|
-
- status: one worktree and its uncommitted files. Requires 'id'.
|
|
9
|
-
- merge: merge a worktree's branch into this checkout. Requires 'id'. Refuses when this checkout is dirty.
|
|
10
|
-
- prune: remove orphaned worktrees. Pass 'dryRun' to see the plan without destroying anything.
|
|
11
|
-
- send: send a message to the other side of a worktree. Requires 'id' and 'message'.
|
|
12
|
-
- messages: read messages waiting for you from a worktree. Requires 'id'.
|
|
13
|
-
|
|
14
|
-
Worktrees live outside the repository, under ~/.pi/.doom/git/worktrees.`,exports.RUN_WORKTREE_TOOL_NAME=`run_worktree`,exports.executeRunWorktreeTool=o,exports.validateParams=r;
|
|
15
|
-
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["WORKTREE_ACTIONS","invalidRequest","WORKTREE_ACTION_FIELDS"],"sources":["../../../src/services/runWorktree/index.ts"],"sourcesContent":["import {\n RunWorktreeParams,\n WORKTREE_ACTION_FIELDS,\n WORKTREE_ACTIONS,\n type WorktreeAction,\n type RunWorktreeToolParams,\n} from '../../schemas/runWorktreeTool';\nimport type { WorktreeRecord } from '../../types/worktreeRegistry';\nimport { invalidRequest } from '../errors';\nimport type { WorktreeOperations } from '../worktreeOperations';\n\nexport { RunWorktreeParams };\nexport type { RunWorktreeToolParams, WorktreeAction };\n\nexport const RUN_WORKTREE_TOOL_NAME = 'run_worktree';\n\nconst IMPLEMENTED_ACTIONS: ReadonlySet<string> = new Set(Object.values(WORKTREE_ACTIONS));\n\nexport const RUN_WORKTREE_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\nexport interface RunWorktreeToolResult {\n content: [{ type: 'text'; text: string }];\n details: RunWorktreeToolDetails;\n}\n\nexport interface RunWorktreeExecutionContext {\n readonly cwd: string;\n readonly sessionId: string;\n}\n\nexport interface RunWorktreeExecutionOptions {\n readonly signal?: AbortSignal;\n readonly onProgress?: (label: string) => void;\n}\n\n/** Rejects calls that do not match one documented action shape. */\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 const stringFields = ['baseRef', 'name', 'task', 'message'] as const;\n for (const field of stringFields) {\n if (field in record && (typeof record[field] !== 'string' || record[field].trim() === '')) {\n throw invalidRequest(`action='${action}' requires a nonblank '${field}'.`, `Pass a ${field}.`);\n }\n }\n for (const field of ['force', 'dryRun'] as const) {\n if (field in record && typeof record[field] !== 'boolean') {\n throw invalidRequest(`action='${action}' requires boolean '${field}'.`, `Pass true or false for ${field}.`);\n }\n }\n if (action === WORKTREE_ACTIONS.send && typeof record.message !== 'string') {\n throw invalidRequest(\"action='send' requires 'message'.\", 'Pass the message to send.');\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 action === WORKTREE_ACTIONS.send ||\n action === WORKTREE_ACTIONS.messages) &&\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\nfunction text(...lines: readonly string[]): [{ type: 'text'; text: string }] {\n return [{ type: 'text', text: lines.join('\\n') }];\n}\n\nexport async function executeRunWorktreeTool(\n params: RunWorktreeToolParams,\n operations: WorktreeOperations,\n context: RunWorktreeExecutionContext,\n options: RunWorktreeExecutionOptions = {},\n): Promise<RunWorktreeToolResult> {\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 options,\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"],"mappings":"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"}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import{invalidRequest as e}from"../errors/index.mjs";import{WORKTREE_ACTIONS as t,WORKTREE_ACTION_FIELDS as n}from"../../schemas/runWorktreeTool.mjs";const r=`run_worktree`,i=new Set(Object.values(t)),a=`Create and manage git worktrees, each with its own DoomPi session nested under this one.
|
|
2
|
-
|
|
3
|
-
Actions:
|
|
4
|
-
- 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.
|
|
5
|
-
- close_worktree: stop a worktree's session and delete its directory. Requires 'id'. Refuses a dirty tree unless 'force' is true.
|
|
6
|
-
- list: every worktree for this repository, with status.
|
|
7
|
-
- status: one worktree and its uncommitted files. Requires 'id'.
|
|
8
|
-
- merge: merge a worktree's branch into this checkout. Requires 'id'. Refuses when this checkout is dirty.
|
|
9
|
-
- prune: remove orphaned worktrees. Pass 'dryRun' to see the plan without destroying anything.
|
|
10
|
-
- send: send a message to the other side of a worktree. Requires 'id' and 'message'.
|
|
11
|
-
- messages: read messages waiting for you from a worktree. Requires 'id'.
|
|
12
|
-
|
|
13
|
-
Worktrees live outside the repository, under ~/.pi/.doom/git/worktrees.`;function o(r){if(typeof r!=`object`||!r||Array.isArray(r))throw e(`run_worktree parameters must be an object.`,`Call it with one documented action shape.`);let a=r,o=a.action;if(typeof o!=`string`||!i.has(o))throw e(`Unknown action ${typeof o==`string`?`'${o}'`:`value`}.`,`Use one of: ${[...i].join(`, `)}.`);let s=n[o],c=Object.keys(a).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 a){let n=a[t];if(typeof n!=`string`||n.trim()===``)throw e(`action='${o}' requires a nonblank '${t}'.`,`Pass a ${t}.`)}for(let t of[`baseRef`,`name`,`task`,`message`])if(t in a&&(typeof a[t]!=`string`||a[t].trim()===``))throw e(`action='${o}' requires a nonblank '${t}'.`,`Pass a ${t}.`);for(let t of[`force`,`dryRun`])if(t in a&&typeof a[t]!=`boolean`)throw e(`action='${o}' requires boolean '${t}'.`,`Pass true or false for ${t}.`);if(o===t.send&&typeof a.message!=`string`)throw e(`action='send' requires 'message'.`,`Pass the message to send.`);if(o===t.spawn_worktree&&typeof a.branch!=`string`)throw e(`action='spawn_worktree' requires 'branch'.`,`Name the branch to create.`);if((o===t.close_worktree||o===t.status||o===t.merge||o===t.send||o===t.messages)&&typeof a.id!=`string`)throw e(`action='${o}' requires 'id'.`,`Call list to see the current ids.`);return a}function s(e){return`${e.id} ${e.branch} ${e.status} ${e.path}`}function c(...e){return[{type:`text`,text:e.join(`
|
|
14
|
-
`)}]}async function l(e,n,r,i={}){switch(e.action){case t.spawn_worktree:{let t=await n.spawn(r,{branch:e.branch,...e.baseRef===void 0?{}:{baseRef:e.baseRef},...e.name===void 0?{}:{name:e.name}},i);return{content:c(`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.close_worktree:{let t=await n.close(r,e.id,e.force??!1);return{content:c(`Worktree ${t.id} (${t.branch}) closed and removed.`),details:{action:e.action,records:[t]}}}case t.list:{let t=await n.list(r);return{content:t.length===0?c(`No worktrees for this repository.`):c(`${String(t.length)} worktree(s):`,...t.map(s)),details:{action:e.action,records:t}}}case t.status:{let{record:t,dirtyFiles:i}=await n.status(r,e.id);return{content:c(s(t),`Session: ${t.sessionId}`,i.length===0?`Clean.`:`${String(i.length)} uncommitted file(s): ${i.slice(0,20).join(`, `)}`),details:{action:e.action,records:[t]}}}case t.merge:{let t=await n.merge(r,e.id,...e.message===void 0?[]:[e.message]);return{content:c(`Merged ${t.branch} into this checkout.`),details:{action:e.action,records:[t]}}}case t.send:return await n.send(r,e.id,e.message),{content:c(`Message sent to worktree ${e.id}.`),details:{action:e.action}};case t.messages:{let t=await n.messages(r,e.id);return{content:t.length===0?c(`No messages.`):c(`${String(t.length)} message(s):`,...t.map(e=>`[${e.sentAt}] ${e.from}: ${e.text}`)),details:{action:e.action}}}case t.prune:{let t=e.dryRun??!1,i=await n.prune(r,t);return{content:c(t?`Prune plan (nothing was changed):`:`Pruned:`,`Remove: ${i.remove.length===0?`none`:i.remove.map(e=>e.id).join(`, `)}`,`Forget: ${i.forget.length===0?`none`:i.forget.map(e=>e.id).join(`, `)}`,`Kept, uncommitted work: ${i.keptDirty.length===0?`none`:i.keptDirty.map(e=>e.id).join(`, `)}`,`Untracked directories, left alone: ${i.untracked.length===0?`none`:i.untracked.join(`, `)}`,...i.keptBranches.length===0?[]:[`Branches kept, they hold commits: ${i.keptBranches.join(`, `)}`]),details:{action:e.action}}}}}export{a as RUN_WORKTREE_DESCRIPTION,r as RUN_WORKTREE_TOOL_NAME,l as executeRunWorktreeTool,o as validateParams};
|
|
15
|
-
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/runWorktree/index.ts"],"sourcesContent":["import {\n RunWorktreeParams,\n WORKTREE_ACTION_FIELDS,\n WORKTREE_ACTIONS,\n type WorktreeAction,\n type RunWorktreeToolParams,\n} from '../../schemas/runWorktreeTool';\nimport type { WorktreeRecord } from '../../types/worktreeRegistry';\nimport { invalidRequest } from '../errors';\nimport type { WorktreeOperations } from '../worktreeOperations';\n\nexport { RunWorktreeParams };\nexport type { RunWorktreeToolParams, WorktreeAction };\n\nexport const RUN_WORKTREE_TOOL_NAME = 'run_worktree';\n\nconst IMPLEMENTED_ACTIONS: ReadonlySet<string> = new Set(Object.values(WORKTREE_ACTIONS));\n\nexport const RUN_WORKTREE_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\nexport interface RunWorktreeToolResult {\n content: [{ type: 'text'; text: string }];\n details: RunWorktreeToolDetails;\n}\n\nexport interface RunWorktreeExecutionContext {\n readonly cwd: string;\n readonly sessionId: string;\n}\n\nexport interface RunWorktreeExecutionOptions {\n readonly signal?: AbortSignal;\n readonly onProgress?: (label: string) => void;\n}\n\n/** Rejects calls that do not match one documented action shape. */\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 const stringFields = ['baseRef', 'name', 'task', 'message'] as const;\n for (const field of stringFields) {\n if (field in record && (typeof record[field] !== 'string' || record[field].trim() === '')) {\n throw invalidRequest(`action='${action}' requires a nonblank '${field}'.`, `Pass a ${field}.`);\n }\n }\n for (const field of ['force', 'dryRun'] as const) {\n if (field in record && typeof record[field] !== 'boolean') {\n throw invalidRequest(`action='${action}' requires boolean '${field}'.`, `Pass true or false for ${field}.`);\n }\n }\n if (action === WORKTREE_ACTIONS.send && typeof record.message !== 'string') {\n throw invalidRequest(\"action='send' requires 'message'.\", 'Pass the message to send.');\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 action === WORKTREE_ACTIONS.send ||\n action === WORKTREE_ACTIONS.messages) &&\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\nfunction text(...lines: readonly string[]): [{ type: 'text'; text: string }] {\n return [{ type: 'text', text: lines.join('\\n') }];\n}\n\nexport async function executeRunWorktreeTool(\n params: RunWorktreeToolParams,\n operations: WorktreeOperations,\n context: RunWorktreeExecutionContext,\n options: RunWorktreeExecutionOptions = {},\n): Promise<RunWorktreeToolResult> {\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 options,\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"],"mappings":"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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=`git_worktree_message`;function t(e){return e===`parent`||e===`child`}function n(e){if(typeof e!=`object`||!e)return;let n=e;if(n.version===1&&typeof n.worktreeId==`string`&&n.worktreeId!==``&&typeof n.fromSessionId==`string`&&n.fromSessionId!==``&&t(n.from)&&typeof n.text==`string`&&n.text!==``&&!(Buffer.byteLength(n.text,`utf8`)>65536)&&typeof n.sentAt==`string`&&n.sentAt!==``)return n}function r(t,r){if(r===``)throw Error(`The Git message service requires a session identity.`);let i=new Map,a=t.subscribe(e,r,e=>{let t=n(e);if(t===void 0)return;let r=i.get(t.worktreeId)??[];r.length>=100&&r.shift(),r.push(t),i.set(t.worktreeId,r)}),o=!1;return{send(n,r){if(o)throw Error(`The Git message service is closed.`);t.publish(e,n,r)},receive(e){if(o)return[];let t=i.get(e);return t===void 0?[]:(i.delete(e),t)},close(){o||(o=!0,a(),i.clear())}}}function i(e){if(typeof e!=`object`||!e)return!1;let t=e;return t.version===1&&typeof t.repositoryRoot==`string`&&t.repositoryRoot!==``}exports.GIT_WORKTREE_LIFECYCLE_EVENT=`git_worktree_lifecycle`,exports.GIT_WORKTREE_MESSAGE_EVENT=e,exports.MAX_WORKTREE_MESSAGE_BYTES=65536,exports.createWorktreeMessageInbox=r,exports.isWorktreeLifecycleEvent=i;
|
|
2
|
-
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/services/worktreeEvents/index.ts"],"sourcesContent":["import type { DoomDirectEventBus } from '@agimon-ai/doompi-core/hub-channel';\n\nexport const GIT_WORKTREE_LIFECYCLE_EVENT = 'git_worktree_lifecycle';\nexport const GIT_WORKTREE_MESSAGE_EVENT = 'git_worktree_message';\nexport const WORKTREE_MESSAGE_VERSION = 1 as const;\nexport const MAX_WORKTREE_MESSAGE_BYTES = 64 * 1024;\nexport const MAX_WORKTREE_INBOX_MESSAGES = 100;\n\nexport interface WorktreeLifecycleEvent {\n readonly version: typeof WORKTREE_MESSAGE_VERSION;\n readonly repositoryRoot: string;\n}\n\nexport type WorktreeMessageParty = 'parent' | 'child';\n\nexport interface WorktreeMessage {\n readonly version: typeof WORKTREE_MESSAGE_VERSION;\n readonly worktreeId: string;\n readonly fromSessionId: string;\n readonly from: WorktreeMessageParty;\n readonly text: string;\n readonly sentAt: string;\n}\n\nexport interface WorktreeMessageInbox {\n send(targetSessionId: string, message: WorktreeMessage): void;\n receive(worktreeId: string): WorktreeMessage[];\n close(): void;\n}\n\nfunction isMessageParty(value: unknown): value is WorktreeMessageParty {\n return value === 'parent' || value === 'child';\n}\n\nfunction parseMessage(value: unknown): WorktreeMessage | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const record = value as Partial<WorktreeMessage>;\n if (record.version !== WORKTREE_MESSAGE_VERSION) return undefined;\n if (typeof record.worktreeId !== 'string' || record.worktreeId === '') return undefined;\n if (typeof record.fromSessionId !== 'string' || record.fromSessionId === '') return undefined;\n if (!isMessageParty(record.from)) return undefined;\n if (typeof record.text !== 'string' || record.text === '') return undefined;\n if (Buffer.byteLength(record.text, 'utf8') > MAX_WORKTREE_MESSAGE_BYTES) return undefined;\n if (typeof record.sentAt !== 'string' || record.sentAt === '') return undefined;\n return record as WorktreeMessage;\n}\n\nexport function createWorktreeMessageInbox(directEvents: DoomDirectEventBus, sessionId: string): WorktreeMessageInbox {\n if (sessionId === '') throw new Error('The Git message service requires a session identity.');\n const messages = new Map<string, WorktreeMessage[]>();\n const unsubscribe = directEvents.subscribe(GIT_WORKTREE_MESSAGE_EVENT, sessionId, (payload) => {\n const message = parseMessage(payload);\n if (message === undefined) return;\n const inbox = messages.get(message.worktreeId) ?? [];\n if (inbox.length >= MAX_WORKTREE_INBOX_MESSAGES) inbox.shift();\n inbox.push(message);\n messages.set(message.worktreeId, inbox);\n });\n let closed = false;\n return {\n send(targetSessionId, message) {\n if (closed) throw new Error('The Git message service is closed.');\n directEvents.publish(GIT_WORKTREE_MESSAGE_EVENT, targetSessionId, message);\n },\n receive(worktreeId) {\n if (closed) return [];\n const inbox = messages.get(worktreeId);\n if (inbox === undefined) return [];\n messages.delete(worktreeId);\n return inbox;\n },\n close() {\n if (closed) return;\n closed = true;\n unsubscribe();\n messages.clear();\n },\n };\n}\n\nexport function isWorktreeLifecycleEvent(value: unknown): value is WorktreeLifecycleEvent {\n if (typeof value !== 'object' || value === null) return false;\n const record = value as Partial<WorktreeLifecycleEvent>;\n return (\n record.version === WORKTREE_MESSAGE_VERSION &&\n typeof record.repositoryRoot === 'string' &&\n record.repositoryRoot !== ''\n );\n}\n"],"mappings":"AAEA,MACa,EAA6B,uBA2B1C,SAAS,EAAe,EAA+C,CACrE,OAAO,IAAU,UAAY,IAAU,OACzC,CAEA,SAAS,EAAa,EAA6C,CACjE,GAAI,OAAO,GAAU,WAAY,EAAgB,OACjD,IAAM,EAAS,EACX,KAAO,UAAA,GACP,OAAO,EAAO,YAAe,UAAY,EAAO,aAAe,IAC/D,OAAO,EAAO,eAAkB,UAAY,EAAO,gBAAkB,IACpE,EAAe,EAAO,IAAI,GAC3B,OAAO,EAAO,MAAS,UAAY,EAAO,OAAS,IACnD,SAAO,WAAW,EAAO,KAAM,MAAM,EAAA,QACrC,OAAO,EAAO,QAAW,UAAY,EAAO,SAAW,GAC3D,OAAO,CACT,CAEA,SAAgB,EAA2B,EAAkC,EAAyC,CACpH,GAAI,IAAc,GAAI,MAAU,MAAM,sDAAsD,EAC5F,IAAM,EAAW,IAAI,IACf,EAAc,EAAa,UAAU,EAA4B,EAAY,GAAY,CAC7F,IAAM,EAAU,EAAa,CAAO,EACpC,GAAI,IAAY,IAAA,GAAW,OAC3B,IAAM,EAAQ,EAAS,IAAI,EAAQ,UAAU,GAAK,CAAC,EAC/C,EAAM,QAAA,KAAuC,EAAM,MAAM,EAC7D,EAAM,KAAK,CAAO,EAClB,EAAS,IAAI,EAAQ,WAAY,CAAK,CACxC,CAAC,EACG,EAAS,GACb,MAAO,CACL,KAAK,EAAiB,EAAS,CAC7B,GAAI,EAAQ,MAAU,MAAM,oCAAoC,EAChE,EAAa,QAAQ,EAA4B,EAAiB,CAAO,CAC3E,EACA,QAAQ,EAAY,CAClB,GAAI,EAAQ,MAAO,CAAC,EACpB,IAAM,EAAQ,EAAS,IAAI,CAAU,EAGrC,OAFI,IAAU,IAAA,GAAkB,CAAC,GACjC,EAAS,OAAO,CAAU,EACnB,EACT,EACA,OAAQ,CACF,IACJ,EAAS,GACT,EAAY,EACZ,EAAS,MAAM,EACjB,CACF,CACF,CAEA,SAAgB,EAAyB,EAAiD,CACxF,GAAI,OAAO,GAAU,WAAY,EAAgB,MAAO,GACxD,IAAM,EAAS,EACf,OACE,EAAO,UAAA,GACP,OAAO,EAAO,gBAAmB,UACjC,EAAO,iBAAmB,EAE9B"}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import "@agimon-ai/doompi-core/hub-channel";
|
|
2
|
-
//#region src/services/worktreeEvents/index.d.ts
|
|
3
|
-
declare const WORKTREE_MESSAGE_VERSION: 1;
|
|
4
|
-
type WorktreeMessageParty = 'parent' | 'child';
|
|
5
|
-
interface WorktreeMessage {
|
|
6
|
-
readonly version: typeof WORKTREE_MESSAGE_VERSION;
|
|
7
|
-
readonly worktreeId: string;
|
|
8
|
-
readonly fromSessionId: string;
|
|
9
|
-
readonly from: WorktreeMessageParty;
|
|
10
|
-
readonly text: string;
|
|
11
|
-
readonly sentAt: string;
|
|
12
|
-
}
|
|
13
|
-
//#endregion
|
|
14
|
-
export { WORKTREE_MESSAGE_VERSION, WorktreeMessage, WorktreeMessageParty };
|
|
15
|
-
//# sourceMappingURL=index.d.cts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../../src/services/worktreeEvents/index.ts"],"mappings":";;cAIa;KASD;UAEK;WACN,gBAAgB;WAChB;WACA;WACA,MAAM;WACN;WACA"}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import "@agimon-ai/doompi-core/hub-channel";
|
|
2
|
-
//#region src/services/worktreeEvents/index.d.ts
|
|
3
|
-
declare const WORKTREE_MESSAGE_VERSION: 1;
|
|
4
|
-
type WorktreeMessageParty = 'parent' | 'child';
|
|
5
|
-
interface WorktreeMessage {
|
|
6
|
-
readonly version: typeof WORKTREE_MESSAGE_VERSION;
|
|
7
|
-
readonly worktreeId: string;
|
|
8
|
-
readonly fromSessionId: string;
|
|
9
|
-
readonly from: WorktreeMessageParty;
|
|
10
|
-
readonly text: string;
|
|
11
|
-
readonly sentAt: string;
|
|
12
|
-
}
|
|
13
|
-
//#endregion
|
|
14
|
-
export { WORKTREE_MESSAGE_VERSION, WorktreeMessage, WorktreeMessageParty };
|
|
15
|
-
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/worktreeEvents/index.ts"],"mappings":";;cAIa;KASD;UAEK;WACN,gBAAgB;WAChB;WACA;WACA,MAAM;WACN;WACA"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=`git_worktree_lifecycle`,t=`git_worktree_message`,n=65536;function r(e){return e===`parent`||e===`child`}function i(e){if(typeof e!=`object`||!e)return;let t=e;if(t.version===1&&typeof t.worktreeId==`string`&&t.worktreeId!==``&&typeof t.fromSessionId==`string`&&t.fromSessionId!==``&&r(t.from)&&typeof t.text==`string`&&t.text!==``&&!(Buffer.byteLength(t.text,`utf8`)>65536)&&typeof t.sentAt==`string`&&t.sentAt!==``)return t}function a(e,n){if(n===``)throw Error(`The Git message service requires a session identity.`);let r=new Map,a=e.subscribe(t,n,e=>{let t=i(e);if(t===void 0)return;let n=r.get(t.worktreeId)??[];n.length>=100&&n.shift(),n.push(t),r.set(t.worktreeId,n)}),o=!1;return{send(n,r){if(o)throw Error(`The Git message service is closed.`);e.publish(t,n,r)},receive(e){if(o)return[];let t=r.get(e);return t===void 0?[]:(r.delete(e),t)},close(){o||(o=!0,a(),r.clear())}}}function o(e){if(typeof e!=`object`||!e)return!1;let t=e;return t.version===1&&typeof t.repositoryRoot==`string`&&t.repositoryRoot!==``}export{e as GIT_WORKTREE_LIFECYCLE_EVENT,t as GIT_WORKTREE_MESSAGE_EVENT,n as MAX_WORKTREE_MESSAGE_BYTES,a as createWorktreeMessageInbox,o as isWorktreeLifecycleEvent};
|
|
2
|
-
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/worktreeEvents/index.ts"],"sourcesContent":["import type { DoomDirectEventBus } from '@agimon-ai/doompi-core/hub-channel';\n\nexport const GIT_WORKTREE_LIFECYCLE_EVENT = 'git_worktree_lifecycle';\nexport const GIT_WORKTREE_MESSAGE_EVENT = 'git_worktree_message';\nexport const WORKTREE_MESSAGE_VERSION = 1 as const;\nexport const MAX_WORKTREE_MESSAGE_BYTES = 64 * 1024;\nexport const MAX_WORKTREE_INBOX_MESSAGES = 100;\n\nexport interface WorktreeLifecycleEvent {\n readonly version: typeof WORKTREE_MESSAGE_VERSION;\n readonly repositoryRoot: string;\n}\n\nexport type WorktreeMessageParty = 'parent' | 'child';\n\nexport interface WorktreeMessage {\n readonly version: typeof WORKTREE_MESSAGE_VERSION;\n readonly worktreeId: string;\n readonly fromSessionId: string;\n readonly from: WorktreeMessageParty;\n readonly text: string;\n readonly sentAt: string;\n}\n\nexport interface WorktreeMessageInbox {\n send(targetSessionId: string, message: WorktreeMessage): void;\n receive(worktreeId: string): WorktreeMessage[];\n close(): void;\n}\n\nfunction isMessageParty(value: unknown): value is WorktreeMessageParty {\n return value === 'parent' || value === 'child';\n}\n\nfunction parseMessage(value: unknown): WorktreeMessage | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const record = value as Partial<WorktreeMessage>;\n if (record.version !== WORKTREE_MESSAGE_VERSION) return undefined;\n if (typeof record.worktreeId !== 'string' || record.worktreeId === '') return undefined;\n if (typeof record.fromSessionId !== 'string' || record.fromSessionId === '') return undefined;\n if (!isMessageParty(record.from)) return undefined;\n if (typeof record.text !== 'string' || record.text === '') return undefined;\n if (Buffer.byteLength(record.text, 'utf8') > MAX_WORKTREE_MESSAGE_BYTES) return undefined;\n if (typeof record.sentAt !== 'string' || record.sentAt === '') return undefined;\n return record as WorktreeMessage;\n}\n\nexport function createWorktreeMessageInbox(directEvents: DoomDirectEventBus, sessionId: string): WorktreeMessageInbox {\n if (sessionId === '') throw new Error('The Git message service requires a session identity.');\n const messages = new Map<string, WorktreeMessage[]>();\n const unsubscribe = directEvents.subscribe(GIT_WORKTREE_MESSAGE_EVENT, sessionId, (payload) => {\n const message = parseMessage(payload);\n if (message === undefined) return;\n const inbox = messages.get(message.worktreeId) ?? [];\n if (inbox.length >= MAX_WORKTREE_INBOX_MESSAGES) inbox.shift();\n inbox.push(message);\n messages.set(message.worktreeId, inbox);\n });\n let closed = false;\n return {\n send(targetSessionId, message) {\n if (closed) throw new Error('The Git message service is closed.');\n directEvents.publish(GIT_WORKTREE_MESSAGE_EVENT, targetSessionId, message);\n },\n receive(worktreeId) {\n if (closed) return [];\n const inbox = messages.get(worktreeId);\n if (inbox === undefined) return [];\n messages.delete(worktreeId);\n return inbox;\n },\n close() {\n if (closed) return;\n closed = true;\n unsubscribe();\n messages.clear();\n },\n };\n}\n\nexport function isWorktreeLifecycleEvent(value: unknown): value is WorktreeLifecycleEvent {\n if (typeof value !== 'object' || value === null) return false;\n const record = value as Partial<WorktreeLifecycleEvent>;\n return (\n record.version === WORKTREE_MESSAGE_VERSION &&\n typeof record.repositoryRoot === 'string' &&\n record.repositoryRoot !== ''\n );\n}\n"],"mappings":"AAEA,MAAa,EAA+B,yBAC/B,EAA6B,uBAE7B,EAA6B,MAyB1C,SAAS,EAAe,EAA+C,CACrE,OAAO,IAAU,UAAY,IAAU,OACzC,CAEA,SAAS,EAAa,EAA6C,CACjE,GAAI,OAAO,GAAU,WAAY,EAAgB,OACjD,IAAM,EAAS,EACX,KAAO,UAAA,GACP,OAAO,EAAO,YAAe,UAAY,EAAO,aAAe,IAC/D,OAAO,EAAO,eAAkB,UAAY,EAAO,gBAAkB,IACpE,EAAe,EAAO,IAAI,GAC3B,OAAO,EAAO,MAAS,UAAY,EAAO,OAAS,IACnD,SAAO,WAAW,EAAO,KAAM,MAAM,EAAA,QACrC,OAAO,EAAO,QAAW,UAAY,EAAO,SAAW,GAC3D,OAAO,CACT,CAEA,SAAgB,EAA2B,EAAkC,EAAyC,CACpH,GAAI,IAAc,GAAI,MAAU,MAAM,sDAAsD,EAC5F,IAAM,EAAW,IAAI,IACf,EAAc,EAAa,UAAU,EAA4B,EAAY,GAAY,CAC7F,IAAM,EAAU,EAAa,CAAO,EACpC,GAAI,IAAY,IAAA,GAAW,OAC3B,IAAM,EAAQ,EAAS,IAAI,EAAQ,UAAU,GAAK,CAAC,EAC/C,EAAM,QAAA,KAAuC,EAAM,MAAM,EAC7D,EAAM,KAAK,CAAO,EAClB,EAAS,IAAI,EAAQ,WAAY,CAAK,CACxC,CAAC,EACG,EAAS,GACb,MAAO,CACL,KAAK,EAAiB,EAAS,CAC7B,GAAI,EAAQ,MAAU,MAAM,oCAAoC,EAChE,EAAa,QAAQ,EAA4B,EAAiB,CAAO,CAC3E,EACA,QAAQ,EAAY,CAClB,GAAI,EAAQ,MAAO,CAAC,EACpB,IAAM,EAAQ,EAAS,IAAI,CAAU,EAGrC,OAFI,IAAU,IAAA,GAAkB,CAAC,GACjC,EAAS,OAAO,CAAU,EACnB,EACT,EACA,OAAQ,CACF,IACJ,EAAS,GACT,EAAY,EACZ,EAAS,MAAM,EACjB,CACF,CACF,CAEA,SAAgB,EAAyB,EAAiD,CACxF,GAAI,OAAO,GAAU,WAAY,EAAgB,MAAO,GACxD,IAAM,EAAS,EACf,OACE,EAAO,UAAA,GACP,OAAO,EAAO,gBAAmB,UACjC,EAAO,iBAAmB,EAE9B"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
function e(e){let t=e.toLowerCase().replace(/[^a-z0-9]+/gu,`-`).replace(/^-+|-+$/gu,``).slice(0,64).replace(/-+$/gu,``);return t===``?`worktree`:t}function t(t){let n=`${t.repositoryLabel}--${t.repositoryId}`;return`${t.worktreesRoot}/${n}/${e(t.branch)}--${t.shortId}`}function n(e){if(e.branch.trim()===``)return`A branch name is required.`;let t=e.existing.find(t=>t.branch===e.branch&&t.status!==`orphaned`);if(t)return`Branch ${e.branch} already has a worktree (${t.id}).`}function r(e){if(!e.record)return`No worktree with that id. Call list to see the current ids.`;if(e.dirtyFiles.length===0||e.force)return;let t=e.dirtyFiles.slice(0,10).join(`, `),n=e.dirtyFiles.length>10?`, and ${String(e.dirtyFiles.length-10)} more`:``;return`${String(e.dirtyFiles.length)} uncommitted file(s): ${t}${n}. Pass force: true to discard them.`}function i(e,t){let n=[];return{records:e.map(e=>e.status===`orphaned`||t.exists(e.path)&&t.alive(e)?e:(n.push(e.id),{...e,status:`orphaned`})),changed:n}}function a(e){let t={remove:[],forget:[],untracked:[],keptDirty:[],keptBranches:[]},n=new Set(e.records.map(e=>e.path)),r=new Set(e.gitPaths);for(let n of e.records)if(n.status===`orphaned`){if(!r.has(n.path)){t.forget.push(n);continue}if((e.dirtyByPath.get(n.path)??[]).length>0){t.keptDirty.push(n);continue}t.remove.push(n)}for(let r of e.gitPaths)r.startsWith(`${e.worktreesRoot}/`)&&!n.has(r)&&t.untracked.push(r);return t}exports.branchSlug=e,exports.planPrune=a,exports.reconcile=i,exports.refuseClose=r,exports.refuseSpawn=n,exports.worktreeDirectory=t;
|
|
2
|
-
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/services/worktreeNaming/index.ts"],"sourcesContent":["/**\n * Naming and reconciliation rules for worktrees, as pure functions.\n *\n * Everything here is a function of its arguments so the interesting decisions,\n * which are the ones about destroying work, can be tested without a repository\n * or a filesystem.\n */\n\nimport type { WorktreeRecord } from '../../types/worktreeRegistry';\n\nconst MAX_SLUG_LENGTH = 64;\n\n/**\n * A branch name reduced to one safe path segment.\n *\n * Slashes are the point: `wt/fix-auth` is a perfectly good branch and a\n * terrible directory name, because the nested directory it implies is not the\n * one the worktree lives in.\n */\nexport function branchSlug(branch: string): string {\n const slug = branch\n .toLowerCase()\n .replace(/[^a-z0-9]+/gu, '-')\n .replace(/^-+|-+$/gu, '')\n .slice(0, MAX_SLUG_LENGTH)\n .replace(/-+$/gu, '');\n return slug === '' ? 'worktree' : slug;\n}\n\n/**\n * The directory for one worktree.\n *\n * The short id disambiguates two branches whose slugs collide, such as\n * `feature/x` and `feature_x`, which would otherwise want the same directory.\n */\nexport function worktreeDirectory(input: {\n worktreesRoot: string;\n repositoryLabel: string;\n repositoryId: string;\n branch: string;\n shortId: string;\n}): string {\n const repository = `${input.repositoryLabel}--${input.repositoryId}`;\n return `${input.worktreesRoot}/${repository}/${branchSlug(input.branch)}--${input.shortId}`;\n}\n\n/** Why a worktree cannot be created, or undefined when it can. */\nexport function refuseSpawn(input: { branch: string; existing: readonly WorktreeRecord[] }): string | undefined {\n if (input.branch.trim() === '') return 'A branch name is required.';\n // Git refuses a second worktree on one branch, but its message names a path\n // the caller never chose, so this says it in the caller's own terms first.\n const live = input.existing.find((record) => record.branch === input.branch && record.status !== 'orphaned');\n if (live) return `Branch ${input.branch} already has a worktree (${live.id}).`;\n return undefined;\n}\n\n/** Why a worktree cannot be closed, or undefined when it can. */\nexport function refuseClose(input: {\n record: WorktreeRecord | undefined;\n dirtyFiles: readonly string[];\n force: boolean;\n}): string | undefined {\n if (!input.record) return 'No worktree with that id. Call list to see the current ids.';\n if (input.dirtyFiles.length === 0 || input.force) return undefined;\n // Naming the files is the point: \"the tree is dirty\" sends the reader to go\n // look, and the answer is usually a file they forgot rather than one they\n // meant to keep.\n const names = input.dirtyFiles.slice(0, 10).join(', ');\n const more = input.dirtyFiles.length > 10 ? `, and ${String(input.dirtyFiles.length - 10)} more` : '';\n return `${String(input.dirtyFiles.length)} uncommitted file(s): ${names}${more}. Pass force: true to discard them.`;\n}\n\nexport interface Reconciliation {\n records: WorktreeRecord[];\n /** Ids whose status changed, so a caller can report what it noticed. */\n changed: string[];\n}\n\n/**\n * Marks records whose process or directory is gone as orphaned.\n *\n * Nothing is deleted here. A doompi-git orphan owns a directory that may hold\n * uncommitted work, so losing the record would lose the only pointer to it.\n * Deletion is always a separate, named decision.\n */\nexport function reconcile(\n records: readonly WorktreeRecord[],\n probe: { alive: (record: WorktreeRecord) => boolean; exists: (path: string) => boolean },\n): Reconciliation {\n const changed: string[] = [];\n const next = records.map((record) => {\n if (record.status === 'orphaned') return record;\n const gone = !probe.exists(record.path) || !probe.alive(record);\n if (!gone) return record;\n changed.push(record.id);\n return { ...record, status: 'orphaned' as const };\n });\n return { records: next, changed };\n}\n\nexport interface PrunePlan {\n /** Orphans whose directory git still lists, safe to remove. */\n remove: WorktreeRecord[];\n /** Orphans whose directory is already gone; drop the record only. */\n forget: WorktreeRecord[];\n /** Paths git lists under our root that we have no record of; report, never touch. */\n untracked: string[];\n /** Orphans left alone because they still hold uncommitted work. */\n keptDirty: WorktreeRecord[];\n /**\n * Branches git refused to delete because they hold commits.\n *\n * Filled by the prune itself, not by the plan: a dry run destroys nothing, so\n * it has nothing to report here and leaves it empty.\n */\n keptBranches: string[];\n}\n\n/**\n * What a prune would do, decided before anything is destroyed.\n *\n * Separating the plan from the execution is what makes dryRun honest and lets\n * the destructive half be reviewed on its own.\n */\nexport function planPrune(input: {\n records: readonly WorktreeRecord[];\n gitPaths: readonly string[];\n worktreesRoot: string;\n dirtyByPath: ReadonlyMap<string, readonly string[]>;\n}): PrunePlan {\n const plan: PrunePlan = { remove: [], forget: [], untracked: [], keptDirty: [], keptBranches: [] };\n const known = new Set(input.records.map((record) => record.path));\n const listed = new Set(input.gitPaths);\n for (const record of input.records) {\n if (record.status !== 'orphaned') continue;\n if (!listed.has(record.path)) {\n plan.forget.push(record);\n continue;\n }\n if ((input.dirtyByPath.get(record.path) ?? []).length > 0) {\n plan.keptDirty.push(record);\n continue;\n }\n plan.remove.push(record);\n }\n for (const path of input.gitPaths) {\n // Only ever inside our own root: a worktree the user made by hand is none\n // of this package's business, even when it looks abandoned.\n if (path.startsWith(`${input.worktreesRoot}/`) && !known.has(path)) plan.untracked.push(path);\n }\n return plan;\n}\n"],"mappings":"AAmBA,SAAgB,EAAW,EAAwB,CACjD,IAAM,EAAO,EACV,YAAY,CAAC,CACb,QAAQ,eAAgB,GAAG,CAAC,CAC5B,QAAQ,YAAa,EAAE,CAAC,CACxB,MAAM,EAAG,EAAe,CAAC,CACzB,QAAQ,QAAS,EAAE,EACtB,OAAO,IAAS,GAAK,WAAa,CACpC,CAQA,SAAgB,EAAkB,EAMvB,CACT,IAAM,EAAa,GAAG,EAAM,gBAAgB,IAAI,EAAM,eACtD,MAAO,GAAG,EAAM,cAAc,GAAG,EAAW,GAAG,EAAW,EAAM,MAAM,EAAE,IAAI,EAAM,SACpF,CAGA,SAAgB,EAAY,EAAoF,CAC9G,GAAI,EAAM,OAAO,KAAK,IAAM,GAAI,MAAO,6BAGvC,IAAM,EAAO,EAAM,SAAS,KAAM,GAAW,EAAO,SAAW,EAAM,QAAU,EAAO,SAAW,UAAU,EAC3G,GAAI,EAAM,MAAO,UAAU,EAAM,OAAO,2BAA2B,EAAK,GAAG,GAE7E,CAGA,SAAgB,EAAY,EAIL,CACrB,GAAI,CAAC,EAAM,OAAQ,MAAO,8DAC1B,GAAI,EAAM,WAAW,SAAW,GAAK,EAAM,MAAO,OAIlD,IAAM,EAAQ,EAAM,WAAW,MAAM,EAAG,EAAE,CAAC,CAAC,KAAK,IAAI,EAC/C,EAAO,EAAM,WAAW,OAAS,GAAK,SAAS,OAAO,EAAM,WAAW,OAAS,EAAE,EAAE,OAAS,GACnG,MAAO,GAAG,OAAO,EAAM,WAAW,MAAM,EAAE,wBAAwB,IAAQ,EAAK,oCACjF,CAeA,SAAgB,EACd,EACA,EACgB,CAChB,IAAM,EAAoB,CAAC,EAQ3B,MAAO,CAAE,QAPI,EAAQ,IAAK,GACpB,EAAO,SAAW,YACR,EAAM,OAAO,EAAO,IAAI,GAAM,EAAM,MAAM,CAAM,EAC5C,GAClB,EAAQ,KAAK,EAAO,EAAE,EACf,CAAE,GAAG,EAAQ,OAAQ,UAAoB,EAE7B,EAAG,SAAQ,CAClC,CA0BA,SAAgB,EAAU,EAKZ,CACZ,IAAM,EAAkB,CAAE,OAAQ,CAAC,EAAG,OAAQ,CAAC,EAAG,UAAW,CAAC,EAAG,UAAW,CAAC,EAAG,aAAc,CAAC,CAAE,EAC3F,EAAQ,IAAI,IAAI,EAAM,QAAQ,IAAK,GAAW,EAAO,IAAI,CAAC,EAC1D,EAAS,IAAI,IAAI,EAAM,QAAQ,EACrC,IAAK,IAAM,KAAU,EAAM,QACrB,KAAO,SAAW,WACtB,IAAI,CAAC,EAAO,IAAI,EAAO,IAAI,EAAG,CAC5B,EAAK,OAAO,KAAK,CAAM,EACvB,QACF,CACA,IAAK,EAAM,YAAY,IAAI,EAAO,IAAI,GAAK,CAAC,EAAA,CAAG,OAAS,EAAG,CACzD,EAAK,UAAU,KAAK,CAAM,EAC1B,QACF,CACA,EAAK,OAAO,KAAK,CAAM,CALvB,CAOF,IAAK,IAAM,KAAQ,EAAM,SAGnB,EAAK,WAAW,GAAG,EAAM,cAAc,EAAE,GAAK,CAAC,EAAM,IAAI,CAAI,GAAG,EAAK,UAAU,KAAK,CAAI,EAE9F,OAAO,CACT"}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { WorktreeRecord } from "../../types/worktreeRegistry.cjs";
|
|
2
|
-
//#region src/services/worktreeNaming/index.d.ts
|
|
3
|
-
interface PrunePlan {
|
|
4
|
-
/** Orphans whose directory git still lists, safe to remove. */
|
|
5
|
-
remove: WorktreeRecord[];
|
|
6
|
-
/** Orphans whose directory is already gone; drop the record only. */
|
|
7
|
-
forget: WorktreeRecord[];
|
|
8
|
-
/** Paths git lists under our root that we have no record of; report, never touch. */
|
|
9
|
-
untracked: string[];
|
|
10
|
-
/** Orphans left alone because they still hold uncommitted work. */
|
|
11
|
-
keptDirty: WorktreeRecord[];
|
|
12
|
-
/**
|
|
13
|
-
* Branches git refused to delete because they hold commits.
|
|
14
|
-
*
|
|
15
|
-
* Filled by the prune itself, not by the plan: a dry run destroys nothing, so
|
|
16
|
-
* it has nothing to report here and leaves it empty.
|
|
17
|
-
*/
|
|
18
|
-
keptBranches: string[];
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* What a prune would do, decided before anything is destroyed.
|
|
22
|
-
*
|
|
23
|
-
* Separating the plan from the execution is what makes dryRun honest and lets
|
|
24
|
-
* the destructive half be reviewed on its own.
|
|
25
|
-
*/
|
|
26
|
-
declare function planPrune(input: {
|
|
27
|
-
records: readonly WorktreeRecord[];
|
|
28
|
-
gitPaths: readonly string[];
|
|
29
|
-
worktreesRoot: string;
|
|
30
|
-
dirtyByPath: ReadonlyMap<string, readonly string[]>;
|
|
31
|
-
}): PrunePlan;
|
|
32
|
-
//#endregion
|
|
33
|
-
export { PrunePlan, planPrune };
|
|
34
|
-
//# sourceMappingURL=index.d.cts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../../src/services/worktreeNaming/index.ts"],"mappings":";;UAoGiB;;EAEf,QAAQ;;EAER,QAAQ;;EAER;;EAEA,WAAW;;;;;;;EAOX;;;;;;;;iBASc,UAAU;EACxB,kBAAkB;EAClB;EACA;EACA,aAAa;IACX"}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { WorktreeRecord } from "../../types/worktreeRegistry.mjs";
|
|
2
|
-
//#region src/services/worktreeNaming/index.d.ts
|
|
3
|
-
interface PrunePlan {
|
|
4
|
-
/** Orphans whose directory git still lists, safe to remove. */
|
|
5
|
-
remove: WorktreeRecord[];
|
|
6
|
-
/** Orphans whose directory is already gone; drop the record only. */
|
|
7
|
-
forget: WorktreeRecord[];
|
|
8
|
-
/** Paths git lists under our root that we have no record of; report, never touch. */
|
|
9
|
-
untracked: string[];
|
|
10
|
-
/** Orphans left alone because they still hold uncommitted work. */
|
|
11
|
-
keptDirty: WorktreeRecord[];
|
|
12
|
-
/**
|
|
13
|
-
* Branches git refused to delete because they hold commits.
|
|
14
|
-
*
|
|
15
|
-
* Filled by the prune itself, not by the plan: a dry run destroys nothing, so
|
|
16
|
-
* it has nothing to report here and leaves it empty.
|
|
17
|
-
*/
|
|
18
|
-
keptBranches: string[];
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* What a prune would do, decided before anything is destroyed.
|
|
22
|
-
*
|
|
23
|
-
* Separating the plan from the execution is what makes dryRun honest and lets
|
|
24
|
-
* the destructive half be reviewed on its own.
|
|
25
|
-
*/
|
|
26
|
-
declare function planPrune(input: {
|
|
27
|
-
records: readonly WorktreeRecord[];
|
|
28
|
-
gitPaths: readonly string[];
|
|
29
|
-
worktreesRoot: string;
|
|
30
|
-
dirtyByPath: ReadonlyMap<string, readonly string[]>;
|
|
31
|
-
}): PrunePlan;
|
|
32
|
-
//#endregion
|
|
33
|
-
export { PrunePlan, planPrune };
|
|
34
|
-
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/worktreeNaming/index.ts"],"mappings":";;UAoGiB;;EAEf,QAAQ;;EAER,QAAQ;;EAER;;EAEA,WAAW;;;;;;;EAOX;;;;;;;;iBASc,UAAU;EACxB,kBAAkB;EAClB;EACA;EACA,aAAa;IACX"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
function e(e){let t=e.toLowerCase().replace(/[^a-z0-9]+/gu,`-`).replace(/^-+|-+$/gu,``).slice(0,64).replace(/-+$/gu,``);return t===``?`worktree`:t}function t(t){let n=`${t.repositoryLabel}--${t.repositoryId}`;return`${t.worktreesRoot}/${n}/${e(t.branch)}--${t.shortId}`}function n(e){if(e.branch.trim()===``)return`A branch name is required.`;let t=e.existing.find(t=>t.branch===e.branch&&t.status!==`orphaned`);if(t)return`Branch ${e.branch} already has a worktree (${t.id}).`}function r(e){if(!e.record)return`No worktree with that id. Call list to see the current ids.`;if(e.dirtyFiles.length===0||e.force)return;let t=e.dirtyFiles.slice(0,10).join(`, `),n=e.dirtyFiles.length>10?`, and ${String(e.dirtyFiles.length-10)} more`:``;return`${String(e.dirtyFiles.length)} uncommitted file(s): ${t}${n}. Pass force: true to discard them.`}function i(e,t){let n=[];return{records:e.map(e=>e.status===`orphaned`||t.exists(e.path)&&t.alive(e)?e:(n.push(e.id),{...e,status:`orphaned`})),changed:n}}function a(e){let t={remove:[],forget:[],untracked:[],keptDirty:[],keptBranches:[]},n=new Set(e.records.map(e=>e.path)),r=new Set(e.gitPaths);for(let n of e.records)if(n.status===`orphaned`){if(!r.has(n.path)){t.forget.push(n);continue}if((e.dirtyByPath.get(n.path)??[]).length>0){t.keptDirty.push(n);continue}t.remove.push(n)}for(let r of e.gitPaths)r.startsWith(`${e.worktreesRoot}/`)&&!n.has(r)&&t.untracked.push(r);return t}export{e as branchSlug,a as planPrune,i as reconcile,r as refuseClose,n as refuseSpawn,t as worktreeDirectory};
|
|
2
|
-
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/worktreeNaming/index.ts"],"sourcesContent":["/**\n * Naming and reconciliation rules for worktrees, as pure functions.\n *\n * Everything here is a function of its arguments so the interesting decisions,\n * which are the ones about destroying work, can be tested without a repository\n * or a filesystem.\n */\n\nimport type { WorktreeRecord } from '../../types/worktreeRegistry';\n\nconst MAX_SLUG_LENGTH = 64;\n\n/**\n * A branch name reduced to one safe path segment.\n *\n * Slashes are the point: `wt/fix-auth` is a perfectly good branch and a\n * terrible directory name, because the nested directory it implies is not the\n * one the worktree lives in.\n */\nexport function branchSlug(branch: string): string {\n const slug = branch\n .toLowerCase()\n .replace(/[^a-z0-9]+/gu, '-')\n .replace(/^-+|-+$/gu, '')\n .slice(0, MAX_SLUG_LENGTH)\n .replace(/-+$/gu, '');\n return slug === '' ? 'worktree' : slug;\n}\n\n/**\n * The directory for one worktree.\n *\n * The short id disambiguates two branches whose slugs collide, such as\n * `feature/x` and `feature_x`, which would otherwise want the same directory.\n */\nexport function worktreeDirectory(input: {\n worktreesRoot: string;\n repositoryLabel: string;\n repositoryId: string;\n branch: string;\n shortId: string;\n}): string {\n const repository = `${input.repositoryLabel}--${input.repositoryId}`;\n return `${input.worktreesRoot}/${repository}/${branchSlug(input.branch)}--${input.shortId}`;\n}\n\n/** Why a worktree cannot be created, or undefined when it can. */\nexport function refuseSpawn(input: { branch: string; existing: readonly WorktreeRecord[] }): string | undefined {\n if (input.branch.trim() === '') return 'A branch name is required.';\n // Git refuses a second worktree on one branch, but its message names a path\n // the caller never chose, so this says it in the caller's own terms first.\n const live = input.existing.find((record) => record.branch === input.branch && record.status !== 'orphaned');\n if (live) return `Branch ${input.branch} already has a worktree (${live.id}).`;\n return undefined;\n}\n\n/** Why a worktree cannot be closed, or undefined when it can. */\nexport function refuseClose(input: {\n record: WorktreeRecord | undefined;\n dirtyFiles: readonly string[];\n force: boolean;\n}): string | undefined {\n if (!input.record) return 'No worktree with that id. Call list to see the current ids.';\n if (input.dirtyFiles.length === 0 || input.force) return undefined;\n // Naming the files is the point: \"the tree is dirty\" sends the reader to go\n // look, and the answer is usually a file they forgot rather than one they\n // meant to keep.\n const names = input.dirtyFiles.slice(0, 10).join(', ');\n const more = input.dirtyFiles.length > 10 ? `, and ${String(input.dirtyFiles.length - 10)} more` : '';\n return `${String(input.dirtyFiles.length)} uncommitted file(s): ${names}${more}. Pass force: true to discard them.`;\n}\n\nexport interface Reconciliation {\n records: WorktreeRecord[];\n /** Ids whose status changed, so a caller can report what it noticed. */\n changed: string[];\n}\n\n/**\n * Marks records whose process or directory is gone as orphaned.\n *\n * Nothing is deleted here. A doompi-git orphan owns a directory that may hold\n * uncommitted work, so losing the record would lose the only pointer to it.\n * Deletion is always a separate, named decision.\n */\nexport function reconcile(\n records: readonly WorktreeRecord[],\n probe: { alive: (record: WorktreeRecord) => boolean; exists: (path: string) => boolean },\n): Reconciliation {\n const changed: string[] = [];\n const next = records.map((record) => {\n if (record.status === 'orphaned') return record;\n const gone = !probe.exists(record.path) || !probe.alive(record);\n if (!gone) return record;\n changed.push(record.id);\n return { ...record, status: 'orphaned' as const };\n });\n return { records: next, changed };\n}\n\nexport interface PrunePlan {\n /** Orphans whose directory git still lists, safe to remove. */\n remove: WorktreeRecord[];\n /** Orphans whose directory is already gone; drop the record only. */\n forget: WorktreeRecord[];\n /** Paths git lists under our root that we have no record of; report, never touch. */\n untracked: string[];\n /** Orphans left alone because they still hold uncommitted work. */\n keptDirty: WorktreeRecord[];\n /**\n * Branches git refused to delete because they hold commits.\n *\n * Filled by the prune itself, not by the plan: a dry run destroys nothing, so\n * it has nothing to report here and leaves it empty.\n */\n keptBranches: string[];\n}\n\n/**\n * What a prune would do, decided before anything is destroyed.\n *\n * Separating the plan from the execution is what makes dryRun honest and lets\n * the destructive half be reviewed on its own.\n */\nexport function planPrune(input: {\n records: readonly WorktreeRecord[];\n gitPaths: readonly string[];\n worktreesRoot: string;\n dirtyByPath: ReadonlyMap<string, readonly string[]>;\n}): PrunePlan {\n const plan: PrunePlan = { remove: [], forget: [], untracked: [], keptDirty: [], keptBranches: [] };\n const known = new Set(input.records.map((record) => record.path));\n const listed = new Set(input.gitPaths);\n for (const record of input.records) {\n if (record.status !== 'orphaned') continue;\n if (!listed.has(record.path)) {\n plan.forget.push(record);\n continue;\n }\n if ((input.dirtyByPath.get(record.path) ?? []).length > 0) {\n plan.keptDirty.push(record);\n continue;\n }\n plan.remove.push(record);\n }\n for (const path of input.gitPaths) {\n // Only ever inside our own root: a worktree the user made by hand is none\n // of this package's business, even when it looks abandoned.\n if (path.startsWith(`${input.worktreesRoot}/`) && !known.has(path)) plan.untracked.push(path);\n }\n return plan;\n}\n"],"mappings":"AAmBA,SAAgB,EAAW,EAAwB,CACjD,IAAM,EAAO,EACV,YAAY,CAAC,CACb,QAAQ,eAAgB,GAAG,CAAC,CAC5B,QAAQ,YAAa,EAAE,CAAC,CACxB,MAAM,EAAG,EAAe,CAAC,CACzB,QAAQ,QAAS,EAAE,EACtB,OAAO,IAAS,GAAK,WAAa,CACpC,CAQA,SAAgB,EAAkB,EAMvB,CACT,IAAM,EAAa,GAAG,EAAM,gBAAgB,IAAI,EAAM,eACtD,MAAO,GAAG,EAAM,cAAc,GAAG,EAAW,GAAG,EAAW,EAAM,MAAM,EAAE,IAAI,EAAM,SACpF,CAGA,SAAgB,EAAY,EAAoF,CAC9G,GAAI,EAAM,OAAO,KAAK,IAAM,GAAI,MAAO,6BAGvC,IAAM,EAAO,EAAM,SAAS,KAAM,GAAW,EAAO,SAAW,EAAM,QAAU,EAAO,SAAW,UAAU,EAC3G,GAAI,EAAM,MAAO,UAAU,EAAM,OAAO,2BAA2B,EAAK,GAAG,GAE7E,CAGA,SAAgB,EAAY,EAIL,CACrB,GAAI,CAAC,EAAM,OAAQ,MAAO,8DAC1B,GAAI,EAAM,WAAW,SAAW,GAAK,EAAM,MAAO,OAIlD,IAAM,EAAQ,EAAM,WAAW,MAAM,EAAG,EAAE,CAAC,CAAC,KAAK,IAAI,EAC/C,EAAO,EAAM,WAAW,OAAS,GAAK,SAAS,OAAO,EAAM,WAAW,OAAS,EAAE,EAAE,OAAS,GACnG,MAAO,GAAG,OAAO,EAAM,WAAW,MAAM,EAAE,wBAAwB,IAAQ,EAAK,oCACjF,CAeA,SAAgB,EACd,EACA,EACgB,CAChB,IAAM,EAAoB,CAAC,EAQ3B,MAAO,CAAE,QAPI,EAAQ,IAAK,GACpB,EAAO,SAAW,YACR,EAAM,OAAO,EAAO,IAAI,GAAM,EAAM,MAAM,CAAM,EAC5C,GAClB,EAAQ,KAAK,EAAO,EAAE,EACf,CAAE,GAAG,EAAQ,OAAQ,UAAoB,EAE7B,EAAG,SAAQ,CAClC,CA0BA,SAAgB,EAAU,EAKZ,CACZ,IAAM,EAAkB,CAAE,OAAQ,CAAC,EAAG,OAAQ,CAAC,EAAG,UAAW,CAAC,EAAG,UAAW,CAAC,EAAG,aAAc,CAAC,CAAE,EAC3F,EAAQ,IAAI,IAAI,EAAM,QAAQ,IAAK,GAAW,EAAO,IAAI,CAAC,EAC1D,EAAS,IAAI,IAAI,EAAM,QAAQ,EACrC,IAAK,IAAM,KAAU,EAAM,QACrB,KAAO,SAAW,WACtB,IAAI,CAAC,EAAO,IAAI,EAAO,IAAI,EAAG,CAC5B,EAAK,OAAO,KAAK,CAAM,EACvB,QACF,CACA,IAAK,EAAM,YAAY,IAAI,EAAO,IAAI,GAAK,CAAC,EAAA,CAAG,OAAS,EAAG,CACzD,EAAK,UAAU,KAAK,CAAM,EAC1B,QACF,CACA,EAAK,OAAO,KAAK,CAAM,CALvB,CAOF,IAAK,IAAM,KAAQ,EAAM,SAGnB,EAAK,WAAW,GAAG,EAAM,cAAc,EAAE,GAAK,CAAC,EAAM,IAAI,CAAI,GAAG,EAAK,UAAU,KAAK,CAAI,EAE9F,OAAO,CACT"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../compositionMirror/index.cjs"),n=require("../errors/index.cjs"),r=require("../repositoryIdentity/index.cjs"),i=require("../paths/index.cjs"),a=require("../worktreeEvents/index.cjs"),o=require("../worktreeNaming/index.cjs"),s=require("../worktreeRegistry/index.cjs");let c=require("node:fs");c=e.__toESM(c,1);let l=require("node:crypto");function u(e){let{git:u}=e,d=e.sessionService,f=e.mirror??t.mirrorComposition,p=e.now??(()=>new Date),m=()=>{if(d===void 0)throw new n.HubUnavailableError(`The cockpit session service is unavailable. Start the cockpit and try again.`);return d},h=()=>{if(e.messageInbox===void 0)throw new n.HubUnavailableError(`The Git message service is unavailable. Start the cockpit and try again.`);return e.messageInbox},g={alive:e=>m().isLive(e.sessionId),exists:e=>c.default.existsSync(e)},_=async t=>{let r=await u.repositoryRoot(t.cwd);if(r===void 0)throw new n.DoomGitExpectedError(`not_a_repository`,`${t.cwd} is not inside a git repository.`,!1,`Run this from a repository checkout.`);let a=s.createWorktreeRegistry(i.registryFile(r,e.homeDir));return{root:r,store:a,records:a.list()}},v=(e,t)=>{let r=e.find(e=>e.id===t);if(r===void 0)throw new n.DoomGitExpectedError(`worktree_not_found`,`No worktree with id ${t}.`,!1,`Call list to see the current ids.`);return r},y=(e,t,r)=>{if(t.parentSessionId!==e.sessionId&&m().isLive(t.parentSessionId))throw new n.DoomGitExpectedError(`worktree_not_owned`,`Worktree ${t.id} belongs to another session.`,!1,`Ask the session that created it to ${r} it, or close that session first.`)},b=(e,t)=>{if(e.sessionId===t.parentSessionId)return{sessionId:t.sessionId,party:`parent`};if(e.sessionId===t.sessionId)return{sessionId:t.parentSessionId,party:`child`};throw new n.DoomGitExpectedError(`worktree_not_owned`,`Worktree ${t.id} belongs to another session.`,!1,`Use a worktree owned by this session.`)},x=e=>{if(!m().isLive(e))throw new n.DoomGitExpectedError(`worktree_peer_unavailable`,`The other worktree session is no longer live.`,!1,`Start a new worktree session before sending a message.`)};return{async spawn(t,a,s){let{root:c,store:d,records:h}=await _(t),g=o.refuseSpawn({branch:a.branch,existing:h});if(g!==void 0)throw new n.DoomGitExpectedError(`worktree_exists`,g,!1,`Pick another branch name, or close it.`);let v=a.baseRef??await u.currentBranch(c)??`HEAD`,y=r.shortId((0,l.randomUUID)()),b=o.worktreeDirectory({worktreesRoot:i.worktreesRoot(e.homeDir),repositoryLabel:r.repositoryLabel(c),repositoryId:r.repositoryId(c),branch:a.branch,shortId:y}),x=()=>s?.signal?.aborted===!0,S=async()=>(await u.removeWorktree({repositoryRoot:c,path:b,force:!0}).catch(()=>void 0),await u.deleteBranch({repositoryRoot:c,branch:a.branch}).catch(()=>!1)?``:` The branch ${a.branch} has work on it and was kept.`);if(s?.onProgress?.(`creating branch ${a.branch}\u2026`),await u.addWorktree({repositoryRoot:c,path:b,branch:a.branch,baseRef:v}),x()){let e=await S();throw new n.DoomGitExpectedError(`spawn_cancelled`,`Cancelled before the session started.${e}`,!1,`The worktree was removed; run it again when you are ready.`)}s?.onProgress?.(`mirroring build output…`),f(c,b),s?.onProgress?.(`starting session…`);let C;try{C=(await m().create({cwd:b,name:a.name??a.branch,parentSessionId:t.sessionId,sessionProvenance:`worktree`,...s?.signal===void 0?{}:{signal:s.signal}})).sessionId}catch(e){let t=await S();throw x()?new n.DoomGitExpectedError(`spawn_cancelled`,`Cancelled while the session was starting.${t}`,!1,`The worktree was removed; run it again when you are ready.`):e instanceof n.HubUnavailableError?new n.DoomGitExpectedError(`hub_unavailable`,`${e.message}${t}`,!0,`Start the cockpit and try again.`):e}let w={version:1,id:y,branch:a.branch,baseRef:v,path:b,repositoryRoot:c,sessionId:C,parentSessionId:t.sessionId,status:`running`,createdAt:p().toISOString()};try{d.replace([...h,w])}catch(e){await m().close(C).catch(()=>void 0);let t=await S();throw new n.DoomGitExpectedError(`registry_write_failed`,`The worktree was created but the registry could not be written (${e.message}).${t}`,!0,`Check the registry file for permissions or free space, then try again.`)}return w},async close(e,t,r){let{root:i,store:a,records:s}=await _(e),l=v(s,t);y(e,l,`close`);let d=c.default.existsSync(l.path)?await u.dirtyFiles(l.path):[],f=o.refuseClose({record:l,dirtyFiles:d,force:r});if(f!==void 0)throw new n.DoomGitExpectedError(`worktree_dirty`,f,!1,`Commit the work, or pass force: true.`);return await m().close(l.sessionId),await u.removeWorktree({repositoryRoot:i,path:l.path,force:r}),a.replace(s.filter(e=>e.id!==t)),l},async list(e){let{store:t,records:n}=await _(e),r=o.reconcile(n,g);return r.changed.length>0&&t.replace(r.records),r.records},async status(e,t){let{records:n}=await _(e),r=v(n,t);return{record:r,dirtyFiles:c.default.existsSync(r.path)?await u.dirtyFiles(r.path):[]}},async merge(e,t,r){let{root:i,records:a}=await _(e),o=v(a,t);y(e,o,`merge`);let s=await u.dirtyFiles(i);if(s.length>0)throw new n.DoomGitExpectedError(`worktree_dirty`,`The parent checkout has ${String(s.length)} uncommitted file(s): ${s.slice(0,10).join(`, `)}.`,!1,`Commit or stash the parent checkout first.`);return await u.mergeBranch({repositoryRoot:i,branch:o.branch,...r===void 0?{}:{message:r}}),o},async send(e,t,r){if(Buffer.byteLength(r,`utf8`)>65536)throw new n.DoomGitExpectedError(`message_too_large`,`A message may not exceed ${String(a.MAX_WORKTREE_MESSAGE_BYTES)} bytes.`,!1,`Send a shorter message.`);let{records:i}=await _(e),o=v(i,t),s=b(e,o);x(s.sessionId),h().send(s.sessionId,{version:1,worktreeId:o.id,fromSessionId:e.sessionId,from:s.party,text:r,sentAt:new Date().toISOString()})},async messages(e,t){let{records:n}=await _(e),r=v(n,t);return b(e,r),h().receive(r.id)},async prune(t,n){let{root:r,store:a,records:s}=await _(t),l=o.reconcile(s,g),d=await u.listWorktreePaths(r),f=new Map;for(let e of l.records)e.status===`orphaned`&&c.default.existsSync(e.path)&&f.set(e.path,await u.dirtyFiles(e.path));let p=o.planPrune({records:l.records,gitPaths:d,worktreesRoot:i.worktreesRoot(e.homeDir),dirtyByPath:f});if(n)return a.replace(l.records),p;for(let e of p.remove)await u.removeWorktree({repositoryRoot:r,path:e.path,force:!1});for(let e of[...p.remove,...p.forget])await u.deleteBranch({repositoryRoot:r,branch:e.branch}).catch(()=>!1)||p.keptBranches.push(e.branch);let m=new Set([...p.remove,...p.forget].map(e=>e.id));return a.replace(l.records.filter(e=>!m.has(e.id))),p}}}exports.createWorktreeOperations=u;
|
|
2
|
-
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["mirrorComposition","HubUnavailableError","fs","DoomGitExpectedError","createWorktreeRegistry","registryFile","require","refuseSpawn","shortId","randomUUID","worktreeDirectory","worktreesRoot","repositoryLabel","repositoryId","refuseClose","reconcile","MAX_WORKTREE_MESSAGE_BYTES","planPrune"],"sources":["../../../src/services/worktreeOperations/index.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\n\nimport type { DoomHubSessionService } from '@agimon-ai/doompi-core/hub-channel';\n\nimport { WORKTREE_RECORD_VERSION } from '../../types/worktreeRegistry';\nimport type { WorktreeGit, WorktreeRecord } from '../../types/worktreeRegistry';\nimport { mirrorComposition } from '../compositionMirror';\nimport { DoomGitExpectedError, HubUnavailableError } from '../errors';\nimport { registryFile, worktreesRoot } from '../paths';\nimport { repositoryId, repositoryLabel, shortId } from '../repositoryIdentity';\nimport { MAX_WORKTREE_MESSAGE_BYTES, type WorktreeMessageInbox, type WorktreeMessageParty } from '../worktreeEvents';\nimport { planPrune, reconcile, refuseClose, refuseSpawn, worktreeDirectory } from '../worktreeNaming';\nimport { createWorktreeRegistry } from '../worktreeRegistry';\n\nexport interface WorktreeContext {\n /** Where the calling session is working, used to find the repository. */\n cwd: string;\n /** The calling session, recorded as the new session's parent. */\n sessionId: string;\n}\n\nexport interface SpawnWorktreeRequest {\n branch: string;\n baseRef?: string;\n name?: string;\n}\n\nexport interface SpawnOptions {\n /** Aborted when the caller gives up, so a spawn nobody is waiting for stops. */\n signal?: AbortSignal;\n /** Phase labels, for a caller that can show them while the work runs. */\n onProgress?: (label: string) => void;\n}\n\nexport interface WorktreeOperations {\n spawn(context: WorktreeContext, request: SpawnWorktreeRequest, options?: SpawnOptions): Promise<WorktreeRecord>;\n close(context: WorktreeContext, id: string, force: boolean): Promise<WorktreeRecord>;\n list(context: WorktreeContext): Promise<WorktreeRecord[]>;\n status(context: WorktreeContext, id: string): Promise<{ record: WorktreeRecord; dirtyFiles: string[] }>;\n merge(context: WorktreeContext, id: string, message?: string): Promise<WorktreeRecord>;\n prune(context: WorktreeContext, dryRun: boolean): Promise<ReturnType<typeof planPrune>>;\n send(context: WorktreeContext, id: string, message: string): Promise<void>;\n messages(context: WorktreeContext, id: string): Promise<import('../worktreeEvents').WorktreeMessage[]>;\n}\n\nexport interface WorktreeOperationsDeps {\n git: WorktreeGit;\n /** The canonical hub-owned session lifecycle. */\n sessionService?: DoomHubSessionService;\n /** Session-local inbox for parent/worktree messages. */\n messageInbox?: WorktreeMessageInbox;\n /** Injected so a test never copies a real dependency tree. */\n mirror?: typeof mirrorComposition;\n homeDir?: string;\n /** Injected so a record's createdAt is reproducible in a test. */\n now?: () => Date;\n}\n\nexport function createWorktreeOperations(deps: WorktreeOperationsDeps): WorktreeOperations {\n const { git } = deps;\n const sessionService = deps.sessionService;\n const mirror = deps.mirror ?? mirrorComposition;\n const now = deps.now ?? (() => new Date());\n const requireSessionService = (): DoomHubSessionService => {\n if (sessionService === undefined)\n throw new HubUnavailableError('The cockpit session service is unavailable. Start the cockpit and try again.');\n return sessionService;\n };\n const requireMessageInbox = (): WorktreeMessageInbox => {\n if (deps.messageInbox === undefined)\n throw new HubUnavailableError('The Git message service is unavailable. Start the cockpit and try again.');\n return deps.messageInbox;\n };\n const probe = {\n alive: (record: WorktreeRecord) => requireSessionService().isLive(record.sessionId),\n exists: (path: string) => fs.existsSync(path),\n };\n\n /**\n * Everything an action needs about the repository it is acting on.\n *\n * Resolved per call rather than cached: a session's cwd is stable, but the\n * worktree registry is shared and another process may have written it since\n * the last call.\n */\n const resolve = async (context: WorktreeContext) => {\n const root = await git.repositoryRoot(context.cwd);\n if (root === undefined) {\n throw new DoomGitExpectedError(\n 'not_a_repository',\n `${context.cwd} is not inside a git repository.`,\n false,\n 'Run this from a repository checkout.',\n );\n }\n const store = createWorktreeRegistry(registryFile(root, deps.homeDir));\n return { root, store, records: store.list() };\n };\n\n const require = (records: readonly WorktreeRecord[], id: string): WorktreeRecord => {\n const record = records.find((entry) => entry.id === id);\n if (record === undefined) {\n throw new DoomGitExpectedError(\n 'worktree_not_found',\n `No worktree with id ${id}.`,\n false,\n 'Call list to see the current ids.',\n );\n }\n return record;\n };\n\n /**\n * Refuses to destroy or merge a worktree another live session is using.\n *\n * `parentSessionId` records who asked for the worktree. Two sessions in one\n * checkout share this registry, so without the check either could close the\n * other's work in progress by id.\n *\n * Once that session is gone there is no owner left to protect, and refusing\n * would strand the worktree: nothing else could ever close it.\n */\n const requireOwner = (context: WorktreeContext, record: WorktreeRecord, action: string): void => {\n if (record.parentSessionId === context.sessionId) return;\n if (!requireSessionService().isLive(record.parentSessionId)) return;\n throw new DoomGitExpectedError(\n 'worktree_not_owned',\n `Worktree ${record.id} belongs to another session.`,\n false,\n `Ask the session that created it to ${action} it, or close that session first.`,\n );\n };\n\n const peerFor = (\n context: WorktreeContext,\n record: WorktreeRecord,\n ): { sessionId: string; party: WorktreeMessageParty } => {\n if (context.sessionId === record.parentSessionId) return { sessionId: record.sessionId, party: 'parent' };\n if (context.sessionId === record.sessionId) return { sessionId: record.parentSessionId, party: 'child' };\n throw new DoomGitExpectedError(\n 'worktree_not_owned',\n `Worktree ${record.id} belongs to another session.`,\n false,\n 'Use a worktree owned by this session.',\n );\n };\n\n const requirePeerLive = (peerSessionId: string): void => {\n if (!requireSessionService().isLive(peerSessionId)) {\n throw new DoomGitExpectedError(\n 'worktree_peer_unavailable',\n 'The other worktree session is no longer live.',\n false,\n 'Start a new worktree session before sending a message.',\n );\n }\n };\n\n return {\n async spawn(context, request, options) {\n const { root, store, records } = await resolve(context);\n const refusal = refuseSpawn({ branch: request.branch, existing: records });\n if (refusal !== undefined) {\n throw new DoomGitExpectedError('worktree_exists', refusal, false, 'Pick another branch name, or close it.');\n }\n const baseRef = request.baseRef ?? (await git.currentBranch(root)) ?? 'HEAD';\n const id = shortId(randomUUID());\n const path = worktreeDirectory({\n worktreesRoot: worktreesRoot(deps.homeDir),\n repositoryLabel: repositoryLabel(root),\n repositoryId: repositoryId(root),\n branch: request.branch,\n shortId: id,\n });\n\n // The worktree and the branch are made by one command, so they are undone\n // by one too. `worktree remove` leaves the branch behind, and a branch\n // nobody asked for is what a failed spawn used to leave on the floor.\n const cancelled = (): boolean => options?.signal?.aborted === true;\n const rollback = async (): Promise<string> => {\n await git.removeWorktree({ repositoryRoot: root, path, force: true }).catch(() => undefined);\n const deleted = await git.deleteBranch({ repositoryRoot: root, branch: request.branch }).catch(() => false);\n return deleted ? '' : ` The branch ${request.branch} has work on it and was kept.`;\n };\n\n options?.onProgress?.(`creating branch ${request.branch}\\u2026`);\n await git.addWorktree({ repositoryRoot: root, path, branch: request.branch, baseRef });\n\n // Checked here rather than only inside the session call: this is the\n // point where a checkout exists that nothing has recorded yet, which is\n // exactly the state an interrupted spawn used to leave behind.\n if (cancelled()) {\n const kept = await rollback();\n throw new DoomGitExpectedError(\n 'spawn_cancelled',\n `Cancelled before the session started.${kept}`,\n false,\n 'The worktree was removed; run it again when you are ready.',\n );\n }\n\n // Before the session, because the session composes the extensions this\n // repository's config names and those live in build output git does not\n // track. A worktree without it starts and dies on the first package it\n // cannot resolve. Nothing here can fail the spawn: a repository with no\n // build output to mirror simply has none.\n options?.onProgress?.('mirroring build output\\u2026');\n mirror(root, path);\n\n // The worktree exists from here on, so every later failure has to leave\n // it removed or recorded. An unrecorded directory on disk is the one\n // outcome with no way back: nothing would ever list it again.\n options?.onProgress?.('starting session\\u2026');\n let sessionId: string;\n try {\n const session = await requireSessionService().create({\n cwd: path,\n name: request.name ?? request.branch,\n parentSessionId: context.sessionId,\n sessionProvenance: 'worktree',\n ...(options?.signal === undefined ? {} : { signal: options.signal }),\n });\n sessionId = session.sessionId;\n } catch (error) {\n const kept = await rollback();\n if (cancelled()) {\n throw new DoomGitExpectedError(\n 'spawn_cancelled',\n `Cancelled while the session was starting.${kept}`,\n false,\n 'The worktree was removed; run it again when you are ready.',\n );\n }\n if (error instanceof HubUnavailableError) {\n throw new DoomGitExpectedError(\n 'hub_unavailable',\n `${error.message}${kept}`,\n true,\n 'Start the cockpit and try again.',\n );\n }\n throw error;\n }\n\n const record: WorktreeRecord = {\n version: WORKTREE_RECORD_VERSION,\n id,\n branch: request.branch,\n baseRef,\n path,\n repositoryRoot: root,\n sessionId,\n parentSessionId: context.sessionId,\n status: 'running',\n createdAt: now().toISOString(),\n };\n try {\n store.replace([...records, record]);\n } catch (error) {\n // A live session with no record is the worst outcome available here:\n // it runs, and no id anything can name points at it. The session goes\n // back too, rather than being left for someone to find by hand.\n await requireSessionService()\n .close(sessionId)\n .catch(() => undefined);\n const kept = await rollback();\n throw new DoomGitExpectedError(\n 'registry_write_failed',\n `The worktree was created but the registry could not be written (${(error as Error).message}).${kept}`,\n true,\n 'Check the registry file for permissions or free space, then try again.',\n );\n }\n return record;\n },\n\n async close(context, id, force) {\n const { root, store, records } = await resolve(context);\n const record = require(records, id);\n requireOwner(context, record, 'close');\n const dirtyFiles = fs.existsSync(record.path) ? await git.dirtyFiles(record.path) : [];\n const refusal = refuseClose({ record, dirtyFiles, force });\n if (refusal !== undefined) {\n throw new DoomGitExpectedError('worktree_dirty', refusal, false, 'Commit the work, or pass force: true.');\n }\n // The session is stopped before the directory goes. Removing a checkout\n // while a process still holds files open in it is how a half-removed\n // worktree and a stale git administrative entry are made.\n await requireSessionService().close(record.sessionId);\n await git.removeWorktree({ repositoryRoot: root, path: record.path, force });\n store.replace(records.filter((entry) => entry.id !== id));\n return record;\n },\n\n async list(context) {\n const { store, records } = await resolve(context);\n const outcome = reconcile(records, probe);\n if (outcome.changed.length > 0) store.replace(outcome.records);\n return outcome.records;\n },\n\n async status(context, id) {\n const { records } = await resolve(context);\n const record = require(records, id);\n const dirtyFiles = fs.existsSync(record.path) ? await git.dirtyFiles(record.path) : [];\n return { record, dirtyFiles };\n },\n\n async merge(context, id, message) {\n const { root, records } = await resolve(context);\n const record = require(records, id);\n requireOwner(context, record, 'merge');\n // The merge lands in the parent, so the parent is what must be clean. A\n // dirty parent would mix the person's in-flight edits into a merge commit\n // they did not intend to make.\n const parentDirty = await git.dirtyFiles(root);\n if (parentDirty.length > 0) {\n throw new DoomGitExpectedError(\n 'worktree_dirty',\n `The parent checkout has ${String(parentDirty.length)} uncommitted file(s): ${parentDirty.slice(0, 10).join(', ')}.`,\n false,\n 'Commit or stash the parent checkout first.',\n );\n }\n await git.mergeBranch({\n repositoryRoot: root,\n branch: record.branch,\n ...(message === undefined ? {} : { message }),\n });\n return record;\n },\n\n /** Sends only between the parent session and its own worktree session. */\n async send(context, id, message) {\n if (Buffer.byteLength(message, 'utf8') > MAX_WORKTREE_MESSAGE_BYTES) {\n throw new DoomGitExpectedError(\n 'message_too_large',\n `A message may not exceed ${String(MAX_WORKTREE_MESSAGE_BYTES)} bytes.`,\n false,\n 'Send a shorter message.',\n );\n }\n const { records } = await resolve(context);\n const record = require(records, id);\n const peer = peerFor(context, record);\n requirePeerLive(peer.sessionId);\n requireMessageInbox().send(peer.sessionId, {\n version: 1,\n worktreeId: record.id,\n fromSessionId: context.sessionId,\n from: peer.party,\n text: message,\n sentAt: new Date().toISOString(),\n });\n },\n\n async messages(context, id) {\n const { records } = await resolve(context);\n const record = require(records, id);\n peerFor(context, record);\n return requireMessageInbox().receive(record.id);\n },\n\n async prune(context, dryRun) {\n const { root, store, records } = await resolve(context);\n const reconciled = reconcile(records, probe);\n const gitPaths = await git.listWorktreePaths(root);\n const dirtyByPath = new Map<string, readonly string[]>();\n for (const record of reconciled.records) {\n if (record.status === 'orphaned' && fs.existsSync(record.path)) {\n dirtyByPath.set(record.path, await git.dirtyFiles(record.path));\n }\n }\n const plan = planPrune({\n records: reconciled.records,\n gitPaths,\n worktreesRoot: worktreesRoot(deps.homeDir),\n dirtyByPath,\n });\n if (dryRun) {\n store.replace(reconciled.records);\n return plan;\n }\n for (const record of plan.remove) {\n await git.removeWorktree({ repositoryRoot: root, path: record.path, force: false });\n }\n // The checkout goes, so the branch it was made for goes with it. Safe\n // delete only: git keeps anything holding commits, and the plan says\n // which ones it kept rather than leaving the reader to notice later.\n for (const record of [...plan.remove, ...plan.forget]) {\n const deleted = await git.deleteBranch({ repositoryRoot: root, branch: record.branch }).catch(() => false);\n if (!deleted) plan.keptBranches.push(record.branch);\n }\n const dropped = new Set([...plan.remove, ...plan.forget].map((record) => record.id));\n store.replace(reconciled.records.filter((record) => !dropped.has(record.id)));\n return plan;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { WorktreeMessage } from "../worktreeEvents/index.cjs";
|
|
2
|
-
import { WorktreeRecord } from "../../types/worktreeRegistry.cjs";
|
|
3
|
-
import { planPrune } from "../worktreeNaming/index.cjs";
|
|
4
|
-
import "@agimon-ai/doompi-core/hub-channel";
|
|
5
|
-
//#region src/services/worktreeOperations/index.d.ts
|
|
6
|
-
interface WorktreeContext {
|
|
7
|
-
/** Where the calling session is working, used to find the repository. */
|
|
8
|
-
cwd: string;
|
|
9
|
-
/** The calling session, recorded as the new session's parent. */
|
|
10
|
-
sessionId: string;
|
|
11
|
-
}
|
|
12
|
-
interface SpawnWorktreeRequest {
|
|
13
|
-
branch: string;
|
|
14
|
-
baseRef?: string;
|
|
15
|
-
name?: string;
|
|
16
|
-
}
|
|
17
|
-
interface SpawnOptions {
|
|
18
|
-
/** Aborted when the caller gives up, so a spawn nobody is waiting for stops. */
|
|
19
|
-
signal?: AbortSignal;
|
|
20
|
-
/** Phase labels, for a caller that can show them while the work runs. */
|
|
21
|
-
onProgress?: (label: string) => void;
|
|
22
|
-
}
|
|
23
|
-
interface WorktreeOperations {
|
|
24
|
-
spawn(context: WorktreeContext, request: SpawnWorktreeRequest, options?: SpawnOptions): Promise<WorktreeRecord>;
|
|
25
|
-
close(context: WorktreeContext, id: string, force: boolean): Promise<WorktreeRecord>;
|
|
26
|
-
list(context: WorktreeContext): Promise<WorktreeRecord[]>;
|
|
27
|
-
status(context: WorktreeContext, id: string): Promise<{
|
|
28
|
-
record: WorktreeRecord;
|
|
29
|
-
dirtyFiles: string[];
|
|
30
|
-
}>;
|
|
31
|
-
merge(context: WorktreeContext, id: string, message?: string): Promise<WorktreeRecord>;
|
|
32
|
-
prune(context: WorktreeContext, dryRun: boolean): Promise<ReturnType<typeof planPrune>>;
|
|
33
|
-
send(context: WorktreeContext, id: string, message: string): Promise<void>;
|
|
34
|
-
messages(context: WorktreeContext, id: string): Promise<WorktreeMessage[]>;
|
|
35
|
-
}
|
|
36
|
-
//#endregion
|
|
37
|
-
export { SpawnOptions, SpawnWorktreeRequest, WorktreeContext, WorktreeOperations };
|
|
38
|
-
//# sourceMappingURL=index.d.cts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../../src/services/worktreeOperations/index.ts"],"mappings":";;;;;UAeiB;;EAEf;;EAEA;;UAGe;EACf;EACA;EACA;;UAGe;;EAEf,SAAS;;EAET,cAAc;;UAGC;EACf,MAAM,SAAS,iBAAiB,SAAS,sBAAsB,UAAU,eAAe,QAAQ;EAChG,MAAM,SAAS,iBAAiB,YAAY,iBAAiB,QAAQ;EACrE,KAAK,SAAS,kBAAkB,QAAQ;EACxC,OAAO,SAAS,iBAAiB,aAAa;IAAU,QAAQ;IAAgB;;EAChF,MAAM,SAAS,iBAAiB,YAAY,mBAAmB,QAAQ;EACvE,MAAM,SAAS,iBAAiB,kBAAkB,QAAQ,kBAAkB;EAC5E,KAAK,SAAS,iBAAiB,YAAY,kBAAkB;EAC7D,SAAS,SAAS,iBAAiB,aAAa,QAA9B"}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { WorktreeMessage } from "../worktreeEvents/index.mjs";
|
|
2
|
-
import { WorktreeRecord } from "../../types/worktreeRegistry.mjs";
|
|
3
|
-
import { planPrune } from "../worktreeNaming/index.mjs";
|
|
4
|
-
import "@agimon-ai/doompi-core/hub-channel";
|
|
5
|
-
//#region src/services/worktreeOperations/index.d.ts
|
|
6
|
-
interface WorktreeContext {
|
|
7
|
-
/** Where the calling session is working, used to find the repository. */
|
|
8
|
-
cwd: string;
|
|
9
|
-
/** The calling session, recorded as the new session's parent. */
|
|
10
|
-
sessionId: string;
|
|
11
|
-
}
|
|
12
|
-
interface SpawnWorktreeRequest {
|
|
13
|
-
branch: string;
|
|
14
|
-
baseRef?: string;
|
|
15
|
-
name?: string;
|
|
16
|
-
}
|
|
17
|
-
interface SpawnOptions {
|
|
18
|
-
/** Aborted when the caller gives up, so a spawn nobody is waiting for stops. */
|
|
19
|
-
signal?: AbortSignal;
|
|
20
|
-
/** Phase labels, for a caller that can show them while the work runs. */
|
|
21
|
-
onProgress?: (label: string) => void;
|
|
22
|
-
}
|
|
23
|
-
interface WorktreeOperations {
|
|
24
|
-
spawn(context: WorktreeContext, request: SpawnWorktreeRequest, options?: SpawnOptions): Promise<WorktreeRecord>;
|
|
25
|
-
close(context: WorktreeContext, id: string, force: boolean): Promise<WorktreeRecord>;
|
|
26
|
-
list(context: WorktreeContext): Promise<WorktreeRecord[]>;
|
|
27
|
-
status(context: WorktreeContext, id: string): Promise<{
|
|
28
|
-
record: WorktreeRecord;
|
|
29
|
-
dirtyFiles: string[];
|
|
30
|
-
}>;
|
|
31
|
-
merge(context: WorktreeContext, id: string, message?: string): Promise<WorktreeRecord>;
|
|
32
|
-
prune(context: WorktreeContext, dryRun: boolean): Promise<ReturnType<typeof planPrune>>;
|
|
33
|
-
send(context: WorktreeContext, id: string, message: string): Promise<void>;
|
|
34
|
-
messages(context: WorktreeContext, id: string): Promise<WorktreeMessage[]>;
|
|
35
|
-
}
|
|
36
|
-
//#endregion
|
|
37
|
-
export { SpawnOptions, SpawnWorktreeRequest, WorktreeContext, WorktreeOperations };
|
|
38
|
-
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/worktreeOperations/index.ts"],"mappings":";;;;;UAeiB;;EAEf;;EAEA;;UAGe;EACf;EACA;EACA;;UAGe;;EAEf,SAAS;;EAET,cAAc;;UAGC;EACf,MAAM,SAAS,iBAAiB,SAAS,sBAAsB,UAAU,eAAe,QAAQ;EAChG,MAAM,SAAS,iBAAiB,YAAY,iBAAiB,QAAQ;EACrE,KAAK,SAAS,kBAAkB,QAAQ;EACxC,OAAO,SAAS,iBAAiB,aAAa;IAAU,QAAQ;IAAgB;;EAChF,MAAM,SAAS,iBAAiB,YAAY,mBAAmB,QAAQ;EACvE,MAAM,SAAS,iBAAiB,kBAAkB,QAAQ,kBAAkB;EAC5E,KAAK,SAAS,iBAAiB,YAAY,kBAAkB;EAC7D,SAAS,SAAS,iBAAiB,aAAa,QAA9B"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{mirrorComposition as e}from"../compositionMirror/index.mjs";import{DoomGitExpectedError as t,HubUnavailableError as n}from"../errors/index.mjs";import{repositoryId as r,repositoryLabel as i,shortId as a}from"../repositoryIdentity/index.mjs";import{registryFile as o,worktreesRoot as s}from"../paths/index.mjs";import{MAX_WORKTREE_MESSAGE_BYTES as c}from"../worktreeEvents/index.mjs";import{planPrune as l,reconcile as u,refuseClose as d,refuseSpawn as f,worktreeDirectory as p}from"../worktreeNaming/index.mjs";import{createWorktreeRegistry as m}from"../worktreeRegistry/index.mjs";import h from"node:fs";import{randomUUID as g}from"node:crypto";function _(_){let{git:v}=_,y=_.sessionService,b=_.mirror??e,x=_.now??(()=>new Date),S=()=>{if(y===void 0)throw new n(`The cockpit session service is unavailable. Start the cockpit and try again.`);return y},C=()=>{if(_.messageInbox===void 0)throw new n(`The Git message service is unavailable. Start the cockpit and try again.`);return _.messageInbox},w={alive:e=>S().isLive(e.sessionId),exists:e=>h.existsSync(e)},T=async e=>{let n=await v.repositoryRoot(e.cwd);if(n===void 0)throw new t(`not_a_repository`,`${e.cwd} is not inside a git repository.`,!1,`Run this from a repository checkout.`);let r=m(o(n,_.homeDir));return{root:n,store:r,records:r.list()}},E=(e,n)=>{let r=e.find(e=>e.id===n);if(r===void 0)throw new t(`worktree_not_found`,`No worktree with id ${n}.`,!1,`Call list to see the current ids.`);return r},D=(e,n,r)=>{if(n.parentSessionId!==e.sessionId&&S().isLive(n.parentSessionId))throw new t(`worktree_not_owned`,`Worktree ${n.id} belongs to another session.`,!1,`Ask the session that created it to ${r} it, or close that session first.`)},O=(e,n)=>{if(e.sessionId===n.parentSessionId)return{sessionId:n.sessionId,party:`parent`};if(e.sessionId===n.sessionId)return{sessionId:n.parentSessionId,party:`child`};throw new t(`worktree_not_owned`,`Worktree ${n.id} belongs to another session.`,!1,`Use a worktree owned by this session.`)},k=e=>{if(!S().isLive(e))throw new t(`worktree_peer_unavailable`,`The other worktree session is no longer live.`,!1,`Start a new worktree session before sending a message.`)};return{async spawn(e,o,c){let{root:l,store:u,records:d}=await T(e),m=f({branch:o.branch,existing:d});if(m!==void 0)throw new t(`worktree_exists`,m,!1,`Pick another branch name, or close it.`);let h=o.baseRef??await v.currentBranch(l)??`HEAD`,y=a(g()),C=p({worktreesRoot:s(_.homeDir),repositoryLabel:i(l),repositoryId:r(l),branch:o.branch,shortId:y}),w=()=>c?.signal?.aborted===!0,E=async()=>(await v.removeWorktree({repositoryRoot:l,path:C,force:!0}).catch(()=>void 0),await v.deleteBranch({repositoryRoot:l,branch:o.branch}).catch(()=>!1)?``:` The branch ${o.branch} has work on it and was kept.`);if(c?.onProgress?.(`creating branch ${o.branch}\u2026`),await v.addWorktree({repositoryRoot:l,path:C,branch:o.branch,baseRef:h}),w()){let e=await E();throw new t(`spawn_cancelled`,`Cancelled before the session started.${e}`,!1,`The worktree was removed; run it again when you are ready.`)}c?.onProgress?.(`mirroring build output…`),b(l,C),c?.onProgress?.(`starting session…`);let D;try{D=(await S().create({cwd:C,name:o.name??o.branch,parentSessionId:e.sessionId,sessionProvenance:`worktree`,...c?.signal===void 0?{}:{signal:c.signal}})).sessionId}catch(e){let r=await E();throw w()?new t(`spawn_cancelled`,`Cancelled while the session was starting.${r}`,!1,`The worktree was removed; run it again when you are ready.`):e instanceof n?new t(`hub_unavailable`,`${e.message}${r}`,!0,`Start the cockpit and try again.`):e}let O={version:1,id:y,branch:o.branch,baseRef:h,path:C,repositoryRoot:l,sessionId:D,parentSessionId:e.sessionId,status:`running`,createdAt:x().toISOString()};try{u.replace([...d,O])}catch(e){await S().close(D).catch(()=>void 0);let n=await E();throw new t(`registry_write_failed`,`The worktree was created but the registry could not be written (${e.message}).${n}`,!0,`Check the registry file for permissions or free space, then try again.`)}return O},async close(e,n,r){let{root:i,store:a,records:o}=await T(e),s=E(o,n);D(e,s,`close`);let c=h.existsSync(s.path)?await v.dirtyFiles(s.path):[],l=d({record:s,dirtyFiles:c,force:r});if(l!==void 0)throw new t(`worktree_dirty`,l,!1,`Commit the work, or pass force: true.`);return await S().close(s.sessionId),await v.removeWorktree({repositoryRoot:i,path:s.path,force:r}),a.replace(o.filter(e=>e.id!==n)),s},async list(e){let{store:t,records:n}=await T(e),r=u(n,w);return r.changed.length>0&&t.replace(r.records),r.records},async status(e,t){let{records:n}=await T(e),r=E(n,t);return{record:r,dirtyFiles:h.existsSync(r.path)?await v.dirtyFiles(r.path):[]}},async merge(e,n,r){let{root:i,records:a}=await T(e),o=E(a,n);D(e,o,`merge`);let s=await v.dirtyFiles(i);if(s.length>0)throw new t(`worktree_dirty`,`The parent checkout has ${String(s.length)} uncommitted file(s): ${s.slice(0,10).join(`, `)}.`,!1,`Commit or stash the parent checkout first.`);return await v.mergeBranch({repositoryRoot:i,branch:o.branch,...r===void 0?{}:{message:r}}),o},async send(e,n,r){if(Buffer.byteLength(r,`utf8`)>65536)throw new t(`message_too_large`,`A message may not exceed ${String(c)} bytes.`,!1,`Send a shorter message.`);let{records:i}=await T(e),a=E(i,n),o=O(e,a);k(o.sessionId),C().send(o.sessionId,{version:1,worktreeId:a.id,fromSessionId:e.sessionId,from:o.party,text:r,sentAt:new Date().toISOString()})},async messages(e,t){let{records:n}=await T(e),r=E(n,t);return O(e,r),C().receive(r.id)},async prune(e,t){let{root:n,store:r,records:i}=await T(e),a=u(i,w),o=await v.listWorktreePaths(n),c=new Map;for(let e of a.records)e.status===`orphaned`&&h.existsSync(e.path)&&c.set(e.path,await v.dirtyFiles(e.path));let d=l({records:a.records,gitPaths:o,worktreesRoot:s(_.homeDir),dirtyByPath:c});if(t)return r.replace(a.records),d;for(let e of d.remove)await v.removeWorktree({repositoryRoot:n,path:e.path,force:!1});for(let e of[...d.remove,...d.forget])await v.deleteBranch({repositoryRoot:n,branch:e.branch}).catch(()=>!1)||d.keptBranches.push(e.branch);let f=new Set([...d.remove,...d.forget].map(e=>e.id));return r.replace(a.records.filter(e=>!f.has(e.id))),d}}}export{_ as createWorktreeOperations};
|
|
2
|
-
//# sourceMappingURL=index.mjs.map
|