infra-kit 0.1.97 → 0.1.99

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.
@@ -2,92 +2,145 @@ import confirm from '@inquirer/confirm'
2
2
  import select from '@inquirer/select'
3
3
  import process from 'node:process'
4
4
  import { z } from 'zod/v4'
5
- import { $, question } from 'zx'
5
+ import { question } from 'zx'
6
6
 
7
7
  import { loadJiraConfig } from 'src/integrations/jira'
8
8
  import { commandEcho } from 'src/lib/command-echo'
9
9
  import { logger } from 'src/lib/logger'
10
10
  import { createSingleRelease, prepareGitForRelease } from 'src/lib/release-utils'
11
- import type { ReleaseType } from 'src/lib/release-utils'
11
+ import type { ReleaseCreationResult, ReleaseType } from 'src/lib/release-utils'
12
+ import {
13
+ NoPriorVersionsError,
14
+ computeNextVersion,
15
+ hasNextToken,
16
+ loadExistingVersions,
17
+ parseVersion,
18
+ resolveReleaseEntries,
19
+ } from 'src/lib/version-utils'
20
+ import type { ReleaseEntry, SemVer } from 'src/lib/version-utils'
12
21
  import type { RequiredConfirmedOptionArg, ToolsExecutionResult } from 'src/types'
13
22
 
14
23
  interface ReleaseCreateArgs extends RequiredConfirmedOptionArg {
15
- version?: string
16
- description?: string
17
- type?: ReleaseType
18
- checkout?: boolean
24
+ releases?: ReleaseEntry[]
19
25
  }
20
26
 
21
- /**
22
- * Create a single release branch for the specified version
23
- * Includes Jira version creation and GitHub release branch creation
24
- */
25
- export const releaseCreate = async (args: ReleaseCreateArgs): Promise<ToolsExecutionResult> => {
26
- const {
27
- version: inputVersion,
28
- description: inputDescription,
29
- type: inputType,
30
- confirmedCommand,
31
- checkout: inputCheckout,
32
- } = args
33
-
34
- commandEcho.start('release-create')
27
+ const VERSION_PROMPT_HINT = '"1.2.5" or "next"'
35
28
 
36
- let version = inputVersion
37
- let description = inputDescription
38
- let type: ReleaseType = inputType || 'regular'
39
- let checkout = inputCheckout
29
+ const trySuggestNext = (known: SemVer[], type: ReleaseType): string | null => {
30
+ try {
31
+ return computeNextVersion(known, type)
32
+ } catch (err) {
33
+ if (err instanceof NoPriorVersionsError) return null
40
34
 
41
- // Load Jira config - it is now mandatory
42
- const jiraConfig = await loadJiraConfig()
43
-
44
- if (!version) {
45
- commandEcho.setInteractive()
46
- version = await question('Enter version (e.g. 1.2.5): ')
35
+ throw err
47
36
  }
37
+ }
48
38
 
49
- // Validate input (validate the version is a valid semver)
50
- if (!version || version.trim() === '') {
51
- logger.error('No version provided. Exiting...')
52
- process.exit(1)
53
- }
39
+ const resolveOrExit = (entries: ReleaseEntry[], known: SemVer[]): ReleaseEntry[] => {
40
+ try {
41
+ return resolveReleaseEntries(entries, known)
42
+ } catch (err) {
43
+ if (err instanceof NoPriorVersionsError) {
44
+ logger.error(err.message)
45
+ process.exit(1)
46
+ }
54
47
 
55
- const trimmedVersion = version.trim()
48
+ throw err
49
+ }
50
+ }
56
51
 
57
- commandEcho.addOption('--version', trimmedVersion)
52
+ const promptForReleasesInteractive = async (ensureKnown: () => Promise<SemVer[]>): Promise<ReleaseEntry[]> => {
53
+ commandEcho.setInteractive()
58
54
 
59
- if (!inputType) {
60
- commandEcho.setInteractive()
55
+ const baseKnown = await ensureKnown()
56
+ const running: SemVer[] = [...baseKnown]
57
+ const entries: ReleaseEntry[] = []
58
+ let addAnother = true
61
59
 
62
- type = await select<ReleaseType>({
63
- message: 'Select release type:',
60
+ while (addAnother) {
61
+ const ordinal = entries.length + 1
62
+ const type = await select<ReleaseType>({
63
+ message: `Release #${ordinal} — select type:`,
64
64
  choices: [
65
65
  { name: 'regular', value: 'regular' },
66
66
  { name: 'hotfix', value: 'hotfix' },
67
67
  ],
68
68
  default: 'regular',
69
69
  })
70
+
71
+ const suggestion = trySuggestNext(running, type)
72
+ const defaultHint = suggestion ? ` [${suggestion}]` : ''
73
+ const versionAnswer = (await question(` Version (e.g. ${VERSION_PROMPT_HINT})${defaultHint}: `)).trim()
74
+ const versionInput = versionAnswer === '' ? (suggestion ?? '') : versionAnswer
75
+
76
+ if (versionInput === '') {
77
+ logger.error('No version provided. Exiting...')
78
+ process.exit(1)
79
+ }
80
+
81
+ const resolved = resolveOrExit([{ version: versionInput, type }], running)[0] as ReleaseEntry
82
+
83
+ running.push(parseVersion(`v${resolved.version}`))
84
+
85
+ const description = (await question(' Description (optional, press Enter to skip): ')).trim()
86
+
87
+ entries.push({ ...resolved, ...(description !== '' ? { description } : {}) })
88
+
89
+ addAnother = await confirm({ message: 'Add another release?', default: false })
70
90
  }
71
91
 
72
- commandEcho.addOption('--type', type)
92
+ return entries
93
+ }
73
94
 
74
- if (description === undefined) {
75
- commandEcho.setInteractive()
76
- description = await question('Enter description (optional, press Enter to skip): ')
95
+ const formatReleaseSummary = (entry: ReleaseEntry): string => {
96
+ const parts = [`v${entry.version}`, entry.type]
77
97
 
78
- if (description.trim() === '') {
79
- description = ''
80
- }
98
+ if (entry.description) parts.push(entry.description)
99
+
100
+ return parts.join(' · ')
101
+ }
102
+
103
+ const echoReleases = (entries: ReleaseEntry[]): void => {
104
+ for (const entry of entries) {
105
+ const spec = entry.description
106
+ ? `${entry.version}:${entry.type}:${entry.description}`
107
+ : `${entry.version}:${entry.type}`
108
+
109
+ commandEcho.addOption('--release', spec)
81
110
  }
111
+ }
112
+
113
+ interface FailedRelease {
114
+ version: string
115
+ error: string
116
+ }
117
+
118
+ const collectEntries = async (
119
+ inputReleases: ReleaseEntry[] | undefined,
120
+ ensureKnown: () => Promise<SemVer[]>,
121
+ ): Promise<ReleaseEntry[]> => {
122
+ if (inputReleases && inputReleases.length > 0) {
123
+ const known = hasNextToken(inputReleases) ? await ensureKnown() : []
124
+ const resolved = resolveOrExit(inputReleases, known)
82
125
 
83
- if (description) {
84
- commandEcho.addOption('--description', description)
126
+ echoReleases(resolved)
127
+
128
+ return resolved
85
129
  }
86
130
 
131
+ const interactive = await promptForReleasesInteractive(ensureKnown)
132
+
133
+ echoReleases(interactive)
134
+
135
+ return interactive
136
+ }
137
+
138
+ const confirmReleases = async (entries: ReleaseEntry[], confirmedCommand: boolean): Promise<void> => {
139
+ const summary = entries.map(formatReleaseSummary).join('\n - ')
87
140
  const answer = confirmedCommand
88
141
  ? true
89
142
  : await confirm({
90
- message: `Are you sure you want to create release branch for version ${trimmedVersion}?`,
143
+ message: `Create the following ${entries.length} release(s)?\n - ${summary}\n`,
91
144
  })
92
145
 
93
146
  if (!confirmedCommand) {
@@ -99,51 +152,105 @@ export const releaseCreate = async (args: ReleaseCreateArgs): Promise<ToolsExecu
99
152
  process.exit(0)
100
153
  }
101
154
 
102
- // Track --yes flag if confirmation was interactive (user confirmed)
103
155
  commandEcho.addOption('--yes', true)
156
+ }
104
157
 
105
- await prepareGitForRelease(type)
106
-
107
- // Create the release
108
- const release = await createSingleRelease({ version: trimmedVersion, jiraConfig, description, type })
158
+ interface ExecuteOneArgs {
159
+ entry: ReleaseEntry
160
+ jiraConfig: Awaited<ReturnType<typeof loadJiraConfig>>
161
+ }
109
162
 
110
- logger.info(`✅ Successfully created release: v${trimmedVersion}`)
111
- logger.info(`🔗 GitHub PR: ${release.prUrl}`)
112
- logger.info(`🔗 Jira Version: ${release.jiraVersionUrl}`)
163
+ const executeOne = async (
164
+ args: ExecuteOneArgs,
165
+ ): Promise<{ result?: ReleaseCreationResult; failure?: FailedRelease }> => {
166
+ const { entry, jiraConfig } = args
113
167
 
114
- // Ask about checkout if not specified
115
- if (checkout === undefined) {
116
- commandEcho.setInteractive()
168
+ try {
169
+ await prepareGitForRelease(entry.type)
117
170
 
118
- checkout = await confirm({
119
- message: `Do you want to checkout to the created branch ${release.branchName}?`,
120
- default: true,
171
+ const result = await createSingleRelease({
172
+ version: entry.version,
173
+ jiraConfig,
174
+ description: entry.description,
175
+ type: entry.type,
121
176
  })
177
+
178
+ logger.info(`✅ Successfully created release: v${entry.version} (${entry.type})`)
179
+ logger.info(`🔗 GitHub PR: ${result.prUrl}`)
180
+ logger.info(`🔗 Jira Version: ${result.jiraVersionUrl}\n`)
181
+
182
+ return { result }
183
+ } catch (error) {
184
+ const errorMessage = error instanceof Error ? error.message : String(error)
185
+
186
+ logger.error(`❌ Failed to create release: v${entry.version}`)
187
+ logger.error(` Error: ${errorMessage}\n`)
188
+
189
+ return { failure: { version: entry.version, error: errorMessage } }
122
190
  }
191
+ }
123
192
 
124
- // Track checkout option (--no-checkout if false)
125
- if (!checkout) {
126
- commandEcho.addOption('--no-checkout', true)
193
+ const logFinalSummary = (total: number, successCount: number, failureCount: number): void => {
194
+ if (successCount === total) {
195
+ logger.info(`✅ All ${total} release branch(es) were created successfully.`)
196
+ } else if (successCount > 0) {
197
+ logger.warn(`⚠️ ${successCount} of ${total} release branches were created successfully.`)
198
+ logger.warn(`❌ ${failureCount} release(s) failed.`)
199
+ } else {
200
+ logger.error(`❌ All ${total} release branch(es) failed to create.`)
127
201
  }
202
+ }
128
203
 
129
- // Checkout to the created branch by default
130
- if (checkout) {
131
- $.quiet = true
132
- await $`git switch ${release.branchName}`
133
- $.quiet = false
204
+ /**
205
+ * Create one or more release branches. Each release carries its own type
206
+ * (regular/hotfix) and optional Jira description, so a single invocation
207
+ * may mix regular and hotfix releases off their respective base branches.
208
+ */
209
+ export const releaseCreate = async (args: ReleaseCreateArgs): Promise<ToolsExecutionResult> => {
210
+ const { releases: inputReleases, confirmedCommand } = args
211
+
212
+ commandEcho.start('release-create')
213
+
214
+ const jiraConfig = await loadJiraConfig()
215
+
216
+ let known: SemVer[] | null = null
217
+ const ensureKnown = async (): Promise<SemVer[]> => {
218
+ if (known === null) known = await loadExistingVersions()
219
+
220
+ return known
221
+ }
134
222
 
135
- logger.info(`🔄 Switched to branch ${release.branchName}`)
223
+ const entries = await collectEntries(inputReleases, ensureKnown)
224
+
225
+ if (entries.length === 0) {
226
+ logger.error('No releases provided. Exiting...')
227
+ process.exit(1)
228
+ }
229
+
230
+ await confirmReleases(entries, Boolean(confirmedCommand))
231
+
232
+ const created: ReleaseCreationResult[] = []
233
+ const failed: FailedRelease[] = []
234
+
235
+ for (const entry of entries) {
236
+ const { result, failure } = await executeOne({ entry, jiraConfig })
237
+
238
+ if (result) created.push(result)
239
+ if (failure) failed.push(failure)
136
240
  }
137
241
 
242
+ logFinalSummary(entries.length, created.length, failed.length)
243
+
138
244
  commandEcho.print()
139
245
 
140
246
  const structuredContent = {
141
- version: trimmedVersion,
142
- type,
143
- branchName: release.branchName,
144
- prUrl: release.prUrl,
145
- jiraVersionUrl: release.jiraVersionUrl,
146
- isCheckedOut: checkout,
247
+ createdBranches: created.map((r) => {
248
+ return r.branchName
249
+ }),
250
+ successCount: created.length,
251
+ failureCount: failed.length,
252
+ releases: created,
253
+ failedReleases: failed,
147
254
  }
148
255
 
149
256
  return {
@@ -161,24 +268,48 @@ export const releaseCreate = async (args: ReleaseCreateArgs): Promise<ToolsExecu
161
268
  export const releaseCreateMcpTool = {
162
269
  name: 'release-create',
163
270
  description:
164
- 'Create a new release: cuts the release branch off the appropriate base (dev for regular releases, main for hotfixes), opens a GitHub release PR, creates the matching Jira fix version, and optionally checks out to the new branch. Confirmation is auto-skipped for MCP calls, so the caller is responsible for gating. "version" is required when invoked via MCP (the interactive input prompt is unreachable without a TTY); "type" / "description" / "checkout" default to regular / empty / true when omitted.',
271
+ 'Create one or more releases in a single call. Each entry in "releases" carries its own version, type (regular|hotfix, default regular), and optional description, so regular and hotfix releases can be mixed in the same invocation. For each release this tool switches to the appropriate base branch (dev for regular, main for hotfix), cuts the release branch, opens a GitHub release PR, and creates the matching Jira fix version. The literal token "next" auto-increments from the union of remote release branches and Jira fix versions (regular bumps minor + resets patch; hotfix bumps patch on the highest minor); multiple "next" tokens advance sequentially across mixed types. Confirmation is auto-skipped for MCP calls, so the caller is responsible for gating. Continues on per-release failure and reports successes/failures.',
165
272
  inputSchema: {
166
- version: z.string().describe('Version to create (e.g., "1.2.5"). Required for MCP calls.'),
167
- description: z.string().optional().describe('Optional description for the Jira version'),
168
- type: z
169
- .enum(['regular', 'hotfix'])
170
- .optional()
171
- .default('regular')
172
- .describe('Release type: "regular" or "hotfix" (default: "regular")'),
173
- checkout: z.boolean().optional().default(true).describe('Checkout to the created branch (default: true)'),
273
+ releases: z
274
+ .array(
275
+ z.object({
276
+ version: z
277
+ .string()
278
+ .describe('Version to create (e.g., "1.2.5") or the literal token "next" for auto-increment.'),
279
+ type: z
280
+ .enum(['regular', 'hotfix'])
281
+ .optional()
282
+ .default('regular')
283
+ .describe('Release type: "regular" (branches off dev) or "hotfix" (branches off main).'),
284
+ description: z.string().optional().describe('Optional description for the Jira version.'),
285
+ }),
286
+ )
287
+ .min(1)
288
+ .describe('One or more releases to create. Each entry has its own version, type, and optional description.'),
174
289
  },
175
290
  outputSchema: {
176
- version: z.string().describe('Version number'),
177
- type: z.enum(['regular', 'hotfix']).describe('Release type'),
178
- branchName: z.string().describe('Release branch name'),
179
- prUrl: z.string().describe('GitHub PR URL'),
180
- jiraVersionUrl: z.string().describe('Jira version URL'),
181
- isCheckedOut: z.boolean().describe('Whether the branch was checked out'),
291
+ createdBranches: z.array(z.string()).describe('List of created release branch names'),
292
+ successCount: z.number().describe('Number of releases created successfully'),
293
+ failureCount: z.number().describe('Number of releases that failed'),
294
+ releases: z
295
+ .array(
296
+ z.object({
297
+ version: z.string().describe('Version number'),
298
+ type: z.enum(['regular', 'hotfix']).describe('Release type'),
299
+ branchName: z.string().describe('Release branch name'),
300
+ prUrl: z.string().describe('GitHub PR URL'),
301
+ jiraVersionUrl: z.string().describe('Jira version URL'),
302
+ }),
303
+ )
304
+ .describe('Detailed information for each created release with URLs'),
305
+ failedReleases: z
306
+ .array(
307
+ z.object({
308
+ version: z.string().describe('Version number that failed'),
309
+ error: z.string().describe('Error message'),
310
+ }),
311
+ )
312
+ .describe('List of releases that failed with error messages'),
182
313
  },
183
314
  handler: releaseCreate,
184
315
  }
@@ -130,8 +130,12 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
130
130
  commandEcho.addOption('--yes', true)
131
131
  }
132
132
 
133
+ const config = await getInfraKitConfig()
134
+ const cursorConfig = config.ide?.provider === 'cursor' ? config.ide.config : undefined
135
+
133
136
  const cursorMode: CursorMode =
134
137
  cursor ??
138
+ cursorConfig?.mode ??
135
139
  (await select<CursorMode>({
136
140
  message: 'Cursor mode for created worktrees?',
137
141
  default: 'workspace',
@@ -150,16 +154,18 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
150
154
  ],
151
155
  }))
152
156
 
153
- if (typeof cursor === 'undefined') {
157
+ if (typeof cursor === 'undefined' && !cursorConfig?.mode) {
154
158
  commandEcho.setInteractive()
155
159
  }
156
160
 
157
161
  commandEcho.addOption('--cursor', cursorMode)
158
162
 
159
163
  const openInGithubDesktop =
160
- githubDesktop ?? (await confirm({ message: 'Open created worktrees in GitHub Desktop?' }))
164
+ githubDesktop ??
165
+ config.worktrees?.openInGithubDesktop ??
166
+ (await confirm({ message: 'Open created worktrees in GitHub Desktop?' }))
161
167
 
162
- if (typeof githubDesktop === 'undefined') {
168
+ if (typeof githubDesktop === 'undefined' && config.worktrees?.openInGithubDesktop === undefined) {
163
169
  commandEcho.setInteractive()
164
170
  }
165
171
 
@@ -169,9 +175,10 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
169
175
  commandEcho.addOption('--no-github-desktop', true)
170
176
  }
171
177
 
172
- const openInCmux = cmux ?? (await confirm({ message: 'Open created worktrees in cmux?' }))
178
+ const openInCmux =
179
+ cmux ?? config.worktrees?.openInCmux ?? (await confirm({ message: 'Open created worktrees in cmux?' }))
173
180
 
174
- if (typeof cmux === 'undefined') {
181
+ if (typeof cmux === 'undefined' && config.worktrees?.openInCmux === undefined) {
175
182
  commandEcho.setInteractive()
176
183
  }
177
184
 
@@ -191,11 +198,8 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
191
198
  logResults(createdWorktrees)
192
199
 
193
200
  if (cursorMode === 'workspace') {
194
- const config = await getInfraKitConfig()
195
- const cursorConfig = config.ide?.provider === 'cursor' ? config.ide.config : undefined
196
-
197
201
  if (!cursorConfig?.workspaceConfigPath) {
198
- logger.warn('⚠️ Skipping Cursor: ide.config.workspaceConfigPath is not set in infra-kit.yml')
202
+ logger.warn('⚠️ Skipping Cursor: ide.config.workspaceConfigPath is not set in infra-kit config')
199
203
  } else {
200
204
  const workspacePath = resolveCursorWorkspacePath(cursorConfig.workspaceConfigPath, projectRoot)
201
205
 
@@ -355,19 +359,19 @@ export const worktreesAddMcpTool = {
355
359
  .enum(CURSOR_MODES)
356
360
  .optional()
357
361
  .describe(
358
- 'Cursor open mode for created worktrees. "workspace" (default behavior when set interactively) appends each worktree as a folder to "ide.config.workspaceConfigPath" in infra-kit.yml and opens the workspace. "windows" opens each worktree in its own Cursor window. "none" skips Cursor. Defaults to "none" in MCP mode (the follow-up prompt is not shown).',
362
+ 'Cursor open mode for created worktrees. "workspace" appends each worktree as a folder to "ide.config.workspaceConfigPath" in infra-kit config and opens the workspace. "windows" opens each worktree in its own Cursor window. "none" skips Cursor. Resolution order: this flag "ide.config.mode" from infra-kit config → interactive prompt (CLI) / "none" (MCP, no TTY).',
359
363
  ),
360
364
  githubDesktop: z
361
365
  .boolean()
362
366
  .optional()
363
367
  .describe(
364
- 'Open each created worktree in GitHub Desktop. Defaults to false in MCP mode (the follow-up prompt is not shown).',
368
+ 'Open each created worktree in GitHub Desktop. Resolution order: this flag "worktrees.openInGithubDesktop" from infra-kit config → interactive prompt (CLI) / false (MCP, no TTY).',
365
369
  ),
366
370
  cmux: z
367
371
  .boolean()
368
372
  .optional()
369
373
  .describe(
370
- 'Open each created worktree in a new cmux workspace with a 3-pane layout (left-top, left-bottom, full-height right), all rooted at the worktree directory. Defaults to false in MCP mode (the follow-up prompt is not shown).',
374
+ 'Open each created worktree in a new cmux workspace with a 3-pane layout (left-top, left-bottom, full-height right), all rooted at the worktree directory. Resolution order: this flag "worktrees.openInCmux" from infra-kit config → interactive prompt (CLI) / false (MCP, no TTY).',
371
375
  ),
372
376
  },
373
377
  outputSchema: {
package/src/entry/cli.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import select, { Separator } from '@inquirer/select'
2
- import { Command, Option } from 'commander'
2
+ import { Command } from 'commander'
3
3
  import process from 'node:process'
4
4
 
5
+ import { configEdit, configPath } from 'src/commands/config'
5
6
  import { doctor } from 'src/commands/doctor'
6
7
  import { envClear } from 'src/commands/env-clear'
7
8
  import { envList } from 'src/commands/env-list'
@@ -14,7 +15,6 @@ import { ghReleaseDeploySelected } from 'src/commands/gh-release-deploy-selected
14
15
  import { ghReleaseList } from 'src/commands/gh-release-list'
15
16
  import { init } from 'src/commands/init'
16
17
  import { releaseCreate } from 'src/commands/release-create'
17
- import { releaseCreateBatch } from 'src/commands/release-create-batch'
18
18
  import { version } from 'src/commands/version'
19
19
  import { CURSOR_MODES, worktreesAdd } from 'src/commands/worktrees-add'
20
20
  import type { CursorMode } from 'src/commands/worktrees-add'
@@ -23,9 +23,14 @@ import { worktreesOpen } from 'src/commands/worktrees-open'
23
23
  import { worktreesRemove } from 'src/commands/worktrees-remove'
24
24
  import { worktreesSync } from 'src/commands/worktrees-sync'
25
25
  import { logger } from 'src/lib/logger'
26
+ import { parseReleaseSpec } from 'src/lib/version-utils'
26
27
 
27
28
  const program = new Command()
28
29
 
30
+ const collectReleaseSpec = (value: string, prev: string[]): string[] => {
31
+ return [...prev, value]
32
+ }
33
+
29
34
  const normalizeCursorMode = (value: unknown): CursorMode | undefined => {
30
35
  if (typeof value === 'undefined') {
31
36
  return undefined
@@ -79,32 +84,20 @@ program
79
84
 
80
85
  program
81
86
  .command('release-create')
82
- .description('Create a single release branch')
83
- .option('-v, --version <version>', 'Specify the version to create, e.g. 1.2.5')
84
- .option('-d, --description <description>', 'Optional description for the Jira version')
85
- .addOption(new Option('-t, --type <type>', 'Release type (default: regular)').choices(['regular', 'hotfix']))
87
+ .description('Create one or more release branches (each entry can mix regular/hotfix and its own description)')
88
+ .option(
89
+ '-r, --release <spec>',
90
+ 'Release spec "version[:type[:description]]" (repeatable). Examples: "1.2.5", "1.2.5:hotfix", "next:regular:Holiday backend"',
91
+ collectReleaseSpec,
92
+ [],
93
+ )
86
94
  .option('-y, --yes', 'Skip confirmation prompt')
87
- .option('--no-checkout', 'Do not checkout the created branch after creation (checkout is default)')
88
95
  .action(async (options) => {
89
- await releaseCreate({
90
- version: options.version,
91
- description: options.description,
92
- type: options.type,
93
- confirmedCommand: options.yes,
94
- checkout: options.checkout,
95
- })
96
- })
96
+ const specs = options.release as string[]
97
+ const releases = specs.length > 0 ? specs.map(parseReleaseSpec) : undefined
97
98
 
98
- program
99
- .command('release-create-batch')
100
- .description('Create multiple release branches (batch operation)')
101
- .option('-v, --versions <versions>', 'Specify the versions to create by comma, e.g. 1.2.5, 1.2.6')
102
- .addOption(new Option('-t, --type <type>', 'Release type (default: regular)').choices(['regular', 'hotfix']))
103
- .option('-y, --yes', 'Skip confirmation prompt')
104
- .action(async (options) => {
105
- await releaseCreateBatch({
106
- versions: options.versions,
107
- type: options.type,
99
+ await releaseCreate({
100
+ releases,
108
101
  confirmedCommand: options.yes,
109
102
  })
110
103
  })
@@ -199,6 +192,22 @@ program
199
192
  await worktreesOpen()
200
193
  })
201
194
 
195
+ const configCmd = program.command('config').description('Manage infra-kit configuration files')
196
+
197
+ configCmd
198
+ .command('path')
199
+ .description('Show the resolved config merge chain and file paths')
200
+ .action(async () => {
201
+ await configPath()
202
+ })
203
+
204
+ configCmd
205
+ .command('edit')
206
+ .description('Open the user-scope per-project override file in $EDITOR')
207
+ .action(async () => {
208
+ await configEdit()
209
+ })
210
+
202
211
  program
203
212
  .command('doctor')
204
213
  .description('Check installation and authentication status of gh and doppler CLIs')
@@ -254,13 +263,12 @@ if (process.argv.length <= 2) {
254
263
  'merge-dev',
255
264
  'release-list',
256
265
  'release-create',
257
- 'release-create-batch',
258
266
  'release-deploy-all',
259
267
  'release-deploy-selected',
260
268
  'release-deliver',
261
269
  ]
262
270
  const worktreeCommands = ['worktrees-add', 'worktrees-list', 'worktrees-open', 'worktrees-remove', 'worktrees-sync']
263
- const envCommands = ['doctor', 'init', 'version', 'env-status', 'env-list', 'env-load', 'env-clear']
271
+ const envCommands = ['doctor', 'init', 'version', 'config', 'env-status', 'env-list', 'env-load', 'env-clear']
264
272
 
265
273
  const commandMap = new Map(
266
274
  program.commands.map((cmd) => {