infra-kit 0.1.111 → 0.1.113

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 (85) hide show
  1. package/.eslintcache +1 -1
  2. package/.omc/state/agent-replay-127837d3-a000-4656-9453-2324ab353723.jsonl +5 -0
  3. package/.omc/state/agent-replay-15c409af-24fa-4026-88d3-a82dd1e68b87.jsonl +11 -0
  4. package/.omc/state/agent-replay-74afa912-1443-4011-b4f2-db9f055cdd9f.jsonl +3 -0
  5. package/.omc/state/agent-replay-8dd0ad2f-3a90-4158-b9de-ec8e19a7c983.jsonl +3 -0
  6. package/.omc/state/idle-notif-cooldown.json +1 -1
  7. package/.omc/state/sessions/127837d3-a000-4656-9453-2324ab353723/mission-state.json +61 -0
  8. package/.omc/state/sessions/127837d3-a000-4656-9453-2324ab353723/pre-tool-advisory-throttle.json +22 -0
  9. package/.omc/state/sessions/127837d3-a000-4656-9453-2324ab353723/skill-active-state.json +15 -0
  10. package/.omc/state/sessions/127837d3-a000-4656-9453-2324ab353723/subagent-tracking-state.json +26 -0
  11. package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/last-tool-error-state.json +7 -0
  12. package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/mission-state.json +89 -0
  13. package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/pre-tool-advisory-throttle.json +34 -0
  14. package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/ralph-state.json +13 -0
  15. package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/skill-active-state.json +15 -0
  16. package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/subagent-tracking-state.json +44 -0
  17. package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/ultrawork-state.json +11 -0
  18. package/.omc/state/sessions/74afa912-1443-4011-b4f2-db9f055cdd9f/pre-tool-advisory-throttle.json +18 -0
  19. package/.omc/state/sessions/74afa912-1443-4011-b4f2-db9f055cdd9f/skill-active-state.json +15 -0
  20. package/.omc/state/sessions/74afa912-1443-4011-b4f2-db9f055cdd9f/subagent-tracking-state.json +17 -0
  21. package/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/pre-tool-advisory-throttle.json +14 -0
  22. package/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/ralph-state.json +13 -0
  23. package/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/subagent-tracking-state.json +17 -0
  24. package/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/ultrawork-state.json +11 -0
  25. package/.turbo/turbo-build.log +3 -4
  26. package/.turbo/turbo-eslint-check.log +1 -2
  27. package/.turbo/turbo-prettier-check.log +1 -2
  28. package/.turbo/turbo-prettier-fix.log +1 -2
  29. package/.turbo/turbo-test.log +25 -18
  30. package/.turbo/turbo-ts-check.log +1 -2
  31. package/dist/cli.js +64 -56
  32. package/dist/cli.js.map +4 -4
  33. package/dist/mcp.js +39 -39
  34. package/dist/mcp.js.map +4 -4
  35. package/package.json +1 -1
  36. package/src/.omc/state/agent-replay-8dd0ad2f-3a90-4158-b9de-ec8e19a7c983.jsonl +11 -0
  37. package/src/.omc/state/idle-notif-cooldown.json +3 -0
  38. package/src/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/mission-state.json +79 -0
  39. package/src/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/pre-tool-advisory-throttle.json +22 -0
  40. package/src/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/ralph-state.json +13 -0
  41. package/src/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/subagent-tracking-state.json +26 -0
  42. package/src/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/ultrawork-state.json +11 -0
  43. package/src/commands/doctor/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/pre-tool-advisory-throttle.json +14 -0
  44. package/src/commands/doctor/__tests__/check-ide-installed.test.ts +142 -0
  45. package/src/commands/doctor/doctor.ts +98 -1
  46. package/src/commands/gh-release-deliver/gh-release-deliver.ts +32 -1
  47. package/src/commands/init/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/pre-tool-advisory-throttle.json +18 -0
  48. package/src/commands/init/__tests__/migrate-config.test.ts +132 -1
  49. package/src/commands/init/init.ts +19 -6
  50. package/src/commands/init/migrate-config.ts +104 -0
  51. package/src/commands/worktrees-add/index.ts +1 -2
  52. package/src/commands/worktrees-add/worktrees-add.ts +24 -60
  53. package/src/commands/worktrees-open/worktrees-open.ts +55 -68
  54. package/src/commands/worktrees-reload/__tests__/reload-cmux.test.ts +119 -0
  55. package/src/commands/worktrees-reload/index.ts +1 -0
  56. package/src/commands/worktrees-reload/worktrees-reload.ts +229 -0
  57. package/src/commands/worktrees-remove/worktrees-remove.ts +2 -44
  58. package/src/commands/worktrees-sync/worktrees-sync.ts +2 -44
  59. package/src/entry/cli.ts +32 -11
  60. package/src/integrations/cursor/index.ts +1 -0
  61. package/src/integrations/cursor/open-cursor-workspace.ts +70 -0
  62. package/src/integrations/ide/__tests__/ide-facade.test.ts +242 -0
  63. package/src/integrations/ide/add-ide-worktree-folders.ts +79 -0
  64. package/src/integrations/ide/index.ts +6 -0
  65. package/src/integrations/ide/open-ide-workspace.ts +51 -0
  66. package/src/integrations/ide/provider-label.ts +24 -0
  67. package/src/integrations/ide/remove-ide-worktree-folders.ts +80 -0
  68. package/src/integrations/ide/types.ts +32 -0
  69. package/src/integrations/zed/__tests__/open-zed-workspace.test.ts +85 -0
  70. package/src/integrations/zed/add-folders-to-zed-workspace.ts +30 -0
  71. package/src/integrations/zed/index.ts +2 -0
  72. package/src/integrations/zed/open-zed-workspace.ts +57 -0
  73. package/src/lib/assert-never/assert-never.ts +15 -0
  74. package/src/lib/assert-never/index.ts +1 -0
  75. package/src/lib/git-utils/__tests__/git-primitives.test.ts +122 -1
  76. package/src/lib/git-utils/git-utils.ts +35 -0
  77. package/src/lib/git-utils/index.ts +2 -0
  78. package/src/lib/infra-kit-config/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/pre-tool-advisory-throttle.json +18 -0
  79. package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +172 -9
  80. package/src/lib/infra-kit-config/index.ts +2 -1
  81. package/src/lib/infra-kit-config/infra-kit-config.ts +80 -22
  82. package/src/lib/package-config/.omc/state/sessions/dde22a92-a0e9-4948-905d-25c9a812ab35/last-tool-error-state.json +7 -0
  83. package/src/lib/package-config/.omc/state/sessions/dde22a92-a0e9-4948-905d-25c9a812ab35/pre-tool-advisory-throttle.json +10 -0
  84. package/src/mcp/tools/index.ts +2 -0
  85. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,229 @@
1
+ import { z } from 'zod'
2
+
3
+ import {
4
+ buildCmuxWorkspaceTitle,
5
+ canonicalizeCmuxTitle,
6
+ closeCmuxWorkspaceByTitle,
7
+ listCmuxWorkspaceTitles,
8
+ openCmuxWorkspaceWithLayout,
9
+ } from 'src/integrations/cmux'
10
+ import { ideProviderLabel, openIdeWorkspace } from 'src/integrations/ide'
11
+ import type { IdeProvider } from 'src/integrations/ide'
12
+ import { commandEcho } from 'src/lib/command-echo'
13
+ import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
14
+ import { OperationError } from 'src/lib/errors/operation-error'
15
+ import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
16
+ import { logger } from 'src/lib/logger'
17
+ import { defineMcpTool, textContent } from 'src/types'
18
+
19
+ interface WorktreesReloadResult {
20
+ closedCmux: string[]
21
+ openedCmux: string[]
22
+ ideProviders: IdeProvider[]
23
+ ideFoldersAdded: number
24
+ ideFoldersRemoved: number
25
+ }
26
+
27
+ /**
28
+ * Reload command: closes every open cmux workspace for the release worktrees on
29
+ * disk, then reopens the full set — cmux workspaces are recreated fresh and the
30
+ * configured editor workspace is reconciled and relaunched. Unlike the additive
31
+ * `worktrees-open`, this is disruptive: it tears down live cmux windows first.
32
+ *
33
+ * The editor has no per-window close primitive, so its "reload" is a reconcile +
34
+ * relaunch of the configured workspace. Safe to run from any branch
35
+ * (non-destructive to git — only cmux/editor view state is touched).
36
+ */
37
+ export const worktreesReload = async () => {
38
+ commandEcho.start('worktrees-reload')
39
+
40
+ try {
41
+ const projectRoot = await getProjectRoot()
42
+ const worktreeDir = `${projectRoot}${WORKTREES_DIR_SUFFIX}`
43
+ const currentBranches = await getCurrentWorktrees('release')
44
+ const repoName = await getRepoName()
45
+
46
+ // Phase 1 — close (cmux only; Cursor has no per-window close).
47
+ const closedCmux = await closeCmux({ currentBranches, repoName })
48
+
49
+ // Phase 2 — reopen. cmux and Cursor are independent surfaces, so reopen them
50
+ // concurrently to shrink the window where the just-closed cmux workspaces are gone.
51
+ const [ideOutcomes, { opened: openedCmux }] = await Promise.all([
52
+ openIdeWorkspace({ projectRoot, worktreeDir, currentBranches, skipRelaunchWhenEmpty: true }),
53
+ reopenCmux({ worktreeDir, currentBranches, repoName }),
54
+ ])
55
+
56
+ const result: WorktreesReloadResult = {
57
+ closedCmux,
58
+ openedCmux,
59
+ ideProviders: ideOutcomes
60
+ .filter((outcome) => {
61
+ return outcome.ran
62
+ })
63
+ .map((outcome) => {
64
+ return outcome.provider
65
+ }),
66
+ ideFoldersAdded: ideOutcomes.reduce((sum, outcome) => {
67
+ return sum + outcome.added
68
+ }, 0),
69
+ ideFoldersRemoved: ideOutcomes.reduce((sum, outcome) => {
70
+ return sum + outcome.removed
71
+ }, 0),
72
+ }
73
+
74
+ logResults(result, {
75
+ ideRan: ideOutcomes.some((outcome) => {
76
+ return outcome.ran
77
+ }),
78
+ })
79
+
80
+ commandEcho.print()
81
+
82
+ return {
83
+ content: textContent(JSON.stringify(result, null, 2)),
84
+ structuredContent: { ...result },
85
+ }
86
+ } catch (error) {
87
+ logger.error({ error }, '❌ Error reloading worktrees')
88
+ throw new OperationError(error, {
89
+ operation: 'reload worktrees',
90
+ remediation: "run 'worktrees-list' to confirm the branches exist",
91
+ })
92
+ }
93
+ }
94
+
95
+ interface CloseCmuxArgs {
96
+ currentBranches: string[]
97
+ repoName: string
98
+ }
99
+
100
+ /**
101
+ * Close the cmux workspace for each release worktree that is currently open.
102
+ * Snapshots `listCmuxWorkspaceTitles()` up front purely so the returned list
103
+ * reflects which workspaces were actually open (the close itself is best-effort
104
+ * and per-title). Returns the titles a close was attempted for.
105
+ */
106
+ export const closeCmux = async (args: CloseCmuxArgs): Promise<string[]> => {
107
+ const { currentBranches, repoName } = args
108
+
109
+ if (currentBranches.length === 0) {
110
+ return []
111
+ }
112
+
113
+ const openBefore = await listCmuxWorkspaceTitles()
114
+
115
+ const closed: string[] = []
116
+
117
+ for (const branch of currentBranches) {
118
+ const title = buildCmuxWorkspaceTitle({ repoName, branch })
119
+
120
+ // openBefore holds canonical keys; match the built title the same way so a
121
+ // workspace stored under a drifted title (whitespace / old v-prefix) resolves.
122
+ if (!openBefore.has(canonicalizeCmuxTitle(title))) {
123
+ continue
124
+ }
125
+
126
+ await closeCmuxWorkspaceByTitle(title)
127
+ closed.push(title)
128
+ }
129
+
130
+ return closed
131
+ }
132
+
133
+ interface ReopenCmuxArgs {
134
+ worktreeDir: string
135
+ currentBranches: string[]
136
+ repoName: string
137
+ }
138
+
139
+ interface ReopenCmuxOutcome {
140
+ opened: string[]
141
+ }
142
+
143
+ /**
144
+ * Recreate a cmux workspace for every release worktree on disk. Force-opens each
145
+ * branch (no dedup against the live workspace list): reload has just awaited the
146
+ * close of these workspaces, so it favours liveness — guaranteeing every branch
147
+ * ends up open — over the additive dedup that `worktrees-open` relies on.
148
+ */
149
+ export const reopenCmux = async (args: ReopenCmuxArgs): Promise<ReopenCmuxOutcome> => {
150
+ const { worktreeDir, currentBranches, repoName } = args
151
+
152
+ const opened: string[] = []
153
+
154
+ for (const branch of currentBranches) {
155
+ const title = buildCmuxWorkspaceTitle({ repoName, branch })
156
+
157
+ try {
158
+ await openCmuxWorkspaceWithLayout({ cwd: `${worktreeDir}/${branch}`, title })
159
+ opened.push(title)
160
+ } catch (error) {
161
+ logger.warn({ error, title }, `⚠️ Failed to reopen cmux workspace for ${branch}`)
162
+ }
163
+ }
164
+
165
+ return { opened }
166
+ }
167
+
168
+ interface LogResultsContext {
169
+ ideRan: boolean
170
+ }
171
+
172
+ const logResults = (result: WorktreesReloadResult, context: LogResultsContext): void => {
173
+ if (result.closedCmux.length > 0) {
174
+ logger.info(`🧹 Closed ${result.closedCmux.length} cmux workspace(s)`)
175
+ }
176
+
177
+ if (context.ideRan) {
178
+ const ideLabels = result.ideProviders
179
+ .map((provider) => {
180
+ return ideProviderLabel(provider)
181
+ })
182
+ .join(', ')
183
+
184
+ if (result.ideFoldersAdded > 0) {
185
+ logger.info(`✅ Added ${result.ideFoldersAdded} folder(s) to ${ideLabels} workspace(s)`)
186
+ }
187
+
188
+ if (result.ideFoldersRemoved > 0) {
189
+ logger.info(`🧹 Removed ${result.ideFoldersRemoved} dangling folder(s) from ${ideLabels} workspace(s)`)
190
+ }
191
+ }
192
+
193
+ if (result.openedCmux.length > 0) {
194
+ logger.info('✅ Reopened cmux workspaces:')
195
+ for (const title of result.openedCmux) {
196
+ logger.info(title)
197
+ }
198
+ }
199
+
200
+ if (
201
+ !context.ideRan &&
202
+ result.closedCmux.length === 0 &&
203
+ result.openedCmux.length === 0 &&
204
+ result.ideFoldersAdded === 0 &&
205
+ result.ideFoldersRemoved === 0
206
+ ) {
207
+ logger.info('ℹ️ Nothing to reload')
208
+ }
209
+ }
210
+
211
+ // MCP Tool Registration
212
+ export const worktreesReloadMcpTool = defineMcpTool({
213
+ name: 'worktrees-reload',
214
+ description:
215
+ 'Close all open cmux workspaces for the current release worktrees, then reopen the full set. Disruptive — closes live cmux windows (unlike the additive worktrees-open). Every configured editor (Cursor and/or Zed) is reconciled + relaunched (no per-window close). Safe to run from any branch. Use to refresh stale windows after restarting cmux/the editor.',
216
+ inputSchema: {},
217
+ outputSchema: {
218
+ closedCmux: z.array(z.string()).describe('Titles of cmux workspaces a close was attempted for (best-effort)'),
219
+ openedCmux: z.array(z.string()).describe('Titles of cmux workspaces reopened during this run'),
220
+ ideProviders: z
221
+ .array(z.string())
222
+ .describe('Configured IDE providers that were reloaded (cursor | zed); empty if none configured'),
223
+ ideFoldersAdded: z.number().describe('Total worktree folders added across all configured editor workspaces'),
224
+ ideFoldersRemoved: z
225
+ .number()
226
+ .describe('Total dangling worktree folders removed across all configured editor workspaces'),
227
+ },
228
+ handler: worktreesReload,
229
+ })
@@ -3,14 +3,13 @@ import confirm from '@inquirer/confirm'
3
3
  import process from 'node:process'
4
4
  import { z } from 'zod'
5
5
 
6
- import { removeFoldersFromCursorWorkspace, resolveCursorWorkspacePath } from 'src/integrations/cursor'
7
6
  import { getReleasePRsWithInfo } from 'src/integrations/gh'
7
+ import { removeIdeWorktreeFolders } from 'src/integrations/ide'
8
8
  import { commandEcho } from 'src/lib/command-echo'
9
9
  import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
10
10
  import { OperationError } from 'src/lib/errors/operation-error'
11
11
  import { assertManagementContext } from 'src/lib/git-guard'
12
12
  import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
13
- import { getInfraKitConfig } from 'src/lib/infra-kit-config'
14
13
  import { logger } from 'src/lib/logger'
15
14
  import { formatBranchName, parseReleaseRef } from 'src/lib/release-id'
16
15
  import { detectReleaseType, formatBranchChoices, getJiraDescriptions, releaseBranchLabels } from 'src/lib/release-utils'
@@ -119,7 +118,7 @@ export const worktreesRemove = async (options: WorktreeManagementArgs) => {
119
118
  pruneFolder: allSelected,
120
119
  })
121
120
 
122
- await syncCursorWorkspaceOnRemove({ removedWorktrees, worktreeDir, projectRoot })
121
+ await removeIdeWorktreeFolders({ projectRoot, worktreeDir, branches: removedWorktrees })
123
122
 
124
123
  logResults(removedWorktrees)
125
124
 
@@ -143,47 +142,6 @@ export const worktreesRemove = async (options: WorktreeManagementArgs) => {
143
142
  }
144
143
  }
145
144
 
146
- interface SyncCursorWorkspaceOnRemoveArgs {
147
- removedWorktrees: string[]
148
- worktreeDir: string
149
- projectRoot: string
150
- }
151
-
152
- /**
153
- * Strip removed worktrees from the configured Cursor workspace's `folders` array.
154
- * No-op if Cursor isn't configured, mode isn't "workspace", or no worktrees were removed.
155
- */
156
- const syncCursorWorkspaceOnRemove = async (args: SyncCursorWorkspaceOnRemoveArgs): Promise<void> => {
157
- const { removedWorktrees, worktreeDir, projectRoot } = args
158
-
159
- if (removedWorktrees.length === 0) {
160
- return
161
- }
162
-
163
- const config = await getInfraKitConfig()
164
- const cursorConfig = config.ide?.provider === 'cursor' ? config.ide.config : undefined
165
-
166
- if (!cursorConfig || cursorConfig.mode !== 'workspace' || !cursorConfig.workspaceConfigPath) {
167
- return
168
- }
169
-
170
- const workspacePath = resolveCursorWorkspacePath(cursorConfig.workspaceConfigPath, projectRoot)
171
-
172
- const folderPaths = removedWorktrees.map((branch) => {
173
- return `${worktreeDir}/${branch}`
174
- })
175
-
176
- try {
177
- const { removed } = await removeFoldersFromCursorWorkspace({ workspacePath, folderPaths })
178
-
179
- if (removed.length > 0) {
180
- logger.info(`✅ Removed ${removed.length} folder(s) from ${workspacePath}`)
181
- }
182
- } catch (error) {
183
- logger.warn({ error }, `⚠️ Failed to update Cursor workspace at ${workspacePath}`)
184
- }
185
- }
186
-
187
145
  /**
188
146
  * Log the results of worktree management
189
147
  */
@@ -4,14 +4,13 @@ import { z } from 'zod'
4
4
  import { $ } from 'zx'
5
5
 
6
6
  import { buildCmuxWorkspaceTitle, closeCmuxWorkspaceByTitle } from 'src/integrations/cmux'
7
- import { removeFoldersFromCursorWorkspace, resolveCursorWorkspacePath } from 'src/integrations/cursor'
8
7
  import { getReleasePRs } from 'src/integrations/gh'
8
+ import { removeIdeWorktreeFolders } from 'src/integrations/ide'
9
9
  import { commandEcho } from 'src/lib/command-echo'
10
10
  import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
11
11
  import { OperationError } from 'src/lib/errors/operation-error'
12
12
  import { assertManagementContext } from 'src/lib/git-guard'
13
13
  import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
14
- import { getInfraKitConfig } from 'src/lib/infra-kit-config'
15
14
  import { logger } from 'src/lib/logger'
16
15
  import { isReleaseBranch } from 'src/lib/release-id'
17
16
  import { defineMcpTool, textContent } from 'src/types'
@@ -73,7 +72,7 @@ export const worktreesSync = async (options: WorktreeSyncArgs) => {
73
72
  repoName,
74
73
  })
75
74
 
76
- await syncCursorWorkspaceOnRemove({ removedWorktrees, worktreeDir, projectRoot })
75
+ await removeIdeWorktreeFolders({ projectRoot, worktreeDir, branches: removedWorktrees })
77
76
 
78
77
  logResults(removedWorktrees)
79
78
 
@@ -156,47 +155,6 @@ const removeWorktrees = async (args: RemoveWorktreesArgs): Promise<string[]> =>
156
155
  return removed
157
156
  }
158
157
 
159
- interface SyncCursorWorkspaceOnRemoveArgs {
160
- removedWorktrees: string[]
161
- worktreeDir: string
162
- projectRoot: string
163
- }
164
-
165
- /**
166
- * Strip removed worktrees from the configured Cursor workspace's `folders` array.
167
- * No-op if Cursor isn't configured, mode isn't "workspace", or no worktrees were removed.
168
- */
169
- const syncCursorWorkspaceOnRemove = async (args: SyncCursorWorkspaceOnRemoveArgs): Promise<void> => {
170
- const { removedWorktrees, worktreeDir, projectRoot } = args
171
-
172
- if (removedWorktrees.length === 0) {
173
- return
174
- }
175
-
176
- const config = await getInfraKitConfig()
177
- const cursorConfig = config.ide?.provider === 'cursor' ? config.ide.config : undefined
178
-
179
- if (!cursorConfig || cursorConfig.mode !== 'workspace' || !cursorConfig.workspaceConfigPath) {
180
- return
181
- }
182
-
183
- const workspacePath = resolveCursorWorkspacePath(cursorConfig.workspaceConfigPath, projectRoot)
184
-
185
- const folderPaths = removedWorktrees.map((branch) => {
186
- return `${worktreeDir}/${branch}`
187
- })
188
-
189
- try {
190
- const { removed: removedEntries } = await removeFoldersFromCursorWorkspace({ workspacePath, folderPaths })
191
-
192
- if (removedEntries.length > 0) {
193
- logger.info(`✅ Removed ${removedEntries.length} folder(s) from ${workspacePath}`)
194
- }
195
- } catch (error) {
196
- logger.warn({ error }, `⚠️ Failed to update Cursor workspace at ${workspacePath}`)
197
- }
198
- }
199
-
200
158
  /**
201
159
  * Log the results of worktree management
202
160
  */
package/src/entry/cli.ts CHANGED
@@ -18,12 +18,14 @@ import { init } from 'src/commands/init'
18
18
  import { releaseCreate } from 'src/commands/release-create'
19
19
  import { releaseDescEdit } from 'src/commands/release-desc-edit'
20
20
  import { version } from 'src/commands/version'
21
- import { CURSOR_MODES, worktreesAdd } from 'src/commands/worktrees-add'
22
- import type { CursorMode } from 'src/commands/worktrees-add'
21
+ import { worktreesAdd } from 'src/commands/worktrees-add'
23
22
  import { worktreesList } from 'src/commands/worktrees-list'
24
23
  import { worktreesOpen } from 'src/commands/worktrees-open'
24
+ import { worktreesReload } from 'src/commands/worktrees-reload'
25
25
  import { worktreesRemove } from 'src/commands/worktrees-remove'
26
26
  import { worktreesSync } from 'src/commands/worktrees-sync'
27
+ import { IDE_MODES } from 'src/integrations/ide'
28
+ import type { IdeMode } from 'src/integrations/ide'
27
29
  import { logger } from 'src/lib/logger'
28
30
  import { parseReleaseSpec } from 'src/lib/version-utils'
29
31
  import type { ReleaseInput } from 'src/lib/version-utils'
@@ -34,7 +36,7 @@ const collectReleaseSpec = (value: string, prev: string[]): string[] => {
34
36
  return [...prev, value]
35
37
  }
36
38
 
37
- const normalizeCursorMode = (value: unknown): CursorMode | undefined => {
39
+ const normalizeIdeMode = (value: unknown, flagName: '--ide' | '--cursor'): IdeMode | undefined => {
38
40
  if (typeof value === 'undefined') {
39
41
  return undefined
40
42
  }
@@ -47,11 +49,11 @@ const normalizeCursorMode = (value: unknown): CursorMode | undefined => {
47
49
  return 'none'
48
50
  }
49
51
 
50
- if (typeof value === 'string' && (CURSOR_MODES as readonly string[]).includes(value)) {
51
- return value as CursorMode
52
+ if (typeof value === 'string' && (IDE_MODES as readonly string[]).includes(value)) {
53
+ return value as IdeMode
52
54
  }
53
55
 
54
- throw new Error(`Invalid --cursor value "${String(value)}". Expected one of: ${CURSOR_MODES.join(', ')}.`)
56
+ throw new Error(`Invalid ${flagName} value "${String(value)}". Expected one of: ${IDE_MODES.join(', ')}.`)
55
57
  }
56
58
 
57
59
  const runProgram = async (argv?: string[]): Promise<void> => {
@@ -175,18 +177,23 @@ program
175
177
  .option('-y, --yes', 'Skip confirmation prompt')
176
178
  .option('-a, --all', 'Select all active release branches')
177
179
  .option('-v, --versions <versions>', 'Specify versions by comma, e.g. 1.2.5, 1.2.6')
178
- .option('-c, --cursor [mode]', 'Cursor mode for created worktrees: workspace (default) | windows | none')
179
- .option('--no-cursor', 'Skip Cursor (alias for --cursor none)')
180
+ .option('-i, --ide [mode]', 'Editor mode for created worktrees: workspace (default) | none')
181
+ .option('--no-ide', 'Skip the editor (alias for --ide none)')
182
+ .option('-c, --cursor [mode]', 'Deprecated alias for --ide')
183
+ .option('--no-cursor', 'Deprecated alias for --no-ide')
180
184
  .option('-g, --github-desktop', 'Open created worktrees in GitHub Desktop')
181
185
  .option('--no-github-desktop', 'Skip GitHub Desktop prompt')
182
186
  .option('-m, --cmux', 'Open created worktrees in cmux (3-pane layout)')
183
187
  .option('--no-cmux', 'Skip cmux prompt')
184
188
  .action(async (options) => {
189
+ // `--ide` wins over the deprecated `--cursor` alias when both are provided.
190
+ const ide = normalizeIdeMode(options.ide, '--ide') ?? normalizeIdeMode(options.cursor, '--cursor')
191
+
185
192
  await worktreesAdd({
186
193
  confirmedCommand: options.yes,
187
194
  all: options.all,
188
195
  versions: options.versions,
189
- cursor: normalizeCursorMode(options.cursor),
196
+ ide,
190
197
  githubDesktop: options.githubDesktop,
191
198
  cmux: options.cmux,
192
199
  })
@@ -211,11 +218,18 @@ program
211
218
 
212
219
  program
213
220
  .command('worktrees-open')
214
- .description('Open Cursor + cmux for existing release worktrees (cold-start restore)')
221
+ .description('Open the configured editor (Cursor/Zed) + cmux for existing release worktrees (cold-start restore)')
215
222
  .action(async () => {
216
223
  await worktreesOpen()
217
224
  })
218
225
 
226
+ program
227
+ .command('worktrees-reload')
228
+ .description('Close all cmux/editor worktree windows, then reopen the current release worktrees')
229
+ .action(async () => {
230
+ await worktreesReload()
231
+ })
232
+
219
233
  const configCmd = program.command('config').description('Manage infra-kit configuration files')
220
234
 
221
235
  configCmd
@@ -305,7 +319,14 @@ if (process.argv.length <= 2) {
305
319
  'release-deploy-selected',
306
320
  'release-deliver',
307
321
  ]
308
- const worktreeCommands = ['worktrees-add', 'worktrees-list', 'worktrees-open', 'worktrees-remove', 'worktrees-sync']
322
+ const worktreeCommands = [
323
+ 'worktrees-add',
324
+ 'worktrees-list',
325
+ 'worktrees-open',
326
+ 'worktrees-reload',
327
+ 'worktrees-remove',
328
+ 'worktrees-sync',
329
+ ]
309
330
  const envCommands = [
310
331
  'audit',
311
332
  'doctor',
@@ -1,4 +1,5 @@
1
1
  export { addFoldersToCursorWorkspace } from './add-folders-to-workspace'
2
+ export { openCursorWorkspace } from './open-cursor-workspace'
2
3
  export { reconcileCursorWorkspaceFolders } from './reconcile-workspace-folders'
3
4
  export { removeFoldersFromCursorWorkspace } from './remove-folders-from-workspace'
4
5
  export { resolveCursorWorkspacePath } from './resolve-workspace-path'
@@ -0,0 +1,70 @@
1
+ import { $ } from 'zx'
2
+
3
+ import { logger } from 'src/lib/logger'
4
+
5
+ import { reconcileCursorWorkspaceFolders } from './reconcile-workspace-folders'
6
+ import { resolveCursorWorkspacePath } from './resolve-workspace-path'
7
+
8
+ interface OpenCursorWorkspaceArgs {
9
+ projectRoot: string
10
+ worktreeDir: string
11
+ currentBranches: string[]
12
+ /**
13
+ * When true, skip the `cursor <workspace>` relaunch if there are no release
14
+ * worktrees (folder reconcile still runs to strip dangling entries). Lets
15
+ * `worktrees-reload` avoid popping an empty Cursor window while keeping the
16
+ * cold-start `worktrees-open` behaviour (relaunch unconditionally) intact.
17
+ */
18
+ skipRelaunchWhenEmpty: boolean
19
+ /**
20
+ * The Cursor entry's config, selected by the facade from the (possibly
21
+ * multi-IDE) infra-kit config. Passed in rather than re-read here so this
22
+ * opener acts on the exact entry the caller chose — essential once `ide` can
23
+ * be an array of providers.
24
+ */
25
+ cursorConfig: { workspaceConfigPath: string }
26
+ }
27
+
28
+ interface OpenCursorWorkspaceOutcome {
29
+ ran: boolean
30
+ added: number
31
+ removed: number
32
+ }
33
+
34
+ /**
35
+ * Reconciles the configured Cursor `.code-workspace` `folders` array against the
36
+ * set of release worktrees on disk, then launches Cursor against it. Shared by
37
+ * `worktrees-open` (cold-start restore) and `worktrees-reload` (close + reopen).
38
+ *
39
+ * No-ops (returns `ran: false`) when the Cursor entry has no `workspaceConfigPath`.
40
+ * All failures are swallowed into a warning — opening Cursor is best-effort.
41
+ */
42
+ export const openCursorWorkspace = async (args: OpenCursorWorkspaceArgs): Promise<OpenCursorWorkspaceOutcome> => {
43
+ const { projectRoot, worktreeDir, currentBranches, skipRelaunchWhenEmpty, cursorConfig } = args
44
+
45
+ if (!cursorConfig.workspaceConfigPath) {
46
+ logger.warn('⚠️ Skipping Cursor: workspaceConfigPath is not set.')
47
+
48
+ return { ran: false, added: 0, removed: 0 }
49
+ }
50
+
51
+ const workspacePath = resolveCursorWorkspacePath(cursorConfig.workspaceConfigPath, projectRoot)
52
+
53
+ try {
54
+ const { added, removed } = await reconcileCursorWorkspaceFolders({
55
+ workspacePath,
56
+ worktreeDir,
57
+ currentBranches,
58
+ })
59
+
60
+ if (!(skipRelaunchWhenEmpty && currentBranches.length === 0)) {
61
+ await $`cursor ${workspacePath}`
62
+ }
63
+
64
+ return { ran: true, added: added.length, removed: removed.length }
65
+ } catch (error) {
66
+ logger.warn({ error }, `⚠️ Failed to reconcile/open Cursor workspace at ${workspacePath}`)
67
+
68
+ return { ran: false, added: 0, removed: 0 }
69
+ }
70
+ }