infra-kit 0.1.99 → 0.1.100
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/.omc/state/agent-replay-d367c3be-9c2a-48e7-bcea-b45861af568c.jsonl +2 -0
- package/.omc/state/agent-replay-f2846d8f-974c-486c-b16f-4bdaa28ca45f.jsonl +1 -0
- package/.omc/state/last-tool-error.json +7 -0
- package/.omc/state/subagent-tracking.json +7 -0
- package/.turbo/turbo-eslint-check.log +1 -4
- package/.turbo/turbo-eslint-fix.log +1 -0
- package/.turbo/turbo-prettier-check.log +1 -4
- package/.turbo/turbo-prettier-fix.log +1 -10
- package/.turbo/turbo-test.log +5 -11
- package/.turbo/turbo-ts-check.log +1 -4
- package/dist/cli.js +45 -36
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +42 -34
- package/dist/mcp.js.map +4 -4
- package/package.json +11 -11
- package/src/commands/doctor/doctor.ts +62 -12
- package/src/commands/env-clear/env-clear.ts +5 -10
- package/src/commands/env-list/env-list.ts +5 -10
- package/src/commands/env-load/env-load.ts +5 -10
- package/src/commands/env-status/env-status.ts +5 -10
- package/src/commands/gh-merge-dev/gh-merge-dev.ts +17 -18
- package/src/commands/gh-release-deliver/gh-release-deliver.ts +290 -89
- package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +15 -14
- package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +30 -23
- package/src/commands/gh-release-list/gh-release-list.ts +5 -10
- package/src/commands/release-create/release-create.ts +22 -18
- package/src/commands/release-desc-edit/index.ts +1 -0
- package/src/commands/release-desc-edit/release-desc-edit.ts +207 -0
- package/src/commands/version/version.ts +5 -10
- package/src/commands/worktrees-add/worktrees-add.ts +18 -14
- package/src/commands/worktrees-list/worktrees-list.ts +6 -11
- package/src/commands/worktrees-open/worktrees-open.ts +10 -6
- package/src/commands/worktrees-remove/worktrees-remove.ts +18 -14
- package/src/commands/worktrees-sync/worktrees-sync.ts +17 -12
- package/src/entry/cli.ts +16 -0
- package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +21 -0
- package/src/integrations/gh/gh-release-prs/index.ts +1 -1
- package/src/integrations/gh/index.ts +1 -1
- package/src/integrations/jira/api.ts +8 -17
- package/src/integrations/jira/index.ts +2 -0
- package/src/lib/errors/__tests__/operation-error.test.ts +62 -0
- package/src/lib/errors/format-zx-error.ts +54 -0
- package/src/lib/errors/operation-error.ts +80 -0
- package/src/mcp/tools/index.ts +2 -0
- package/src/types.ts +56 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -3,12 +3,12 @@ import { z } from 'zod/v4'
|
|
|
3
3
|
import { getReleasePRsWithInfo } from 'src/integrations/gh'
|
|
4
4
|
import { logger } from 'src/lib/logger'
|
|
5
5
|
import { detectReleaseType, formatVersionLabel, getJiraDescriptions } from 'src/lib/release-utils'
|
|
6
|
-
import
|
|
6
|
+
import { defineMcpTool, textContent } from 'src/types'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* List all open release branches
|
|
10
10
|
*/
|
|
11
|
-
export const ghReleaseList = async ()
|
|
11
|
+
export const ghReleaseList = async () => {
|
|
12
12
|
const releasePRs = await getReleasePRsWithInfo()
|
|
13
13
|
|
|
14
14
|
const releases = releasePRs.map((pr) => {
|
|
@@ -52,18 +52,13 @@ export const ghReleaseList = async (): Promise<ToolsExecutionResult> => {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
return {
|
|
55
|
-
content:
|
|
56
|
-
{
|
|
57
|
-
type: 'text',
|
|
58
|
-
text: JSON.stringify(structuredContent, null, 2),
|
|
59
|
-
},
|
|
60
|
-
],
|
|
55
|
+
content: textContent(JSON.stringify(structuredContent, null, 2)),
|
|
61
56
|
structuredContent,
|
|
62
57
|
}
|
|
63
58
|
}
|
|
64
59
|
|
|
65
60
|
// MCP Tool Registration
|
|
66
|
-
export const ghReleaseListMcpTool = {
|
|
61
|
+
export const ghReleaseListMcpTool = defineMcpTool({
|
|
67
62
|
name: 'gh-release-list',
|
|
68
63
|
description:
|
|
69
64
|
'List every open release PR with its version, type (regular / hotfix), and associated Jira fix-version description. Read-only; sourced from GitHub and Jira.',
|
|
@@ -81,4 +76,4 @@ export const ghReleaseListMcpTool = {
|
|
|
81
76
|
count: z.number().describe('Number of release branches'),
|
|
82
77
|
},
|
|
83
78
|
handler: ghReleaseList,
|
|
84
|
-
}
|
|
79
|
+
})
|
|
@@ -6,6 +6,7 @@ import { question } from 'zx'
|
|
|
6
6
|
|
|
7
7
|
import { loadJiraConfig } from 'src/integrations/jira'
|
|
8
8
|
import { commandEcho } from 'src/lib/command-echo'
|
|
9
|
+
import { OperationError } from 'src/lib/errors/operation-error'
|
|
9
10
|
import { logger } from 'src/lib/logger'
|
|
10
11
|
import { createSingleRelease, prepareGitForRelease } from 'src/lib/release-utils'
|
|
11
12
|
import type { ReleaseCreationResult, ReleaseType } from 'src/lib/release-utils'
|
|
@@ -18,7 +19,8 @@ import {
|
|
|
18
19
|
resolveReleaseEntries,
|
|
19
20
|
} from 'src/lib/version-utils'
|
|
20
21
|
import type { ReleaseEntry, SemVer } from 'src/lib/version-utils'
|
|
21
|
-
import
|
|
22
|
+
import { defineMcpTool, textContent } from 'src/types'
|
|
23
|
+
import type { RequiredConfirmedOptionArg } from 'src/types'
|
|
22
24
|
|
|
23
25
|
interface ReleaseCreateArgs extends RequiredConfirmedOptionArg {
|
|
24
26
|
releases?: ReleaseEntry[]
|
|
@@ -41,8 +43,10 @@ const resolveOrExit = (entries: ReleaseEntry[], known: SemVer[]): ReleaseEntry[]
|
|
|
41
43
|
return resolveReleaseEntries(entries, known)
|
|
42
44
|
} catch (err) {
|
|
43
45
|
if (err instanceof NoPriorVersionsError) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
throw new OperationError(err, {
|
|
47
|
+
operation: 'resolve release version',
|
|
48
|
+
remediation: 'pass an explicit version (e.g. "1.2.5") instead of "next" when there are no prior versions',
|
|
49
|
+
})
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
throw err
|
|
@@ -181,12 +185,14 @@ const executeOne = async (
|
|
|
181
185
|
|
|
182
186
|
return { result }
|
|
183
187
|
} catch (error) {
|
|
184
|
-
const
|
|
188
|
+
const err = new OperationError(error, {
|
|
189
|
+
operation: `create release v${entry.version} (${entry.type})`,
|
|
190
|
+
remediation: 'verify the version is unique and the base branch is clean',
|
|
191
|
+
})
|
|
185
192
|
|
|
186
|
-
logger.error(`❌
|
|
187
|
-
logger.error(` Error: ${errorMessage}\n`)
|
|
193
|
+
logger.error(`❌ ${err.message}\n`)
|
|
188
194
|
|
|
189
|
-
return { failure: { version: entry.version, error:
|
|
195
|
+
return { failure: { version: entry.version, error: err.message } }
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
198
|
|
|
@@ -206,7 +212,7 @@ const logFinalSummary = (total: number, successCount: number, failureCount: numb
|
|
|
206
212
|
* (regular/hotfix) and optional Jira description, so a single invocation
|
|
207
213
|
* may mix regular and hotfix releases off their respective base branches.
|
|
208
214
|
*/
|
|
209
|
-
export const releaseCreate = async (args: ReleaseCreateArgs)
|
|
215
|
+
export const releaseCreate = async (args: ReleaseCreateArgs) => {
|
|
210
216
|
const { releases: inputReleases, confirmedCommand } = args
|
|
211
217
|
|
|
212
218
|
commandEcho.start('release-create')
|
|
@@ -223,8 +229,11 @@ export const releaseCreate = async (args: ReleaseCreateArgs): Promise<ToolsExecu
|
|
|
223
229
|
const entries = await collectEntries(inputReleases, ensureKnown)
|
|
224
230
|
|
|
225
231
|
if (entries.length === 0) {
|
|
226
|
-
|
|
227
|
-
|
|
232
|
+
throw new OperationError(undefined, {
|
|
233
|
+
operation: 'create release',
|
|
234
|
+
remediation: 'pass at least one entry in "releases" (e.g. [{ version: "1.2.5", type: "regular" }])',
|
|
235
|
+
stderrExcerpt: 'no releases provided',
|
|
236
|
+
})
|
|
228
237
|
}
|
|
229
238
|
|
|
230
239
|
await confirmReleases(entries, Boolean(confirmedCommand))
|
|
@@ -254,18 +263,13 @@ export const releaseCreate = async (args: ReleaseCreateArgs): Promise<ToolsExecu
|
|
|
254
263
|
}
|
|
255
264
|
|
|
256
265
|
return {
|
|
257
|
-
content:
|
|
258
|
-
{
|
|
259
|
-
type: 'text',
|
|
260
|
-
text: JSON.stringify(structuredContent, null, 2),
|
|
261
|
-
},
|
|
262
|
-
],
|
|
266
|
+
content: textContent(JSON.stringify(structuredContent, null, 2)),
|
|
263
267
|
structuredContent,
|
|
264
268
|
}
|
|
265
269
|
}
|
|
266
270
|
|
|
267
271
|
// MCP Tool Registration
|
|
268
|
-
export const releaseCreateMcpTool = {
|
|
272
|
+
export const releaseCreateMcpTool = defineMcpTool({
|
|
269
273
|
name: 'release-create',
|
|
270
274
|
description:
|
|
271
275
|
'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.',
|
|
@@ -312,4 +316,4 @@ export const releaseCreateMcpTool = {
|
|
|
312
316
|
.describe('List of releases that failed with error messages'),
|
|
313
317
|
},
|
|
314
318
|
handler: releaseCreate,
|
|
315
|
-
}
|
|
319
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { releaseDescEdit, releaseDescEditMcpTool } from './release-desc-edit'
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import confirm from '@inquirer/confirm'
|
|
2
|
+
import select from '@inquirer/select'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
import { z } from 'zod/v4'
|
|
5
|
+
import { question } from 'zx'
|
|
6
|
+
|
|
7
|
+
import { getReleasePRsWithInfo, updateReleasePRBody } from 'src/integrations/gh'
|
|
8
|
+
import { findVersionByName, loadJiraConfig, updateJiraVersion } from 'src/integrations/jira'
|
|
9
|
+
import type { JiraConfig, JiraVersion } from 'src/integrations/jira'
|
|
10
|
+
import { commandEcho } from 'src/lib/command-echo'
|
|
11
|
+
import { OperationError } from 'src/lib/errors/operation-error'
|
|
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'
|
|
15
|
+
import { defineMcpTool, textContent } from 'src/types'
|
|
16
|
+
import type { RequiredConfirmedOptionArg } from 'src/types'
|
|
17
|
+
|
|
18
|
+
interface ReleaseDescEditArgs extends RequiredConfirmedOptionArg {
|
|
19
|
+
version?: string
|
|
20
|
+
description?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const buildJiraVersionUrl = (jiraConfig: JiraConfig, version: JiraVersion): string => {
|
|
24
|
+
return `${jiraConfig.baseUrl}/projects/${version.projectId}/versions/${version.id}/tab/release-report-all-issues`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const buildPRBody = (jiraVersionUrl: string, description: string): string => {
|
|
28
|
+
return description.trim() !== '' ? `${jiraVersionUrl}\n\n${description}` : `${jiraVersionUrl} \n`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const pickReleaseBranch = async (): Promise<{ branch: string; type: ReleaseType }> => {
|
|
32
|
+
const releasePRsInfo = await getReleasePRsWithInfo()
|
|
33
|
+
const branches = releasePRsInfo.map((pr) => {
|
|
34
|
+
return pr.branch
|
|
35
|
+
})
|
|
36
|
+
const types = new Map<string, ReleaseType>(
|
|
37
|
+
releasePRsInfo.map((pr) => {
|
|
38
|
+
return [pr.branch, detectReleaseType(pr.title)]
|
|
39
|
+
}),
|
|
40
|
+
)
|
|
41
|
+
const descriptions = await getJiraDescriptions()
|
|
42
|
+
|
|
43
|
+
const branch = await select({
|
|
44
|
+
message: '🌿 Select release branch',
|
|
45
|
+
choices: formatBranchChoices({ branches, descriptions, types }),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return { branch, type: types.get(branch) || 'regular' }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const verifyReleasePRExists = async (selectedBranch: string): Promise<ReleaseType> => {
|
|
52
|
+
const releasePRsInfo = await getReleasePRsWithInfo()
|
|
53
|
+
const prInfo = releasePRsInfo.find((pr) => {
|
|
54
|
+
return pr.branch === selectedBranch
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
if (!prInfo) {
|
|
58
|
+
throw new OperationError(undefined, {
|
|
59
|
+
operation: `edit description for ${selectedBranch}`,
|
|
60
|
+
remediation: `confirm an open PR exists for ${selectedBranch} ('gh pr list')`,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return detectReleaseType(prInfo.title)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const promptDescription = async (current: string): Promise<string> => {
|
|
68
|
+
const hint = current === '' ? '(no current description)' : `current: "${current}"`
|
|
69
|
+
const answer = await question(` New description ${hint}\n (press Enter to keep current): `)
|
|
70
|
+
const trimmed = answer.replace(/\n$/, '')
|
|
71
|
+
|
|
72
|
+
return trimmed === '' ? current : trimmed
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Edit a release's description in Jira (fix version) and in the matching
|
|
77
|
+
* GitHub release PR body. The PR body is rewritten canonically to
|
|
78
|
+
* `<jiraVersionUrl>\n\n<description>` (matching `release-create`).
|
|
79
|
+
*/
|
|
80
|
+
export const releaseDescEdit = async (args: ReleaseDescEditArgs) => {
|
|
81
|
+
const { version: versionArg, description: descriptionArg, confirmedCommand } = args
|
|
82
|
+
|
|
83
|
+
commandEcho.start('release-desc-edit')
|
|
84
|
+
|
|
85
|
+
const jiraConfig = await loadJiraConfig()
|
|
86
|
+
|
|
87
|
+
let selectedBranch: string
|
|
88
|
+
|
|
89
|
+
if (versionArg) {
|
|
90
|
+
selectedBranch = `release/v${versionArg}`
|
|
91
|
+
await verifyReleasePRExists(selectedBranch)
|
|
92
|
+
} else {
|
|
93
|
+
commandEcho.setInteractive()
|
|
94
|
+
const picked = await pickReleaseBranch()
|
|
95
|
+
|
|
96
|
+
selectedBranch = picked.branch
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const selectedVersion = selectedBranch.replace('release/v', '')
|
|
100
|
+
|
|
101
|
+
commandEcho.addOption('--version', selectedVersion)
|
|
102
|
+
|
|
103
|
+
const versionName = `v${selectedVersion}`
|
|
104
|
+
const jiraVersion = await findVersionByName(versionName, jiraConfig)
|
|
105
|
+
|
|
106
|
+
if (!jiraVersion) {
|
|
107
|
+
throw new OperationError(undefined, {
|
|
108
|
+
operation: `edit description for ${versionName}`,
|
|
109
|
+
remediation: `create the Jira fix version "${versionName}" first or pick a different release`,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const previousDescription = jiraVersion.description ?? ''
|
|
114
|
+
|
|
115
|
+
let newDescription: string
|
|
116
|
+
|
|
117
|
+
if (descriptionArg !== undefined) {
|
|
118
|
+
newDescription = descriptionArg
|
|
119
|
+
commandEcho.addOption('--description', newDescription)
|
|
120
|
+
} else {
|
|
121
|
+
commandEcho.setInteractive()
|
|
122
|
+
newDescription = await promptDescription(previousDescription)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (newDescription === previousDescription) {
|
|
126
|
+
logger.info(`No change — description for ${versionName} is already: "${previousDescription}"`)
|
|
127
|
+
commandEcho.print()
|
|
128
|
+
|
|
129
|
+
const structuredContent = {
|
|
130
|
+
version: selectedVersion,
|
|
131
|
+
branch: selectedBranch,
|
|
132
|
+
jiraVersionUrl: buildJiraVersionUrl(jiraConfig, jiraVersion),
|
|
133
|
+
previousDescription,
|
|
134
|
+
newDescription,
|
|
135
|
+
changed: false,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
content: textContent(JSON.stringify(structuredContent, null, 2)),
|
|
140
|
+
structuredContent,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const answer = confirmedCommand
|
|
145
|
+
? true
|
|
146
|
+
: await confirm({
|
|
147
|
+
message: `Update description for ${versionName}?\n from: "${previousDescription}"\n to: "${newDescription}"\n`,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
if (!confirmedCommand) {
|
|
151
|
+
commandEcho.setInteractive()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!answer) {
|
|
155
|
+
logger.info('Operation cancelled. Exiting...')
|
|
156
|
+
process.exit(0)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
commandEcho.addOption('--yes', true)
|
|
160
|
+
|
|
161
|
+
await updateJiraVersion({ versionId: jiraVersion.id, description: newDescription }, jiraConfig)
|
|
162
|
+
|
|
163
|
+
const jiraVersionUrl = buildJiraVersionUrl(jiraConfig, jiraVersion)
|
|
164
|
+
const body = buildPRBody(jiraVersionUrl, newDescription)
|
|
165
|
+
|
|
166
|
+
await updateReleasePRBody({ branch: selectedBranch, body })
|
|
167
|
+
|
|
168
|
+
logger.info(`✅ Updated description for ${versionName}`)
|
|
169
|
+
logger.info(`🔗 Jira Version: ${jiraVersionUrl}`)
|
|
170
|
+
logger.info(`🔗 PR branch: ${selectedBranch}\n`)
|
|
171
|
+
|
|
172
|
+
commandEcho.print()
|
|
173
|
+
|
|
174
|
+
const structuredContent = {
|
|
175
|
+
version: selectedVersion,
|
|
176
|
+
branch: selectedBranch,
|
|
177
|
+
jiraVersionUrl,
|
|
178
|
+
previousDescription,
|
|
179
|
+
newDescription,
|
|
180
|
+
changed: true,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
content: textContent(JSON.stringify(structuredContent, null, 2)),
|
|
185
|
+
structuredContent,
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// MCP Tool Registration
|
|
190
|
+
export const releaseDescEditMcpTool = defineMcpTool({
|
|
191
|
+
name: 'release-desc-edit',
|
|
192
|
+
description:
|
|
193
|
+
"Edit a release's description in Jira and in the matching GitHub release PR body. Targets the Jira fix version named `v<version>` and the open PR on branch `release/v<version>`. The PR body is rewritten canonically to `<jiraVersionUrl>\\n\\n<description>` — any prior manual edits to the body are overwritten. Both `version` and `description` are required for MCP calls (the picker/prompt are unreachable without a TTY). Empty `description` clears the description on both sides. Confirmation is auto-skipped for MCP, so the caller is responsible for gating.",
|
|
194
|
+
inputSchema: {
|
|
195
|
+
version: z.string().describe('Release version, e.g. "1.2.5".'),
|
|
196
|
+
description: z.string().describe('New description. Empty string clears the description.'),
|
|
197
|
+
},
|
|
198
|
+
outputSchema: {
|
|
199
|
+
version: z.string().describe('Release version'),
|
|
200
|
+
branch: z.string().describe('Release branch name (e.g. "release/v1.2.5")'),
|
|
201
|
+
jiraVersionUrl: z.string().describe('Jira fix version URL'),
|
|
202
|
+
previousDescription: z.string().describe('The description before the update'),
|
|
203
|
+
newDescription: z.string().describe('The description after the update'),
|
|
204
|
+
changed: z.boolean().describe('Whether the description actually changed'),
|
|
205
|
+
},
|
|
206
|
+
handler: releaseDescEdit,
|
|
207
|
+
})
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { z } from 'zod/v4'
|
|
2
2
|
|
|
3
3
|
import { logger } from 'src/lib/logger'
|
|
4
|
-
import
|
|
4
|
+
import { defineMcpTool, textContent } from 'src/types'
|
|
5
5
|
|
|
6
6
|
import packageJson from '../../../package.json' with { type: 'json' }
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Print the infra-kit CLI version
|
|
10
10
|
*/
|
|
11
|
-
export const version = async ()
|
|
11
|
+
export const version = async () => {
|
|
12
12
|
const cliVersion = packageJson.version
|
|
13
13
|
|
|
14
14
|
logger.info(cliVersion)
|
|
@@ -16,18 +16,13 @@ export const version = async (): Promise<ToolsExecutionResult> => {
|
|
|
16
16
|
const structuredContent = { version: cliVersion }
|
|
17
17
|
|
|
18
18
|
return {
|
|
19
|
-
content:
|
|
20
|
-
{
|
|
21
|
-
type: 'text',
|
|
22
|
-
text: JSON.stringify(structuredContent, null, 2),
|
|
23
|
-
},
|
|
24
|
-
],
|
|
19
|
+
content: textContent(JSON.stringify(structuredContent, null, 2)),
|
|
25
20
|
structuredContent,
|
|
26
21
|
}
|
|
27
22
|
}
|
|
28
23
|
|
|
29
24
|
// MCP Tool Registration
|
|
30
|
-
export const versionMcpTool = {
|
|
25
|
+
export const versionMcpTool = defineMcpTool({
|
|
31
26
|
name: 'version',
|
|
32
27
|
description: 'Print the installed infra-kit CLI version',
|
|
33
28
|
inputSchema: {},
|
|
@@ -35,4 +30,4 @@ export const versionMcpTool = {
|
|
|
35
30
|
version: z.string().describe('Installed infra-kit CLI version (from package.json)'),
|
|
36
31
|
},
|
|
37
32
|
handler: version,
|
|
38
|
-
}
|
|
33
|
+
})
|
|
@@ -11,12 +11,14 @@ import { addFoldersToCursorWorkspace, resolveCursorWorkspacePath } from 'src/int
|
|
|
11
11
|
import { getReleasePRsWithInfo } from 'src/integrations/gh'
|
|
12
12
|
import { commandEcho } from 'src/lib/command-echo'
|
|
13
13
|
import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
|
|
14
|
+
import { OperationError } from 'src/lib/errors/operation-error'
|
|
14
15
|
import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
|
|
15
16
|
import { getInfraKitConfig } from 'src/lib/infra-kit-config'
|
|
16
17
|
import { logger } from 'src/lib/logger'
|
|
17
18
|
import { detectReleaseType, formatBranchChoices, getJiraDescriptions } from 'src/lib/release-utils'
|
|
18
19
|
import type { ReleaseType } from 'src/lib/release-utils'
|
|
19
|
-
import
|
|
20
|
+
import { defineMcpTool, textContent } from 'src/types'
|
|
21
|
+
import type { RequiredConfirmedOptionArg } from 'src/types'
|
|
20
22
|
|
|
21
23
|
// Constants
|
|
22
24
|
const FEATURE_DIR = 'feature'
|
|
@@ -27,7 +29,7 @@ export const CURSOR_MODES = ['workspace', 'windows', 'none'] as const
|
|
|
27
29
|
export type CursorMode = (typeof CURSOR_MODES)[number]
|
|
28
30
|
|
|
29
31
|
interface WorktreeManagementArgs extends RequiredConfirmedOptionArg {
|
|
30
|
-
all
|
|
32
|
+
all?: boolean
|
|
31
33
|
versions?: string
|
|
32
34
|
cursor?: CursorMode
|
|
33
35
|
githubDesktop?: boolean
|
|
@@ -38,7 +40,7 @@ interface WorktreeManagementArgs extends RequiredConfirmedOptionArg {
|
|
|
38
40
|
* Manage git worktrees for release branches
|
|
39
41
|
* Creates worktrees for active release branches and removes unused ones
|
|
40
42
|
*/
|
|
41
|
-
export const worktreesAdd = async (options: WorktreeManagementArgs)
|
|
43
|
+
export const worktreesAdd = async (options: WorktreeManagementArgs) => {
|
|
42
44
|
const { confirmedCommand, all, versions, cursor, githubDesktop, cmux } = options
|
|
43
45
|
|
|
44
46
|
commandEcho.start('worktrees-add')
|
|
@@ -71,7 +73,7 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
|
|
|
71
73
|
commandEcho.print()
|
|
72
74
|
|
|
73
75
|
return {
|
|
74
|
-
content:
|
|
76
|
+
content: textContent(JSON.stringify({ createdWorktrees: [], count: 0 }, null, 2)),
|
|
75
77
|
structuredContent: { createdWorktrees: [], count: 0 },
|
|
76
78
|
}
|
|
77
79
|
}
|
|
@@ -249,17 +251,15 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
|
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
return {
|
|
252
|
-
content:
|
|
253
|
-
{
|
|
254
|
-
type: 'text',
|
|
255
|
-
text: JSON.stringify(structuredContent, null, 2),
|
|
256
|
-
},
|
|
257
|
-
],
|
|
254
|
+
content: textContent(JSON.stringify(structuredContent, null, 2)),
|
|
258
255
|
structuredContent,
|
|
259
256
|
}
|
|
260
257
|
} catch (error) {
|
|
261
258
|
logger.error({ error }, '❌ Error managing worktrees')
|
|
262
|
-
throw error
|
|
259
|
+
throw new OperationError(error, {
|
|
260
|
+
operation: 'create worktrees',
|
|
261
|
+
remediation: "verify branches don't already exist as worktrees: 'git worktree list'",
|
|
262
|
+
})
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
|
|
@@ -314,8 +314,12 @@ const createWorktrees = async (branches: string[], worktreeDir: string): Promise
|
|
|
314
314
|
created.push(result.value)
|
|
315
315
|
} else {
|
|
316
316
|
const branch = branches[index]
|
|
317
|
+
const err = new OperationError(result.reason, {
|
|
318
|
+
operation: `git worktree add for ${branch}`,
|
|
319
|
+
remediation: 'check the branch name and that the parent dir is writable',
|
|
320
|
+
})
|
|
317
321
|
|
|
318
|
-
logger.error({ error: result.reason
|
|
322
|
+
logger.error({ error: result.reason, msg: err.message })
|
|
319
323
|
}
|
|
320
324
|
}
|
|
321
325
|
|
|
@@ -338,7 +342,7 @@ const logResults = (created: string[]): void => {
|
|
|
338
342
|
}
|
|
339
343
|
|
|
340
344
|
// MCP Tool Registration
|
|
341
|
-
export const worktreesAddMcpTool = {
|
|
345
|
+
export const worktreesAddMcpTool = defineMcpTool({
|
|
342
346
|
name: 'worktrees-add',
|
|
343
347
|
description:
|
|
344
348
|
'Create local git worktrees for release branches under the worktrees directory and run "pnpm install" in each. Mutates the local filesystem. When invoked via MCP, pass either "versions" (comma-separated) or all=true — the branch picker and "open in Cursor / GitHub Desktop / cmux" follow-up prompts are unreachable without a TTY, and the CLI confirmation is auto-skipped for MCP calls.',
|
|
@@ -379,4 +383,4 @@ export const worktreesAddMcpTool = {
|
|
|
379
383
|
count: z.number().describe('Number of git worktrees created'),
|
|
380
384
|
},
|
|
381
385
|
handler: worktreesAdd,
|
|
382
|
-
}
|
|
386
|
+
})
|
|
@@ -5,7 +5,7 @@ import { getCurrentWorktrees } from 'src/lib/git-utils'
|
|
|
5
5
|
import { logger } from 'src/lib/logger'
|
|
6
6
|
import { detectReleaseType, formatVersionLabel, getJiraDescriptions } from 'src/lib/release-utils'
|
|
7
7
|
import type { ReleaseType } from 'src/lib/release-utils'
|
|
8
|
-
import
|
|
8
|
+
import { defineMcpTool, textContent } from 'src/types'
|
|
9
9
|
|
|
10
10
|
interface WorktreeInfo {
|
|
11
11
|
version: string
|
|
@@ -16,14 +16,14 @@ interface WorktreeInfo {
|
|
|
16
16
|
/**
|
|
17
17
|
* List all release git worktrees with version, type, and Jira description
|
|
18
18
|
*/
|
|
19
|
-
export const worktreesList = async ()
|
|
19
|
+
export const worktreesList = async () => {
|
|
20
20
|
const currentWorktrees = await getCurrentWorktrees('release')
|
|
21
21
|
|
|
22
22
|
if (currentWorktrees.length === 0) {
|
|
23
23
|
logger.info('ℹ️ No active worktrees found')
|
|
24
24
|
|
|
25
25
|
return {
|
|
26
|
-
content:
|
|
26
|
+
content: textContent(JSON.stringify({ worktrees: [], count: 0 }, null, 2)),
|
|
27
27
|
structuredContent: { worktrees: [], count: 0 },
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -70,18 +70,13 @@ export const worktreesList = async (): Promise<ToolsExecutionResult> => {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
return {
|
|
73
|
-
content:
|
|
74
|
-
{
|
|
75
|
-
type: 'text',
|
|
76
|
-
text: JSON.stringify(structuredContent, null, 2),
|
|
77
|
-
},
|
|
78
|
-
],
|
|
73
|
+
content: textContent(JSON.stringify(structuredContent, null, 2)),
|
|
79
74
|
structuredContent,
|
|
80
75
|
}
|
|
81
76
|
}
|
|
82
77
|
|
|
83
78
|
// MCP Tool Registration
|
|
84
|
-
export const worktreesListMcpTool = {
|
|
79
|
+
export const worktreesListMcpTool = defineMcpTool({
|
|
85
80
|
name: 'worktrees-list',
|
|
86
81
|
description:
|
|
87
82
|
'List existing release-branch worktrees with version, release type (regular / hotfix), and Jira fix-version description. Read-only.',
|
|
@@ -99,4 +94,4 @@ export const worktreesListMcpTool = {
|
|
|
99
94
|
count: z.number().describe('Number of worktrees'),
|
|
100
95
|
},
|
|
101
96
|
handler: worktreesList,
|
|
102
|
-
}
|
|
97
|
+
})
|
|
@@ -5,10 +5,11 @@ import { buildCmuxWorkspaceTitle, listCmuxWorkspaceTitles, openCmuxWorkspaceWith
|
|
|
5
5
|
import { reconcileCursorWorkspaceFolders, resolveCursorWorkspacePath } from 'src/integrations/cursor'
|
|
6
6
|
import { commandEcho } from 'src/lib/command-echo'
|
|
7
7
|
import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
|
|
8
|
+
import { OperationError } from 'src/lib/errors/operation-error'
|
|
8
9
|
import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
|
|
9
10
|
import { getInfraKitConfig } from 'src/lib/infra-kit-config'
|
|
10
11
|
import { logger } from 'src/lib/logger'
|
|
11
|
-
import
|
|
12
|
+
import { defineMcpTool, textContent } from 'src/types'
|
|
12
13
|
|
|
13
14
|
interface WorktreesOpenResult {
|
|
14
15
|
openedCmux: string[]
|
|
@@ -23,7 +24,7 @@ interface WorktreesOpenResult {
|
|
|
23
24
|
* workspace exists per worktree. Idempotent and additive — never removes
|
|
24
25
|
* worktrees, never recreates running cmux workspaces.
|
|
25
26
|
*/
|
|
26
|
-
export const worktreesOpen = async ()
|
|
27
|
+
export const worktreesOpen = async () => {
|
|
27
28
|
commandEcho.start('worktrees-open')
|
|
28
29
|
|
|
29
30
|
try {
|
|
@@ -46,12 +47,15 @@ export const worktreesOpen = async (): Promise<ToolsExecutionResult> => {
|
|
|
46
47
|
commandEcho.print()
|
|
47
48
|
|
|
48
49
|
return {
|
|
49
|
-
content:
|
|
50
|
+
content: textContent(JSON.stringify(result, null, 2)),
|
|
50
51
|
structuredContent: { ...result },
|
|
51
52
|
}
|
|
52
53
|
} catch (error) {
|
|
53
54
|
logger.error({ error }, '❌ Error opening worktrees')
|
|
54
|
-
throw error
|
|
55
|
+
throw new OperationError(error, {
|
|
56
|
+
operation: 'open worktrees',
|
|
57
|
+
remediation: "run 'worktrees-list' to confirm the branches exist",
|
|
58
|
+
})
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
|
|
@@ -180,7 +184,7 @@ const logResults = (result: WorktreesOpenResult, context: LogResultsContext): vo
|
|
|
180
184
|
}
|
|
181
185
|
|
|
182
186
|
// MCP Tool Registration
|
|
183
|
-
export const worktreesOpenMcpTool = {
|
|
187
|
+
export const worktreesOpenMcpTool = defineMcpTool({
|
|
184
188
|
name: 'worktrees-open',
|
|
185
189
|
description:
|
|
186
190
|
'Open Cursor against the configured workspace file and ensure a cmux workspace exists for each existing release worktree. Idempotent and additive — never removes worktrees, never recreates running cmux workspaces. Use after a cold start (Cursor + cmux closed). For stale-worktree cleanup, use worktrees-sync.',
|
|
@@ -194,4 +198,4 @@ export const worktreesOpenMcpTool = {
|
|
|
194
198
|
.describe('Number of dangling worktree folders removed from the Cursor workspace file'),
|
|
195
199
|
},
|
|
196
200
|
handler: worktreesOpen,
|
|
197
|
-
}
|
|
201
|
+
})
|