infra-kit 0.1.100 → 0.1.102
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-f2846d8f-974c-486c-b16f-4bdaa28ca45f.jsonl +1 -0
- package/.omc/state/subagent-tracking.json +1 -1
- package/.turbo/turbo-test.log +2 -2
- package/dist/cli.js +39 -39
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +34 -34
- package/dist/mcp.js.map +4 -4
- package/package.json +6 -6
- package/src/commands/gh-release-deliver/gh-release-deliver.ts +30 -0
- package/src/commands/worktrees-remove/worktrees-remove.ts +2 -57
- package/src/lib/worktrees/index.ts +1 -0
- package/src/lib/worktrees/remove-worktrees.ts +65 -0
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infra-kit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.102",
|
|
5
5
|
"description": "infra-kit",
|
|
6
6
|
"main": "dist/cli.js",
|
|
7
7
|
"module": "dist/cli.js",
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
"fix": "pnpm run prettier-fix && pnpm run eslint-fix && pnpm run qa"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@inquirer/checkbox": "
|
|
34
|
-
"@inquirer/confirm": "
|
|
35
|
-
"@inquirer/select": "
|
|
33
|
+
"@inquirer/checkbox": "^5.1.5",
|
|
34
|
+
"@inquirer/confirm": "^6.0.13",
|
|
35
|
+
"@inquirer/select": "^5.1.5",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37
37
|
"commander": "^14.0.3",
|
|
38
38
|
"pino": "^10.3.1",
|
|
39
39
|
"pino-pretty": "^13.1.3",
|
|
40
|
-
"yaml": "
|
|
40
|
+
"yaml": "^2.9.0",
|
|
41
41
|
"zod": "^3.25.76",
|
|
42
42
|
"zx": "^8.8.5"
|
|
43
43
|
},
|
|
@@ -45,6 +45,6 @@
|
|
|
45
45
|
"@pkg/eslint-config": "workspace:*",
|
|
46
46
|
"@pkg/vitest-config": "workspace:*",
|
|
47
47
|
"esbuild": "^0.28.0",
|
|
48
|
-
"typescript": "
|
|
48
|
+
"typescript": "^6.0.3"
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -7,11 +7,14 @@ import { $ } from 'zx'
|
|
|
7
7
|
import { getReleasePRsWithInfo } from 'src/integrations/gh'
|
|
8
8
|
import { deliverJiraRelease, loadJiraConfigOptional } from 'src/integrations/jira'
|
|
9
9
|
import { commandEcho } from 'src/lib/command-echo'
|
|
10
|
+
import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
|
|
10
11
|
import { formatZxError } from 'src/lib/errors/format-zx-error'
|
|
11
12
|
import { OperationError } from 'src/lib/errors/operation-error'
|
|
13
|
+
import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
|
|
12
14
|
import { logger } from 'src/lib/logger'
|
|
13
15
|
import { detectReleaseType, formatBranchChoices, getJiraDescriptions } from 'src/lib/release-utils'
|
|
14
16
|
import type { ReleaseType } from 'src/lib/release-utils'
|
|
17
|
+
import { removeWorktrees } from 'src/lib/worktrees'
|
|
15
18
|
import { defineMcpTool, textContent } from 'src/types'
|
|
16
19
|
import type { RequiredConfirmedOptionArg } from 'src/types'
|
|
17
20
|
|
|
@@ -140,6 +143,30 @@ const resolveTargetInteractively = async (): Promise<ResolvedTarget> => {
|
|
|
140
143
|
return { selectedReleaseBranch, releasePrTitle: prInfo.title }
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
/**
|
|
147
|
+
* `gh pr merge --delete-branch` also deletes the local branch, which fails if a
|
|
148
|
+
* worktree has it checked out (the actual root cause of the "Failed to merge
|
|
149
|
+
* release PR" surface error). Pre-remove any worktree for the release branch
|
|
150
|
+
* so the local delete can succeed.
|
|
151
|
+
*/
|
|
152
|
+
const removeReleaseWorktreeIfPresent = async (releaseBranch: string): Promise<void> => {
|
|
153
|
+
const worktreeBranches = await getCurrentWorktrees('release')
|
|
154
|
+
|
|
155
|
+
if (!worktreeBranches.includes(releaseBranch)) return
|
|
156
|
+
|
|
157
|
+
const [projectRoot, repoName] = await Promise.all([getProjectRoot(), getRepoName()])
|
|
158
|
+
const worktreeDir = `${projectRoot}${WORKTREES_DIR_SUFFIX}`
|
|
159
|
+
|
|
160
|
+
const removed = await removeWorktrees({ branches: [releaseBranch], worktreeDir, repoName })
|
|
161
|
+
|
|
162
|
+
if (removed.length === 0) {
|
|
163
|
+
throw new OperationError(undefined, {
|
|
164
|
+
operation: `remove worktree for ${releaseBranch} before merge`,
|
|
165
|
+
remediation: `run manually: git worktree remove ${worktreeDir}/${releaseBranch} (use --force if uncommitted changes)`,
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
143
170
|
interface MergeReleasePRArgs {
|
|
144
171
|
selectedReleaseBranch: string
|
|
145
172
|
releaseType: ReleaseType
|
|
@@ -147,7 +174,9 @@ interface MergeReleasePRArgs {
|
|
|
147
174
|
|
|
148
175
|
const mergeReleasePR = async (args: MergeReleasePRArgs): Promise<void> => {
|
|
149
176
|
const { selectedReleaseBranch, releaseType } = args
|
|
177
|
+
|
|
150
178
|
const mergeTarget = releaseType === 'hotfix' ? 'main' : 'dev'
|
|
179
|
+
|
|
151
180
|
const releasePr = await fetchPRByHead(selectedReleaseBranch)
|
|
152
181
|
|
|
153
182
|
if (!releasePr) {
|
|
@@ -330,6 +359,7 @@ export const ghReleaseDeliver = async (args: GhReleaseDeliverArgs) => {
|
|
|
330
359
|
|
|
331
360
|
$.quiet = true
|
|
332
361
|
|
|
362
|
+
await removeReleaseWorktreeIfPresent(selectedReleaseBranch)
|
|
333
363
|
await mergeReleasePR({ selectedReleaseBranch, releaseType })
|
|
334
364
|
|
|
335
365
|
if (releaseType !== 'hotfix') {
|
|
@@ -2,9 +2,7 @@ import checkbox from '@inquirer/checkbox'
|
|
|
2
2
|
import confirm from '@inquirer/confirm'
|
|
3
3
|
import process from 'node:process'
|
|
4
4
|
import { z } from 'zod/v4'
|
|
5
|
-
import { $ } from 'zx'
|
|
6
5
|
|
|
7
|
-
import { buildCmuxWorkspaceTitle, closeCmuxWorkspaceByTitle } from 'src/integrations/cmux'
|
|
8
6
|
import { removeFoldersFromCursorWorkspace, resolveCursorWorkspacePath } from 'src/integrations/cursor'
|
|
9
7
|
import { getReleasePRsWithInfo } from 'src/integrations/gh'
|
|
10
8
|
import { commandEcho } from 'src/lib/command-echo'
|
|
@@ -15,6 +13,7 @@ import { getInfraKitConfig } from 'src/lib/infra-kit-config'
|
|
|
15
13
|
import { logger } from 'src/lib/logger'
|
|
16
14
|
import { detectReleaseType, formatBranchChoices, getJiraDescriptions } from 'src/lib/release-utils'
|
|
17
15
|
import type { ReleaseType } from 'src/lib/release-utils'
|
|
16
|
+
import { removeWorktrees } from 'src/lib/worktrees'
|
|
18
17
|
import { defineMcpTool, textContent } from 'src/types'
|
|
19
18
|
import type { RequiredConfirmedOptionArg } from 'src/types'
|
|
20
19
|
|
|
@@ -118,7 +117,7 @@ export const worktreesRemove = async (options: WorktreeManagementArgs) => {
|
|
|
118
117
|
branches: selectedReleaseBranches,
|
|
119
118
|
worktreeDir,
|
|
120
119
|
repoName,
|
|
121
|
-
allSelected,
|
|
120
|
+
pruneFolder: allSelected,
|
|
122
121
|
})
|
|
123
122
|
|
|
124
123
|
await syncCursorWorkspaceOnRemove({ removedWorktrees, worktreeDir, projectRoot })
|
|
@@ -145,60 +144,6 @@ export const worktreesRemove = async (options: WorktreeManagementArgs) => {
|
|
|
145
144
|
}
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
interface RemoveWorktreesArgs {
|
|
149
|
-
branches: string[]
|
|
150
|
-
worktreeDir: string
|
|
151
|
-
repoName: string
|
|
152
|
-
allSelected: boolean
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Remove worktrees for the specified branches and whole folder
|
|
157
|
-
*/
|
|
158
|
-
const removeWorktrees = async (args: RemoveWorktreesArgs): Promise<string[]> => {
|
|
159
|
-
const { branches, worktreeDir, repoName, allSelected } = args
|
|
160
|
-
|
|
161
|
-
const results = await Promise.allSettled(
|
|
162
|
-
branches.map(async (branch) => {
|
|
163
|
-
const worktreePath = `${worktreeDir}/${branch}`
|
|
164
|
-
|
|
165
|
-
const title = buildCmuxWorkspaceTitle({ repoName, branch })
|
|
166
|
-
|
|
167
|
-
await closeCmuxWorkspaceByTitle(title)
|
|
168
|
-
|
|
169
|
-
await $`git worktree remove ${worktreePath}`
|
|
170
|
-
|
|
171
|
-
return branch
|
|
172
|
-
}),
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
const removed: string[] = []
|
|
176
|
-
|
|
177
|
-
for (const [index, result] of results.entries()) {
|
|
178
|
-
if (result.status === 'fulfilled') {
|
|
179
|
-
removed.push(result.value)
|
|
180
|
-
} else {
|
|
181
|
-
const branch = branches[index]
|
|
182
|
-
const err = new OperationError(result.reason, {
|
|
183
|
-
operation: `remove worktree for ${branch}`,
|
|
184
|
-
remediation: "check 'git worktree list' for the path; uncommitted changes block removal",
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
logger.error({ error: result.reason, msg: err.message })
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (allSelected && removed.length === branches.length) {
|
|
192
|
-
await $`git worktree prune`
|
|
193
|
-
await $`rm -rf ${worktreeDir}`
|
|
194
|
-
|
|
195
|
-
logger.info(`🗑️ Removed worktree folder: ${worktreeDir}`)
|
|
196
|
-
logger.info('')
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return removed
|
|
200
|
-
}
|
|
201
|
-
|
|
202
147
|
interface SyncCursorWorkspaceOnRemoveArgs {
|
|
203
148
|
removedWorktrees: string[]
|
|
204
149
|
worktreeDir: string
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { removeWorktrees } from './remove-worktrees'
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { $ } from 'zx'
|
|
2
|
+
|
|
3
|
+
import { buildCmuxWorkspaceTitle, closeCmuxWorkspaceByTitle } from 'src/integrations/cmux'
|
|
4
|
+
import { OperationError } from 'src/lib/errors/operation-error'
|
|
5
|
+
import { logger } from 'src/lib/logger'
|
|
6
|
+
|
|
7
|
+
interface RemoveWorktreesArgs {
|
|
8
|
+
branches: string[]
|
|
9
|
+
worktreeDir: string
|
|
10
|
+
repoName: string
|
|
11
|
+
pruneFolder?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Close any cmux workspace for each branch and run `git worktree remove`,
|
|
16
|
+
* returning the branches that were removed cleanly. Failures are logged but
|
|
17
|
+
* never thrown, so a single bad worktree doesn't poison a batch removal.
|
|
18
|
+
*
|
|
19
|
+
* When `pruneFolder` is true and every branch was removed, also prune the
|
|
20
|
+
* worktree metadata and delete the worktrees folder — used by the
|
|
21
|
+
* `worktrees-remove` "all" path to leave the filesystem clean.
|
|
22
|
+
*/
|
|
23
|
+
export const removeWorktrees = async (args: RemoveWorktreesArgs): Promise<string[]> => {
|
|
24
|
+
const { branches, worktreeDir, repoName, pruneFolder = false } = args
|
|
25
|
+
|
|
26
|
+
const results = await Promise.allSettled(
|
|
27
|
+
branches.map(async (branch) => {
|
|
28
|
+
const worktreePath = `${worktreeDir}/${branch}`
|
|
29
|
+
|
|
30
|
+
const title = buildCmuxWorkspaceTitle({ repoName, branch })
|
|
31
|
+
|
|
32
|
+
await closeCmuxWorkspaceByTitle(title)
|
|
33
|
+
|
|
34
|
+
await $`git worktree remove ${worktreePath}`
|
|
35
|
+
|
|
36
|
+
return branch
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const removed: string[] = []
|
|
41
|
+
|
|
42
|
+
for (const [index, result] of results.entries()) {
|
|
43
|
+
if (result.status === 'fulfilled') {
|
|
44
|
+
removed.push(result.value)
|
|
45
|
+
} else {
|
|
46
|
+
const branch = branches[index]
|
|
47
|
+
const err = new OperationError(result.reason, {
|
|
48
|
+
operation: `remove worktree for ${branch}`,
|
|
49
|
+
remediation: "check 'git worktree list' for the path; uncommitted changes block removal",
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
logger.error({ error: result.reason, msg: err.message })
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (pruneFolder && removed.length === branches.length) {
|
|
57
|
+
await $`git worktree prune`
|
|
58
|
+
await $`rm -rf ${worktreeDir}`
|
|
59
|
+
|
|
60
|
+
logger.info(`🗑️ Removed worktree folder: ${worktreeDir}`)
|
|
61
|
+
logger.info('')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return removed
|
|
65
|
+
}
|