infra-kit 0.1.98 → 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 +2 -4
- package/.turbo/turbo-eslint-fix.log +1 -0
- package/.turbo/turbo-prettier-check.log +2 -4
- package/.turbo/turbo-prettier-fix.log +1 -10
- package/.turbo/turbo-test.log +12 -191
- package/.turbo/turbo-ts-check.log +2 -9
- package/dist/cli.js +69 -44
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +45 -34
- package/dist/mcp.js.map +4 -4
- package/package.json +11 -11
- package/src/commands/config/config.ts +1 -1
- 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/init/init.ts +17 -6
- package/src/commands/release-create/release-create.ts +223 -139
- 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 +34 -26
- 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 +24 -21
- 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/__tests__/infra-kit-config.test.ts +50 -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/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 +2 -2
- package/src/types.ts +56 -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
|
@@ -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
|
}
|
|
@@ -130,8 +132,12 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
|
|
|
130
132
|
commandEcho.addOption('--yes', true)
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
const config = await getInfraKitConfig()
|
|
136
|
+
const cursorConfig = config.ide?.provider === 'cursor' ? config.ide.config : undefined
|
|
137
|
+
|
|
133
138
|
const cursorMode: CursorMode =
|
|
134
139
|
cursor ??
|
|
140
|
+
cursorConfig?.mode ??
|
|
135
141
|
(await select<CursorMode>({
|
|
136
142
|
message: 'Cursor mode for created worktrees?',
|
|
137
143
|
default: 'workspace',
|
|
@@ -150,16 +156,18 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
|
|
|
150
156
|
],
|
|
151
157
|
}))
|
|
152
158
|
|
|
153
|
-
if (typeof cursor === 'undefined') {
|
|
159
|
+
if (typeof cursor === 'undefined' && !cursorConfig?.mode) {
|
|
154
160
|
commandEcho.setInteractive()
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
commandEcho.addOption('--cursor', cursorMode)
|
|
158
164
|
|
|
159
165
|
const openInGithubDesktop =
|
|
160
|
-
githubDesktop ??
|
|
166
|
+
githubDesktop ??
|
|
167
|
+
config.worktrees?.openInGithubDesktop ??
|
|
168
|
+
(await confirm({ message: 'Open created worktrees in GitHub Desktop?' }))
|
|
161
169
|
|
|
162
|
-
if (typeof githubDesktop === 'undefined') {
|
|
170
|
+
if (typeof githubDesktop === 'undefined' && config.worktrees?.openInGithubDesktop === undefined) {
|
|
163
171
|
commandEcho.setInteractive()
|
|
164
172
|
}
|
|
165
173
|
|
|
@@ -169,9 +177,10 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
|
|
|
169
177
|
commandEcho.addOption('--no-github-desktop', true)
|
|
170
178
|
}
|
|
171
179
|
|
|
172
|
-
const openInCmux =
|
|
180
|
+
const openInCmux =
|
|
181
|
+
cmux ?? config.worktrees?.openInCmux ?? (await confirm({ message: 'Open created worktrees in cmux?' }))
|
|
173
182
|
|
|
174
|
-
if (typeof cmux === 'undefined') {
|
|
183
|
+
if (typeof cmux === 'undefined' && config.worktrees?.openInCmux === undefined) {
|
|
175
184
|
commandEcho.setInteractive()
|
|
176
185
|
}
|
|
177
186
|
|
|
@@ -191,11 +200,8 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
|
|
|
191
200
|
logResults(createdWorktrees)
|
|
192
201
|
|
|
193
202
|
if (cursorMode === 'workspace') {
|
|
194
|
-
const config = await getInfraKitConfig()
|
|
195
|
-
const cursorConfig = config.ide?.provider === 'cursor' ? config.ide.config : undefined
|
|
196
|
-
|
|
197
203
|
if (!cursorConfig?.workspaceConfigPath) {
|
|
198
|
-
logger.warn('⚠️ Skipping Cursor: ide.config.workspaceConfigPath is not set in infra-kit
|
|
204
|
+
logger.warn('⚠️ Skipping Cursor: ide.config.workspaceConfigPath is not set in infra-kit config')
|
|
199
205
|
} else {
|
|
200
206
|
const workspacePath = resolveCursorWorkspacePath(cursorConfig.workspaceConfigPath, projectRoot)
|
|
201
207
|
|
|
@@ -245,17 +251,15 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
|
|
|
245
251
|
}
|
|
246
252
|
|
|
247
253
|
return {
|
|
248
|
-
content:
|
|
249
|
-
{
|
|
250
|
-
type: 'text',
|
|
251
|
-
text: JSON.stringify(structuredContent, null, 2),
|
|
252
|
-
},
|
|
253
|
-
],
|
|
254
|
+
content: textContent(JSON.stringify(structuredContent, null, 2)),
|
|
254
255
|
structuredContent,
|
|
255
256
|
}
|
|
256
257
|
} catch (error) {
|
|
257
258
|
logger.error({ error }, '❌ Error managing worktrees')
|
|
258
|
-
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
|
+
})
|
|
259
263
|
}
|
|
260
264
|
}
|
|
261
265
|
|
|
@@ -310,8 +314,12 @@ const createWorktrees = async (branches: string[], worktreeDir: string): Promise
|
|
|
310
314
|
created.push(result.value)
|
|
311
315
|
} else {
|
|
312
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
|
+
})
|
|
313
321
|
|
|
314
|
-
logger.error({ error: result.reason
|
|
322
|
+
logger.error({ error: result.reason, msg: err.message })
|
|
315
323
|
}
|
|
316
324
|
}
|
|
317
325
|
|
|
@@ -334,7 +342,7 @@ const logResults = (created: string[]): void => {
|
|
|
334
342
|
}
|
|
335
343
|
|
|
336
344
|
// MCP Tool Registration
|
|
337
|
-
export const worktreesAddMcpTool = {
|
|
345
|
+
export const worktreesAddMcpTool = defineMcpTool({
|
|
338
346
|
name: 'worktrees-add',
|
|
339
347
|
description:
|
|
340
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.',
|
|
@@ -355,19 +363,19 @@ export const worktreesAddMcpTool = {
|
|
|
355
363
|
.enum(CURSOR_MODES)
|
|
356
364
|
.optional()
|
|
357
365
|
.describe(
|
|
358
|
-
'Cursor open mode for created worktrees. "workspace"
|
|
366
|
+
'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
367
|
),
|
|
360
368
|
githubDesktop: z
|
|
361
369
|
.boolean()
|
|
362
370
|
.optional()
|
|
363
371
|
.describe(
|
|
364
|
-
'Open each created worktree in GitHub Desktop.
|
|
372
|
+
'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
373
|
),
|
|
366
374
|
cmux: z
|
|
367
375
|
.boolean()
|
|
368
376
|
.optional()
|
|
369
377
|
.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.
|
|
378
|
+
'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
379
|
),
|
|
372
380
|
},
|
|
373
381
|
outputSchema: {
|
|
@@ -375,4 +383,4 @@ export const worktreesAddMcpTool = {
|
|
|
375
383
|
count: z.number().describe('Number of git worktrees created'),
|
|
376
384
|
},
|
|
377
385
|
handler: worktreesAdd,
|
|
378
|
-
}
|
|
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
|
+
})
|
|
@@ -9,16 +9,18 @@ import { removeFoldersFromCursorWorkspace, resolveCursorWorkspacePath } from 'sr
|
|
|
9
9
|
import { getReleasePRsWithInfo } from 'src/integrations/gh'
|
|
10
10
|
import { commandEcho } from 'src/lib/command-echo'
|
|
11
11
|
import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
|
|
12
|
+
import { OperationError } from 'src/lib/errors/operation-error'
|
|
12
13
|
import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
|
|
13
14
|
import { getInfraKitConfig } from 'src/lib/infra-kit-config'
|
|
14
15
|
import { logger } from 'src/lib/logger'
|
|
15
16
|
import { detectReleaseType, formatBranchChoices, getJiraDescriptions } from 'src/lib/release-utils'
|
|
16
17
|
import type { ReleaseType } from 'src/lib/release-utils'
|
|
17
|
-
import
|
|
18
|
+
import { defineMcpTool, textContent } from 'src/types'
|
|
19
|
+
import type { RequiredConfirmedOptionArg } from 'src/types'
|
|
18
20
|
|
|
19
21
|
// Constants
|
|
20
22
|
interface WorktreeManagementArgs extends RequiredConfirmedOptionArg {
|
|
21
|
-
all
|
|
23
|
+
all?: boolean
|
|
22
24
|
versions?: string
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -26,7 +28,7 @@ interface WorktreeManagementArgs extends RequiredConfirmedOptionArg {
|
|
|
26
28
|
* Manage git worktrees for release branches
|
|
27
29
|
* Creates worktrees for active release branches and removes unused ones
|
|
28
30
|
*/
|
|
29
|
-
export const worktreesRemove = async (options: WorktreeManagementArgs)
|
|
31
|
+
export const worktreesRemove = async (options: WorktreeManagementArgs) => {
|
|
30
32
|
const { confirmedCommand, all, versions } = options
|
|
31
33
|
|
|
32
34
|
commandEcho.start('worktrees-remove')
|
|
@@ -40,7 +42,7 @@ export const worktreesRemove = async (options: WorktreeManagementArgs): Promise<
|
|
|
40
42
|
commandEcho.print()
|
|
41
43
|
|
|
42
44
|
return {
|
|
43
|
-
content:
|
|
45
|
+
content: textContent(JSON.stringify({ removedWorktrees: [], count: 0 }, null, 2)),
|
|
44
46
|
structuredContent: { removedWorktrees: [], count: 0 },
|
|
45
47
|
}
|
|
46
48
|
}
|
|
@@ -131,17 +133,15 @@ export const worktreesRemove = async (options: WorktreeManagementArgs): Promise<
|
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
return {
|
|
134
|
-
content:
|
|
135
|
-
{
|
|
136
|
-
type: 'text',
|
|
137
|
-
text: JSON.stringify(structuredContent, null, 2),
|
|
138
|
-
},
|
|
139
|
-
],
|
|
136
|
+
content: textContent(JSON.stringify(structuredContent, null, 2)),
|
|
140
137
|
structuredContent,
|
|
141
138
|
}
|
|
142
139
|
} catch (error) {
|
|
143
140
|
logger.error({ error }, '❌ Error managing worktrees')
|
|
144
|
-
throw error
|
|
141
|
+
throw new OperationError(error, {
|
|
142
|
+
operation: 'remove worktrees',
|
|
143
|
+
remediation: "check 'git worktree list' for the path; uncommitted changes block removal",
|
|
144
|
+
})
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -179,8 +179,12 @@ const removeWorktrees = async (args: RemoveWorktreesArgs): Promise<string[]> =>
|
|
|
179
179
|
removed.push(result.value)
|
|
180
180
|
} else {
|
|
181
181
|
const branch = branches[index]
|
|
182
|
+
const err = new OperationError(result.reason, {
|
|
183
|
+
operation: `remove worktree for ${branch}`,
|
|
184
|
+
remediation: "check 'git worktree list' for the path; uncommitted changes block removal",
|
|
185
|
+
})
|
|
182
186
|
|
|
183
|
-
logger.error({ error: result.reason
|
|
187
|
+
logger.error({ error: result.reason, msg: err.message })
|
|
184
188
|
}
|
|
185
189
|
}
|
|
186
190
|
|
|
@@ -252,7 +256,7 @@ const logResults = (removed: string[]): void => {
|
|
|
252
256
|
}
|
|
253
257
|
|
|
254
258
|
// MCP Tool Registration
|
|
255
|
-
export const worktreesRemoveMcpTool = {
|
|
259
|
+
export const worktreesRemoveMcpTool = defineMcpTool({
|
|
256
260
|
name: 'worktrees-remove',
|
|
257
261
|
description:
|
|
258
262
|
'Remove local git worktrees for release branches. When everything is removed, also runs "git worktree prune" and deletes the worktrees directory. When invoked via MCP, pass either "versions" (comma-separated) or all=true — the branch picker is unreachable without a TTY, and the CLI confirmation is auto-skipped for MCP calls, so the caller is responsible for gating.',
|
|
@@ -275,4 +279,4 @@ export const worktreesRemoveMcpTool = {
|
|
|
275
279
|
count: z.number().describe('Number of git worktrees removed'),
|
|
276
280
|
},
|
|
277
281
|
handler: worktreesRemove,
|
|
278
|
-
}
|
|
282
|
+
})
|