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/.eslintcache +1 -1
- package/.turbo/turbo-eslint-check.log +5 -4
- package/.turbo/turbo-prettier-check.log +5 -4
- package/.turbo/turbo-test.log +18 -191
- package/.turbo/turbo-ts-check.log +5 -9
- package/dist/cli.js +59 -43
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +34 -31
- package/dist/mcp.js.map +4 -4
- package/package.json +1 -1
- package/src/commands/config/config.ts +1 -1
- package/src/commands/init/init.ts +17 -6
- package/src/commands/release-create/release-create.ts +210 -130
- package/src/commands/worktrees-add/worktrees-add.ts +16 -12
- package/src/entry/cli.ts +15 -28
- package/src/lib/__tests__/infra-kit-config.test.ts +50 -0
- package/src/lib/infra-kit-config/infra-kit-config.ts +7 -0
- package/src/lib/version-utils/__tests__/next-version.test.ts +128 -23
- package/src/lib/version-utils/index.ts +4 -2
- package/src/lib/version-utils/next-version.ts +64 -25
- package/src/mcp/tools/index.ts +0 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/src/commands/release-create-batch/index.ts +0 -1
- package/src/commands/release-create-batch/release-create-batch.ts +0 -222
package/package.json
CHANGED
|
@@ -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#
|
|
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
|
-
#
|
|
16
|
-
# 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
|
-
#
|
|
19
|
-
#
|
|
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:
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
27
|
-
description?: string
|
|
28
|
-
type?: ReleaseType
|
|
24
|
+
releases?: ReleaseEntry[]
|
|
29
25
|
}
|
|
30
26
|
|
|
31
|
-
const VERSION_PROMPT_HINT = '"1.2.5"
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
48
|
+
throw err
|
|
49
|
+
}
|
|
63
50
|
}
|
|
64
51
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
type: ReleaseType
|
|
68
|
-
ensureKnown: () => Promise<SemVer[]>
|
|
69
|
-
}
|
|
52
|
+
const promptForReleasesInteractive = async (ensureKnown: () => Promise<SemVer[]>): Promise<ReleaseEntry[]> => {
|
|
53
|
+
commandEcho.setInteractive()
|
|
70
54
|
|
|
71
|
-
const
|
|
72
|
-
const
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
76
|
+
if (versionInput === '') {
|
|
77
|
+
logger.error('No version provided. Exiting...')
|
|
78
|
+
process.exit(1)
|
|
79
|
+
}
|
|
79
80
|
|
|
80
|
-
|
|
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
|
-
|
|
83
|
+
running.push(parseVersion(`v${resolved.version}`))
|
|
85
84
|
|
|
86
|
-
|
|
87
|
-
}
|
|
85
|
+
const description = (await question(' Description (optional, press Enter to skip): ')).trim()
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
const rawTokens = await resolveRawTokens(args)
|
|
87
|
+
entries.push({ ...resolved, ...(description !== '' ? { description } : {}) })
|
|
91
88
|
|
|
92
|
-
|
|
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
|
-
|
|
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
|
|
109
|
-
|
|
95
|
+
const formatReleaseSummary = (entry: ReleaseEntry): string => {
|
|
96
|
+
const parts = [`v${entry.version}`, entry.type]
|
|
110
97
|
|
|
111
|
-
|
|
112
|
-
const answer = await question('Enter description (optional, press Enter to skip): ')
|
|
98
|
+
if (entry.description) parts.push(entry.description)
|
|
113
99
|
|
|
114
|
-
return
|
|
100
|
+
return parts.join(' · ')
|
|
115
101
|
}
|
|
116
102
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
+
interface FailedRelease {
|
|
114
|
+
version: string
|
|
115
|
+
error: string
|
|
116
|
+
}
|
|
141
117
|
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
146
|
-
versions: resolvedVersions.join(','),
|
|
147
|
-
type,
|
|
148
|
-
confirmedCommand,
|
|
149
|
-
})
|
|
150
|
-
}
|
|
126
|
+
echoReleases(resolved)
|
|
151
127
|
|
|
152
|
-
|
|
128
|
+
return resolved
|
|
129
|
+
}
|
|
153
130
|
|
|
154
|
-
|
|
131
|
+
const interactive = await promptForReleasesInteractive(ensureKnown)
|
|
155
132
|
|
|
156
|
-
|
|
133
|
+
echoReleases(interactive)
|
|
157
134
|
|
|
158
|
-
|
|
159
|
-
|
|
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: `
|
|
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
|
-
|
|
212
|
+
commandEcho.start('release-create')
|
|
213
|
+
|
|
214
|
+
const jiraConfig = await loadJiraConfig()
|
|
181
215
|
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
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
|
-
|
|
216
|
-
.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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 ??
|
|
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 =
|
|
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
|
|
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"
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
87
|
+
.description('Create one or more release branches (each entry can mix regular/hotfix and its own description)')
|
|
84
88
|
.option(
|
|
85
|
-
'-
|
|
86
|
-
'
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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',
|