infra-kit 0.1.101 → 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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "infra-kit",
3
3
  "type": "module",
4
- "version": "0.1.101",
4
+ "version": "0.1.102",
5
5
  "description": "infra-kit",
6
6
  "main": "dist/cli.js",
7
7
  "module": "dist/cli.js",
@@ -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
+ }