infra-kit 0.1.98 → 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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "infra-kit",
3
3
  "type": "module",
4
- "version": "0.1.98",
4
+ "version": "0.1.99",
5
5
  "description": "infra-kit",
6
6
  "main": "dist/cli.js",
7
7
  "module": "dist/cli.js",
@@ -105,7 +105,7 @@ export const configEdit = async (): Promise<ToolsExecutionResult> => {
105
105
  await fs.mkdir(path.dirname(paths.userProject), { recursive: true })
106
106
 
107
107
  if (!(await fileExists(paths.userProject))) {
108
- const stub = `# infra-kit user override for ${paths.projectName}\n# This file is shallow-merged on top of project infra-kit.yml.\n# Top-level keys (envManagement, ide, taskManager, environments) replace wholesale.\n`
108
+ const stub = `# infra-kit user override for ${paths.projectName} — ~/.infra-kit/projects/${paths.projectName}/infra-kit.yml\n#\n# Layer 3 (highest precedence) of the config merge chain. Shallow-merged on\n# top of <repo>/infra-kit.yml and ~/.infra-kit/config.yml — top-level keys\n# (environments, envManagement, ide, taskManager, worktrees) replace wholesale.\n`
109
109
 
110
110
  await fs.writeFile(paths.userProject, stub, 'utf-8')
111
111
  }
@@ -10,20 +10,31 @@ export const MARKER_END = '# -- infra-kit:end --'
10
10
  const LEGACY_PAIRED: [start: string, end: string][] = [['# region infra-kit', '# endregion infra-kit']]
11
11
  const LEGACY_SINGLE = '# infra-kit shell functions'
12
12
 
13
- const USER_GLOBAL_CONFIG_STUB = `# infra-kit user-global config
13
+ const USER_GLOBAL_CONFIG_STUB = `# infra-kit user-global config — ~/.infra-kit/config.yml
14
14
  #
15
- # Shared across every project on this machine. Loaded after each project's
16
- # infra-kit.yml and before ~/.infra-kit/projects/<repo-name>/infra-kit.yml.
15
+ # Merge chain (later layers override earlier ones at top-level keys):
16
+ # 1. <repo>/infra-kit.yml committed project config (required)
17
+ # 2. ~/.infra-kit/config.yml — this file (user-global)
18
+ # 3. ~/.infra-kit/projects/<repo-name>/infra-kit.yml — user-scope per-project override
17
19
  #
18
- # Top-level keys (envManagement, ide, taskManager, environments) replace
19
- # wholesale when set here. Uncomment values you want to apply globally.
20
+ # Merge is shallow: setting a top-level key here replaces that whole section
21
+ # from layer 1. Arrays do not concatenate. Top-level keys recognized:
22
+ # environments, envManagement, ide, taskManager, worktrees.
23
+ #
24
+ # Uncomment the blocks you want to apply globally across every project on this
25
+ # machine. Per-project tweaks belong in layer 3 — run \`infra-kit config edit\`.
20
26
 
21
27
  # Per-developer IDE config
22
28
  # ide:
23
29
  # provider: cursor
24
30
  # config:
25
31
  # mode: workspace
26
- # workspaceConfigPath: ../Main.code-workspace
32
+ # workspaceConfigPath: /path/to/your.code-workspace
33
+
34
+ # Worktree prompt defaults — silences the follow-up prompts in \`worktrees-add\`
35
+ # worktrees:
36
+ # openInGithubDesktop: false
37
+ # openInCmux: true
27
38
  `
28
39
 
29
40
  /**
@@ -8,40 +8,23 @@ 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
12
  import {
13
- NEXT_TOKEN,
14
13
  NoPriorVersionsError,
15
14
  computeNextVersion,
15
+ hasNextToken,
16
16
  loadExistingVersions,
17
- resolveVersionTokens,
18
- splitVersionInput,
17
+ parseVersion,
18
+ resolveReleaseEntries,
19
19
  } from 'src/lib/version-utils'
20
- import type { SemVer } from 'src/lib/version-utils'
20
+ import type { ReleaseEntry, SemVer } from 'src/lib/version-utils'
21
21
  import type { RequiredConfirmedOptionArg, ToolsExecutionResult } from 'src/types'
22
22
 
23
- import { releaseCreateBatch } from '../release-create-batch/release-create-batch'
24
-
25
23
  interface ReleaseCreateArgs extends RequiredConfirmedOptionArg {
26
- version?: string
27
- description?: string
28
- type?: ReleaseType
24
+ releases?: ReleaseEntry[]
29
25
  }
30
26
 
31
- const VERSION_PROMPT_HINT = '"1.2.5", "next", or "next,next,1.2.7"'
32
-
33
- const promptForType = async (): Promise<ReleaseType> => {
34
- commandEcho.setInteractive()
35
-
36
- return select<ReleaseType>({
37
- message: 'Select release type:',
38
- choices: [
39
- { name: 'regular', value: 'regular' },
40
- { name: 'hotfix', value: 'hotfix' },
41
- ],
42
- default: 'regular',
43
- })
44
- }
27
+ const VERSION_PROMPT_HINT = '"1.2.5" or "next"'
45
28
 
46
29
  const trySuggestNext = (known: SemVer[], type: ReleaseType): string | null => {
47
30
  try {
@@ -53,116 +36,111 @@ const trySuggestNext = (known: SemVer[], type: ReleaseType): string | null => {
53
36
  }
54
37
  }
55
38
 
56
- const exitOnNoPrior = (err: unknown): never => {
57
- if (err instanceof NoPriorVersionsError) {
58
- logger.error(err.message)
59
- process.exit(1)
60
- }
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
+ }
61
47
 
62
- throw err
48
+ throw err
49
+ }
63
50
  }
64
51
 
65
- interface ResolveTokensArgs {
66
- inputVersion: string | undefined
67
- type: ReleaseType
68
- ensureKnown: () => Promise<SemVer[]>
69
- }
52
+ const promptForReleasesInteractive = async (ensureKnown: () => Promise<SemVer[]>): Promise<ReleaseEntry[]> => {
53
+ commandEcho.setInteractive()
70
54
 
71
- const resolveRawTokens = async (args: ResolveTokensArgs): Promise<string[]> => {
72
- const { inputVersion, type, ensureKnown } = args
55
+ const baseKnown = await ensureKnown()
56
+ const running: SemVer[] = [...baseKnown]
57
+ const entries: ReleaseEntry[] = []
58
+ let addAnother = true
59
+
60
+ while (addAnother) {
61
+ const ordinal = entries.length + 1
62
+ const type = await select<ReleaseType>({
63
+ message: `Release #${ordinal} — select type:`,
64
+ choices: [
65
+ { name: 'regular', value: 'regular' },
66
+ { name: 'hotfix', value: 'hotfix' },
67
+ ],
68
+ default: 'regular',
69
+ })
73
70
 
74
- if (inputVersion && inputVersion.trim() !== '') {
75
- return splitVersionInput(inputVersion)
76
- }
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
77
75
 
78
- commandEcho.setInteractive()
76
+ if (versionInput === '') {
77
+ logger.error('No version provided. Exiting...')
78
+ process.exit(1)
79
+ }
79
80
 
80
- const suggestion = trySuggestNext(await ensureKnown(), type)
81
- const defaultHint = suggestion ? ` [${suggestion}]` : ''
82
- const answer = (await question(`Enter version(s) (e.g. ${VERSION_PROMPT_HINT})${defaultHint}: `)).trim()
81
+ const resolved = resolveOrExit([{ version: versionInput, type }], running)[0] as ReleaseEntry
83
82
 
84
- if (answer === '') return suggestion ? [suggestion] : []
83
+ running.push(parseVersion(`v${resolved.version}`))
85
84
 
86
- return splitVersionInput(answer)
87
- }
85
+ const description = (await question(' Description (optional, press Enter to skip): ')).trim()
88
86
 
89
- const resolveVersionList = async (args: ResolveTokensArgs): Promise<string[]> => {
90
- const rawTokens = await resolveRawTokens(args)
87
+ entries.push({ ...resolved, ...(description !== '' ? { description } : {}) })
91
88
 
92
- if (rawTokens.length === 0) {
93
- logger.error('No version provided. Exiting...')
94
- process.exit(1)
89
+ addAnother = await confirm({ message: 'Add another release?', default: false })
95
90
  }
96
91
 
97
- const needsKnown = rawTokens.some((t) => {
98
- return t.toLowerCase() === NEXT_TOKEN
99
- })
100
-
101
- try {
102
- return resolveVersionTokens(rawTokens, args.type, needsKnown ? await args.ensureKnown() : [])
103
- } catch (err) {
104
- return exitOnNoPrior(err)
105
- }
92
+ return entries
106
93
  }
107
94
 
108
- const resolveDescription = async (input: string | undefined): Promise<string> => {
109
- if (input !== undefined) return input
95
+ const formatReleaseSummary = (entry: ReleaseEntry): string => {
96
+ const parts = [`v${entry.version}`, entry.type]
110
97
 
111
- commandEcho.setInteractive()
112
- const answer = await question('Enter description (optional, press Enter to skip): ')
98
+ if (entry.description) parts.push(entry.description)
113
99
 
114
- return answer.trim()
100
+ return parts.join(' · ')
115
101
  }
116
102
 
117
- /**
118
- * Create a single release branch for the specified version
119
- * Includes Jira version creation and GitHub release branch creation
120
- */
121
- export const releaseCreate = async (args: ReleaseCreateArgs): Promise<ToolsExecutionResult> => {
122
- const { version: inputVersion, description: inputDescription, type: inputType, confirmedCommand } = args
123
-
124
- commandEcho.start('release-create')
125
-
126
- // Load Jira config - it is now mandatory
127
- const jiraConfig = await loadJiraConfig()
128
-
129
- const type: ReleaseType = inputType ?? (await promptForType())
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}`
130
108
 
131
- commandEcho.addOption('--type', type)
132
-
133
- let known: SemVer[] | null = null
134
- const ensureKnown = async (): Promise<SemVer[]> => {
135
- if (known === null) known = await loadExistingVersions()
136
-
137
- return known
109
+ commandEcho.addOption('--release', spec)
138
110
  }
111
+ }
139
112
 
140
- const resolvedVersions = await resolveVersionList({ inputVersion, type, ensureKnown })
113
+ interface FailedRelease {
114
+ version: string
115
+ error: string
116
+ }
141
117
 
142
- if (resolvedVersions.length > 1) {
143
- logger.info(`Detected ${resolvedVersions.length} versions, routing to release-create-batch...`)
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)
144
125
 
145
- return releaseCreateBatch({
146
- versions: resolvedVersions.join(','),
147
- type,
148
- confirmedCommand,
149
- })
150
- }
126
+ echoReleases(resolved)
151
127
 
152
- const trimmedVersion = resolvedVersions[0] as string
128
+ return resolved
129
+ }
153
130
 
154
- commandEcho.addOption('--version', trimmedVersion)
131
+ const interactive = await promptForReleasesInteractive(ensureKnown)
155
132
 
156
- const description = await resolveDescription(inputDescription)
133
+ echoReleases(interactive)
157
134
 
158
- if (description) {
159
- commandEcho.addOption('--description', description)
160
- }
135
+ return interactive
136
+ }
161
137
 
138
+ const confirmReleases = async (entries: ReleaseEntry[], confirmedCommand: boolean): Promise<void> => {
139
+ const summary = entries.map(formatReleaseSummary).join('\n - ')
162
140
  const answer = confirmedCommand
163
141
  ? true
164
142
  : await confirm({
165
- 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`,
166
144
  })
167
145
 
168
146
  if (!confirmedCommand) {
@@ -174,25 +152,105 @@ export const releaseCreate = async (args: ReleaseCreateArgs): Promise<ToolsExecu
174
152
  process.exit(0)
175
153
  }
176
154
 
177
- // Track --yes flag if confirmation was interactive (user confirmed)
178
155
  commandEcho.addOption('--yes', true)
156
+ }
157
+
158
+ interface ExecuteOneArgs {
159
+ entry: ReleaseEntry
160
+ jiraConfig: Awaited<ReturnType<typeof loadJiraConfig>>
161
+ }
162
+
163
+ const executeOne = async (
164
+ args: ExecuteOneArgs,
165
+ ): Promise<{ result?: ReleaseCreationResult; failure?: FailedRelease }> => {
166
+ const { entry, jiraConfig } = args
167
+
168
+ try {
169
+ await prepareGitForRelease(entry.type)
170
+
171
+ const result = await createSingleRelease({
172
+ version: entry.version,
173
+ jiraConfig,
174
+ description: entry.description,
175
+ type: entry.type,
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 } }
190
+ }
191
+ }
192
+
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.`)
201
+ }
202
+ }
203
+
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
179
211
 
180
- await prepareGitForRelease(type)
212
+ commandEcho.start('release-create')
213
+
214
+ const jiraConfig = await loadJiraConfig()
181
215
 
182
- const release = await createSingleRelease({ version: trimmedVersion, jiraConfig, description, type })
216
+ let known: SemVer[] | null = null
217
+ const ensureKnown = async (): Promise<SemVer[]> => {
218
+ if (known === null) known = await loadExistingVersions()
219
+
220
+ return known
221
+ }
222
+
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)
240
+ }
183
241
 
184
- logger.info(`✅ Successfully created release: v${trimmedVersion}`)
185
- logger.info(`🔗 GitHub PR: ${release.prUrl}`)
186
- logger.info(`🔗 Jira Version: ${release.jiraVersionUrl}`)
242
+ logFinalSummary(entries.length, created.length, failed.length)
187
243
 
188
244
  commandEcho.print()
189
245
 
190
246
  const structuredContent = {
191
- version: trimmedVersion,
192
- type,
193
- branchName: release.branchName,
194
- prUrl: release.prUrl,
195
- jiraVersionUrl: release.jiraVersionUrl,
247
+ createdBranches: created.map((r) => {
248
+ return r.branchName
249
+ }),
250
+ successCount: created.length,
251
+ failureCount: failed.length,
252
+ releases: created,
253
+ failedReleases: failed,
196
254
  }
197
255
 
198
256
  return {
@@ -210,26 +268,48 @@ export const releaseCreate = async (args: ReleaseCreateArgs): Promise<ToolsExecu
210
268
  export const releaseCreateMcpTool = {
211
269
  name: 'release-create',
212
270
  description:
213
- 'Create a new release: cuts the release branch off the appropriate base (dev for regular releases, main for hotfixes), opens a GitHub release PR, and creates the matching Jira fix version. Does not switch the working tree to the new branch the caller stays on the base 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); pass "next" to auto-compute the next version (regular bumps minor + resets patch; hotfix bumps patch on the highest minor) using the union of remote release branches and Jira fix versions. "type" / "description" default to regular / empty when omitted. For multiple versions in one call, prefer release-create-batch.',
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.',
214
272
  inputSchema: {
215
- version: z
216
- .string()
217
- .describe(
218
- 'Version to create (e.g., "1.2.5") or the literal token "next" for auto-increment. Required for MCP calls.',
219
- ),
220
- description: z.string().optional().describe('Optional description for the Jira version'),
221
- type: z
222
- .enum(['regular', 'hotfix'])
223
- .optional()
224
- .default('regular')
225
- .describe('Release type: "regular" or "hotfix" (default: "regular")'),
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.'),
226
289
  },
227
290
  outputSchema: {
228
- version: z.string().describe('Version number'),
229
- type: z.enum(['regular', 'hotfix']).describe('Release type'),
230
- branchName: z.string().describe('Release branch name'),
231
- prUrl: z.string().describe('GitHub PR URL'),
232
- jiraVersionUrl: z.string().describe('Jira version URL'),
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'),
233
313
  },
234
314
  handler: releaseCreate,
235
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,5 +1,5 @@
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
5
  import { configEdit, configPath } from 'src/commands/config'
@@ -15,7 +15,6 @@ import { ghReleaseDeploySelected } from 'src/commands/gh-release-deploy-selected
15
15
  import { ghReleaseList } from 'src/commands/gh-release-list'
16
16
  import { init } from 'src/commands/init'
17
17
  import { releaseCreate } from 'src/commands/release-create'
18
- import { releaseCreateBatch } from 'src/commands/release-create-batch'
19
18
  import { version } from 'src/commands/version'
20
19
  import { CURSOR_MODES, worktreesAdd } from 'src/commands/worktrees-add'
21
20
  import type { CursorMode } from 'src/commands/worktrees-add'
@@ -24,9 +23,14 @@ import { worktreesOpen } from 'src/commands/worktrees-open'
24
23
  import { worktreesRemove } from 'src/commands/worktrees-remove'
25
24
  import { worktreesSync } from 'src/commands/worktrees-sync'
26
25
  import { logger } from 'src/lib/logger'
26
+ import { parseReleaseSpec } from 'src/lib/version-utils'
27
27
 
28
28
  const program = new Command()
29
29
 
30
+ const collectReleaseSpec = (value: string, prev: string[]): string[] => {
31
+ return [...prev, value]
32
+ }
33
+
30
34
  const normalizeCursorMode = (value: unknown): CursorMode | undefined => {
31
35
  if (typeof value === 'undefined') {
32
36
  return undefined
@@ -80,36 +84,20 @@ program
80
84
 
81
85
  program
82
86
  .command('release-create')
83
- .description('Create a single release branch')
87
+ .description('Create one or more release branches (each entry can mix regular/hotfix and its own description)')
84
88
  .option(
85
- '-v, --version <version>',
86
- 'Version to create, e.g. "1.2.5", "next", or "next,next,1.2.7" (multi-value routes to batch)',
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
+ [],
87
93
  )
88
- .option('-d, --description <description>', 'Optional description for the Jira version')
89
- .addOption(new Option('-t, --type <type>', 'Release type (default: regular)').choices(['regular', 'hotfix']))
90
94
  .option('-y, --yes', 'Skip confirmation prompt')
91
95
  .action(async (options) => {
92
- await releaseCreate({
93
- version: options.version,
94
- description: options.description,
95
- type: options.type,
96
- confirmedCommand: options.yes,
97
- })
98
- })
96
+ const specs = options.release as string[]
97
+ const releases = specs.length > 0 ? specs.map(parseReleaseSpec) : undefined
99
98
 
100
- program
101
- .command('release-create-batch')
102
- .description('Create multiple release branches (batch operation)')
103
- .option(
104
- '-v, --versions <versions>',
105
- 'Comma-separated versions, e.g. "1.2.5, 1.2.6", "next,next", or "next,next,1.2.7"',
106
- )
107
- .addOption(new Option('-t, --type <type>', 'Release type (default: regular)').choices(['regular', 'hotfix']))
108
- .option('-y, --yes', 'Skip confirmation prompt')
109
- .action(async (options) => {
110
- await releaseCreateBatch({
111
- versions: options.versions,
112
- type: options.type,
99
+ await releaseCreate({
100
+ releases,
113
101
  confirmedCommand: options.yes,
114
102
  })
115
103
  })
@@ -275,7 +263,6 @@ if (process.argv.length <= 2) {
275
263
  'merge-dev',
276
264
  'release-list',
277
265
  'release-create',
278
- 'release-create-batch',
279
266
  'release-deploy-all',
280
267
  'release-deploy-selected',
281
268
  'release-deliver',