infra-kit 0.1.94 → 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 +36 -34
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +28 -26
- 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 +52 -11
- package/src/commands/worktrees-list/worktrees-list.ts +1 -1
- package/src/commands/worktrees-remove/worktrees-remove.ts +46 -1
- 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/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
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
|
}
|
|
@@ -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,14 +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
7
|
import { buildCmuxWorkspaceTitle, closeCmuxWorkspaceByTitle } from 'src/integrations/cmux'
|
|
8
|
+
import { removeFoldersFromCursorWorkspace, resolveCursorWorkspacePath } from 'src/integrations/cursor'
|
|
8
9
|
import { getReleasePRsWithInfo } from 'src/integrations/gh'
|
|
9
10
|
import { commandEcho } from 'src/lib/command-echo'
|
|
10
11
|
import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
|
|
11
12
|
import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
|
|
13
|
+
import { getInfraKitConfig } from 'src/lib/infra-kit-config'
|
|
12
14
|
import { logger } from 'src/lib/logger'
|
|
13
15
|
import { detectReleaseType, formatBranchChoices, getJiraDescriptions } from 'src/lib/release-utils'
|
|
14
16
|
import type { ReleaseType } from 'src/lib/release-utils'
|
|
@@ -116,6 +118,8 @@ export const worktreesRemove = async (options: WorktreeManagementArgs): Promise<
|
|
|
116
118
|
repoName,
|
|
117
119
|
})
|
|
118
120
|
|
|
121
|
+
await syncCursorWorkspaceOnRemove({ removedWorktrees, worktreeDir, projectRoot })
|
|
122
|
+
|
|
119
123
|
logResults(removedWorktrees)
|
|
120
124
|
|
|
121
125
|
commandEcho.print()
|
|
@@ -189,6 +193,47 @@ const removeWorktrees = async (args: RemoveWorktreesArgs): Promise<string[]> =>
|
|
|
189
193
|
return removed
|
|
190
194
|
}
|
|
191
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
|
+
|
|
192
237
|
/**
|
|
193
238
|
* Log the results of worktree management
|
|
194
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
|
*/
|
package/src/entry/cli.ts
CHANGED
|
@@ -16,7 +16,8 @@ import { init } from 'src/commands/init'
|
|
|
16
16
|
import { releaseCreate } from 'src/commands/release-create'
|
|
17
17
|
import { releaseCreateBatch } from 'src/commands/release-create-batch'
|
|
18
18
|
import { version } from 'src/commands/version'
|
|
19
|
-
import { worktreesAdd } from 'src/commands/worktrees-add'
|
|
19
|
+
import { CURSOR_MODES, worktreesAdd } from 'src/commands/worktrees-add'
|
|
20
|
+
import type { CursorMode } from 'src/commands/worktrees-add'
|
|
20
21
|
import { worktreesList } from 'src/commands/worktrees-list'
|
|
21
22
|
import { worktreesRemove } from 'src/commands/worktrees-remove'
|
|
22
23
|
import { worktreesSync } from 'src/commands/worktrees-sync'
|
|
@@ -24,6 +25,26 @@ import { logger } from 'src/lib/logger'
|
|
|
24
25
|
|
|
25
26
|
const program = new Command()
|
|
26
27
|
|
|
28
|
+
const normalizeCursorMode = (value: unknown): CursorMode | undefined => {
|
|
29
|
+
if (typeof value === 'undefined') {
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (value === true) {
|
|
34
|
+
return 'workspace'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (value === false) {
|
|
38
|
+
return 'none'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof value === 'string' && (CURSOR_MODES as readonly string[]).includes(value)) {
|
|
42
|
+
return value as CursorMode
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
throw new Error(`Invalid --cursor value "${String(value)}". Expected one of: ${CURSOR_MODES.join(', ')}.`)
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
const runProgram = async (argv?: string[]): Promise<void> => {
|
|
28
49
|
try {
|
|
29
50
|
if (argv) {
|
|
@@ -136,8 +157,8 @@ program
|
|
|
136
157
|
.option('-y, --yes', 'Skip confirmation prompt')
|
|
137
158
|
.option('-a, --all', 'Select all active release branches')
|
|
138
159
|
.option('-v, --versions <versions>', 'Specify versions by comma, e.g. 1.2.5, 1.2.6')
|
|
139
|
-
.option('-c, --cursor', '
|
|
140
|
-
.option('--no-cursor', 'Skip Cursor
|
|
160
|
+
.option('-c, --cursor [mode]', 'Cursor mode for created worktrees: workspace (default) | windows | none')
|
|
161
|
+
.option('--no-cursor', 'Skip Cursor (alias for --cursor none)')
|
|
141
162
|
.option('-g, --github-desktop', 'Open created worktrees in GitHub Desktop')
|
|
142
163
|
.option('--no-github-desktop', 'Skip GitHub Desktop prompt')
|
|
143
164
|
.option('-m, --cmux', 'Open created worktrees in cmux (3-pane layout)')
|
|
@@ -147,7 +168,7 @@ program
|
|
|
147
168
|
confirmedCommand: options.yes,
|
|
148
169
|
all: options.all,
|
|
149
170
|
versions: options.versions,
|
|
150
|
-
cursor: options.cursor,
|
|
171
|
+
cursor: normalizeCursorMode(options.cursor),
|
|
151
172
|
githubDesktop: options.githubDesktop,
|
|
152
173
|
cmux: options.cmux,
|
|
153
174
|
})
|
|
File without changes
|