infra-kit 0.1.80 → 0.1.82

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.
@@ -1,300 +1,82 @@
1
1
  import { z } from 'zod'
2
- import { $ } from 'zx'
3
2
 
4
- import { getCurrentWorktrees, getProjectRoot } from 'src/lib/git-utils'
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
- branch: string
10
- path: string
11
- commit: string
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 (features and releases) git worktrees with detailed information
17
+ * List all release git worktrees with version, type, and Jira description
21
18
  */
22
19
  export const worktreesList = async (): Promise<ToolsExecutionResult> => {
23
- try {
24
- const [releaseWorktrees, featureWorktrees] = await Promise.all([
25
- getCurrentWorktrees('release'),
26
- getCurrentWorktrees('feature'),
27
- ])
20
+ const currentWorktrees = await getCurrentWorktrees('release')
28
21
 
29
- const projectRoot = await getProjectRoot()
30
- const worktreesInfo = await processWorktrees(releaseWorktrees, featureWorktrees, projectRoot)
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
- logger.info('\n🌿 Git Worktrees')
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
- // Display features second
215
- if (features.length > 0 && releases.length > 0) {
216
- logger.info('')
217
- }
33
+ const releaseTypes = new Map<string, ReleaseType>(
34
+ releasePRsInfo.map((pr) => {
35
+ return [pr.branch, detectReleaseType(pr.title)]
36
+ }),
37
+ )
218
38
 
219
- displayWorktreeSection('✨ Features', features)
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
- // Summary
222
- const current = worktrees.find((w) => {
223
- return w.isCurrent
44
+ return { version, type, description }
224
45
  })
225
46
 
226
- logger.info(`\n${'═'.repeat(100)}`)
227
- logger.info(
228
- `📊 Summary: ${worktrees.length} total git worktrees (${releases.length} releases, ${features.length} features)`,
47
+ // Log formatted output
48
+ const maxVersionLength = Math.max(
49
+ ...worktrees.map((w) => {
50
+ return w.version.length
51
+ }),
229
52
  )
230
53
 
231
- if (current) {
232
- logger.info(`📍 Currently on: ${current.branch}`)
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
- for (const [index, worktree] of worktrees.entries()) {
248
- displayWorktree(worktree)
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
- // Last commit message
277
- logger.info(` 💬 ${worktree.lastCommitMessage}`)
61
+ return label
62
+ })
278
63
 
279
- // Path (shortened for display)
280
- const shortPath = worktree.path.split('/').slice(-2).join('/')
64
+ logger.info('🌿 Active worktrees:')
65
+ logger.info(`\n${formattedLines.join('\n')}\n`)
281
66
 
282
- logger.info(` 📁 ${shortPath}`)
283
- }
67
+ const structuredContent = {
68
+ worktrees,
69
+ count: worktrees.length,
70
+ }
284
71
 
285
- /**
286
- * Get status indicator based on worktree status
287
- */
288
- const getStatusIndicator = (status: string): string => {
289
- switch (status) {
290
- case 'clean':
291
- return '✅'
292
- case 'modified':
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
- branch: z.string(),
311
- path: z.string(),
312
- commit: z.string(),
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
- totalCount: z.number().describe('Total number of worktrees'),
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
  }
@@ -78,7 +78,12 @@ export const worktreesRemove = async (options: WorktreeManagementArgs): Promise<
78
78
  if (allSelected) {
79
79
  commandEcho.addOption('--all', true)
80
80
  } else {
81
- commandEcho.addOption('--versions', selectedReleaseBranches)
81
+ commandEcho.addOption(
82
+ '--versions',
83
+ selectedReleaseBranches.map((branch) => {
84
+ return branch.replace('release/v', '')
85
+ }),
86
+ )
82
87
  }
83
88
 
84
89
  // Ask for confirmation
@@ -98,7 +103,7 @@ export const worktreesRemove = async (options: WorktreeManagementArgs): Promise<
98
103
  }
99
104
 
100
105
  // Track --yes flag if confirmation was interactive (user confirmed)
101
- if (allSelected) {
106
+ if (!confirmedCommand) {
102
107
  commandEcho.addOption('--yes', true)
103
108
  }
104
109
 
package/src/entry/cli.ts CHANGED
@@ -1,8 +1,9 @@
1
+ import select, { Separator } from '@inquirer/select'
1
2
  import { Command, Option } from 'commander'
3
+ import process from 'node:process'
2
4
 
3
5
  import { doctor } from 'src/commands/doctor'
4
6
  import { envClear } from 'src/commands/env-clear'
5
- import { envInit } from 'src/commands/env-init'
6
7
  import { envList } from 'src/commands/env-list'
7
8
  import { envLoad } from 'src/commands/env-load'
8
9
  import { envStatus } from 'src/commands/env-status'
@@ -12,6 +13,7 @@ import { ghReleaseDeployAll } from 'src/commands/gh-release-deploy-all'
12
13
  import { ghReleaseDeploySelected } from 'src/commands/gh-release-deploy-selected'
13
14
  import { ghReleaseDeployService } from 'src/commands/gh-release-deploy-service'
14
15
  import { ghReleaseList } from 'src/commands/gh-release-list'
16
+ import { init } from 'src/commands/init'
15
17
  import { releaseCreate } from 'src/commands/release-create'
16
18
  import { releaseCreateBatch } from 'src/commands/release-create-batch'
17
19
  import { worktreesAdd } from 'src/commands/worktrees-add'
@@ -187,10 +189,10 @@ program
187
189
  })
188
190
 
189
191
  program
190
- .command('env-init')
191
- .description('Set up shell functions for env-load/env-clear in .zshrc')
192
+ .command('init')
193
+ .description('Inject shell integration into your profile .zshrc')
192
194
  .action(async () => {
193
- await envInit()
195
+ await init()
194
196
  })
195
197
 
196
198
  program
@@ -208,4 +210,65 @@ program
208
210
  await envClear()
209
211
  })
210
212
 
211
- program.parse()
213
+ if (process.argv.length <= 2) {
214
+ const releaseCommands = [
215
+ 'merge-dev',
216
+ 'release-list',
217
+ 'release-create',
218
+ 'release-create-batch',
219
+ 'release-deploy-all',
220
+ 'release-deploy-service',
221
+ 'release-deploy-selected',
222
+ 'release-deliver',
223
+ ]
224
+ const worktreeCommands = ['worktrees-add', 'worktrees-list', 'worktrees-remove', 'worktrees-sync']
225
+ const envCommands = ['doctor', 'init', 'env-status', 'env-list', 'env-load', 'env-clear']
226
+
227
+ const commandMap = new Map(
228
+ program.commands.map((cmd) => {
229
+ return [cmd.name(), cmd]
230
+ }),
231
+ )
232
+
233
+ const allNames = [...releaseCommands, ...worktreeCommands, ...envCommands]
234
+ const maxLen = Math.max(
235
+ ...allNames.map((n) => {
236
+ return n.length
237
+ }),
238
+ )
239
+
240
+ const toChoices = (names: string[]) => {
241
+ return names
242
+ .filter((n) => {
243
+ return commandMap.has(n)
244
+ })
245
+ .map((n) => {
246
+ return {
247
+ name: `${n.padEnd(maxLen)} ${commandMap.get(n)!.description()}`,
248
+ value: n,
249
+ }
250
+ })
251
+ }
252
+
253
+ const selected = await select(
254
+ {
255
+ message: 'Select a command to run',
256
+ choices: [
257
+ new Separator(' '),
258
+ new Separator('— Release Management —'),
259
+ ...toChoices(releaseCommands),
260
+ new Separator(' '),
261
+ new Separator('— Worktrees —'),
262
+ ...toChoices(worktreeCommands),
263
+ new Separator(' '),
264
+ new Separator('— Environment —'),
265
+ ...toChoices(envCommands),
266
+ ],
267
+ },
268
+ { output: process.stderr },
269
+ )
270
+
271
+ program.parse(['node', 'infra-kit', selected])
272
+ } else {
273
+ program.parse()
274
+ }
@@ -60,8 +60,7 @@ const createCommandEcho = () => {
60
60
  .filter(Boolean)
61
61
  .join(' ')
62
62
 
63
- logger.info(`📟 Equivalent command: \npnpm exec infra-kit ${commandName} ${formattedOptions}`)
64
- logger.info('')
63
+ logger.info(`📟 Equivalent command: \npnpm exec infra-kit ${commandName} ${formattedOptions}\n`)
65
64
  },
66
65
 
67
66
  /**
@@ -40,7 +40,7 @@ export const getSessionCacheDir = (): string => {
40
40
  const session = process.env[INFRA_KIT_SESSION_VAR]
41
41
 
42
42
  if (!session) {
43
- throw new Error('INFRA_KIT_SESSION is not set. Run `source ~/.zshrc` or `infra-kit env-init` first.')
43
+ throw new Error('INFRA_KIT_SESSION is not set. Run `source ~/.zshrc` or `infra-kit init` first.')
44
44
  }
45
45
 
46
46
  return path.join(ENV_CACHE_DIR, session)
@@ -134,6 +134,7 @@ interface FormatBranchChoicesArgs {
134
134
  */
135
135
  export const formatBranchChoices = (args: FormatBranchChoicesArgs): { name: string; value: string }[] => {
136
136
  const { branches, descriptions, types } = args
137
+
137
138
  const versionNames = branches.map((b) => {
138
139
  return b.replace('release/v', '')
139
140
  })
@@ -1 +0,0 @@
1
- export { envInit } from './env-init'