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.
@@ -1,4 +1,5 @@
1
1
  import confirm from '@inquirer/confirm'
2
+ import select from '@inquirer/select'
2
3
  import process from 'node:process'
3
4
  import { z } from 'zod'
4
5
  import { question } from 'zx'
@@ -7,47 +8,57 @@ import { loadJiraConfig } from 'src/integrations/jira'
7
8
  import { commandEcho } from 'src/lib/command-echo'
8
9
  import { logger } from 'src/lib/logger'
9
10
  import { createSingleRelease, prepareGitForRelease } from 'src/lib/release-utils'
10
- import type { ReleaseCreationResult } from 'src/lib/release-utils'
11
+ import type { ReleaseCreationResult, ReleaseType } from 'src/lib/release-utils'
11
12
  import type { RequiredConfirmedOptionArg, ToolsExecutionResult } from 'src/types'
12
13
 
13
14
  interface ReleaseCreateBatchArgs extends RequiredConfirmedOptionArg {
14
15
  versions: string
15
- description?: string
16
+ type?: ReleaseType
16
17
  }
17
18
 
18
19
  /**
19
- * Create multiple release branches for the specified versions
20
- * Includes Jira version creation and GitHub release branch creation for each version
20
+ * Gather and validate batch release inputs interactively if needed
21
21
  */
22
- // eslint-disable-next-line sonarjs/cognitive-complexity
23
- export const releaseCreateBatch = async (args: ReleaseCreateBatchArgs): Promise<ToolsExecutionResult> => {
24
- const { versions: inputVersions, description, confirmedCommand } = args
25
-
26
- commandEcho.start('release-create-batch')
22
+ const resolveInputs = async (args: ReleaseCreateBatchArgs): Promise<{ versionsList: string[]; type: ReleaseType }> => {
23
+ const { versions: inputVersions, type: inputType, confirmedCommand } = args
27
24
 
28
25
  let versionInput = inputVersions
29
-
30
- // Load Jira config - it is now mandatory
31
- const jiraConfig = await loadJiraConfig()
26
+ let type: ReleaseType = inputType || 'regular'
32
27
 
33
28
  if (!versionInput) {
34
29
  commandEcho.setInteractive()
35
30
  versionInput = await question('Enter versions by comma (e.g. 1.2.5, 1.2.6): ')
36
31
  }
37
32
 
38
- const versionsList = versionInput.split(',').map((version) => {
39
- return version.trim()
40
- })
33
+ const versionsList = versionInput
34
+ .split(',')
35
+ .map((version) => {
36
+ return version.trim()
37
+ })
38
+ .filter(Boolean)
41
39
 
42
40
  commandEcho.addOption('--versions', versionsList.join(', '))
43
41
 
44
- // Validate input
45
42
  if (versionsList.length === 0) {
46
43
  logger.error('No versions provided. Exiting...')
47
44
  process.exit(1)
48
45
  }
49
46
 
50
- // Inform user if they only provided one version
47
+ if (!inputType) {
48
+ commandEcho.setInteractive()
49
+
50
+ type = await select<ReleaseType>({
51
+ message: 'Select release type:',
52
+ choices: [
53
+ { name: 'regular', value: 'regular' },
54
+ { name: 'hotfix', value: 'hotfix' },
55
+ ],
56
+ default: 'regular',
57
+ })
58
+ }
59
+
60
+ commandEcho.addOption('--type', type)
61
+
51
62
  if (versionsList.length === 1) {
52
63
  logger.warn('💡 You are creating only one release. Consider using "create-release" command for single releases.')
53
64
  }
@@ -67,14 +78,23 @@ export const releaseCreateBatch = async (args: ReleaseCreateBatchArgs): Promise<
67
78
  process.exit(0)
68
79
  }
69
80
 
70
- // Track --yes flag if confirmation was interactive (user confirmed)
71
81
  commandEcho.addOption('--yes', true)
72
82
 
73
- if (description) {
74
- commandEcho.addOption('--description', description)
75
- }
83
+ return { versionsList, type }
84
+ }
85
+
86
+ /**
87
+ * Create multiple release branches for the specified versions
88
+ * Includes Jira version creation and GitHub release branch creation for each version
89
+ */
90
+ export const releaseCreateBatch = async (args: ReleaseCreateBatchArgs): Promise<ToolsExecutionResult> => {
91
+ commandEcho.start('release-create-batch')
92
+
93
+ const jiraConfig = await loadJiraConfig()
94
+
95
+ const { versionsList, type } = await resolveInputs(args)
76
96
 
77
- await prepareGitForRelease()
97
+ await prepareGitForRelease(type)
78
98
 
79
99
  const releases: ReleaseCreationResult[] = []
80
100
  const failedReleases: Array<{ version: string; error: string }> = []
@@ -82,7 +102,7 @@ export const releaseCreateBatch = async (args: ReleaseCreateBatchArgs): Promise<
82
102
  for (const version of versionsList) {
83
103
  try {
84
104
  // Create each release
85
- const release = await createSingleRelease(version, jiraConfig, description)
105
+ const release = await createSingleRelease({ version, jiraConfig, type })
86
106
 
87
107
  releases.push(release)
88
108
 
@@ -141,7 +161,11 @@ export const releaseCreateBatchMcpTool = {
141
161
  description: 'Create multiple release branches for specified versions with Jira version creation (batch operation)',
142
162
  inputSchema: {
143
163
  versions: z.string().describe('Comma-separated list of versions to create (e.g., "1.2.5, 1.2.6")'),
144
- description: z.string().optional().describe('Optional description for the Jira versions'),
164
+ type: z
165
+ .enum(['regular', 'hotfix'])
166
+ .optional()
167
+ .default('regular')
168
+ .describe('Release type: "regular" or "hotfix" (default: "regular")'),
145
169
  },
146
170
  outputSchema: {
147
171
  createdBranches: z.array(z.string()).describe('List of created release branches'),
@@ -151,6 +175,7 @@ export const releaseCreateBatchMcpTool = {
151
175
  .array(
152
176
  z.object({
153
177
  version: z.string().describe('Version number'),
178
+ type: z.enum(['regular', 'hotfix']).describe('Release type'),
154
179
  branchName: z.string().describe('Release branch name'),
155
180
  prUrl: z.string().describe('GitHub PR URL'),
156
181
  jiraVersionUrl: z.string().describe('Jira version URL'),
@@ -1,16 +1,17 @@
1
1
  /* eslint-disable sonarjs/cognitive-complexity */
2
2
  import checkbox from '@inquirer/checkbox'
3
3
  import confirm from '@inquirer/confirm'
4
- import { copyFileSync, existsSync } from 'node:fs'
5
4
  import process from 'node:process'
6
5
  import { z } from 'zod'
7
6
  import { $ } from 'zx'
8
7
 
9
- import { getReleasePRs } from 'src/integrations/gh'
8
+ import { getReleasePRsWithInfo } from 'src/integrations/gh'
10
9
  import { commandEcho } from 'src/lib/command-echo'
11
10
  import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
12
11
  import { getCurrentWorktrees, getProjectRoot } from 'src/lib/git-utils'
13
12
  import { logger } from 'src/lib/logger'
13
+ import { detectReleaseType, formatBranchChoices, getJiraDescriptions } from 'src/lib/release-utils'
14
+ import type { ReleaseType } from 'src/lib/release-utils'
14
15
  import type { RequiredConfirmedOptionArg, ToolsExecutionResult } from 'src/types'
15
16
 
16
17
  // Constants
@@ -20,7 +21,9 @@ const RELEASE_BRANCH_PREFIX = 'release/v'
20
21
 
21
22
  interface WorktreeManagementArgs extends RequiredConfirmedOptionArg {
22
23
  all: boolean
24
+ versions?: string
23
25
  cursor?: boolean
26
+ githubDesktop?: boolean
24
27
  }
25
28
 
26
29
  /**
@@ -28,7 +31,7 @@ interface WorktreeManagementArgs extends RequiredConfirmedOptionArg {
28
31
  * Creates worktrees for active release branches and removes unused ones
29
32
  */
30
33
  export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<ToolsExecutionResult> => {
31
- const { confirmedCommand, all, cursor } = options
34
+ const { confirmedCommand, all, versions, cursor, githubDesktop } = options
32
35
 
33
36
  commandEcho.start('worktrees-add')
34
37
 
@@ -41,45 +44,61 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
41
44
  await ensureWorktreeDirectory(`${worktreeDir}/${RELEASE_DIR}`)
42
45
  await ensureWorktreeDirectory(`${worktreeDir}/${FEATURE_DIR}`)
43
46
 
44
- const releasePRsList = await getReleasePRs()
47
+ let selectedReleaseBranches: string[] = []
48
+
49
+ if (versions) {
50
+ selectedReleaseBranches = versions.split(',').map((v) => {
51
+ return `release/v${v.trim()}`
52
+ })
53
+ } else {
54
+ const releasePRsInfo = await getReleasePRsWithInfo()
55
+
56
+ const releasePRsList = releasePRsInfo.map((pr) => {
57
+ return pr.branch
58
+ })
45
59
 
46
- if (releasePRsList.length === 0) {
47
- logger.info('â„šī¸ No open release branches found')
60
+ if (releasePRsList.length === 0) {
61
+ logger.info('â„šī¸ No open release branches found')
48
62
 
49
- commandEcho.print()
63
+ commandEcho.print()
50
64
 
51
- return {
52
- content: [{ type: 'text', text: JSON.stringify({ createdWorktrees: [], count: 0 }, null, 2) }],
53
- structuredContent: { createdWorktrees: [], count: 0 },
65
+ return {
66
+ content: [{ type: 'text', text: JSON.stringify({ createdWorktrees: [], count: 0 }, null, 2) }],
67
+ structuredContent: { createdWorktrees: [], count: 0 },
68
+ }
54
69
  }
55
- }
56
70
 
57
- let selectedReleaseBranches: string[] = []
71
+ if (all) {
72
+ selectedReleaseBranches = releasePRsList
73
+ } else {
74
+ commandEcho.setInteractive()
58
75
 
59
- if (all) {
60
- selectedReleaseBranches = releasePRsList
61
- } else {
62
- commandEcho.setInteractive()
76
+ const releaseTypes = new Map<string, ReleaseType>(
77
+ releasePRsInfo.map((pr) => {
78
+ return [pr.branch, detectReleaseType(pr.title)]
79
+ }),
80
+ )
63
81
 
64
- selectedReleaseBranches = await checkbox({
65
- required: true,
66
- message: 'đŸŒŋ Select release branches',
67
- choices: releasePRsList.map((pr) => {
68
- return {
69
- name: pr.replace('release/v', ''),
70
- value: pr,
71
- }
72
- }),
73
- })
82
+ const descriptions = await getJiraDescriptions()
83
+
84
+ selectedReleaseBranches = await checkbox({
85
+ required: true,
86
+ message: 'đŸŒŋ Select release branches',
87
+ choices: formatBranchChoices({ branches: releasePRsList, descriptions, types: releaseTypes }),
88
+ })
89
+ }
74
90
  }
75
91
 
76
92
  // Track --all flag if all branches were selected (either via flag or interactively)
77
- const allSelected = selectedReleaseBranches.length === releasePRsList.length
78
-
79
- if (allSelected) {
93
+ if (all) {
80
94
  commandEcho.addOption('--all', true)
81
95
  } else {
82
- commandEcho.addOption('--branches', selectedReleaseBranches)
96
+ commandEcho.addOption(
97
+ '--versions',
98
+ selectedReleaseBranches.map((branch) => {
99
+ return branch.replace('release/v', '')
100
+ }),
101
+ )
83
102
  }
84
103
 
85
104
  // Ask for confirmation
@@ -99,18 +118,33 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
99
118
  }
100
119
 
101
120
  // Track --yes flag if confirmation was interactive (user confirmed)
102
- if (allSelected) {
121
+ if (!confirmedCommand) {
103
122
  commandEcho.addOption('--yes', true)
104
123
  }
105
124
 
106
- const openInCursor = cursor ? true : await confirm({ message: 'Open created worktrees in Cursor?' })
125
+ const openInCursor = cursor ?? (await confirm({ message: 'Open created worktrees in Cursor?' }))
107
126
 
108
- if (!openInCursor) {
127
+ if (typeof cursor === 'undefined') {
109
128
  commandEcho.setInteractive()
110
129
  }
111
130
 
112
131
  if (openInCursor) {
113
132
  commandEcho.addOption('--cursor', true)
133
+ } else {
134
+ commandEcho.addOption('--no-cursor', true)
135
+ }
136
+
137
+ const openInGithubDesktop =
138
+ githubDesktop ?? (await confirm({ message: 'Open created worktrees in GitHub Desktop?' }))
139
+
140
+ if (typeof githubDesktop === 'undefined') {
141
+ commandEcho.setInteractive()
142
+ }
143
+
144
+ if (openInGithubDesktop) {
145
+ commandEcho.addOption('--github-desktop', true)
146
+ } else {
147
+ commandEcho.addOption('--no-github-desktop', true)
114
148
  }
115
149
 
116
150
  const { branchesToCreate } = categorizeWorktrees({
@@ -118,7 +152,7 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
118
152
  currentWorktrees,
119
153
  })
120
154
 
121
- const createdWorktrees = await createWorktrees(branchesToCreate, worktreeDir, projectRoot)
155
+ const createdWorktrees = await createWorktrees(branchesToCreate, worktreeDir)
122
156
 
123
157
  logResults(createdWorktrees)
124
158
 
@@ -128,6 +162,13 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
128
162
  }
129
163
  }
130
164
 
165
+ if (openInGithubDesktop) {
166
+ for (const branch of createdWorktrees) {
167
+ await $`github ${worktreeDir}/${branch}`
168
+ await $`sleep 5`
169
+ }
170
+ }
171
+
131
172
  commandEcho.print()
132
173
 
133
174
  const structuredContent = {
@@ -182,26 +223,27 @@ const categorizeWorktrees = (args: CategorizeWorktreesArgs): { branchesToCreate:
182
223
  /**
183
224
  * Create worktrees for the specified branches
184
225
  */
185
- const createWorktrees = async (branches: string[], worktreeDir: string, projectRoot: string): Promise<string[]> => {
186
- const created: string[] = []
187
-
188
- for (const branch of branches) {
189
- try {
226
+ const createWorktrees = async (branches: string[], worktreeDir: string): Promise<string[]> => {
227
+ const results = await Promise.allSettled(
228
+ branches.map(async (branch) => {
190
229
  const worktreePath = `${worktreeDir}/${branch}`
191
230
 
192
231
  await $`git worktree add ${worktreePath} ${branch}`
232
+ await $({ cwd: worktreePath })`pnpm install`
193
233
 
194
- const rootEnvPath = `${projectRoot}/.env`
234
+ return branch
235
+ }),
236
+ )
195
237
 
196
- if (existsSync(rootEnvPath)) {
197
- copyFileSync(rootEnvPath, `${worktreePath}/.env`)
238
+ const created: string[] = []
198
239
 
199
- logger.info('📋 Copied .env to worktree')
200
- }
240
+ for (const [index, result] of results.entries()) {
241
+ if (result.status === 'fulfilled') {
242
+ created.push(result.value)
243
+ } else {
244
+ const branch = branches[index]
201
245
 
202
- created.push(branch)
203
- } catch (error) {
204
- logger.error({ error, branch }, `❌ Failed to create worktree for ${branch}`)
246
+ logger.error({ error: result.reason }, `❌ Failed to create worktree for ${branch}`)
205
247
  }
206
248
  }
207
249
 
@@ -213,26 +255,29 @@ const createWorktrees = async (branches: string[], worktreeDir: string, projectR
213
255
  */
214
256
  const logResults = (created: string[]): void => {
215
257
  if (created.length > 0) {
216
- logger.info('\n')
217
- logger.info('✅ Created worktrees:')
218
- logger.info(created.join('\n'))
258
+ logger.info('✅ Created git worktrees:')
259
+ for (const branch of created) {
260
+ logger.info(branch)
261
+ }
219
262
  logger.info('')
220
263
  } else {
221
- logger.info('â„šī¸ No new worktrees to create')
264
+ logger.info('â„šī¸ No new git worktrees to create')
222
265
  }
223
266
  }
224
267
 
225
268
  // MCP Tool Registration
226
269
  export const worktreesAddMcpTool = {
227
270
  name: 'worktrees-add',
228
- description: 'Create worktrees for selected release branches',
271
+ description: 'Create git worktrees for selected release branches',
229
272
  inputSchema: {
230
- all: z.boolean().describe('Add worktrees for all release branches without prompting'),
231
- cursor: z.boolean().optional().describe('Open created worktrees in Cursor'),
273
+ all: z.boolean().describe('Add git worktrees for all release branches without prompting'),
274
+ versions: z.string().optional().describe('Specify versions by comma, e.g. 1.2.5, 1.2.6'),
275
+ cursor: z.boolean().optional().describe('Open created git worktrees in Cursor'),
276
+ githubDesktop: z.boolean().optional().describe('Open created git worktrees in GitHub Desktop'),
232
277
  },
233
278
  outputSchema: {
234
- createdWorktrees: z.array(z.string()).describe('List of created worktree branches'),
235
- count: z.number().describe('Number of worktrees created'),
279
+ createdWorktrees: z.array(z.string()).describe('List of created git worktree branches'),
280
+ count: z.number().describe('Number of git worktrees created'),
236
281
  },
237
282
  handler: worktreesAdd,
238
283
  }