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,119 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { closeCmuxWorkspaceByTitle, listCmuxWorkspaceTitles, openCmuxWorkspaceWithLayout } from 'src/integrations/cmux'
|
|
4
|
+
import { getRepoName } from 'src/lib/git-utils'
|
|
5
|
+
|
|
6
|
+
import { closeCmux, reopenCmux } from '../worktrees-reload'
|
|
7
|
+
|
|
8
|
+
vi.mock('src/integrations/cmux', async (importActual) => {
|
|
9
|
+
const actual = await importActual<typeof import('src/integrations/cmux')>()
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
...actual,
|
|
13
|
+
listCmuxWorkspaceTitles: vi.fn(),
|
|
14
|
+
closeCmuxWorkspaceByTitle: vi.fn(),
|
|
15
|
+
openCmuxWorkspaceWithLayout: vi.fn(),
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
vi.mock('src/lib/git-utils', () => {
|
|
20
|
+
return {
|
|
21
|
+
getRepoName: vi.fn(),
|
|
22
|
+
getProjectRoot: vi.fn(),
|
|
23
|
+
getCurrentWorktrees: vi.fn(),
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const REPO = 'hulyo-monorepo'
|
|
28
|
+
const WORKTREE_DIR = '/repos/project-worktrees'
|
|
29
|
+
const BRANCHES = ['release/v1.48.0', 'release/checkout-redesign']
|
|
30
|
+
|
|
31
|
+
const titleFor = {
|
|
32
|
+
versioned: `${REPO} 1.48.0`,
|
|
33
|
+
named: `${REPO} checkout-redesign`,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('closeCmux', () => {
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
vi.clearAllMocks()
|
|
39
|
+
vi.mocked(getRepoName).mockResolvedValue(REPO)
|
|
40
|
+
vi.mocked(closeCmuxWorkspaceByTitle).mockResolvedValue(undefined)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('closes only the branches whose workspace is open in the snapshot', async () => {
|
|
44
|
+
vi.mocked(listCmuxWorkspaceTitles).mockResolvedValue(new Set([titleFor.versioned]))
|
|
45
|
+
|
|
46
|
+
const closed = await closeCmux({ currentBranches: BRANCHES, repoName: REPO })
|
|
47
|
+
|
|
48
|
+
expect(closed).toEqual([titleFor.versioned])
|
|
49
|
+
expect(closeCmuxWorkspaceByTitle).toHaveBeenCalledTimes(1)
|
|
50
|
+
expect(closeCmuxWorkspaceByTitle).toHaveBeenCalledWith(titleFor.versioned)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('closes every open branch when all are present in the snapshot', async () => {
|
|
54
|
+
vi.mocked(listCmuxWorkspaceTitles).mockResolvedValue(new Set([titleFor.versioned, titleFor.named]))
|
|
55
|
+
|
|
56
|
+
const closed = await closeCmux({ currentBranches: BRANCHES, repoName: REPO })
|
|
57
|
+
|
|
58
|
+
expect(closed).toEqual([titleFor.versioned, titleFor.named])
|
|
59
|
+
expect(closeCmuxWorkspaceByTitle).toHaveBeenCalledTimes(2)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('closes nothing when none are open', async () => {
|
|
63
|
+
vi.mocked(listCmuxWorkspaceTitles).mockResolvedValue(new Set())
|
|
64
|
+
|
|
65
|
+
const closed = await closeCmux({ currentBranches: BRANCHES, repoName: REPO })
|
|
66
|
+
|
|
67
|
+
expect(closed).toEqual([])
|
|
68
|
+
expect(closeCmuxWorkspaceByTitle).not.toHaveBeenCalled()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns [] and skips the snapshot when there are no worktrees', async () => {
|
|
72
|
+
const closed = await closeCmux({ currentBranches: [], repoName: REPO })
|
|
73
|
+
|
|
74
|
+
expect(closed).toEqual([])
|
|
75
|
+
expect(listCmuxWorkspaceTitles).not.toHaveBeenCalled()
|
|
76
|
+
expect(closeCmuxWorkspaceByTitle).not.toHaveBeenCalled()
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('reopenCmux', () => {
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
vi.clearAllMocks()
|
|
83
|
+
vi.mocked(getRepoName).mockResolvedValue(REPO)
|
|
84
|
+
vi.mocked(openCmuxWorkspaceWithLayout).mockResolvedValue(undefined)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('force-opens a workspace for every branch (no dedup)', async () => {
|
|
88
|
+
const result = await reopenCmux({ worktreeDir: WORKTREE_DIR, currentBranches: BRANCHES, repoName: REPO })
|
|
89
|
+
|
|
90
|
+
expect(result.opened).toEqual([titleFor.versioned, titleFor.named])
|
|
91
|
+
expect(openCmuxWorkspaceWithLayout).toHaveBeenCalledTimes(2)
|
|
92
|
+
expect(openCmuxWorkspaceWithLayout).toHaveBeenCalledWith({
|
|
93
|
+
cwd: `${WORKTREE_DIR}/release/v1.48.0`,
|
|
94
|
+
title: titleFor.versioned,
|
|
95
|
+
})
|
|
96
|
+
expect(openCmuxWorkspaceWithLayout).toHaveBeenCalledWith({
|
|
97
|
+
cwd: `${WORKTREE_DIR}/release/checkout-redesign`,
|
|
98
|
+
title: titleFor.named,
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('continues past a per-branch failure and reopens the rest', async () => {
|
|
103
|
+
vi.mocked(openCmuxWorkspaceWithLayout)
|
|
104
|
+
.mockRejectedValueOnce(new Error('cmux boom'))
|
|
105
|
+
.mockResolvedValueOnce(undefined)
|
|
106
|
+
|
|
107
|
+
const result = await reopenCmux({ worktreeDir: WORKTREE_DIR, currentBranches: BRANCHES, repoName: REPO })
|
|
108
|
+
|
|
109
|
+
expect(result.opened).toEqual([titleFor.named])
|
|
110
|
+
expect(openCmuxWorkspaceWithLayout).toHaveBeenCalledTimes(2)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('opens nothing when there are no worktrees', async () => {
|
|
114
|
+
const result = await reopenCmux({ worktreeDir: WORKTREE_DIR, currentBranches: [], repoName: REPO })
|
|
115
|
+
|
|
116
|
+
expect(result.opened).toEqual([])
|
|
117
|
+
expect(openCmuxWorkspaceWithLayout).not.toHaveBeenCalled()
|
|
118
|
+
})
|
|
119
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { worktreesReload, worktreesReloadMcpTool } from './worktrees-reload'
|
|
@@ -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
|
|
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
|
|
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 {
|
|
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
|
|
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' && (
|
|
51
|
-
return value as
|
|
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
|
|
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('-
|
|
179
|
-
.option('--no-
|
|
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
|
-
|
|
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 = [
|
|
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',
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"entries": {
|
|
4
|
+
"79a93d4a2f8f50b95f852280616242fee1855dc99a3c75211917f55e72e95fae": {
|
|
5
|
+
"last_emitted_at_ms": 1782112155382,
|
|
6
|
+
"message": "Use parallel execution for independent tasks. Use run_in_background for long operations (npm install, builds, tests)."
|
|
7
|
+
},
|
|
8
|
+
"466399dafa2d20f60587180bad0a07358eb7f2bce724df0ff682f038ad33f5ad": {
|
|
9
|
+
"last_emitted_at_ms": 1782112102061,
|
|
10
|
+
"message": "Read multiple files in parallel when possible for faster analysis."
|
|
11
|
+
},
|
|
12
|
+
"445ed27a3872b681d98190bae61ccb84954e1bc4e140df5370be958dee776b3a": {
|
|
13
|
+
"last_emitted_at_ms": 1782112093035,
|
|
14
|
+
"message": "Verify changes work after editing. Test functionality before marking complete."
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"updated_at": "2026-06-22T07:09:15.382Z"
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"agents": [
|
|
3
|
+
{
|
|
4
|
+
"agent_id": "a56b8e33689099584",
|
|
5
|
+
"agent_type": "unknown",
|
|
6
|
+
"started_at": "2026-06-22T07:13:23.151Z",
|
|
7
|
+
"parent_mode": "none",
|
|
8
|
+
"status": "completed",
|
|
9
|
+
"completed_at": "2026-06-22T07:13:23.151Z",
|
|
10
|
+
"duration_ms": 0
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"total_spawned": 0,
|
|
14
|
+
"total_completed": 1,
|
|
15
|
+
"total_failed": 0,
|
|
16
|
+
"last_updated": "2026-06-22T07:13:23.253Z"
|
|
17
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { addFoldersToCursorWorkspace } from './add-folders-to-workspace'
|
|
2
|
+
export { launchCursor } from './launch-cursor'
|
|
3
|
+
export { openCursorWorkspace } from './open-cursor-workspace'
|
|
2
4
|
export { reconcileCursorWorkspaceFolders } from './reconcile-workspace-folders'
|
|
3
5
|
export { removeFoldersFromCursorWorkspace } from './remove-folders-from-workspace'
|
|
4
6
|
export { resolveCursorWorkspacePath } from './resolve-workspace-path'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { $ } from 'zx'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Spawns Cursor against a `.code-workspace` file (or any path). Centralised here
|
|
5
|
+
* — rather than inlined at each call site — so the editor spawn lives inside the
|
|
6
|
+
* cursor provider module and is covered by a single `vi.mock('src/integrations/cursor')`.
|
|
7
|
+
* Keeping it out of the IDE-orchestration layer is what stops tests that mock the
|
|
8
|
+
* provider (but not `zx`) from shelling out to a real editor.
|
|
9
|
+
*/
|
|
10
|
+
export const launchCursor = async (workspacePath: string): Promise<void> => {
|
|
11
|
+
await $`cursor ${workspacePath}`
|
|
12
|
+
}
|