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.
Files changed (56) hide show
  1. package/.eslintcache +1 -1
  2. package/.omc/state/agent-replay-d367c3be-9c2a-48e7-bcea-b45861af568c.jsonl +2 -0
  3. package/.omc/state/agent-replay-f2846d8f-974c-486c-b16f-4bdaa28ca45f.jsonl +1 -0
  4. package/.omc/state/last-tool-error.json +7 -0
  5. package/.omc/state/subagent-tracking.json +7 -0
  6. package/.turbo/turbo-eslint-check.log +2 -4
  7. package/.turbo/turbo-eslint-fix.log +1 -0
  8. package/.turbo/turbo-prettier-check.log +2 -4
  9. package/.turbo/turbo-prettier-fix.log +1 -10
  10. package/.turbo/turbo-test.log +12 -191
  11. package/.turbo/turbo-ts-check.log +2 -9
  12. package/dist/cli.js +69 -44
  13. package/dist/cli.js.map +4 -4
  14. package/dist/mcp.js +45 -34
  15. package/dist/mcp.js.map +4 -4
  16. package/package.json +11 -11
  17. package/src/commands/config/config.ts +1 -1
  18. package/src/commands/doctor/doctor.ts +62 -12
  19. package/src/commands/env-clear/env-clear.ts +5 -10
  20. package/src/commands/env-list/env-list.ts +5 -10
  21. package/src/commands/env-load/env-load.ts +5 -10
  22. package/src/commands/env-status/env-status.ts +5 -10
  23. package/src/commands/gh-merge-dev/gh-merge-dev.ts +17 -18
  24. package/src/commands/gh-release-deliver/gh-release-deliver.ts +290 -89
  25. package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +15 -14
  26. package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +30 -23
  27. package/src/commands/gh-release-list/gh-release-list.ts +5 -10
  28. package/src/commands/init/init.ts +17 -6
  29. package/src/commands/release-create/release-create.ts +223 -139
  30. package/src/commands/release-desc-edit/index.ts +1 -0
  31. package/src/commands/release-desc-edit/release-desc-edit.ts +207 -0
  32. package/src/commands/version/version.ts +5 -10
  33. package/src/commands/worktrees-add/worktrees-add.ts +34 -26
  34. package/src/commands/worktrees-list/worktrees-list.ts +6 -11
  35. package/src/commands/worktrees-open/worktrees-open.ts +10 -6
  36. package/src/commands/worktrees-remove/worktrees-remove.ts +18 -14
  37. package/src/commands/worktrees-sync/worktrees-sync.ts +17 -12
  38. package/src/entry/cli.ts +24 -21
  39. package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +21 -0
  40. package/src/integrations/gh/gh-release-prs/index.ts +1 -1
  41. package/src/integrations/gh/index.ts +1 -1
  42. package/src/integrations/jira/api.ts +8 -17
  43. package/src/integrations/jira/index.ts +2 -0
  44. package/src/lib/__tests__/infra-kit-config.test.ts +50 -0
  45. package/src/lib/errors/__tests__/operation-error.test.ts +62 -0
  46. package/src/lib/errors/format-zx-error.ts +54 -0
  47. package/src/lib/errors/operation-error.ts +80 -0
  48. package/src/lib/infra-kit-config/infra-kit-config.ts +7 -0
  49. package/src/lib/version-utils/__tests__/next-version.test.ts +128 -23
  50. package/src/lib/version-utils/index.ts +4 -2
  51. package/src/lib/version-utils/next-version.ts +64 -25
  52. package/src/mcp/tools/index.ts +2 -2
  53. package/src/types.ts +56 -2
  54. package/tsconfig.tsbuildinfo +1 -1
  55. package/src/commands/release-create-batch/index.ts +0 -1
  56. 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 type { ToolsExecutionResult } from 'src/types'
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 (): Promise<ToolsExecutionResult> => {
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 type { RequiredConfirmedOptionArg, ToolsExecutionResult } from 'src/types'
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: boolean
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): Promise<ToolsExecutionResult> => {
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: [{ type: 'text', text: JSON.stringify({ createdWorktrees: [], count: 0 }, null, 2) }],
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 ?? (await confirm({ message: 'Open created worktrees in GitHub Desktop?' }))
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 = cmux ?? (await confirm({ message: 'Open created worktrees in cmux?' }))
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.yml')
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 }, `❌ Failed to create worktree for ${branch}`)
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" (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).',
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. Defaults to false in MCP mode (the follow-up prompt is not shown).',
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. Defaults to false in MCP mode (the follow-up prompt is not shown).',
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 type { ToolsExecutionResult } from 'src/types'
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 (): Promise<ToolsExecutionResult> => {
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: [{ type: 'text', text: JSON.stringify({ worktrees: [], count: 0 }, null, 2) }],
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 type { ToolsExecutionResult } from 'src/types'
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 (): Promise<ToolsExecutionResult> => {
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: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
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 type { RequiredConfirmedOptionArg, ToolsExecutionResult } from 'src/types'
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: boolean
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): Promise<ToolsExecutionResult> => {
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: [{ type: 'text', text: JSON.stringify({ removedWorktrees: [], count: 0 }, null, 2) }],
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 }, `❌ Failed to remove worktree for ${branch}`)
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
+ })