infra-kit 0.1.79 → 0.1.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +32 -33
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +27 -28
- package/dist/mcp.js.map +4 -4
- package/package.json +1 -1
- package/src/commands/env-status/env-status.ts +7 -5
- package/src/commands/gh-merge-dev/gh-merge-dev.ts +25 -16
- package/src/commands/gh-release-deliver/gh-release-deliver.ts +49 -27
- package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +18 -21
- package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +18 -19
- package/src/commands/gh-release-deploy-service/gh-release-deploy-service.ts +18 -21
- package/src/commands/gh-release-list/gh-release-list.ts +45 -8
- package/src/commands/release-create/release-create.ts +35 -3
- package/src/commands/release-create-batch/release-create-batch.ts +49 -24
- package/src/commands/worktrees-add/worktrees-add.ts +101 -56
- package/src/commands/worktrees-list/worktrees-list.ts +53 -278
- package/src/commands/worktrees-remove/worktrees-remove.ts +57 -21
- package/src/commands/worktrees-sync/worktrees-sync.ts +3 -1
- package/src/entry/cli.ts +20 -12
- package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +85 -16
- package/src/integrations/gh/gh-release-prs/index.ts +2 -1
- package/src/integrations/gh/index.ts +2 -1
- package/src/integrations/jira/api.ts +1 -1
- package/src/integrations/jira/index.ts +7 -1
- package/src/lib/command-echo/command-echo.ts +2 -3
- package/src/lib/release-utils/index.ts +11 -1
- package/src/lib/release-utils/release-utils.ts +111 -15
|
@@ -1,300 +1,82 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import { $ } from 'zx'
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import { getReleasePRsWithInfo } from 'src/integrations/gh'
|
|
4
|
+
import { getCurrentWorktrees } from 'src/lib/git-utils'
|
|
5
5
|
import { logger } from 'src/lib/logger'
|
|
6
|
+
import { detectReleaseType, formatVersionLabel, getJiraDescriptions } from 'src/lib/release-utils'
|
|
7
|
+
import type { ReleaseType } from 'src/lib/release-utils'
|
|
6
8
|
import type { ToolsExecutionResult } from 'src/types'
|
|
7
9
|
|
|
8
10
|
interface WorktreeInfo {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
isCurrent: boolean
|
|
13
|
-
type: 'release' | 'feature'
|
|
14
|
-
status: string
|
|
15
|
-
lastCommitMessage: string
|
|
16
|
-
aheadBehind: string
|
|
11
|
+
version: string
|
|
12
|
+
type: ReleaseType
|
|
13
|
+
description: string | null
|
|
17
14
|
}
|
|
18
15
|
|
|
19
16
|
/**
|
|
20
|
-
* List all
|
|
17
|
+
* List all release git worktrees with version, type, and Jira description
|
|
21
18
|
*/
|
|
22
19
|
export const worktreesList = async (): Promise<ToolsExecutionResult> => {
|
|
23
|
-
|
|
24
|
-
const [releaseWorktrees, featureWorktrees] = await Promise.all([
|
|
25
|
-
getCurrentWorktrees('release'),
|
|
26
|
-
getCurrentWorktrees('feature'),
|
|
27
|
-
])
|
|
20
|
+
const currentWorktrees = await getCurrentWorktrees('release')
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
logResults(worktreesInfo)
|
|
33
|
-
|
|
34
|
-
const structuredContent = {
|
|
35
|
-
worktrees: worktreesInfo,
|
|
36
|
-
totalCount: worktreesInfo.length,
|
|
37
|
-
releaseCount: releaseWorktrees.length,
|
|
38
|
-
featureCount: featureWorktrees.length,
|
|
39
|
-
}
|
|
22
|
+
if (currentWorktrees.length === 0) {
|
|
23
|
+
logger.info('ℹ️ No active worktrees found')
|
|
40
24
|
|
|
41
25
|
return {
|
|
42
|
-
content: [
|
|
43
|
-
|
|
44
|
-
type: 'text',
|
|
45
|
-
text: JSON.stringify(structuredContent, null, 2),
|
|
46
|
-
},
|
|
47
|
-
],
|
|
48
|
-
structuredContent,
|
|
49
|
-
}
|
|
50
|
-
} catch (error) {
|
|
51
|
-
logger.error({ error }, '❌ Error listing worktrees')
|
|
52
|
-
throw error
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Process worktrees to get detailed information
|
|
58
|
-
*/
|
|
59
|
-
const processWorktrees = async (
|
|
60
|
-
releaseWorktrees: string[],
|
|
61
|
-
featureWorktrees: string[],
|
|
62
|
-
projectRoot: string,
|
|
63
|
-
): Promise<WorktreeInfo[]> => {
|
|
64
|
-
const allWorktrees = [
|
|
65
|
-
...releaseWorktrees.map((branch) => {
|
|
66
|
-
return { branch, type: 'release' as const }
|
|
67
|
-
}),
|
|
68
|
-
...featureWorktrees.map((branch) => {
|
|
69
|
-
return { branch, type: 'feature' as const }
|
|
70
|
-
}),
|
|
71
|
-
]
|
|
72
|
-
|
|
73
|
-
const worktreesInfo: WorktreeInfo[] = []
|
|
74
|
-
|
|
75
|
-
for (const { branch, type } of allWorktrees) {
|
|
76
|
-
try {
|
|
77
|
-
const worktreePath = `${projectRoot}/${branch}`
|
|
78
|
-
const isCurrent = await isCurrentWorktree(branch)
|
|
79
|
-
const commit = await getWorktreeCommit(worktreePath)
|
|
80
|
-
const status = await getWorktreeStatus(worktreePath)
|
|
81
|
-
const lastCommitMessage = await getLastCommitMessage(worktreePath)
|
|
82
|
-
const aheadBehind = await getAheadBehind(worktreePath)
|
|
83
|
-
|
|
84
|
-
worktreesInfo.push({
|
|
85
|
-
branch,
|
|
86
|
-
path: worktreePath,
|
|
87
|
-
commit: commit.substring(0, 8),
|
|
88
|
-
isCurrent,
|
|
89
|
-
type,
|
|
90
|
-
status,
|
|
91
|
-
lastCommitMessage: lastCommitMessage.substring(0, 60) + (lastCommitMessage.length > 60 ? '...' : ''),
|
|
92
|
-
aheadBehind,
|
|
93
|
-
})
|
|
94
|
-
} catch (error) {
|
|
95
|
-
logger.warn({ error, branch }, `⚠️ Could not process worktree ${branch}`)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return worktreesInfo.sort((a, b) => {
|
|
100
|
-
// Sort by type first (releases before features), then by branch name
|
|
101
|
-
if (a.type !== b.type) {
|
|
102
|
-
return a.type === 'release' ? -1 : 1
|
|
26
|
+
content: [{ type: 'text', text: JSON.stringify({ worktrees: [], count: 0 }, null, 2) }],
|
|
27
|
+
structuredContent: { worktrees: [], count: 0 },
|
|
103
28
|
}
|
|
104
|
-
|
|
105
|
-
return a.branch.localeCompare(b.branch)
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Check if a worktree is currently active
|
|
111
|
-
*/
|
|
112
|
-
const isCurrentWorktree = async (branch: string): Promise<boolean> => {
|
|
113
|
-
try {
|
|
114
|
-
const currentBranch = await $`git branch --show-current`
|
|
115
|
-
|
|
116
|
-
return currentBranch.stdout.trim() === branch
|
|
117
|
-
} catch {
|
|
118
|
-
return false
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Get the commit hash for a worktree
|
|
124
|
-
*/
|
|
125
|
-
const getWorktreeCommit = async (worktreePath: string): Promise<string> => {
|
|
126
|
-
try {
|
|
127
|
-
const result = await $`cd ${worktreePath} && git rev-parse HEAD`
|
|
128
|
-
|
|
129
|
-
return result.stdout.trim()
|
|
130
|
-
} catch {
|
|
131
|
-
return 'unknown'
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Get the status of a worktree
|
|
137
|
-
*/
|
|
138
|
-
const getWorktreeStatus = async (worktreePath: string): Promise<string> => {
|
|
139
|
-
try {
|
|
140
|
-
const result = await $`cd ${worktreePath} && git status --porcelain`
|
|
141
|
-
const changes = result.stdout.trim().split('\n').filter(Boolean)
|
|
142
|
-
|
|
143
|
-
if (changes.length === 0) return 'clean'
|
|
144
|
-
if (changes.length <= 3) return 'modified'
|
|
145
|
-
|
|
146
|
-
return 'dirty'
|
|
147
|
-
} catch {
|
|
148
|
-
return 'unknown'
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Get the last commit message for a worktree
|
|
154
|
-
*/
|
|
155
|
-
const getLastCommitMessage = async (worktreePath: string): Promise<string> => {
|
|
156
|
-
try {
|
|
157
|
-
const result = await $`cd ${worktreePath} && git log -1 --pretty=format:"%s"`
|
|
158
|
-
|
|
159
|
-
return result.stdout.trim()
|
|
160
|
-
} catch {
|
|
161
|
-
return 'No commit message available'
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Get ahead/behind information for a worktree
|
|
167
|
-
*/
|
|
168
|
-
const getAheadBehind = async (worktreePath: string): Promise<string> => {
|
|
169
|
-
try {
|
|
170
|
-
const result =
|
|
171
|
-
await $`cd ${worktreePath} && git rev-list --count --left-right @{u}...HEAD 2>/dev/null || echo "0 0"`
|
|
172
|
-
|
|
173
|
-
const parts = result.stdout.trim().split('\t').map(Number)
|
|
174
|
-
const behind = parts[0] || 0
|
|
175
|
-
const ahead = parts[1] || 0
|
|
176
|
-
|
|
177
|
-
if (ahead === 0 && behind === 0) return 'up to date'
|
|
178
|
-
if (ahead > 0 && behind === 0) return `↑${ahead} ahead`
|
|
179
|
-
if (behind > 0 && ahead === 0) return `↓${behind} behind`
|
|
180
|
-
|
|
181
|
-
return `↑${ahead} ↓${behind}`
|
|
182
|
-
} catch {
|
|
183
|
-
return 'unknown'
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Log the worktrees list in a beautiful formatted way
|
|
189
|
-
*/
|
|
190
|
-
const logResults = (worktrees: WorktreeInfo[]): void => {
|
|
191
|
-
if (worktrees.length === 0) {
|
|
192
|
-
logger.info('\n🌿 Git Worktrees')
|
|
193
|
-
logger.info('─'.repeat(80))
|
|
194
|
-
logger.info('ℹ️ No worktrees found')
|
|
195
|
-
logger.info('─'.repeat(80))
|
|
196
|
-
|
|
197
|
-
return
|
|
198
29
|
}
|
|
199
30
|
|
|
200
|
-
|
|
201
|
-
logger.info('═'.repeat(100))
|
|
202
|
-
|
|
203
|
-
// Separate releases and features
|
|
204
|
-
const releases = worktrees.filter((w) => {
|
|
205
|
-
return w.type === 'release'
|
|
206
|
-
})
|
|
207
|
-
const features = worktrees.filter((w) => {
|
|
208
|
-
return w.type === 'feature'
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
// Display releases first
|
|
212
|
-
displayWorktreeSection('🚀 Releases', releases)
|
|
31
|
+
const [releasePRsInfo, jiraDescriptions] = await Promise.all([getReleasePRsWithInfo(), getJiraDescriptions()])
|
|
213
32
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
33
|
+
const releaseTypes = new Map<string, ReleaseType>(
|
|
34
|
+
releasePRsInfo.map((pr) => {
|
|
35
|
+
return [pr.branch, detectReleaseType(pr.title)]
|
|
36
|
+
}),
|
|
37
|
+
)
|
|
218
38
|
|
|
219
|
-
|
|
39
|
+
const worktrees: WorktreeInfo[] = currentWorktrees.map((branch) => {
|
|
40
|
+
const version = branch.replace('release/', '')
|
|
41
|
+
const type = releaseTypes.get(branch) || 'regular'
|
|
42
|
+
const description = jiraDescriptions.get(version) || null
|
|
220
43
|
|
|
221
|
-
|
|
222
|
-
const current = worktrees.find((w) => {
|
|
223
|
-
return w.isCurrent
|
|
44
|
+
return { version, type, description }
|
|
224
45
|
})
|
|
225
46
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
47
|
+
// Log formatted output
|
|
48
|
+
const maxVersionLength = Math.max(
|
|
49
|
+
...worktrees.map((w) => {
|
|
50
|
+
return w.version.length
|
|
51
|
+
}),
|
|
229
52
|
)
|
|
230
53
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
logger.info('')
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Display a section of worktrees
|
|
240
|
-
*/
|
|
241
|
-
const displayWorktreeSection = (sectionTitle: string, worktrees: WorktreeInfo[]): void => {
|
|
242
|
-
if (worktrees.length === 0) return
|
|
243
|
-
|
|
244
|
-
logger.info(`\n${sectionTitle}`)
|
|
245
|
-
logger.info('─'.repeat(50))
|
|
54
|
+
const formattedLines = worktrees.map((worktree) => {
|
|
55
|
+
const label = formatVersionLabel(worktree.version, worktree.type, maxVersionLength)
|
|
246
56
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (index < worktrees.length - 1) {
|
|
251
|
-
logger.info('')
|
|
57
|
+
if (worktree.description) {
|
|
58
|
+
return `${label} ${worktree.description}`
|
|
252
59
|
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Display a single worktree entry
|
|
258
|
-
*/
|
|
259
|
-
const displayWorktree = (worktree: WorktreeInfo): void => {
|
|
260
|
-
// Worktree status indicators
|
|
261
|
-
const currentIndicator = worktree.isCurrent ? '📍' : ' '
|
|
262
|
-
const statusIndicator = getStatusIndicator(worktree.status)
|
|
263
|
-
|
|
264
|
-
const typeIcon = worktree.type === 'release' ? '🚀' : '✨'
|
|
265
|
-
|
|
266
|
-
// Branch name with color coding
|
|
267
|
-
const branchDisplay = `${typeIcon} ${worktree.branch}`
|
|
268
|
-
|
|
269
|
-
logger.info(`${currentIndicator} ${statusIndicator} ${branchDisplay}`)
|
|
270
|
-
|
|
271
|
-
// Commit and sync info
|
|
272
|
-
const syncInfo = worktree.aheadBehind !== 'unknown' ? ` | ${worktree.aheadBehind}` : ''
|
|
273
|
-
|
|
274
|
-
logger.info(` 📝 ${worktree.commit}${syncInfo}`)
|
|
275
60
|
|
|
276
|
-
|
|
277
|
-
|
|
61
|
+
return label
|
|
62
|
+
})
|
|
278
63
|
|
|
279
|
-
|
|
280
|
-
|
|
64
|
+
logger.info('🌿 Active worktrees:')
|
|
65
|
+
logger.info(`\n${formattedLines.join('\n')}\n`)
|
|
281
66
|
|
|
282
|
-
|
|
283
|
-
|
|
67
|
+
const structuredContent = {
|
|
68
|
+
worktrees,
|
|
69
|
+
count: worktrees.length,
|
|
70
|
+
}
|
|
284
71
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
return '⚠️ '
|
|
294
|
-
case 'dirty':
|
|
295
|
-
return '🔴'
|
|
296
|
-
default:
|
|
297
|
-
return '❓'
|
|
72
|
+
return {
|
|
73
|
+
content: [
|
|
74
|
+
{
|
|
75
|
+
type: 'text',
|
|
76
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
structuredContent,
|
|
298
80
|
}
|
|
299
81
|
}
|
|
300
82
|
|
|
@@ -307,20 +89,13 @@ export const worktreesListMcpTool = {
|
|
|
307
89
|
worktrees: z
|
|
308
90
|
.array(
|
|
309
91
|
z.object({
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
isCurrent: z.boolean(),
|
|
314
|
-
type: z.enum(['release', 'feature']),
|
|
315
|
-
status: z.string(),
|
|
316
|
-
lastCommitMessage: z.string(),
|
|
317
|
-
aheadBehind: z.string(),
|
|
92
|
+
version: z.string().describe('Release version'),
|
|
93
|
+
type: z.enum(['regular', 'hotfix']).describe('Release type'),
|
|
94
|
+
description: z.string().nullable().describe('Jira version description'),
|
|
318
95
|
}),
|
|
319
96
|
)
|
|
320
97
|
.describe('List of all worktrees with details'),
|
|
321
|
-
|
|
322
|
-
releaseCount: z.number().describe('Number of release worktrees'),
|
|
323
|
-
featureCount: z.number().describe('Number of feature worktrees'),
|
|
98
|
+
count: z.number().describe('Number of worktrees'),
|
|
324
99
|
},
|
|
325
100
|
handler: worktreesList,
|
|
326
101
|
}
|
|
@@ -4,15 +4,19 @@ import process from 'node:process'
|
|
|
4
4
|
import { z } from 'zod'
|
|
5
5
|
import { $ } from 'zx'
|
|
6
6
|
|
|
7
|
+
import { getReleasePRsWithInfo } from 'src/integrations/gh'
|
|
7
8
|
import { commandEcho } from 'src/lib/command-echo'
|
|
8
9
|
import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
|
|
9
10
|
import { getCurrentWorktrees, getProjectRoot } from 'src/lib/git-utils'
|
|
10
11
|
import { logger } from 'src/lib/logger'
|
|
12
|
+
import { detectReleaseType, formatBranchChoices, getJiraDescriptions } from 'src/lib/release-utils'
|
|
13
|
+
import type { ReleaseType } from 'src/lib/release-utils'
|
|
11
14
|
import type { RequiredConfirmedOptionArg, ToolsExecutionResult } from 'src/types'
|
|
12
15
|
|
|
13
16
|
// Constants
|
|
14
17
|
interface WorktreeManagementArgs extends RequiredConfirmedOptionArg {
|
|
15
18
|
all: boolean
|
|
19
|
+
versions?: string
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
/**
|
|
@@ -20,7 +24,7 @@ interface WorktreeManagementArgs extends RequiredConfirmedOptionArg {
|
|
|
20
24
|
* Creates worktrees for active release branches and removes unused ones
|
|
21
25
|
*/
|
|
22
26
|
export const worktreesRemove = async (options: WorktreeManagementArgs): Promise<ToolsExecutionResult> => {
|
|
23
|
-
const { confirmedCommand, all } = options
|
|
27
|
+
const { confirmedCommand, all, versions } = options
|
|
24
28
|
|
|
25
29
|
commandEcho.start('worktrees-remove')
|
|
26
30
|
|
|
@@ -46,18 +50,25 @@ export const worktreesRemove = async (options: WorktreeManagementArgs): Promise<
|
|
|
46
50
|
|
|
47
51
|
if (all) {
|
|
48
52
|
selectedReleaseBranches = currentWorktrees
|
|
53
|
+
} else if (versions) {
|
|
54
|
+
selectedReleaseBranches = versions.split(',').map((v) => {
|
|
55
|
+
return `release/v${v.trim()}`
|
|
56
|
+
})
|
|
49
57
|
} else {
|
|
50
58
|
commandEcho.setInteractive()
|
|
51
59
|
|
|
60
|
+
const [descriptions, prInfo] = await Promise.all([getJiraDescriptions(), getReleasePRsWithInfo()])
|
|
61
|
+
|
|
62
|
+
const releaseTypes = new Map<string, ReleaseType>(
|
|
63
|
+
prInfo.map((pr) => {
|
|
64
|
+
return [pr.branch, detectReleaseType(pr.title)]
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
67
|
+
|
|
52
68
|
selectedReleaseBranches = await checkbox({
|
|
53
69
|
required: true,
|
|
54
70
|
message: '🌿 Select release branches',
|
|
55
|
-
choices:
|
|
56
|
-
return {
|
|
57
|
-
name: pr.replace('release/v', ''),
|
|
58
|
-
value: pr,
|
|
59
|
-
}
|
|
60
|
-
}),
|
|
71
|
+
choices: formatBranchChoices({ branches: currentWorktrees, descriptions, types: releaseTypes }),
|
|
61
72
|
})
|
|
62
73
|
}
|
|
63
74
|
|
|
@@ -67,7 +78,12 @@ export const worktreesRemove = async (options: WorktreeManagementArgs): Promise<
|
|
|
67
78
|
if (allSelected) {
|
|
68
79
|
commandEcho.addOption('--all', true)
|
|
69
80
|
} else {
|
|
70
|
-
commandEcho.addOption(
|
|
81
|
+
commandEcho.addOption(
|
|
82
|
+
'--versions',
|
|
83
|
+
selectedReleaseBranches.map((branch) => {
|
|
84
|
+
return branch.replace('release/v', '')
|
|
85
|
+
}),
|
|
86
|
+
)
|
|
71
87
|
}
|
|
72
88
|
|
|
73
89
|
// Ask for confirmation
|
|
@@ -87,7 +103,7 @@ export const worktreesRemove = async (options: WorktreeManagementArgs): Promise<
|
|
|
87
103
|
}
|
|
88
104
|
|
|
89
105
|
// Track --yes flag if confirmation was interactive (user confirmed)
|
|
90
|
-
if (
|
|
106
|
+
if (!confirmedCommand) {
|
|
91
107
|
commandEcho.addOption('--yes', true)
|
|
92
108
|
}
|
|
93
109
|
|
|
@@ -118,22 +134,39 @@ export const worktreesRemove = async (options: WorktreeManagementArgs): Promise<
|
|
|
118
134
|
}
|
|
119
135
|
|
|
120
136
|
/**
|
|
121
|
-
* Remove worktrees for the specified branches
|
|
137
|
+
* Remove worktrees for the specified branches and whole folder
|
|
122
138
|
*/
|
|
123
139
|
const removeWorktrees = async (branches: string[], worktreeDir: string): Promise<string[]> => {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
for (const branch of branches) {
|
|
127
|
-
try {
|
|
140
|
+
const results = await Promise.allSettled(
|
|
141
|
+
branches.map(async (branch) => {
|
|
128
142
|
const worktreePath = `${worktreeDir}/${branch}`
|
|
129
143
|
|
|
130
144
|
await $`git worktree remove ${worktreePath}`
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
145
|
+
|
|
146
|
+
return branch
|
|
147
|
+
}),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
const removed: string[] = []
|
|
151
|
+
|
|
152
|
+
for (const [index, result] of results.entries()) {
|
|
153
|
+
if (result.status === 'fulfilled') {
|
|
154
|
+
removed.push(result.value)
|
|
155
|
+
} else {
|
|
156
|
+
const branch = branches[index]
|
|
157
|
+
|
|
158
|
+
logger.error({ error: result.reason }, `❌ Failed to remove worktree for ${branch}`)
|
|
134
159
|
}
|
|
135
160
|
}
|
|
136
161
|
|
|
162
|
+
if (removed.length === branches.length) {
|
|
163
|
+
await $`git worktree prune`
|
|
164
|
+
await $`rm -rf ${worktreeDir}`
|
|
165
|
+
|
|
166
|
+
logger.info(`🗑️ Removed worktree folder: ${worktreeDir}`)
|
|
167
|
+
logger.info('')
|
|
168
|
+
}
|
|
169
|
+
|
|
137
170
|
return removed
|
|
138
171
|
}
|
|
139
172
|
|
|
@@ -143,7 +176,9 @@ const removeWorktrees = async (branches: string[], worktreeDir: string): Promise
|
|
|
143
176
|
const logResults = (removed: string[]): void => {
|
|
144
177
|
if (removed.length > 0) {
|
|
145
178
|
logger.info('❌ Removed worktrees:')
|
|
146
|
-
|
|
179
|
+
for (const branch of removed) {
|
|
180
|
+
logger.info(branch)
|
|
181
|
+
}
|
|
147
182
|
logger.info('')
|
|
148
183
|
} else {
|
|
149
184
|
logger.info('ℹ️ No unused worktrees to remove')
|
|
@@ -155,11 +190,12 @@ export const worktreesRemoveMcpTool = {
|
|
|
155
190
|
name: 'worktrees-remove',
|
|
156
191
|
description: 'Remove selected worktrees',
|
|
157
192
|
inputSchema: {
|
|
158
|
-
all: z.boolean().describe('Remove all worktrees without prompting'),
|
|
193
|
+
all: z.boolean().describe('Remove all git worktrees without prompting'),
|
|
194
|
+
versions: z.string().optional().describe('Specify versions by comma, e.g. 1.2.5, 1.2.6'),
|
|
159
195
|
},
|
|
160
196
|
outputSchema: {
|
|
161
|
-
removedWorktrees: z.array(z.string()).describe('List of removed worktree branches'),
|
|
162
|
-
count: z.number().describe('Number of worktrees removed'),
|
|
197
|
+
removedWorktrees: z.array(z.string()).describe('List of removed git worktree branches'),
|
|
198
|
+
count: z.number().describe('Number of git worktrees removed'),
|
|
163
199
|
},
|
|
164
200
|
handler: worktreesRemove,
|
|
165
201
|
}
|
|
@@ -133,7 +133,9 @@ const removeWorktrees = async (branches: string[], worktreeDir: string): Promise
|
|
|
133
133
|
const logResults = (removed: string[]): void => {
|
|
134
134
|
if (removed.length > 0) {
|
|
135
135
|
logger.info('❌ Removed worktrees:')
|
|
136
|
-
|
|
136
|
+
for (const branch of removed) {
|
|
137
|
+
logger.info(branch)
|
|
138
|
+
}
|
|
137
139
|
logger.info('')
|
|
138
140
|
} else {
|
|
139
141
|
logger.info('ℹ️ No unused worktrees to remove')
|
package/src/entry/cli.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Command } from 'commander'
|
|
1
|
+
import { Command, Option } from 'commander'
|
|
2
2
|
|
|
3
3
|
import { doctor } from 'src/commands/doctor'
|
|
4
4
|
import { envClear } from 'src/commands/env-clear'
|
|
@@ -6,7 +6,6 @@ import { envInit } from 'src/commands/env-init'
|
|
|
6
6
|
import { envList } from 'src/commands/env-list'
|
|
7
7
|
import { envLoad } from 'src/commands/env-load'
|
|
8
8
|
import { envStatus } from 'src/commands/env-status'
|
|
9
|
-
// Commands
|
|
10
9
|
import { ghMergeDev } from 'src/commands/gh-merge-dev'
|
|
11
10
|
import { ghReleaseDeliver } from 'src/commands/gh-release-deliver'
|
|
12
11
|
import { ghReleaseDeployAll } from 'src/commands/gh-release-deploy-all'
|
|
@@ -43,12 +42,14 @@ program
|
|
|
43
42
|
.description('Create a single release branch')
|
|
44
43
|
.option('-v, --version <version>', 'Specify the version to create, e.g. 1.2.5')
|
|
45
44
|
.option('-d, --description <description>', 'Optional description for the Jira version')
|
|
45
|
+
.addOption(new Option('-t, --type <type>', 'Release type (default: regular)').choices(['regular', 'hotfix']))
|
|
46
46
|
.option('-y, --yes', 'Skip confirmation prompt')
|
|
47
47
|
.option('--no-checkout', 'Do not checkout the created branch after creation (checkout is default)')
|
|
48
48
|
.action(async (options) => {
|
|
49
49
|
await releaseCreate({
|
|
50
50
|
version: options.version,
|
|
51
51
|
description: options.description,
|
|
52
|
+
type: options.type,
|
|
52
53
|
confirmedCommand: options.yes,
|
|
53
54
|
checkout: options.checkout,
|
|
54
55
|
})
|
|
@@ -58,12 +59,12 @@ program
|
|
|
58
59
|
.command('release-create-batch')
|
|
59
60
|
.description('Create multiple release branches (batch operation)')
|
|
60
61
|
.option('-v, --versions <versions>', 'Specify the versions to create by comma, e.g. 1.2.5, 1.2.6')
|
|
61
|
-
.
|
|
62
|
+
.addOption(new Option('-t, --type <type>', 'Release type (default: regular)').choices(['regular', 'hotfix']))
|
|
62
63
|
.option('-y, --yes', 'Skip confirmation prompt')
|
|
63
64
|
.action(async (options) => {
|
|
64
65
|
await releaseCreateBatch({
|
|
65
66
|
versions: options.versions,
|
|
66
|
-
|
|
67
|
+
type: options.type,
|
|
67
68
|
confirmedCommand: options.yes,
|
|
68
69
|
})
|
|
69
70
|
})
|
|
@@ -132,9 +133,19 @@ program
|
|
|
132
133
|
.description('Add git worktrees for release branches')
|
|
133
134
|
.option('-y, --yes', 'Skip confirmation prompt')
|
|
134
135
|
.option('-a, --all', 'Select all active release branches')
|
|
136
|
+
.option('-v, --versions <versions>', 'Specify versions by comma, e.g. 1.2.5, 1.2.6')
|
|
135
137
|
.option('-c, --cursor', 'Open created worktrees in Cursor')
|
|
138
|
+
.option('--no-cursor', 'Skip Cursor prompt')
|
|
139
|
+
.option('-g, --github-desktop', 'Open created worktrees in GitHub Desktop')
|
|
140
|
+
.option('--no-github-desktop', 'Skip GitHub Desktop prompt')
|
|
136
141
|
.action(async (options) => {
|
|
137
|
-
await worktreesAdd({
|
|
142
|
+
await worktreesAdd({
|
|
143
|
+
confirmedCommand: options.yes,
|
|
144
|
+
all: options.all,
|
|
145
|
+
versions: options.versions,
|
|
146
|
+
cursor: options.cursor,
|
|
147
|
+
githubDesktop: options.githubDesktop,
|
|
148
|
+
})
|
|
138
149
|
})
|
|
139
150
|
|
|
140
151
|
program
|
|
@@ -149,8 +160,9 @@ program
|
|
|
149
160
|
.description('Remove git worktrees for release branches')
|
|
150
161
|
.option('-y, --yes', 'Skip confirmation prompt')
|
|
151
162
|
.option('-a, --all', 'Select all active release branches')
|
|
163
|
+
.option('-v, --versions <versions>', 'Specify versions by comma, e.g. 1.2.5, 1.2.6')
|
|
152
164
|
.action(async (options) => {
|
|
153
|
-
await worktreesRemove({ confirmedCommand: options.yes, all: options.all })
|
|
165
|
+
await worktreesRemove({ confirmedCommand: options.yes, all: options.all, versions: options.versions })
|
|
154
166
|
})
|
|
155
167
|
|
|
156
168
|
program
|
|
@@ -183,9 +195,7 @@ program
|
|
|
183
195
|
|
|
184
196
|
program
|
|
185
197
|
.command('env-load')
|
|
186
|
-
.description(
|
|
187
|
-
'Load Doppler env vars for a config. Prints a file path to stdout — auto-sourced by the env-load shell alias, or run source <path> manually',
|
|
188
|
-
)
|
|
198
|
+
.description('Load Doppler env vars for a config. Source the returned file path to apply.')
|
|
189
199
|
.option('-c, --config <config>', 'Environment config name to load (e.g. dev, arthur)')
|
|
190
200
|
.action(async (options) => {
|
|
191
201
|
await envLoad({ config: options.config })
|
|
@@ -193,9 +203,7 @@ program
|
|
|
193
203
|
|
|
194
204
|
program
|
|
195
205
|
.command('env-clear')
|
|
196
|
-
.description(
|
|
197
|
-
'Clear loaded env vars. Prints a file path to stdout — auto-sourced by the env-clear shell alias, or run source <path> manually',
|
|
198
|
-
)
|
|
206
|
+
.description('Clear loaded env vars. Source the returned file path to apply.')
|
|
199
207
|
.action(async () => {
|
|
200
208
|
await envClear()
|
|
201
209
|
})
|