infra-kit 0.1.111 → 0.1.114
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintcache +1 -1
- package/.omc/state/agent-replay-0d1515ce-4ebe-4ce8-a865-2897f68de168.jsonl +1 -0
- package/.omc/state/agent-replay-127837d3-a000-4656-9453-2324ab353723.jsonl +5 -0
- package/.omc/state/agent-replay-15c409af-24fa-4026-88d3-a82dd1e68b87.jsonl +11 -0
- package/.omc/state/agent-replay-74afa912-1443-4011-b4f2-db9f055cdd9f.jsonl +3 -0
- package/.omc/state/agent-replay-8dd0ad2f-3a90-4158-b9de-ec8e19a7c983.jsonl +3 -0
- package/.omc/state/agent-replay-f99d8dad-81d1-4987-8183-62edf0843d0c.jsonl +2 -0
- package/.omc/state/idle-notif-cooldown.json +1 -1
- package/.omc/state/sessions/0d1515ce-4ebe-4ce8-a865-2897f68de168/pre-tool-advisory-throttle.json +10 -0
- package/.omc/state/sessions/127837d3-a000-4656-9453-2324ab353723/mission-state.json +61 -0
- package/.omc/state/sessions/127837d3-a000-4656-9453-2324ab353723/pre-tool-advisory-throttle.json +22 -0
- package/.omc/state/sessions/127837d3-a000-4656-9453-2324ab353723/skill-active-state.json +15 -0
- package/.omc/state/sessions/127837d3-a000-4656-9453-2324ab353723/subagent-tracking-state.json +26 -0
- package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/mission-state.json +89 -0
- package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/pre-tool-advisory-throttle.json +34 -0
- package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/ralph-state.json +13 -0
- package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/skill-active-state.json +15 -0
- package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/subagent-tracking-state.json +44 -0
- package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/ultrawork-state.json +11 -0
- package/.omc/state/sessions/560ea519-574a-4713-b134-5bf6cfaf3c33/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/74afa912-1443-4011-b4f2-db9f055cdd9f/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/74afa912-1443-4011-b4f2-db9f055cdd9f/skill-active-state.json +15 -0
- package/.omc/state/sessions/74afa912-1443-4011-b4f2-db9f055cdd9f/subagent-tracking-state.json +17 -0
- package/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/pre-tool-advisory-throttle.json +14 -0
- package/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/ralph-state.json +13 -0
- package/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/subagent-tracking-state.json +17 -0
- package/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/ultrawork-state.json +11 -0
- package/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +14 -0
- package/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/ralph-state.json +13 -0
- package/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/ultrawork-state.json +11 -0
- package/.turbo/turbo-build.log +3 -4
- package/.turbo/turbo-eslint-check.log +1 -2
- package/.turbo/turbo-prettier-check.log +1 -2
- package/.turbo/turbo-prettier-fix.log +1 -2
- package/.turbo/turbo-test.log +25 -18
- package/.turbo/turbo-ts-check.log +1 -2
- package/dist/cli.js +62 -54
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +36 -36
- package/dist/mcp.js.map +4 -4
- package/package.json +1 -1
- package/src/.omc/state/agent-replay-8dd0ad2f-3a90-4158-b9de-ec8e19a7c983.jsonl +11 -0
- package/src/.omc/state/idle-notif-cooldown.json +3 -0
- package/src/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/mission-state.json +79 -0
- package/src/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/pre-tool-advisory-throttle.json +22 -0
- package/src/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/ralph-state.json +13 -0
- package/src/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/subagent-tracking-state.json +26 -0
- package/src/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/ultrawork-state.json +11 -0
- package/src/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +14 -0
- package/src/commands/doctor/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/pre-tool-advisory-throttle.json +14 -0
- package/src/commands/doctor/__tests__/check-ide-installed.test.ts +142 -0
- package/src/commands/doctor/doctor.ts +104 -1
- package/src/commands/gh-release-deliver/gh-release-deliver.ts +32 -1
- package/src/commands/init/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/pre-tool-advisory-throttle.json +18 -0
- package/src/commands/init/__tests__/migrate-config.test.ts +132 -1
- package/src/commands/init/init.ts +19 -6
- package/src/commands/init/migrate-config.ts +104 -0
- package/src/commands/worktrees-add/index.ts +1 -2
- package/src/commands/worktrees-add/worktrees-add.ts +24 -60
- package/src/commands/worktrees-open/worktrees-open.ts +55 -68
- package/src/commands/worktrees-reload/__tests__/reload-cmux.test.ts +119 -0
- package/src/commands/worktrees-reload/index.ts +1 -0
- package/src/commands/worktrees-reload/worktrees-reload.ts +229 -0
- package/src/commands/worktrees-remove/worktrees-remove.ts +2 -44
- package/src/commands/worktrees-sync/worktrees-sync.ts +2 -44
- package/src/entry/cli.ts +32 -11
- package/src/integrations/cursor/.omc/state/agent-replay-f99d8dad-81d1-4987-8183-62edf0843d0c.jsonl +2 -0
- package/src/integrations/cursor/.omc/state/idle-notif-cooldown.json +3 -0
- package/src/integrations/cursor/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +18 -0
- package/src/integrations/cursor/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/subagent-tracking-state.json +17 -0
- package/src/integrations/cursor/index.ts +2 -0
- package/src/integrations/cursor/launch-cursor.ts +12 -0
- package/src/integrations/cursor/open-cursor-workspace.ts +69 -0
- package/src/integrations/ide/__tests__/ide-facade.test.ts +250 -0
- package/src/integrations/ide/add-ide-worktree-folders.ts +77 -0
- package/src/integrations/ide/index.ts +6 -0
- package/src/integrations/ide/open-ide-workspace.ts +51 -0
- package/src/integrations/ide/provider-label.ts +24 -0
- package/src/integrations/ide/remove-ide-worktree-folders.ts +80 -0
- package/src/integrations/ide/types.ts +32 -0
- package/src/integrations/zed/__tests__/open-zed-workspace.test.ts +85 -0
- package/src/integrations/zed/add-folders-to-zed-workspace.ts +30 -0
- package/src/integrations/zed/index.ts +2 -0
- package/src/integrations/zed/open-zed-workspace.ts +57 -0
- package/src/lib/assert-never/assert-never.ts +15 -0
- package/src/lib/assert-never/index.ts +1 -0
- package/src/lib/git-utils/__tests__/git-primitives.test.ts +122 -1
- package/src/lib/git-utils/git-utils.ts +35 -0
- package/src/lib/git-utils/index.ts +2 -0
- package/src/lib/infra-kit-config/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/pre-tool-advisory-throttle.json +18 -0
- package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +172 -9
- package/src/lib/infra-kit-config/index.ts +2 -1
- package/src/lib/infra-kit-config/infra-kit-config.ts +80 -22
- package/src/lib/package-config/.omc/state/sessions/dde22a92-a0e9-4948-905d-25c9a812ab35/last-tool-error-state.json +7 -0
- package/src/lib/package-config/.omc/state/sessions/dde22a92-a0e9-4948-905d-25c9a812ab35/pre-tool-advisory-throttle.json +10 -0
- package/src/lib/worktrees/remove-worktrees.ts +15 -0
- package/src/mcp/tools/index.ts +2 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { logger } from 'src/lib/logger'
|
|
2
|
+
|
|
3
|
+
import { launchCursor } from './launch-cursor'
|
|
4
|
+
import { reconcileCursorWorkspaceFolders } from './reconcile-workspace-folders'
|
|
5
|
+
import { resolveCursorWorkspacePath } from './resolve-workspace-path'
|
|
6
|
+
|
|
7
|
+
interface OpenCursorWorkspaceArgs {
|
|
8
|
+
projectRoot: string
|
|
9
|
+
worktreeDir: string
|
|
10
|
+
currentBranches: string[]
|
|
11
|
+
/**
|
|
12
|
+
* When true, skip the `cursor <workspace>` relaunch if there are no release
|
|
13
|
+
* worktrees (folder reconcile still runs to strip dangling entries). Lets
|
|
14
|
+
* `worktrees-reload` avoid popping an empty Cursor window while keeping the
|
|
15
|
+
* cold-start `worktrees-open` behaviour (relaunch unconditionally) intact.
|
|
16
|
+
*/
|
|
17
|
+
skipRelaunchWhenEmpty: boolean
|
|
18
|
+
/**
|
|
19
|
+
* The Cursor entry's config, selected by the facade from the (possibly
|
|
20
|
+
* multi-IDE) infra-kit config. Passed in rather than re-read here so this
|
|
21
|
+
* opener acts on the exact entry the caller chose — essential once `ide` can
|
|
22
|
+
* be an array of providers.
|
|
23
|
+
*/
|
|
24
|
+
cursorConfig: { workspaceConfigPath: string }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface OpenCursorWorkspaceOutcome {
|
|
28
|
+
ran: boolean
|
|
29
|
+
added: number
|
|
30
|
+
removed: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reconciles the configured Cursor `.code-workspace` `folders` array against the
|
|
35
|
+
* set of release worktrees on disk, then launches Cursor against it. Shared by
|
|
36
|
+
* `worktrees-open` (cold-start restore) and `worktrees-reload` (close + reopen).
|
|
37
|
+
*
|
|
38
|
+
* No-ops (returns `ran: false`) when the Cursor entry has no `workspaceConfigPath`.
|
|
39
|
+
* All failures are swallowed into a warning — opening Cursor is best-effort.
|
|
40
|
+
*/
|
|
41
|
+
export const openCursorWorkspace = async (args: OpenCursorWorkspaceArgs): Promise<OpenCursorWorkspaceOutcome> => {
|
|
42
|
+
const { projectRoot, worktreeDir, currentBranches, skipRelaunchWhenEmpty, cursorConfig } = args
|
|
43
|
+
|
|
44
|
+
if (!cursorConfig.workspaceConfigPath) {
|
|
45
|
+
logger.warn('⚠️ Skipping Cursor: workspaceConfigPath is not set.')
|
|
46
|
+
|
|
47
|
+
return { ran: false, added: 0, removed: 0 }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const workspacePath = resolveCursorWorkspacePath(cursorConfig.workspaceConfigPath, projectRoot)
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const { added, removed } = await reconcileCursorWorkspaceFolders({
|
|
54
|
+
workspacePath,
|
|
55
|
+
worktreeDir,
|
|
56
|
+
currentBranches,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
if (!(skipRelaunchWhenEmpty && currentBranches.length === 0)) {
|
|
60
|
+
await launchCursor(workspacePath)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { ran: true, added: added.length, removed: removed.length }
|
|
64
|
+
} catch (error) {
|
|
65
|
+
logger.warn({ error }, `⚠️ Failed to reconcile/open Cursor workspace at ${workspacePath}`)
|
|
66
|
+
|
|
67
|
+
return { ran: false, added: 0, removed: 0 }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { addIdeWorktreeFolders } from '../add-ide-worktree-folders'
|
|
4
|
+
import { openIdeWorkspace } from '../open-ide-workspace'
|
|
5
|
+
import { removeIdeWorktreeFolders } from '../remove-ide-worktree-folders'
|
|
6
|
+
|
|
7
|
+
const config = vi.hoisted(() => {
|
|
8
|
+
return { value: {} as { ide?: unknown } }
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
vi.mock('src/lib/infra-kit-config', () => {
|
|
12
|
+
return {
|
|
13
|
+
getInfraKitConfig: vi.fn(() => {
|
|
14
|
+
return Promise.resolve(config.value)
|
|
15
|
+
}),
|
|
16
|
+
// Mirror the real normalizer (single → [ide], array → as-is, undefined → []).
|
|
17
|
+
resolveConfiguredIdes: vi.fn((cfg: { ide?: unknown }) => {
|
|
18
|
+
const ide = cfg.ide
|
|
19
|
+
|
|
20
|
+
if (!ide) return []
|
|
21
|
+
|
|
22
|
+
return Array.isArray(ide) ? ide : [ide]
|
|
23
|
+
}),
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
vi.mock('src/lib/logger', () => {
|
|
28
|
+
return { logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() } }
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const cursor = vi.hoisted(() => {
|
|
32
|
+
return {
|
|
33
|
+
openCursorWorkspace: vi.fn(),
|
|
34
|
+
addFoldersToCursorWorkspace: vi.fn(),
|
|
35
|
+
removeFoldersFromCursorWorkspace: vi.fn(),
|
|
36
|
+
launchCursor: vi.fn(),
|
|
37
|
+
resolveCursorWorkspacePath: vi.fn((value: string) => {
|
|
38
|
+
return `/abs/${value}`
|
|
39
|
+
}),
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
vi.mock('src/integrations/cursor', () => {
|
|
44
|
+
return cursor
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// Defense-in-depth: never let a test shell out to a real editor. Even if an
|
|
48
|
+
// editor spawn leaks back into a layer this suite exercises directly, `$` is a
|
|
49
|
+
// no-op here so `cursor <path>` can never open a real window during the run.
|
|
50
|
+
vi.mock('zx', () => {
|
|
51
|
+
return { $: vi.fn() }
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const zed = vi.hoisted(() => {
|
|
55
|
+
return {
|
|
56
|
+
openZedWorkspace: vi.fn(),
|
|
57
|
+
addFoldersToZedWorkspace: vi.fn(),
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
vi.mock('src/integrations/zed', () => {
|
|
62
|
+
return zed
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const cursorEntry = { provider: 'cursor', config: { workspaceConfigPath: 'ws' } }
|
|
66
|
+
const zedEntry = { provider: 'zed', config: {} }
|
|
67
|
+
const cursorConfig = { ide: cursorEntry }
|
|
68
|
+
const zedConfig = { ide: zedEntry }
|
|
69
|
+
const bothConfig = { ide: [cursorEntry, zedEntry] }
|
|
70
|
+
const unconfigured = { ide: undefined }
|
|
71
|
+
|
|
72
|
+
const baseOpenArgs = {
|
|
73
|
+
projectRoot: '/repo',
|
|
74
|
+
worktreeDir: '/repo.worktrees',
|
|
75
|
+
currentBranches: ['release/v1.0.0'],
|
|
76
|
+
skipRelaunchWhenEmpty: false,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
vi.clearAllMocks()
|
|
81
|
+
cursor.resolveCursorWorkspacePath.mockImplementation((value: string) => {
|
|
82
|
+
return `/abs/${value}`
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('openIdeWorkspace', () => {
|
|
87
|
+
it('routes to Cursor and tags the provider (passing the cursor config down)', async () => {
|
|
88
|
+
config.value = cursorConfig
|
|
89
|
+
cursor.openCursorWorkspace.mockResolvedValue({ ran: true, added: 1, removed: 0 })
|
|
90
|
+
|
|
91
|
+
const outcomes = await openIdeWorkspace(baseOpenArgs)
|
|
92
|
+
|
|
93
|
+
expect(cursor.openCursorWorkspace).toHaveBeenCalledWith({
|
|
94
|
+
...baseOpenArgs,
|
|
95
|
+
cursorConfig: { workspaceConfigPath: 'ws' },
|
|
96
|
+
})
|
|
97
|
+
expect(zed.openZedWorkspace).not.toHaveBeenCalled()
|
|
98
|
+
expect(outcomes).toEqual([{ ran: true, added: 1, removed: 0, provider: 'cursor' }])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('routes to Zed and tags the provider', async () => {
|
|
102
|
+
config.value = zedConfig
|
|
103
|
+
zed.openZedWorkspace.mockResolvedValue({ ran: true, added: 2, removed: 0 })
|
|
104
|
+
|
|
105
|
+
const outcomes = await openIdeWorkspace(baseOpenArgs)
|
|
106
|
+
|
|
107
|
+
expect(zed.openZedWorkspace).toHaveBeenCalledWith(baseOpenArgs)
|
|
108
|
+
expect(cursor.openCursorWorkspace).not.toHaveBeenCalled()
|
|
109
|
+
expect(outcomes).toEqual([{ ran: true, added: 2, removed: 0, provider: 'zed' }])
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('opens BOTH editors when both are configured (one outcome per provider)', async () => {
|
|
113
|
+
config.value = bothConfig
|
|
114
|
+
cursor.openCursorWorkspace.mockResolvedValue({ ran: true, added: 1, removed: 0 })
|
|
115
|
+
zed.openZedWorkspace.mockResolvedValue({ ran: true, added: 2, removed: 0 })
|
|
116
|
+
|
|
117
|
+
const outcomes = await openIdeWorkspace(baseOpenArgs)
|
|
118
|
+
|
|
119
|
+
expect(cursor.openCursorWorkspace).toHaveBeenCalledTimes(1)
|
|
120
|
+
expect(zed.openZedWorkspace).toHaveBeenCalledTimes(1)
|
|
121
|
+
expect(outcomes).toEqual([
|
|
122
|
+
{ ran: true, added: 1, removed: 0, provider: 'cursor' },
|
|
123
|
+
{ ran: true, added: 2, removed: 0, provider: 'zed' },
|
|
124
|
+
])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('returns an empty array when no IDE is configured', async () => {
|
|
128
|
+
config.value = unconfigured
|
|
129
|
+
|
|
130
|
+
const outcomes = await openIdeWorkspace(baseOpenArgs)
|
|
131
|
+
|
|
132
|
+
expect(cursor.openCursorWorkspace).not.toHaveBeenCalled()
|
|
133
|
+
expect(zed.openZedWorkspace).not.toHaveBeenCalled()
|
|
134
|
+
expect(outcomes).toEqual([])
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('addIdeWorktreeFolders', () => {
|
|
139
|
+
const args = { projectRoot: '/repo', worktreeDir: '/repo.worktrees', branches: ['release/v1.0.0'] }
|
|
140
|
+
|
|
141
|
+
it('adds to the Cursor workspace and reports skipped', async () => {
|
|
142
|
+
config.value = cursorConfig
|
|
143
|
+
cursor.addFoldersToCursorWorkspace.mockResolvedValue({ added: ['a'], skipped: ['b'] })
|
|
144
|
+
|
|
145
|
+
const outcomes = await addIdeWorktreeFolders(args)
|
|
146
|
+
|
|
147
|
+
expect(cursor.resolveCursorWorkspacePath).toHaveBeenCalledWith('ws', '/repo')
|
|
148
|
+
expect(cursor.addFoldersToCursorWorkspace).toHaveBeenCalled()
|
|
149
|
+
expect(outcomes).toEqual([{ ran: true, provider: 'cursor', added: 1, skipped: 1 }])
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('skips Cursor when workspaceConfigPath is missing', async () => {
|
|
153
|
+
config.value = { ide: { provider: 'cursor', config: {} } }
|
|
154
|
+
|
|
155
|
+
const outcomes = await addIdeWorktreeFolders(args)
|
|
156
|
+
|
|
157
|
+
expect(cursor.addFoldersToCursorWorkspace).not.toHaveBeenCalled()
|
|
158
|
+
expect(outcomes).toEqual([{ ran: false, provider: 'cursor', added: 0, skipped: 0 }])
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('adds to Zed via zed --add (skipped always 0)', async () => {
|
|
162
|
+
config.value = zedConfig
|
|
163
|
+
zed.addFoldersToZedWorkspace.mockResolvedValue({ added: ['a', 'b'] })
|
|
164
|
+
|
|
165
|
+
const outcomes = await addIdeWorktreeFolders(args)
|
|
166
|
+
|
|
167
|
+
expect(zed.addFoldersToZedWorkspace).toHaveBeenCalled()
|
|
168
|
+
expect(outcomes).toEqual([{ ran: true, provider: 'zed', added: 2, skipped: 0 }])
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('adds to BOTH editors when both are configured', async () => {
|
|
172
|
+
config.value = bothConfig
|
|
173
|
+
cursor.addFoldersToCursorWorkspace.mockResolvedValue({ added: ['a'], skipped: [] })
|
|
174
|
+
zed.addFoldersToZedWorkspace.mockResolvedValue({ added: ['a', 'b'] })
|
|
175
|
+
|
|
176
|
+
const outcomes = await addIdeWorktreeFolders(args)
|
|
177
|
+
|
|
178
|
+
expect(outcomes).toEqual([
|
|
179
|
+
{ ran: true, provider: 'cursor', added: 1, skipped: 0 },
|
|
180
|
+
{ ran: true, provider: 'zed', added: 2, skipped: 0 },
|
|
181
|
+
])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('returns an empty array when no IDE is configured', async () => {
|
|
185
|
+
config.value = unconfigured
|
|
186
|
+
|
|
187
|
+
const outcomes = await addIdeWorktreeFolders(args)
|
|
188
|
+
|
|
189
|
+
expect(outcomes).toEqual([])
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe('removeIdeWorktreeFolders', () => {
|
|
194
|
+
const args = { projectRoot: '/repo', worktreeDir: '/repo.worktrees', branches: ['release/v1.0.0'] }
|
|
195
|
+
|
|
196
|
+
it('removes from the Cursor workspace', async () => {
|
|
197
|
+
config.value = cursorConfig
|
|
198
|
+
cursor.removeFoldersFromCursorWorkspace.mockResolvedValue({ removed: ['x'], notFound: [] })
|
|
199
|
+
|
|
200
|
+
const outcomes = await removeIdeWorktreeFolders(args)
|
|
201
|
+
|
|
202
|
+
expect(cursor.removeFoldersFromCursorWorkspace).toHaveBeenCalled()
|
|
203
|
+
expect(outcomes).toEqual([{ provider: 'cursor', supported: true, removed: ['x'] }])
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('is a no-op for Cursor when workspaceConfigPath is missing', async () => {
|
|
207
|
+
config.value = { ide: { provider: 'cursor', config: {} } }
|
|
208
|
+
|
|
209
|
+
const outcomes = await removeIdeWorktreeFolders(args)
|
|
210
|
+
|
|
211
|
+
expect(cursor.removeFoldersFromCursorWorkspace).not.toHaveBeenCalled()
|
|
212
|
+
expect(outcomes).toEqual([{ provider: 'cursor', supported: true, removed: [] }])
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('reports unsupported for Zed without throwing', async () => {
|
|
216
|
+
config.value = zedConfig
|
|
217
|
+
|
|
218
|
+
const outcomes = await removeIdeWorktreeFolders(args)
|
|
219
|
+
|
|
220
|
+
expect(outcomes).toEqual([{ provider: 'zed', supported: false, removed: [] }])
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('removes from Cursor AND reports Zed unsupported when both are configured', async () => {
|
|
224
|
+
config.value = bothConfig
|
|
225
|
+
cursor.removeFoldersFromCursorWorkspace.mockResolvedValue({ removed: ['x'], notFound: [] })
|
|
226
|
+
|
|
227
|
+
const outcomes = await removeIdeWorktreeFolders(args)
|
|
228
|
+
|
|
229
|
+
expect(outcomes).toEqual([
|
|
230
|
+
{ provider: 'cursor', supported: true, removed: ['x'] },
|
|
231
|
+
{ provider: 'zed', supported: false, removed: [] },
|
|
232
|
+
])
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('short-circuits to an empty array when there are no branches (no config read needed)', async () => {
|
|
236
|
+
config.value = zedConfig
|
|
237
|
+
|
|
238
|
+
const outcomes = await removeIdeWorktreeFolders({ ...args, branches: [] })
|
|
239
|
+
|
|
240
|
+
expect(outcomes).toEqual([])
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('returns an empty array when no IDE is configured', async () => {
|
|
244
|
+
config.value = unconfigured
|
|
245
|
+
|
|
246
|
+
const outcomes = await removeIdeWorktreeFolders(args)
|
|
247
|
+
|
|
248
|
+
expect(outcomes).toEqual([])
|
|
249
|
+
})
|
|
250
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { addFoldersToCursorWorkspace, launchCursor, resolveCursorWorkspacePath } from 'src/integrations/cursor'
|
|
2
|
+
import { addFoldersToZedWorkspace } from 'src/integrations/zed'
|
|
3
|
+
import { assertNever } from 'src/lib/assert-never'
|
|
4
|
+
import { getInfraKitConfig, resolveConfiguredIdes } from 'src/lib/infra-kit-config'
|
|
5
|
+
import { logger } from 'src/lib/logger'
|
|
6
|
+
|
|
7
|
+
import type { AddIdeWorktreeFoldersOutcome } from './types'
|
|
8
|
+
|
|
9
|
+
interface AddIdeWorktreeFoldersArgs {
|
|
10
|
+
projectRoot: string
|
|
11
|
+
worktreeDir: string
|
|
12
|
+
branches: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Workspace-mode add (used by `worktrees-add`): append the given worktree folders
|
|
17
|
+
* to every configured editor's workspace and open it. Returns one outcome per
|
|
18
|
+
* configured provider (empty array when no IDE is configured), iterating
|
|
19
|
+
* sequentially.
|
|
20
|
+
*
|
|
21
|
+
* Cursor appends to the `.code-workspace` `folders` array (reporting `skipped`
|
|
22
|
+
* for already-present entries) then runs `cursor <workspace>`. Zed has no
|
|
23
|
+
* workspace file, so it `zed --add`s each folder (`skipped` is always 0).
|
|
24
|
+
*/
|
|
25
|
+
export const addIdeWorktreeFolders = async (
|
|
26
|
+
args: AddIdeWorktreeFoldersArgs,
|
|
27
|
+
): Promise<AddIdeWorktreeFoldersOutcome[]> => {
|
|
28
|
+
const { projectRoot, worktreeDir, branches } = args
|
|
29
|
+
|
|
30
|
+
const config = await getInfraKitConfig()
|
|
31
|
+
const ides = resolveConfiguredIdes(config)
|
|
32
|
+
|
|
33
|
+
const folderPaths = branches.map((branch) => {
|
|
34
|
+
return `${worktreeDir}/${branch}`
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const outcomes: AddIdeWorktreeFoldersOutcome[] = []
|
|
38
|
+
|
|
39
|
+
for (const ide of ides) {
|
|
40
|
+
switch (ide.provider) {
|
|
41
|
+
case 'cursor': {
|
|
42
|
+
if (!ide.config.workspaceConfigPath) {
|
|
43
|
+
logger.warn('⚠️ Skipping Cursor: ide.config.workspaceConfigPath is not set in infra-kit config')
|
|
44
|
+
|
|
45
|
+
outcomes.push({ ran: false, provider: 'cursor', added: 0, skipped: 0 })
|
|
46
|
+
break
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const workspacePath = resolveCursorWorkspacePath(ide.config.workspaceConfigPath, projectRoot)
|
|
50
|
+
|
|
51
|
+
const { added, skipped } = await addFoldersToCursorWorkspace({ workspacePath, folderPaths })
|
|
52
|
+
|
|
53
|
+
const skippedSuffix = skipped.length > 0 ? ` (${skipped.length} already present)` : ''
|
|
54
|
+
|
|
55
|
+
logger.info(`✅ Added ${added.length} folder(s) to ${workspacePath}${skippedSuffix}`)
|
|
56
|
+
|
|
57
|
+
await launchCursor(workspacePath)
|
|
58
|
+
|
|
59
|
+
outcomes.push({ ran: true, provider: 'cursor', added: added.length, skipped: skipped.length })
|
|
60
|
+
break
|
|
61
|
+
}
|
|
62
|
+
case 'zed': {
|
|
63
|
+
const { added } = await addFoldersToZedWorkspace({ folderPaths })
|
|
64
|
+
|
|
65
|
+
logger.info(`✅ Added ${added.length} folder(s) to Zed workspace`)
|
|
66
|
+
|
|
67
|
+
outcomes.push({ ran: true, provider: 'zed', added: added.length, skipped: 0 })
|
|
68
|
+
break
|
|
69
|
+
}
|
|
70
|
+
default: {
|
|
71
|
+
assertNever(ide)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return outcomes
|
|
77
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { addIdeWorktreeFolders } from './add-ide-worktree-folders'
|
|
2
|
+
export { openIdeWorkspace } from './open-ide-workspace'
|
|
3
|
+
export { ideProviderLabel } from './provider-label'
|
|
4
|
+
export { removeIdeWorktreeFolders } from './remove-ide-worktree-folders'
|
|
5
|
+
export { IDE_MODES } from './types'
|
|
6
|
+
export type { IdeMode, IdeProvider } from './types'
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { openCursorWorkspace } from 'src/integrations/cursor'
|
|
2
|
+
import { openZedWorkspace } from 'src/integrations/zed'
|
|
3
|
+
import { assertNever } from 'src/lib/assert-never'
|
|
4
|
+
import { getInfraKitConfig, resolveConfiguredIdes } from 'src/lib/infra-kit-config'
|
|
5
|
+
|
|
6
|
+
import type { OpenIdeWorkspaceOutcome } from './types'
|
|
7
|
+
|
|
8
|
+
interface OpenIdeWorkspaceArgs {
|
|
9
|
+
projectRoot: string
|
|
10
|
+
worktreeDir: string
|
|
11
|
+
currentBranches: string[]
|
|
12
|
+
skipRelaunchWhenEmpty: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Provider-agnostic entry point for the cold-start / reload open: for every
|
|
17
|
+
* configured editor, reconciles (or, for Zed, simply assembles) the workspace
|
|
18
|
+
* against the release worktrees on disk and launches it. Returns one outcome per
|
|
19
|
+
* configured provider (empty array when no IDE is configured). Iterates
|
|
20
|
+
* sequentially — `worktrees-reload` already wraps this call in an outer
|
|
21
|
+
* `Promise.all` with cmux, so a `Promise.all` here would compound editor-spawn
|
|
22
|
+
* concurrency. Best-effort — every provider swallows failures into a warning.
|
|
23
|
+
*/
|
|
24
|
+
export const openIdeWorkspace = async (args: OpenIdeWorkspaceArgs): Promise<OpenIdeWorkspaceOutcome[]> => {
|
|
25
|
+
const config = await getInfraKitConfig()
|
|
26
|
+
const ides = resolveConfiguredIdes(config)
|
|
27
|
+
|
|
28
|
+
const outcomes: OpenIdeWorkspaceOutcome[] = []
|
|
29
|
+
|
|
30
|
+
for (const ide of ides) {
|
|
31
|
+
switch (ide.provider) {
|
|
32
|
+
case 'cursor': {
|
|
33
|
+
const outcome = await openCursorWorkspace({ ...args, cursorConfig: ide.config })
|
|
34
|
+
|
|
35
|
+
outcomes.push({ ...outcome, provider: 'cursor' })
|
|
36
|
+
break
|
|
37
|
+
}
|
|
38
|
+
case 'zed': {
|
|
39
|
+
const outcome = await openZedWorkspace(args)
|
|
40
|
+
|
|
41
|
+
outcomes.push({ ...outcome, provider: 'zed' })
|
|
42
|
+
break
|
|
43
|
+
}
|
|
44
|
+
default: {
|
|
45
|
+
assertNever(ide)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return outcomes
|
|
51
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { assertNever } from 'src/lib/assert-never'
|
|
2
|
+
|
|
3
|
+
import type { IdeProvider } from './types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Human-facing display name for an IDE provider, used in log lines. Falls back to
|
|
7
|
+
* a generic "IDE" when no provider is configured.
|
|
8
|
+
*/
|
|
9
|
+
export const ideProviderLabel = (provider: IdeProvider | undefined): string => {
|
|
10
|
+
switch (provider) {
|
|
11
|
+
case 'cursor': {
|
|
12
|
+
return 'Cursor'
|
|
13
|
+
}
|
|
14
|
+
case 'zed': {
|
|
15
|
+
return 'Zed'
|
|
16
|
+
}
|
|
17
|
+
case undefined: {
|
|
18
|
+
return 'IDE'
|
|
19
|
+
}
|
|
20
|
+
default: {
|
|
21
|
+
return assertNever(provider)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { removeFoldersFromCursorWorkspace, resolveCursorWorkspacePath } from 'src/integrations/cursor'
|
|
2
|
+
import { assertNever } from 'src/lib/assert-never'
|
|
3
|
+
import { getInfraKitConfig, resolveConfiguredIdes } from 'src/lib/infra-kit-config'
|
|
4
|
+
import { logger } from 'src/lib/logger'
|
|
5
|
+
|
|
6
|
+
import type { RemoveIdeWorktreeFoldersOutcome } from './types'
|
|
7
|
+
|
|
8
|
+
interface RemoveIdeWorktreeFoldersArgs {
|
|
9
|
+
projectRoot: string
|
|
10
|
+
worktreeDir: string
|
|
11
|
+
branches: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Strip removed worktrees from every configured editor's workspace (used by
|
|
16
|
+
* `worktrees-remove` and `worktrees-sync`). Returns one outcome per configured
|
|
17
|
+
* provider (empty array when no IDE is configured or no worktrees were removed),
|
|
18
|
+
* iterating sequentially.
|
|
19
|
+
*
|
|
20
|
+
* Cursor edits the `.code-workspace` `folders` array. Zed has no folder-remove
|
|
21
|
+
* CLI, so it is a structural no-op (`supported: false`) with an info-level
|
|
22
|
+
* message — never a warning, because nothing failed.
|
|
23
|
+
*/
|
|
24
|
+
export const removeIdeWorktreeFolders = async (
|
|
25
|
+
args: RemoveIdeWorktreeFoldersArgs,
|
|
26
|
+
): Promise<RemoveIdeWorktreeFoldersOutcome[]> => {
|
|
27
|
+
const { projectRoot, worktreeDir, branches } = args
|
|
28
|
+
|
|
29
|
+
if (branches.length === 0) {
|
|
30
|
+
return []
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const config = await getInfraKitConfig()
|
|
34
|
+
const ides = resolveConfiguredIdes(config)
|
|
35
|
+
|
|
36
|
+
const folderPaths = branches.map((branch) => {
|
|
37
|
+
return `${worktreeDir}/${branch}`
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const outcomes: RemoveIdeWorktreeFoldersOutcome[] = []
|
|
41
|
+
|
|
42
|
+
for (const ide of ides) {
|
|
43
|
+
switch (ide.provider) {
|
|
44
|
+
case 'cursor': {
|
|
45
|
+
if (!ide.config.workspaceConfigPath) {
|
|
46
|
+
outcomes.push({ provider: 'cursor', supported: true, removed: [] })
|
|
47
|
+
break
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const workspacePath = resolveCursorWorkspacePath(ide.config.workspaceConfigPath, projectRoot)
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const { removed } = await removeFoldersFromCursorWorkspace({ workspacePath, folderPaths })
|
|
54
|
+
|
|
55
|
+
if (removed.length > 0) {
|
|
56
|
+
logger.info(`✅ Removed ${removed.length} folder(s) from ${workspacePath}`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
outcomes.push({ provider: 'cursor', supported: true, removed })
|
|
60
|
+
} catch (error) {
|
|
61
|
+
logger.warn({ error }, `⚠️ Failed to update Cursor workspace at ${workspacePath}`)
|
|
62
|
+
|
|
63
|
+
outcomes.push({ provider: 'cursor', supported: true, removed: [] })
|
|
64
|
+
}
|
|
65
|
+
break
|
|
66
|
+
}
|
|
67
|
+
case 'zed': {
|
|
68
|
+
logger.info('ℹ️ Zed has no folder-remove CLI; close removed worktree folders in Zed manually if needed.')
|
|
69
|
+
|
|
70
|
+
outcomes.push({ provider: 'zed', supported: false, removed: [] })
|
|
71
|
+
break
|
|
72
|
+
}
|
|
73
|
+
default: {
|
|
74
|
+
assertNever(ide)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return outcomes
|
|
80
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type IdeProvider = 'cursor' | 'zed'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI-level open modes for `worktrees-add` (`--ide`). `workspace` adds each
|
|
5
|
+
* created worktree to the configured editor's workspace and opens it (the only
|
|
6
|
+
* attach style); `none` means "don't open any editor".
|
|
7
|
+
*/
|
|
8
|
+
export const IDE_MODES = ['workspace', 'none'] as const
|
|
9
|
+
export type IdeMode = (typeof IDE_MODES)[number]
|
|
10
|
+
|
|
11
|
+
// Each facade returns one outcome PER configured provider (an array), so every
|
|
12
|
+
// outcome is tagged with its `provider`.
|
|
13
|
+
export interface OpenIdeWorkspaceOutcome {
|
|
14
|
+
ran: boolean
|
|
15
|
+
provider: IdeProvider
|
|
16
|
+
added: number
|
|
17
|
+
removed: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AddIdeWorktreeFoldersOutcome {
|
|
21
|
+
ran: boolean
|
|
22
|
+
provider: IdeProvider
|
|
23
|
+
added: number
|
|
24
|
+
skipped: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RemoveIdeWorktreeFoldersOutcome {
|
|
28
|
+
provider: IdeProvider
|
|
29
|
+
/** False when the configured provider has no folder-remove capability (Zed). */
|
|
30
|
+
supported: boolean
|
|
31
|
+
removed: string[]
|
|
32
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { openZedWorkspace } from '../open-zed-workspace'
|
|
4
|
+
|
|
5
|
+
const zx = vi.hoisted(() => {
|
|
6
|
+
return { calls: [] as unknown[][], shouldThrow: false }
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
vi.mock('zx', () => {
|
|
10
|
+
return {
|
|
11
|
+
$: vi.fn((_strings: TemplateStringsArray, ...values: unknown[]) => {
|
|
12
|
+
zx.calls.push(values)
|
|
13
|
+
|
|
14
|
+
if (zx.shouldThrow) {
|
|
15
|
+
return Promise.reject(new Error('zed failed to launch'))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return Promise.resolve({ stdout: '' })
|
|
19
|
+
}),
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
vi.mock('src/lib/logger', () => {
|
|
24
|
+
return {
|
|
25
|
+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('openZedWorkspace', () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
zx.calls = []
|
|
32
|
+
zx.shouldThrow = false
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('opens a single workspace with the project root plus every worktree path', async () => {
|
|
36
|
+
const outcome = await openZedWorkspace({
|
|
37
|
+
projectRoot: '/repo',
|
|
38
|
+
worktreeDir: '/repo.worktrees',
|
|
39
|
+
currentBranches: ['release/v1.0.0', 'release/v1.1.0'],
|
|
40
|
+
skipRelaunchWhenEmpty: false,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
expect(zx.calls).toHaveLength(1)
|
|
44
|
+
expect(zx.calls[0]?.[0]).toEqual(['/repo', '/repo.worktrees/release/v1.0.0', '/repo.worktrees/release/v1.1.0'])
|
|
45
|
+
expect(outcome).toEqual({ ran: true, added: 2, removed: 0 })
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('still opens the bare project root when there are worktrees but relaunch is not skipped', async () => {
|
|
49
|
+
const outcome = await openZedWorkspace({
|
|
50
|
+
projectRoot: '/repo',
|
|
51
|
+
worktreeDir: '/repo.worktrees',
|
|
52
|
+
currentBranches: [],
|
|
53
|
+
skipRelaunchWhenEmpty: false,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
expect(zx.calls).toHaveLength(1)
|
|
57
|
+
expect(zx.calls[0]?.[0]).toEqual(['/repo'])
|
|
58
|
+
expect(outcome).toEqual({ ran: true, added: 0, removed: 0 })
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('skips launching when skipRelaunchWhenEmpty is set and there are no worktrees', async () => {
|
|
62
|
+
const outcome = await openZedWorkspace({
|
|
63
|
+
projectRoot: '/repo',
|
|
64
|
+
worktreeDir: '/repo.worktrees',
|
|
65
|
+
currentBranches: [],
|
|
66
|
+
skipRelaunchWhenEmpty: true,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
expect(zx.calls).toHaveLength(0)
|
|
70
|
+
expect(outcome).toEqual({ ran: false, added: 0, removed: 0 })
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('swallows a launch failure into a best-effort warning', async () => {
|
|
74
|
+
zx.shouldThrow = true
|
|
75
|
+
|
|
76
|
+
const outcome = await openZedWorkspace({
|
|
77
|
+
projectRoot: '/repo',
|
|
78
|
+
worktreeDir: '/repo.worktrees',
|
|
79
|
+
currentBranches: ['release/v1.0.0'],
|
|
80
|
+
skipRelaunchWhenEmpty: false,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(outcome).toEqual({ ran: false, added: 0, removed: 0 })
|
|
84
|
+
})
|
|
85
|
+
})
|