infra-kit 0.1.117 → 0.1.121
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/.eslintcache +1 -1
- package/.omc/state/agent-replay-707b4784-2117-4b19-ae68-68a031f0d76d.jsonl +2 -0
- package/.omc/state/idle-notif-cooldown.json +1 -1
- package/.omc/state/sessions/707b4784-2117-4b19-ae68-68a031f0d76d/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/707b4784-2117-4b19-ae68-68a031f0d76d/subagent-tracking-state.json +17 -0
- package/.turbo/turbo-eslint-check.log +0 -2
- package/.turbo/turbo-test.log +59 -59
- package/dist/{chunk-FEGOOVZE.js → chunk-PWOX6B3T.js} +44 -44
- package/dist/{chunk-FEGOOVZE.js.map → chunk-PWOX6B3T.js.map} +3 -3
- package/dist/cli.js +1 -1
- package/dist/mcp.js +1 -1
- package/package.json +1 -1
- package/src/.omc/state/agent-replay-707b4784-2117-4b19-ae68-68a031f0d76d.jsonl +2 -0
- package/src/.omc/state/sessions/707b4784-2117-4b19-ae68-68a031f0d76d/last-tool-error-state.json +7 -0
- package/src/.omc/state/sessions/707b4784-2117-4b19-ae68-68a031f0d76d/mission-state.json +53 -0
- package/src/.omc/state/sessions/707b4784-2117-4b19-ae68-68a031f0d76d/pre-tool-advisory-throttle.json +22 -0
- package/src/.omc/state/sessions/707b4784-2117-4b19-ae68-68a031f0d76d/subagent-tracking-state.json +17 -0
- package/src/commands/worktrees-remove/worktrees-remove.ts +1 -1
- package/src/lib/command-catalog/command-catalog.ts +3 -2
- package/src/lib/worktrees/__tests__/remove-worktrees.test.ts +167 -0
- package/src/lib/worktrees/remove-worktrees.ts +8 -22
- package/tsconfig.tsbuildinfo +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{A as de,B as me,C as le,D as pe,E as fe,F as ue,G as ge,H as ve,I as ye,J as he,K as we,L as ke,M as Ce,N as Ae,O as xe,P as _,Q as Ee,R as Se,S as Re,T as Le,U as be,W as j,a as U,d as a,e as B,f as T,g as z,h as Y,i as h,j as u,k as D,l as q,m as Q,n as I,o as X,p as Z,q as ee,r as oe,s as te,t as re,u as ne,v as ie,w as C,x as ae,y as se,z as ce}from"./chunk-
|
|
1
|
+
import{A as de,B as me,C as le,D as pe,E as fe,F as ue,G as ge,H as ve,I as ye,J as he,K as we,L as ke,M as Ce,N as Ae,O as xe,P as _,Q as Ee,R as Se,S as Re,T as Le,U as be,W as j,a as U,d as a,e as B,f as T,g as z,h as Y,i as h,j as u,k as D,l as q,m as Q,n as I,o as X,p as Z,q as ee,r as oe,s as te,t as re,u as ne,v as ie,w as C,x as ae,y as se,z as ce}from"./chunk-PWOX6B3T.js";import{e as $}from"./chunk-WA4BQRDC.js";import vo,{Separator as x}from"@inquirer/select";import{Command as yo}from"commander";import w from"node:process";import N from"node:fs/promises";import Xe from"node:path";import je from"node:process";import{$ as Ze}from"zx";var O=async()=>{let e=await T(),o=await Promise.all([{label:"project (committed)",path:e.main},{label:"user global",path:e.userGlobal},{label:"user project",path:e.userProject}].map(async r=>({...r,exists:await h(r.path)})));a.info(`Project name: ${e.projectName}
|
|
2
2
|
`),a.info(`Config merge chain (later overrides earlier):
|
|
3
3
|
`);for(let r of o){let c=r.exists?" [\u2713]":" [ ]";a.info(`${c} ${r.label.padEnd(22)} ${u(r.path)}`)}let t={projectName:e.projectName,layers:o.map(r=>({label:r.label,path:r.path,exists:r.exists}))};return{content:[{type:"text",text:JSON.stringify(t,null,2)}],structuredContent:t}},M=async()=>{let e=await T(),o=je.env.EDITOR||je.env.VISUAL||"vi";if(await N.mkdir(Xe.dirname(e.userProject),{recursive:!0}),!await h(e.userProject)){let r=eo(e.userProject);await N.writeFile(e.userProject,`{}
|
|
4
4
|
`,"utf-8"),await N.writeFile(r,oo(e.projectName),"utf-8"),a.info(`Created ${u(e.userProject)} \u2014 see ${u(r)} for the annotated reference.`)}a.info(`Opening ${u(e.userProject)} in ${o}`),await Ze({stdio:"inherit"})`${o} ${e.userProject}`,Y();let t={path:e.userProject,editor:o};return{content:[{type:"text",text:JSON.stringify(t,null,2)}],structuredContent:t}},eo=e=>e.replace(/\.json$/,".example.jsonc"),oo=e=>`// infra-kit user override for ${e} \u2014 ~/.infra-kit/projects/${e}/infra-kit.json
|
package/dist/mcp.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{V as m,b as s,c as p,d as c}from"./chunk-
|
|
1
|
+
import{V as m,b as s,c as p,d as c}from"./chunk-PWOX6B3T.js";import"./chunk-WA4BQRDC.js";import{StdioServerTransport as g}from"@modelcontextprotocol/sdk/server/stdio.js";import h from"node:process";import o from"node:process";var l=r=>{o.on("SIGINT",()=>{r.info({msg:"Received SIGINT. Shutting down..."}),o.exit(0)}),o.on("SIGTERM",()=>{r.info({msg:"Received SIGTERM. Shutting down..."}),o.exit(0)}),o.on("uncaughtException",e=>{r.fatal({err:e,msg:"Uncaught Exception"}),r.error(`Uncaught Exception! Check ${s}. Shutting down...`),r.flush(),o.exit(1)}),o.on("unhandledRejection",(e,n)=>{r.fatal({reason:e,promise:n,msg:"Unhandled Rejection"}),r.error(`Unhandled Rejection! Check ${s}. Shutting down...`),r.flush(),o.exit(1)})};import{McpServer as x}from"@modelcontextprotocol/sdk/server/mcp.js";var d=async r=>{};var u=async r=>{};var a=r=>async e=>{let{toolName:n,handler:S}=r;c.info({msg:`Tool execution started: ${n}`,params:e});try{let i=await S({...e,confirmedCommand:!0});return c.info({msg:`Tool execution successful: ${n}`}),i}catch(i){throw c.error({err:i,params:e,msg:`Tool execution failed: ${n}`}),i}};var f=async r=>{for(let e of m())r.registerTool(e.name,{description:e.description,inputSchema:e.inputSchema,outputSchema:e.outputSchema},a({toolName:e.name,handler:e.handler}))};async function v(){let r=new x({name:"infra-kit",version:"1.0.0"},{capabilities:{resources:{},tools:{},prompts:{}}});return await d(r),await u(r),await f(r),r}var t=p(),T=async()=>{let r;try{r=await v(),t.info("MCP Server instance created")}catch(e){t.error({err:e,msg:"Failed to create MCP server"}),t.error("Fatal error during server creation."),h.exit(1)}try{let e=new g;await r.connect(e),t.info({msg:"Server connected to transport. Ready."})}catch(e){t.error({err:e,msg:"Failed to initialize server"}),t.error("Fatal error during server transport init."),h.exit(1)}};l(t);T();
|
|
2
2
|
//# sourceMappingURL=mcp.js.map
|
package/package.json
CHANGED
package/src/.omc/state/sessions/707b4784-2117-4b19-ae68-68a031f0d76d/last-tool-error-state.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tool_name": "Bash",
|
|
3
|
+
"tool_input_preview": "{\"command\":\"cd /Users/arthur/projects/infra-kit && rtk grep -rn \\\"WORKTREES_DIR_SUFFIX\\\" apps/infra-kit/cli/src/lib/constants.ts\",\"description\":\"Verify constants.ts line for suffix\"}",
|
|
4
|
+
"error": "Exit code 2\ngrep: apps/infra-kit/cli/src/lib/constants.ts: No such file or directory",
|
|
5
|
+
"timestamp": "2026-06-30T06:08:21.827Z",
|
|
6
|
+
"retry_count": 1
|
|
7
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"updatedAt": "2026-06-30T06:10:31.962Z",
|
|
3
|
+
"missions": [
|
|
4
|
+
{
|
|
5
|
+
"id": "session:707b4784-2117-4b19-ae68-68a031f0d76d:none",
|
|
6
|
+
"source": "session",
|
|
7
|
+
"name": "none",
|
|
8
|
+
"objective": "Session mission",
|
|
9
|
+
"createdAt": "2026-06-30T06:08:05.704Z",
|
|
10
|
+
"updatedAt": "2026-06-30T06:10:31.962Z",
|
|
11
|
+
"status": "done",
|
|
12
|
+
"workerCount": 1,
|
|
13
|
+
"taskCounts": {
|
|
14
|
+
"total": 1,
|
|
15
|
+
"pending": 0,
|
|
16
|
+
"blocked": 0,
|
|
17
|
+
"inProgress": 0,
|
|
18
|
+
"completed": 1,
|
|
19
|
+
"failed": 0
|
|
20
|
+
},
|
|
21
|
+
"agents": [
|
|
22
|
+
{
|
|
23
|
+
"name": "architect:ab0bb13",
|
|
24
|
+
"role": "architect",
|
|
25
|
+
"ownership": "ab0bb13a5e2ee0436",
|
|
26
|
+
"status": "done",
|
|
27
|
+
"currentStep": null,
|
|
28
|
+
"latestUpdate": "completed",
|
|
29
|
+
"completedSummary": null,
|
|
30
|
+
"updatedAt": "2026-06-30T06:10:31.962Z"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"timeline": [
|
|
34
|
+
{
|
|
35
|
+
"id": "session-start:ab0bb13a5e2ee0436:2026-06-30T06:08:05.704Z",
|
|
36
|
+
"at": "2026-06-30T06:08:05.704Z",
|
|
37
|
+
"kind": "update",
|
|
38
|
+
"agent": "architect:ab0bb13",
|
|
39
|
+
"detail": "started architect:ab0bb13",
|
|
40
|
+
"sourceKey": "session-start:ab0bb13a5e2ee0436"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "session-stop:ab0bb13a5e2ee0436:2026-06-30T06:10:31.962Z",
|
|
44
|
+
"at": "2026-06-30T06:10:31.962Z",
|
|
45
|
+
"kind": "completion",
|
|
46
|
+
"agent": "architect:ab0bb13",
|
|
47
|
+
"detail": "completed",
|
|
48
|
+
"sourceKey": "session-stop:ab0bb13a5e2ee0436"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
package/src/.omc/state/sessions/707b4784-2117-4b19-ae68-68a031f0d76d/pre-tool-advisory-throttle.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"entries": {
|
|
4
|
+
"70618baf7ddd1c017d8e7ffe84ab90f8fa22ca1640c91f45c1731fe6330fc294": {
|
|
5
|
+
"last_emitted_at_ms": 1782799685575,
|
|
6
|
+
"message": "Spawning agent: oh-my-claudecode:architect (inherit) | Task: Architect review of worktree-folder plan"
|
|
7
|
+
},
|
|
8
|
+
"445ed27a3872b681d98190bae61ccb84954e1bc4e140df5370be958dee776b3a": {
|
|
9
|
+
"last_emitted_at_ms": 1782799670035,
|
|
10
|
+
"message": "Verify changes work after editing. Test functionality before marking complete."
|
|
11
|
+
},
|
|
12
|
+
"466399dafa2d20f60587180bad0a07358eb7f2bce724df0ff682f038ad33f5ad": {
|
|
13
|
+
"last_emitted_at_ms": 1782799612172,
|
|
14
|
+
"message": "Read multiple files in parallel when possible for faster analysis."
|
|
15
|
+
},
|
|
16
|
+
"79a93d4a2f8f50b95f852280616242fee1855dc99a3c75211917f55e72e95fae": {
|
|
17
|
+
"last_emitted_at_ms": 1782799606281,
|
|
18
|
+
"message": "Use parallel execution for independent tasks. Use run_in_background for long operations (npm install, builds, tests)."
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"updated_at": "2026-06-30T06:08:05.575Z"
|
|
22
|
+
}
|
package/src/.omc/state/sessions/707b4784-2117-4b19-ae68-68a031f0d76d/subagent-tracking-state.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"agents": [
|
|
3
|
+
{
|
|
4
|
+
"agent_id": "ab0bb13a5e2ee0436",
|
|
5
|
+
"agent_type": "oh-my-claudecode:architect",
|
|
6
|
+
"started_at": "2026-06-30T06:08:05.704Z",
|
|
7
|
+
"parent_mode": "none",
|
|
8
|
+
"status": "completed",
|
|
9
|
+
"completed_at": "2026-06-30T06:10:31.962Z",
|
|
10
|
+
"duration_ms": 146258
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"total_spawned": 1,
|
|
14
|
+
"total_completed": 1,
|
|
15
|
+
"total_failed": 0,
|
|
16
|
+
"last_updated": "2026-06-30T06:10:32.065Z"
|
|
17
|
+
}
|
|
@@ -170,7 +170,7 @@ const logResults = (removed: string[]): void => {
|
|
|
170
170
|
export const worktreesRemoveMcpTool = defineMcpTool({
|
|
171
171
|
name: 'worktrees-remove',
|
|
172
172
|
description:
|
|
173
|
-
'Remove local git worktrees for release branches. When everything is removed, also runs "git worktree prune"
|
|
173
|
+
'Remove local git worktrees for release branches. When everything is removed, also runs "git worktree prune" to clear stale metadata; the worktrees directory and its release/feature subfolders are left in place. When invoked via MCP, pass either "versions" (comma-separated) or all=true — the branch picker is unreachable without a TTY, and the CLI confirmation is auto-skipped for MCP calls, so the caller is responsible for gating.',
|
|
174
174
|
inputSchema: {
|
|
175
175
|
all: z
|
|
176
176
|
.boolean()
|
|
@@ -98,8 +98,9 @@ export const commandCatalog: CommandCatalogEntry[] = [
|
|
|
98
98
|
{ cliName: 'worktrees-add', menuGroup: 'worktrees', mcpTool: worktreesAddMcpTool, mcpExposed: true },
|
|
99
99
|
{ cliName: 'worktrees-list', menuGroup: 'worktrees', mcpTool: worktreesListMcpTool, mcpExposed: true },
|
|
100
100
|
{ cliName: 'worktrees-reload', menuGroup: 'worktrees', mcpTool: worktreesReloadMcpTool, mcpExposed: true },
|
|
101
|
-
// worktrees-remove runs
|
|
102
|
-
// it is CLI-only by
|
|
101
|
+
// worktrees-remove runs `git worktree remove` on each leaf worktree —
|
|
102
|
+
// genuinely irreversible (uncommitted work is lost), so it is CLI-only by
|
|
103
|
+
// design (mirrors the vendor-sync/manifest rationale).
|
|
103
104
|
{ cliName: 'worktrees-remove', menuGroup: 'worktrees', mcpTool: worktreesRemoveMcpTool, mcpExposed: false },
|
|
104
105
|
{ cliName: 'worktrees-sync', menuGroup: 'worktrees', mcpTool: worktreesSyncMcpTool, mcpExposed: true },
|
|
105
106
|
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { $ } from 'zx'
|
|
3
|
+
|
|
4
|
+
import { removeWorktrees } from '../remove-worktrees'
|
|
5
|
+
|
|
6
|
+
// `$` resolves to a bare awaitable result — removeWorktrees only ever `await`s
|
|
7
|
+
// the tagged template (no `.quiet()`/`.nothrow()` chaining anymore), so a plain
|
|
8
|
+
// resolved promise is a faithful stand-in.
|
|
9
|
+
vi.mock('zx', () => {
|
|
10
|
+
return {
|
|
11
|
+
$: vi.fn(() => {
|
|
12
|
+
return Promise.resolve({ stdout: '', exitCode: 0 })
|
|
13
|
+
}),
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
vi.mock('src/integrations/cmux', () => {
|
|
18
|
+
return {
|
|
19
|
+
buildCmuxWorkspaceTitle: vi.fn(({ repoName, branch }: { repoName: string; branch: string }) => {
|
|
20
|
+
return `${repoName}:${branch}`
|
|
21
|
+
}),
|
|
22
|
+
closeCmuxWorkspaceByTitle: vi.fn(() => {
|
|
23
|
+
return Promise.resolve()
|
|
24
|
+
}),
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
vi.mock('src/lib/logger', () => {
|
|
29
|
+
return {
|
|
30
|
+
logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn() },
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
type DollarCall = [TemplateStringsArray, ...unknown[]]
|
|
35
|
+
|
|
36
|
+
/** Reconstruct the full command string for a recorded `$` tagged-template call. */
|
|
37
|
+
const commandOf = (call: DollarCall): string => {
|
|
38
|
+
const [strings, ...values] = call
|
|
39
|
+
|
|
40
|
+
return strings.reduce((acc, part, index) => {
|
|
41
|
+
const value = index < values.length ? String(values[index]) : ''
|
|
42
|
+
|
|
43
|
+
return acc + part + value
|
|
44
|
+
}, '')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Interpolated `${...}` arguments of a recorded `$` call (the dynamic paths). */
|
|
48
|
+
const argsOf = (call: DollarCall): string[] => {
|
|
49
|
+
return call.slice(1).map((value) => {
|
|
50
|
+
return String(value)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const recordedCalls = (): DollarCall[] => {
|
|
55
|
+
return vi.mocked($).mock.calls as unknown as DollarCall[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const WORKTREE_DIR = '/repos/hulyo-monorepo-worktrees'
|
|
59
|
+
const REPO_NAME = 'hulyo-monorepo'
|
|
60
|
+
|
|
61
|
+
describe('removeWorktrees', () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
vi.clearAllMocks()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('removes each leaf worktree and returns the removed branches', async () => {
|
|
67
|
+
const branches = ['release/1.2.5', 'feature/foo']
|
|
68
|
+
|
|
69
|
+
const removed = await removeWorktrees({ branches, worktreeDir: WORKTREE_DIR, repoName: REPO_NAME })
|
|
70
|
+
|
|
71
|
+
expect(removed).toEqual(branches)
|
|
72
|
+
|
|
73
|
+
const removeCommands = recordedCalls()
|
|
74
|
+
.map(commandOf)
|
|
75
|
+
.filter((command) => {
|
|
76
|
+
return command.startsWith('git worktree remove ')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
expect(removeCommands).toContain(`git worktree remove ${WORKTREE_DIR}/release/1.2.5`)
|
|
80
|
+
expect(removeCommands).toContain(`git worktree remove ${WORKTREE_DIR}/feature/foo`)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('never deletes the worktrees container or its group subdirs (regression)', async () => {
|
|
84
|
+
const branches = ['release/1.2.5', 'release/1.2.6']
|
|
85
|
+
|
|
86
|
+
await removeWorktrees({ branches, worktreeDir: WORKTREE_DIR, repoName: REPO_NAME, pruneFolder: true })
|
|
87
|
+
|
|
88
|
+
const calls = recordedCalls()
|
|
89
|
+
const commands = calls.map(commandOf)
|
|
90
|
+
|
|
91
|
+
// No destructive directory removal of any kind.
|
|
92
|
+
expect(
|
|
93
|
+
commands.some((command) => {
|
|
94
|
+
return command.includes('rm -rf')
|
|
95
|
+
}),
|
|
96
|
+
).toBe(false)
|
|
97
|
+
expect(
|
|
98
|
+
commands.some((command) => {
|
|
99
|
+
return command.includes('rmdir')
|
|
100
|
+
}),
|
|
101
|
+
).toBe(false)
|
|
102
|
+
|
|
103
|
+
// Invariant: no `$` call ever targets the bare parent worktreeDir as an
|
|
104
|
+
// interpolated argument — guards against any future deletion mechanism.
|
|
105
|
+
const targetsBareParent = calls.some((call) => {
|
|
106
|
+
return argsOf(call).includes(WORKTREE_DIR)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
expect(targetsBareParent).toBe(false)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('runs `git worktree prune` when pruneFolder is set and every branch was removed', async () => {
|
|
113
|
+
const branches = ['release/1.2.5']
|
|
114
|
+
|
|
115
|
+
await removeWorktrees({ branches, worktreeDir: WORKTREE_DIR, repoName: REPO_NAME, pruneFolder: true })
|
|
116
|
+
|
|
117
|
+
const prunedCalls = recordedCalls()
|
|
118
|
+
.map(commandOf)
|
|
119
|
+
.filter((command) => {
|
|
120
|
+
return command === 'git worktree prune'
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
expect(prunedCalls).toHaveLength(1)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('does not run `git worktree prune` when pruneFolder is false', async () => {
|
|
127
|
+
const branches = ['release/1.2.5']
|
|
128
|
+
|
|
129
|
+
await removeWorktrees({ branches, worktreeDir: WORKTREE_DIR, repoName: REPO_NAME, pruneFolder: false })
|
|
130
|
+
|
|
131
|
+
const pruned = recordedCalls().map(commandOf).includes('git worktree prune')
|
|
132
|
+
|
|
133
|
+
expect(pruned).toBe(false)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('skips `git worktree prune` when a branch removal fails (not every branch removed)', async () => {
|
|
137
|
+
const branches = ['release/1.2.5', 'release/1.2.6']
|
|
138
|
+
|
|
139
|
+
// Reject the `git worktree remove` for the second branch so it is not counted
|
|
140
|
+
// in `removed`, leaving `removed.length !== branches.length`. Cast the whole
|
|
141
|
+
// impl to `typeof $` since zx's `$` is a multi-overload callable.
|
|
142
|
+
const failingDollar = (strings: TemplateStringsArray, ...values: unknown[]) => {
|
|
143
|
+
const command = commandOf([strings, ...values])
|
|
144
|
+
|
|
145
|
+
if (command === `git worktree remove ${WORKTREE_DIR}/release/1.2.6`) {
|
|
146
|
+
return Promise.reject(new Error('uncommitted changes block removal'))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return Promise.resolve({ stdout: '', exitCode: 0 })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
vi.mocked($).mockImplementation(failingDollar as unknown as typeof $)
|
|
153
|
+
|
|
154
|
+
const removed = await removeWorktrees({
|
|
155
|
+
branches,
|
|
156
|
+
worktreeDir: WORKTREE_DIR,
|
|
157
|
+
repoName: REPO_NAME,
|
|
158
|
+
pruneFolder: true,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
expect(removed).toEqual(['release/1.2.5'])
|
|
162
|
+
|
|
163
|
+
const pruned = recordedCalls().map(commandOf).includes('git worktree prune')
|
|
164
|
+
|
|
165
|
+
expect(pruned).toBe(false)
|
|
166
|
+
})
|
|
167
|
+
})
|
|
@@ -16,9 +16,10 @@ interface RemoveWorktreesArgs {
|
|
|
16
16
|
* returning the branches that were removed cleanly. Failures are logged but
|
|
17
17
|
* never thrown, so a single bad worktree doesn't poison a batch removal.
|
|
18
18
|
*
|
|
19
|
-
* When `pruneFolder` is true and every branch was removed, also
|
|
20
|
-
* worktree
|
|
21
|
-
*
|
|
19
|
+
* When `pruneFolder` is true and every branch was removed, also run
|
|
20
|
+
* `git worktree prune` to clear stale worktree metadata. The `<repo>-worktrees`
|
|
21
|
+
* container directory and its `release/`/`feature/` subfolders are deliberately
|
|
22
|
+
* left in place so the per-repo worktree scaffold persists even when empty.
|
|
22
23
|
*/
|
|
23
24
|
export const removeWorktrees = async (args: RemoveWorktreesArgs): Promise<string[]> => {
|
|
24
25
|
const { branches, worktreeDir, repoName, pruneFolder = false } = args
|
|
@@ -53,27 +54,12 @@ export const removeWorktrees = async (args: RemoveWorktreesArgs): Promise<string
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
// `git worktree remove` only
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
for (const branch of removed) {
|
|
62
|
-
if (branch.includes('/')) {
|
|
63
|
-
groupDirs.add(`${worktreeDir}/${branch.split('/')[0]}`)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
for (const groupDir of groupDirs) {
|
|
68
|
-
await $`rmdir ${groupDir}`.nothrow()
|
|
69
|
-
}
|
|
70
|
-
|
|
57
|
+
// `git worktree remove` deletes only the leaf worktree. We intentionally leave
|
|
58
|
+
// the `<repo>-worktrees` container and its `release/`/`feature/` group folders
|
|
59
|
+
// in place so the worktree scaffold survives an empty state (it is recreated
|
|
60
|
+
// lazily by `worktrees-add` via `mkdir -p` regardless).
|
|
71
61
|
if (pruneFolder && removed.length === branches.length) {
|
|
72
62
|
await $`git worktree prune`
|
|
73
|
-
await $`rm -rf ${worktreeDir}`
|
|
74
|
-
|
|
75
|
-
logger.info(`🗑️ Removed worktree folder: ${worktreeDir}`)
|
|
76
|
-
logger.info('')
|
|
77
63
|
}
|
|
78
64
|
|
|
79
65
|
return removed
|