infra-kit 0.1.93 → 0.1.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintcache +1 -1
- package/.turbo/turbo-eslint-check.log +4 -5
- package/.turbo/turbo-prettier-check.log +7 -5
- package/.turbo/turbo-test.log +82 -18
- package/.turbo/turbo-ts-check.log +10 -5
- package/dist/cli.js +37 -34
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +30 -27
- package/dist/mcp.js.map +4 -4
- package/package.json +2 -2
- package/src/commands/doctor/doctor.ts +119 -1
- package/src/commands/env-clear/env-clear.ts +1 -1
- package/src/commands/env-list/env-list.ts +1 -1
- package/src/commands/env-load/env-load.ts +1 -1
- package/src/commands/env-status/env-status.ts +1 -1
- package/src/commands/gh-merge-dev/gh-merge-dev.ts +1 -1
- package/src/commands/gh-release-deliver/gh-release-deliver.ts +1 -1
- package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +1 -1
- package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +1 -1
- package/src/commands/gh-release-list/gh-release-list.ts +1 -1
- package/src/commands/init/init.ts +3 -3
- package/src/commands/release-create/release-create.ts +1 -1
- package/src/commands/release-create-batch/release-create-batch.ts +1 -1
- package/src/commands/version/version.ts +1 -1
- package/src/commands/worktrees-add/index.ts +2 -1
- package/src/commands/worktrees-add/worktrees-add.ts +55 -14
- package/src/commands/worktrees-list/worktrees-list.ts +1 -1
- package/src/commands/worktrees-remove/worktrees-remove.ts +68 -4
- package/src/commands/worktrees-sync/worktrees-sync.ts +69 -5
- package/src/entry/cli.ts +25 -4
- package/src/integrations/clickup/.gitkeep +0 -0
- package/src/integrations/cmux/close-workspace-by-title.ts +51 -0
- package/src/integrations/cmux/index.ts +2 -0
- package/src/integrations/cmux/workspace-title.ts +17 -0
- package/src/integrations/cursor/add-folders-to-workspace.ts +84 -0
- package/src/integrations/cursor/index.ts +3 -0
- package/src/integrations/cursor/remove-folders-from-workspace.ts +93 -0
- package/src/integrations/cursor/resolve-workspace-path.ts +13 -0
- package/src/integrations/doppler/doppler-project.ts +2 -2
- package/src/lib/__tests__/infra-kit-config.test.ts +64 -14
- package/src/lib/infra-kit-config/index.ts +2 -0
- package/src/lib/infra-kit-config/infra-kit-config.ts +143 -0
- package/tsconfig.json +3 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/src/lib/infra-kit-config.ts +0 -69
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infra-kit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.95",
|
|
5
5
|
"description": "infra-kit",
|
|
6
6
|
"main": "dist/cli.js",
|
|
7
7
|
"module": "dist/cli.js",
|
|
@@ -45,6 +45,6 @@
|
|
|
45
45
|
"@pkg/eslint-config": "workspace:*",
|
|
46
46
|
"@pkg/vitest-config": "workspace:*",
|
|
47
47
|
"esbuild": "^0.28.0",
|
|
48
|
-
"typescript": "
|
|
48
|
+
"typescript": "catalog:"
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { z } from 'zod/v4'
|
|
2
5
|
import { $ } from 'zx'
|
|
3
6
|
|
|
7
|
+
import { MARKER_END, MARKER_START, buildShellBlock } from 'src/commands/init/init'
|
|
8
|
+
import { getProjectRoot } from 'src/lib/git-utils/git-utils'
|
|
9
|
+
import { getInfraKitConfig, resetInfraKitConfigCache } from 'src/lib/infra-kit-config'
|
|
4
10
|
import { logger } from 'src/lib/logger'
|
|
5
11
|
import type { ToolsExecutionResult } from 'src/types'
|
|
6
12
|
|
|
13
|
+
const LOCAL_CONFIG_FILE = 'infra-kit.local.yml'
|
|
14
|
+
|
|
7
15
|
interface CheckResult {
|
|
8
16
|
name: string
|
|
9
17
|
status: 'pass' | 'fail'
|
|
@@ -25,6 +33,112 @@ const checkCommand = async (
|
|
|
25
33
|
}
|
|
26
34
|
}
|
|
27
35
|
|
|
36
|
+
const checkZshrcInitialized = (): CheckResult => {
|
|
37
|
+
const name = 'zshrc init block'
|
|
38
|
+
const zshrcPath = path.join(os.homedir(), '.zshrc')
|
|
39
|
+
|
|
40
|
+
if (!fs.existsSync(zshrcPath)) {
|
|
41
|
+
return { name, status: 'fail', message: '~/.zshrc not found. Run: infra-kit init' }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const content = fs.readFileSync(zshrcPath, 'utf-8')
|
|
45
|
+
const startIdx = content.indexOf(MARKER_START)
|
|
46
|
+
const endIdx = content.indexOf(MARKER_END)
|
|
47
|
+
|
|
48
|
+
if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
status: 'fail',
|
|
52
|
+
message: 'infra-kit shell block missing from ~/.zshrc. Run: infra-kit init',
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const installedBlock = content.slice(startIdx, endIdx + MARKER_END.length).trim()
|
|
57
|
+
const expectedBlock = buildShellBlock().trim()
|
|
58
|
+
|
|
59
|
+
if (installedBlock !== expectedBlock) {
|
|
60
|
+
return {
|
|
61
|
+
name,
|
|
62
|
+
status: 'fail',
|
|
63
|
+
message: 'infra-kit shell block in ~/.zshrc is out of date. Run: infra-kit init',
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { name, status: 'pass', message: 'infra-kit shell block in ~/.zshrc is up to date' }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const checkPnpmWorkspaceVirtualStore = async (): Promise<CheckResult> => {
|
|
71
|
+
const name = 'pnpm enableGlobalVirtualStore'
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const root = await getProjectRoot()
|
|
75
|
+
const yamlPath = path.join(root, 'pnpm-workspace.yaml')
|
|
76
|
+
|
|
77
|
+
if (!fs.existsSync(yamlPath)) {
|
|
78
|
+
return { name, status: 'fail', message: `pnpm-workspace.yaml not found at ${yamlPath}` }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const content = fs.readFileSync(yamlPath, 'utf-8')
|
|
82
|
+
// eslint-disable-next-line sonarjs/slow-regex
|
|
83
|
+
const enabled = /^\s*enableGlobalVirtualStore\s*:\s*true\s*$/m.test(content)
|
|
84
|
+
|
|
85
|
+
if (!enabled) {
|
|
86
|
+
return {
|
|
87
|
+
name,
|
|
88
|
+
status: 'fail',
|
|
89
|
+
message: 'enableGlobalVirtualStore: true is missing in pnpm-workspace.yaml',
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { name, status: 'pass', message: 'enableGlobalVirtualStore: true is set' }
|
|
94
|
+
} catch (err) {
|
|
95
|
+
return {
|
|
96
|
+
name,
|
|
97
|
+
status: 'fail',
|
|
98
|
+
message: `Failed to read pnpm-workspace.yaml: ${(err as Error).message}`,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const checkInfraKitConfigValid = async (): Promise<CheckResult> => {
|
|
104
|
+
const name = 'infra-kit config valid'
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
resetInfraKitConfigCache()
|
|
108
|
+
await getInfraKitConfig()
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
name,
|
|
112
|
+
status: 'pass',
|
|
113
|
+
message: 'infra-kit.yml is valid (infra-kit.local.yml overrides applied if present)',
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return { name, status: 'fail', message: (err as Error).message }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const checkLocalConfigGitignored = async (): Promise<CheckResult> => {
|
|
121
|
+
const name = 'infra-kit.local.yml gitignored'
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const root = await getProjectRoot()
|
|
125
|
+
|
|
126
|
+
await $({ cwd: root, nothrow: true })`git check-ignore -q ${LOCAL_CONFIG_FILE}`.then((result) => {
|
|
127
|
+
if (result.exitCode !== 0) {
|
|
128
|
+
throw new Error('not ignored')
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
return { name, status: 'pass', message: `${LOCAL_CONFIG_FILE} is covered by .gitignore` }
|
|
133
|
+
} catch {
|
|
134
|
+
return {
|
|
135
|
+
name,
|
|
136
|
+
status: 'fail',
|
|
137
|
+
message: `${LOCAL_CONFIG_FILE} is not gitignored. Add "${LOCAL_CONFIG_FILE}" to .gitignore.`,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
28
142
|
/**
|
|
29
143
|
* Check installation and authentication status of gh, doppler, and aws CLIs
|
|
30
144
|
*/
|
|
@@ -67,6 +181,10 @@ export const doctor = async (): Promise<ToolsExecutionResult> => {
|
|
|
67
181
|
// 'AWS CLI is authenticated',
|
|
68
182
|
// 'AWS CLI is not authenticated. Run: aws configure (or aws sso login)',
|
|
69
183
|
// ),
|
|
184
|
+
Promise.resolve(checkZshrcInitialized()),
|
|
185
|
+
checkPnpmWorkspaceVirtualStore(),
|
|
186
|
+
checkInfraKitConfigValid(),
|
|
187
|
+
checkLocalConfigGitignored(),
|
|
70
188
|
])
|
|
71
189
|
|
|
72
190
|
logger.info('Doctor check results:\n')
|
|
@@ -3,7 +3,7 @@ import { Buffer } from 'node:buffer'
|
|
|
3
3
|
import fs from 'node:fs'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import process from 'node:process'
|
|
6
|
-
import { z } from 'zod'
|
|
6
|
+
import { z } from 'zod/v4'
|
|
7
7
|
import { $ } from 'zx'
|
|
8
8
|
|
|
9
9
|
import { validateDopplerCliAndAuth } from 'src/integrations/doppler'
|
|
@@ -4,7 +4,7 @@ import fs from 'node:fs/promises'
|
|
|
4
4
|
import { resolve } from 'node:path'
|
|
5
5
|
import process from 'node:process'
|
|
6
6
|
import yaml from 'yaml'
|
|
7
|
-
import { z } from 'zod'
|
|
7
|
+
import { z } from 'zod/v4'
|
|
8
8
|
import { $ } from 'zx'
|
|
9
9
|
|
|
10
10
|
import { getReleasePRsWithInfo } from 'src/integrations/gh'
|
|
@@ -4,8 +4,8 @@ import path from 'node:path'
|
|
|
4
4
|
|
|
5
5
|
import { logger } from 'src/lib/logger'
|
|
6
6
|
|
|
7
|
-
const MARKER_START = '# -- infra-kit:begin --'
|
|
8
|
-
const MARKER_END = '# -- infra-kit:end --'
|
|
7
|
+
export const MARKER_START = '# -- infra-kit:begin --'
|
|
8
|
+
export const MARKER_END = '# -- infra-kit:end --'
|
|
9
9
|
|
|
10
10
|
const LEGACY_PAIRED: [start: string, end: string][] = [['# region infra-kit', '# endregion infra-kit']]
|
|
11
11
|
const LEGACY_SINGLE = '# infra-kit shell functions'
|
|
@@ -94,7 +94,7 @@ const removeExistingBlock = (content: string): string => {
|
|
|
94
94
|
return before + (remaining ? `\n${remaining}` : '')
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
const buildShellBlock = (): string => {
|
|
97
|
+
export const buildShellBlock = (): string => {
|
|
98
98
|
const runCmd = 'pnpm exec infra-kit'
|
|
99
99
|
|
|
100
100
|
return [
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { worktreesAdd, worktreesAddMcpTool } from './worktrees-add'
|
|
1
|
+
export { CURSOR_MODES, worktreesAdd, worktreesAddMcpTool } from './worktrees-add'
|
|
2
|
+
export type { CursorMode } from './worktrees-add'
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/* eslint-disable sonarjs/cognitive-complexity */
|
|
2
2
|
import checkbox from '@inquirer/checkbox'
|
|
3
3
|
import confirm from '@inquirer/confirm'
|
|
4
|
+
import select from '@inquirer/select'
|
|
4
5
|
import process from 'node:process'
|
|
5
|
-
import { z } from 'zod'
|
|
6
|
+
import { z } from 'zod/v4'
|
|
6
7
|
import { $ } from 'zx'
|
|
7
8
|
|
|
8
|
-
import { openCmuxWorkspaceWithLayout } from 'src/integrations/cmux'
|
|
9
|
+
import { buildCmuxWorkspaceTitle, openCmuxWorkspaceWithLayout } from 'src/integrations/cmux'
|
|
10
|
+
import { addFoldersToCursorWorkspace, resolveCursorWorkspacePath } from 'src/integrations/cursor'
|
|
9
11
|
import { getReleasePRsWithInfo } from 'src/integrations/gh'
|
|
10
12
|
import { commandEcho } from 'src/lib/command-echo'
|
|
11
13
|
import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
|
|
12
14
|
import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
|
|
15
|
+
import { getInfraKitConfig } from 'src/lib/infra-kit-config'
|
|
13
16
|
import { logger } from 'src/lib/logger'
|
|
14
17
|
import { detectReleaseType, formatBranchChoices, getJiraDescriptions } from 'src/lib/release-utils'
|
|
15
18
|
import type { ReleaseType } from 'src/lib/release-utils'
|
|
@@ -20,10 +23,13 @@ const FEATURE_DIR = 'feature'
|
|
|
20
23
|
const RELEASE_DIR = 'release'
|
|
21
24
|
const RELEASE_BRANCH_PREFIX = 'release/v'
|
|
22
25
|
|
|
26
|
+
export const CURSOR_MODES = ['workspace', 'windows', 'none'] as const
|
|
27
|
+
export type CursorMode = (typeof CURSOR_MODES)[number]
|
|
28
|
+
|
|
23
29
|
interface WorktreeManagementArgs extends RequiredConfirmedOptionArg {
|
|
24
30
|
all: boolean
|
|
25
31
|
versions?: string
|
|
26
|
-
cursor?:
|
|
32
|
+
cursor?: CursorMode
|
|
27
33
|
githubDesktop?: boolean
|
|
28
34
|
cmux?: boolean
|
|
29
35
|
}
|
|
@@ -124,17 +130,31 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
|
|
|
124
130
|
commandEcho.addOption('--yes', true)
|
|
125
131
|
}
|
|
126
132
|
|
|
127
|
-
const
|
|
133
|
+
const cursorMode: CursorMode =
|
|
134
|
+
cursor ??
|
|
135
|
+
(await select<CursorMode>({
|
|
136
|
+
message: 'Cursor mode for created worktrees?',
|
|
137
|
+
default: 'workspace',
|
|
138
|
+
choices: [
|
|
139
|
+
{
|
|
140
|
+
name: 'Add to workspace file',
|
|
141
|
+
value: 'workspace',
|
|
142
|
+
description: 'Append each worktree as a folder in ide.config.workspaceConfigPath, then open the workspace',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'Open separate windows',
|
|
146
|
+
value: 'windows',
|
|
147
|
+
description: 'Open each created worktree in its own Cursor window',
|
|
148
|
+
},
|
|
149
|
+
{ name: 'Skip', value: 'none', description: 'Do not open Cursor' },
|
|
150
|
+
],
|
|
151
|
+
}))
|
|
128
152
|
|
|
129
153
|
if (typeof cursor === 'undefined') {
|
|
130
154
|
commandEcho.setInteractive()
|
|
131
155
|
}
|
|
132
156
|
|
|
133
|
-
|
|
134
|
-
commandEcho.addOption('--cursor', true)
|
|
135
|
-
} else {
|
|
136
|
-
commandEcho.addOption('--no-cursor', true)
|
|
137
|
-
}
|
|
157
|
+
commandEcho.addOption('--cursor', cursorMode)
|
|
138
158
|
|
|
139
159
|
const openInGithubDesktop =
|
|
140
160
|
githubDesktop ?? (await confirm({ message: 'Open created worktrees in GitHub Desktop?' }))
|
|
@@ -170,7 +190,28 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
|
|
|
170
190
|
|
|
171
191
|
logResults(createdWorktrees)
|
|
172
192
|
|
|
173
|
-
if (
|
|
193
|
+
if (cursorMode === 'workspace') {
|
|
194
|
+
const config = await getInfraKitConfig()
|
|
195
|
+
const cursorConfig = config.ide?.provider === 'cursor' ? config.ide.config : undefined
|
|
196
|
+
|
|
197
|
+
if (!cursorConfig?.workspaceConfigPath) {
|
|
198
|
+
logger.warn('⚠️ Skipping Cursor: ide.config.workspaceConfigPath is not set in infra-kit.yml')
|
|
199
|
+
} else {
|
|
200
|
+
const workspacePath = resolveCursorWorkspacePath(cursorConfig.workspaceConfigPath, projectRoot)
|
|
201
|
+
|
|
202
|
+
const folderPaths = createdWorktrees.map((branch) => {
|
|
203
|
+
return `${worktreeDir}/${branch}`
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const { added, skipped } = await addFoldersToCursorWorkspace({ workspacePath, folderPaths })
|
|
207
|
+
|
|
208
|
+
const skippedSuffix = skipped.length > 0 ? ` (${skipped.length} already present)` : ''
|
|
209
|
+
|
|
210
|
+
logger.info(`✅ Added ${added.length} folder(s) to ${workspacePath}${skippedSuffix}`)
|
|
211
|
+
|
|
212
|
+
await $`cursor ${workspacePath}`
|
|
213
|
+
}
|
|
214
|
+
} else if (cursorMode === 'windows') {
|
|
174
215
|
for (const branch of createdWorktrees) {
|
|
175
216
|
await $`cursor ${worktreeDir}/${branch}`
|
|
176
217
|
}
|
|
@@ -187,11 +228,11 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
|
|
|
187
228
|
const repoName = await getRepoName()
|
|
188
229
|
|
|
189
230
|
for (const branch of createdWorktrees) {
|
|
190
|
-
const
|
|
231
|
+
const title = buildCmuxWorkspaceTitle({ repoName, branch })
|
|
191
232
|
|
|
192
233
|
await openCmuxWorkspaceWithLayout({
|
|
193
234
|
cwd: `${worktreeDir}/${branch}`,
|
|
194
|
-
title
|
|
235
|
+
title,
|
|
195
236
|
})
|
|
196
237
|
}
|
|
197
238
|
}
|
|
@@ -311,10 +352,10 @@ export const worktreesAddMcpTool = {
|
|
|
311
352
|
'Comma-separated release versions to target (e.g. "1.2.5, 1.2.6"). Either "versions" or all=true must be provided for MCP calls. Overrides "all" when set.',
|
|
312
353
|
),
|
|
313
354
|
cursor: z
|
|
314
|
-
.
|
|
355
|
+
.enum(CURSOR_MODES)
|
|
315
356
|
.optional()
|
|
316
357
|
.describe(
|
|
317
|
-
'
|
|
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).',
|
|
318
359
|
),
|
|
319
360
|
githubDesktop: z
|
|
320
361
|
.boolean()
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import checkbox from '@inquirer/checkbox'
|
|
2
2
|
import confirm from '@inquirer/confirm'
|
|
3
3
|
import process from 'node:process'
|
|
4
|
-
import { z } from 'zod'
|
|
4
|
+
import { z } from 'zod/v4'
|
|
5
5
|
import { $ } from 'zx'
|
|
6
6
|
|
|
7
|
+
import { buildCmuxWorkspaceTitle, closeCmuxWorkspaceByTitle } from 'src/integrations/cmux'
|
|
8
|
+
import { removeFoldersFromCursorWorkspace, resolveCursorWorkspacePath } from 'src/integrations/cursor'
|
|
7
9
|
import { getReleasePRsWithInfo } from 'src/integrations/gh'
|
|
8
10
|
import { commandEcho } from 'src/lib/command-echo'
|
|
9
11
|
import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
|
|
10
|
-
import { getCurrentWorktrees, getProjectRoot } from 'src/lib/git-utils'
|
|
12
|
+
import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
|
|
13
|
+
import { getInfraKitConfig } from 'src/lib/infra-kit-config'
|
|
11
14
|
import { logger } from 'src/lib/logger'
|
|
12
15
|
import { detectReleaseType, formatBranchChoices, getJiraDescriptions } from 'src/lib/release-utils'
|
|
13
16
|
import type { ReleaseType } from 'src/lib/release-utils'
|
|
@@ -107,7 +110,15 @@ export const worktreesRemove = async (options: WorktreeManagementArgs): Promise<
|
|
|
107
110
|
commandEcho.addOption('--yes', true)
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
const
|
|
113
|
+
const repoName = await getRepoName()
|
|
114
|
+
|
|
115
|
+
const removedWorktrees = await removeWorktrees({
|
|
116
|
+
branches: selectedReleaseBranches,
|
|
117
|
+
worktreeDir,
|
|
118
|
+
repoName,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
await syncCursorWorkspaceOnRemove({ removedWorktrees, worktreeDir, projectRoot })
|
|
111
122
|
|
|
112
123
|
logResults(removedWorktrees)
|
|
113
124
|
|
|
@@ -133,14 +144,26 @@ export const worktreesRemove = async (options: WorktreeManagementArgs): Promise<
|
|
|
133
144
|
}
|
|
134
145
|
}
|
|
135
146
|
|
|
147
|
+
interface RemoveWorktreesArgs {
|
|
148
|
+
branches: string[]
|
|
149
|
+
worktreeDir: string
|
|
150
|
+
repoName: string
|
|
151
|
+
}
|
|
152
|
+
|
|
136
153
|
/**
|
|
137
154
|
* Remove worktrees for the specified branches and whole folder
|
|
138
155
|
*/
|
|
139
|
-
const removeWorktrees = async (
|
|
156
|
+
const removeWorktrees = async (args: RemoveWorktreesArgs): Promise<string[]> => {
|
|
157
|
+
const { branches, worktreeDir, repoName } = args
|
|
158
|
+
|
|
140
159
|
const results = await Promise.allSettled(
|
|
141
160
|
branches.map(async (branch) => {
|
|
142
161
|
const worktreePath = `${worktreeDir}/${branch}`
|
|
143
162
|
|
|
163
|
+
const title = buildCmuxWorkspaceTitle({ repoName, branch })
|
|
164
|
+
|
|
165
|
+
await closeCmuxWorkspaceByTitle(title)
|
|
166
|
+
|
|
144
167
|
await $`git worktree remove ${worktreePath}`
|
|
145
168
|
|
|
146
169
|
return branch
|
|
@@ -170,6 +193,47 @@ const removeWorktrees = async (branches: string[], worktreeDir: string): Promise
|
|
|
170
193
|
return removed
|
|
171
194
|
}
|
|
172
195
|
|
|
196
|
+
interface SyncCursorWorkspaceOnRemoveArgs {
|
|
197
|
+
removedWorktrees: string[]
|
|
198
|
+
worktreeDir: string
|
|
199
|
+
projectRoot: string
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Strip removed worktrees from the configured Cursor workspace's `folders` array.
|
|
204
|
+
* No-op if Cursor isn't configured, mode isn't "workspace", or no worktrees were removed.
|
|
205
|
+
*/
|
|
206
|
+
const syncCursorWorkspaceOnRemove = async (args: SyncCursorWorkspaceOnRemoveArgs): Promise<void> => {
|
|
207
|
+
const { removedWorktrees, worktreeDir, projectRoot } = args
|
|
208
|
+
|
|
209
|
+
if (removedWorktrees.length === 0) {
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const config = await getInfraKitConfig()
|
|
214
|
+
const cursorConfig = config.ide?.provider === 'cursor' ? config.ide.config : undefined
|
|
215
|
+
|
|
216
|
+
if (!cursorConfig || cursorConfig.mode !== 'workspace' || !cursorConfig.workspaceConfigPath) {
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const workspacePath = resolveCursorWorkspacePath(cursorConfig.workspaceConfigPath, projectRoot)
|
|
221
|
+
|
|
222
|
+
const folderPaths = removedWorktrees.map((branch) => {
|
|
223
|
+
return `${worktreeDir}/${branch}`
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const { removed } = await removeFoldersFromCursorWorkspace({ workspacePath, folderPaths })
|
|
228
|
+
|
|
229
|
+
if (removed.length > 0) {
|
|
230
|
+
logger.info(`✅ Removed ${removed.length} folder(s) from ${workspacePath}`)
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
logger.warn({ error }, `⚠️ Failed to update Cursor workspace at ${workspacePath}`)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
173
237
|
/**
|
|
174
238
|
* Log the results of worktree management
|
|
175
239
|
*/
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import confirm from '@inquirer/confirm'
|
|
2
2
|
import process from 'node:process'
|
|
3
|
-
import { z } from 'zod'
|
|
3
|
+
import { z } from 'zod/v4'
|
|
4
4
|
import { $ } from 'zx'
|
|
5
5
|
|
|
6
|
+
import { buildCmuxWorkspaceTitle, closeCmuxWorkspaceByTitle } from 'src/integrations/cmux'
|
|
7
|
+
import { removeFoldersFromCursorWorkspace, resolveCursorWorkspacePath } from 'src/integrations/cursor'
|
|
6
8
|
import { getReleasePRs } from 'src/integrations/gh'
|
|
7
9
|
import { commandEcho } from 'src/lib/command-echo'
|
|
8
10
|
import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
|
|
9
|
-
import { getCurrentWorktrees, getProjectRoot } from 'src/lib/git-utils'
|
|
11
|
+
import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
|
|
12
|
+
import { getInfraKitConfig } from 'src/lib/infra-kit-config'
|
|
10
13
|
import { logger } from 'src/lib/logger'
|
|
11
14
|
import type { RequiredConfirmedOptionArg, ToolsExecutionResult } from 'src/types'
|
|
12
15
|
|
|
@@ -59,7 +62,15 @@ export const worktreesSync = async (options: WorktreeSyncArgs): Promise<ToolsExe
|
|
|
59
62
|
currentWorktrees,
|
|
60
63
|
})
|
|
61
64
|
|
|
62
|
-
const
|
|
65
|
+
const repoName = await getRepoName()
|
|
66
|
+
|
|
67
|
+
const removedWorktrees = await removeWorktrees({
|
|
68
|
+
branches: branchesToRemove,
|
|
69
|
+
worktreeDir,
|
|
70
|
+
repoName,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
await syncCursorWorkspaceOnRemove({ removedWorktrees, worktreeDir, projectRoot })
|
|
63
74
|
|
|
64
75
|
logResults(removedWorktrees)
|
|
65
76
|
|
|
@@ -107,16 +118,28 @@ const categorizeWorktrees = (args: CategorizeWorktreesArgs): { branchesToRemove:
|
|
|
107
118
|
return { branchesToRemove }
|
|
108
119
|
}
|
|
109
120
|
|
|
121
|
+
interface RemoveWorktreesArgs {
|
|
122
|
+
branches: string[]
|
|
123
|
+
worktreeDir: string
|
|
124
|
+
repoName: string
|
|
125
|
+
}
|
|
126
|
+
|
|
110
127
|
/**
|
|
111
|
-
* Remove worktrees for the specified branches
|
|
128
|
+
* Remove worktrees for the specified branches and close their cmux workspaces
|
|
112
129
|
*/
|
|
113
|
-
const removeWorktrees = async (
|
|
130
|
+
const removeWorktrees = async (args: RemoveWorktreesArgs): Promise<string[]> => {
|
|
131
|
+
const { branches, worktreeDir, repoName } = args
|
|
132
|
+
|
|
114
133
|
const removed: string[] = []
|
|
115
134
|
|
|
116
135
|
for (const branch of branches) {
|
|
117
136
|
try {
|
|
118
137
|
const worktreePath = `${worktreeDir}/${branch}`
|
|
119
138
|
|
|
139
|
+
const title = buildCmuxWorkspaceTitle({ repoName, branch })
|
|
140
|
+
|
|
141
|
+
await closeCmuxWorkspaceByTitle(title)
|
|
142
|
+
|
|
120
143
|
await $`git worktree remove ${worktreePath}`
|
|
121
144
|
removed.push(branch)
|
|
122
145
|
} catch (error) {
|
|
@@ -127,6 +150,47 @@ const removeWorktrees = async (branches: string[], worktreeDir: string): Promise
|
|
|
127
150
|
return removed
|
|
128
151
|
}
|
|
129
152
|
|
|
153
|
+
interface SyncCursorWorkspaceOnRemoveArgs {
|
|
154
|
+
removedWorktrees: string[]
|
|
155
|
+
worktreeDir: string
|
|
156
|
+
projectRoot: string
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Strip removed worktrees from the configured Cursor workspace's `folders` array.
|
|
161
|
+
* No-op if Cursor isn't configured, mode isn't "workspace", or no worktrees were removed.
|
|
162
|
+
*/
|
|
163
|
+
const syncCursorWorkspaceOnRemove = async (args: SyncCursorWorkspaceOnRemoveArgs): Promise<void> => {
|
|
164
|
+
const { removedWorktrees, worktreeDir, projectRoot } = args
|
|
165
|
+
|
|
166
|
+
if (removedWorktrees.length === 0) {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const config = await getInfraKitConfig()
|
|
171
|
+
const cursorConfig = config.ide?.provider === 'cursor' ? config.ide.config : undefined
|
|
172
|
+
|
|
173
|
+
if (!cursorConfig || cursorConfig.mode !== 'workspace' || !cursorConfig.workspaceConfigPath) {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const workspacePath = resolveCursorWorkspacePath(cursorConfig.workspaceConfigPath, projectRoot)
|
|
178
|
+
|
|
179
|
+
const folderPaths = removedWorktrees.map((branch) => {
|
|
180
|
+
return `${worktreeDir}/${branch}`
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const { removed: removedEntries } = await removeFoldersFromCursorWorkspace({ workspacePath, folderPaths })
|
|
185
|
+
|
|
186
|
+
if (removedEntries.length > 0) {
|
|
187
|
+
logger.info(`✅ Removed ${removedEntries.length} folder(s) from ${workspacePath}`)
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
logger.warn({ error }, `⚠️ Failed to update Cursor workspace at ${workspacePath}`)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
130
194
|
/**
|
|
131
195
|
* Log the results of worktree management
|
|
132
196
|
*/
|