infra-kit 0.1.110 → 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.
- package/.eslintcache +1 -1
- 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-d5aae73c-340d-4aa2-9fe0-1bf2d79b69dc.jsonl +10 -0
- package/.omc/state/idle-notif-cooldown.json +1 -1
- 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/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/d5aae73c-340d-4aa2-9fe0-1bf2d79b69dc/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/d5aae73c-340d-4aa2-9fe0-1bf2d79b69dc/mission-state.json +103 -0
- package/.omc/state/sessions/d5aae73c-340d-4aa2-9fe0-1bf2d79b69dc/pre-tool-advisory-throttle.json +30 -0
- package/.omc/state/sessions/d5aae73c-340d-4aa2-9fe0-1bf2d79b69dc/ralph-state.json +13 -0
- package/.omc/state/sessions/d5aae73c-340d-4aa2-9fe0-1bf2d79b69dc/subagent-tracking-state.json +62 -0
- package/.omc/state/sessions/d5aae73c-340d-4aa2-9fe0-1bf2d79b69dc/ultrawork-state.json +11 -0
- package/.turbo/turbo-build.log +6 -6
- package/.turbo/turbo-eslint-check.log +1 -1
- package/.turbo/turbo-prettier-check.log +1 -1
- package/.turbo/turbo-prettier-fix.log +1 -2
- package/.turbo/turbo-test.log +79 -70
- package/.turbo/turbo-ts-check.log +1 -1
- package/dist/cli.js +64 -56
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +39 -39
- 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/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 +98 -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/release-create/release-create.ts +5 -13
- 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 +35 -25
- package/src/integrations/cursor/index.ts +1 -0
- package/src/integrations/cursor/open-cursor-workspace.ts +70 -0
- package/src/integrations/ide/__tests__/ide-facade.test.ts +242 -0
- package/src/integrations/ide/add-ide-worktree-folders.ts +79 -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/version-utils/__tests__/next-version.test.ts +84 -0
- package/src/lib/version-utils/index.ts +1 -0
- package/src/lib/version-utils/next-version.ts +56 -15
- package/src/mcp/tools/index.ts +2 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -6,7 +6,7 @@ import { logger } from 'src/lib/logger'
|
|
|
6
6
|
import { removeManagedBlock, upsertManagedBlock } from 'src/lib/managed-block'
|
|
7
7
|
|
|
8
8
|
import { writeAgentFiles } from './agent-files'
|
|
9
|
-
import { migrateLegacyConfig } from './migrate-config'
|
|
9
|
+
import { migrateLegacyConfig, normalizeLegacyIdeStructures } from './migrate-config'
|
|
10
10
|
|
|
11
11
|
export const MARKER_START = '# -- infra-kit:begin --'
|
|
12
12
|
export const MARKER_END = '# -- infra-kit:end --'
|
|
@@ -36,18 +36,27 @@ const USER_GLOBAL_CONFIG_EXAMPLE = `// infra-kit user-global config — ~/.infra
|
|
|
36
36
|
{
|
|
37
37
|
// "ide": {
|
|
38
38
|
// "provider": "cursor",
|
|
39
|
-
// "config": { "
|
|
39
|
+
// "config": { "workspaceConfigPath": "/path/to/your.code-workspace" }
|
|
40
40
|
// },
|
|
41
|
+
// // Or, for Zed (no workspace file — one window with all worktrees via "zed <root> <wt...>"):
|
|
42
|
+
// "ide": { "provider": "zed", "config": {} },
|
|
43
|
+
// // Or drive BOTH editors at once with an array (at most one entry per provider):
|
|
44
|
+
// "ide": [
|
|
45
|
+
// { "provider": "cursor", "config": { "workspaceConfigPath": "/path/to/your.code-workspace" } },
|
|
46
|
+
// { "provider": "zed", "config": {} }
|
|
47
|
+
// ],
|
|
41
48
|
// "worktrees": { "openInGithubDesktop": false, "openInCmux": true }
|
|
42
49
|
}
|
|
43
50
|
`
|
|
44
51
|
|
|
45
52
|
/**
|
|
46
53
|
* Append infra-kit shell functions to .zshrc, migrate any legacy
|
|
47
|
-
* `infra-kit.yml` config layers to JSON,
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* the
|
|
54
|
+
* `infra-kit.yml` config layers to JSON, normalize existing JSON configs from
|
|
55
|
+
* the old IDE structure to the new one (strip the removed `ide.config.mode`),
|
|
56
|
+
* and seed the user-global config at ~/.infra-kit/config.json on first run.
|
|
57
|
+
* Idempotent: a subsequent run replaces the existing zshrc block in place, has
|
|
58
|
+
* nothing left to migrate or normalize, and leaves the user-global config
|
|
59
|
+
* untouched.
|
|
51
60
|
*
|
|
52
61
|
* @example
|
|
53
62
|
* // CLI: `infra-kit init` (or via the `pnpm dx-init` alias)
|
|
@@ -79,6 +88,10 @@ export const init = async (): Promise<void> => {
|
|
|
79
88
|
// migrated config.json is not re-seeded as an empty stub.
|
|
80
89
|
await migrateLegacyConfig()
|
|
81
90
|
|
|
91
|
+
// Migrate existing JSON configs from the old IDE structure to the new one
|
|
92
|
+
// (strip the removed `ide.config.mode` field). No-op for already-clean configs.
|
|
93
|
+
await normalizeLegacyIdeStructures()
|
|
94
|
+
|
|
82
95
|
seedUserGlobalConfig()
|
|
83
96
|
|
|
84
97
|
// Best-effort, non-fatal, repo-gated: keep the agent-instruction files in sync
|
|
@@ -144,3 +144,107 @@ export const migrateLegacyConfig = async (): Promise<void> => {
|
|
|
144
144
|
resetInfraKitConfigCache()
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Surgically strip the removed `ide.config.mode` field from a parsed config
|
|
150
|
+
* object (single-provider or array form). The windows-removal made `mode` a dead
|
|
151
|
+
* key — the loader already ignores it, so this only matters for keeping the
|
|
152
|
+
* on-disk file clean. Returns `changed: false` (and the input untouched) when
|
|
153
|
+
* there is no `mode` to remove, so callers can skip rewriting clean files.
|
|
154
|
+
*
|
|
155
|
+
* Only `mode` is removed — every other key is preserved verbatim (no full-schema
|
|
156
|
+
* re-validation), so a config that is otherwise invalid or carries forward-compat
|
|
157
|
+
* keys is never altered beyond the dead field.
|
|
158
|
+
*/
|
|
159
|
+
const stripLegacyIdeMode = (parsed: Record<string, unknown>): { changed: boolean; result: Record<string, unknown> } => {
|
|
160
|
+
const ide = parsed.ide
|
|
161
|
+
|
|
162
|
+
if (ide === null || typeof ide !== 'object') {
|
|
163
|
+
return { changed: false, result: parsed }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let changed = false
|
|
167
|
+
|
|
168
|
+
const stripEntry = (entry: unknown): unknown => {
|
|
169
|
+
if (entry === null || typeof entry !== 'object' || !('config' in entry)) {
|
|
170
|
+
return entry
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const config = (entry as { config: unknown }).config
|
|
174
|
+
|
|
175
|
+
if (config === null || typeof config !== 'object' || !('mode' in config)) {
|
|
176
|
+
return entry
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
changed = true
|
|
180
|
+
|
|
181
|
+
const restConfig = Object.fromEntries(
|
|
182
|
+
Object.entries(config as Record<string, unknown>).filter(([key]) => {
|
|
183
|
+
return key !== 'mode'
|
|
184
|
+
}),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return { ...(entry as Record<string, unknown>), config: restConfig }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const nextIde = Array.isArray(ide) ? ide.map(stripEntry) : stripEntry(ide)
|
|
191
|
+
|
|
192
|
+
if (!changed) {
|
|
193
|
+
return { changed: false, result: parsed }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { changed: true, result: { ...parsed, ide: nextIde } }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Normalize any existing `infra-kit.json` config layers (project, user-global,
|
|
201
|
+
* user-project) from the old IDE structure to the new one by removing the
|
|
202
|
+
* removed `ide.config.mode` field. Run by `infra-kit init` after the YAML→JSON
|
|
203
|
+
* migration. Best-effort and non-fatal per layer; only rewrites a file when its
|
|
204
|
+
* `ide` config actually carries the dead key, so clean configs are left byte-for-
|
|
205
|
+
* byte untouched (idempotent). Resets the config cache when anything changed.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* await normalizeLegacyIdeStructures()
|
|
209
|
+
* // ✓ Normalized ide config in infra-kit.json (removed legacy "mode")
|
|
210
|
+
* // (no output when no config carries a legacy "mode")
|
|
211
|
+
*/
|
|
212
|
+
export const normalizeLegacyIdeStructures = async (): Promise<void> => {
|
|
213
|
+
let paths: Awaited<ReturnType<typeof getInfraKitConfigPaths>>
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
paths = await getInfraKitConfigPaths()
|
|
217
|
+
} catch {
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const jsonPaths = [paths.main, paths.userGlobal, paths.userProject]
|
|
222
|
+
|
|
223
|
+
let normalized = 0
|
|
224
|
+
|
|
225
|
+
for (const jsonPath of jsonPaths) {
|
|
226
|
+
if (!(await fileExists(jsonPath))) continue
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const raw = await fs.readFile(jsonPath, 'utf-8')
|
|
230
|
+
|
|
231
|
+
if (raw.trim() === '') continue
|
|
232
|
+
|
|
233
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>
|
|
234
|
+
const { changed, result } = stripLegacyIdeMode(parsed)
|
|
235
|
+
|
|
236
|
+
if (!changed) continue
|
|
237
|
+
|
|
238
|
+
await fs.writeFile(jsonPath, `${JSON.stringify(result, null, 2)}\n`, 'utf-8')
|
|
239
|
+
|
|
240
|
+
logger.info(`✓ Normalized ide config in ${tildify(jsonPath)} (removed legacy "mode")`)
|
|
241
|
+
normalized++
|
|
242
|
+
} catch (err) {
|
|
243
|
+
logger.info(`⚠ Skipped normalizing ${tildify(jsonPath)} — ${(err as Error).message}`)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (normalized > 0) {
|
|
248
|
+
resetInfraKitConfigCache()
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -15,6 +15,7 @@ import type { ReleaseCreationResult, ReleaseType } from 'src/lib/release-utils'
|
|
|
15
15
|
import {
|
|
16
16
|
NoPriorVersionsError,
|
|
17
17
|
computeNextVersion,
|
|
18
|
+
formatReleaseSpec,
|
|
18
19
|
hasNextToken,
|
|
19
20
|
loadExistingVersions,
|
|
20
21
|
parseVersion,
|
|
@@ -171,20 +172,11 @@ const formatReleaseSummary = (entry: ReleaseEntry): string => {
|
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
const echoReleases = (entries: ReleaseEntry[]): void => {
|
|
175
|
+
// Every entry — versions, "next" (resolved to a concrete version), and named
|
|
176
|
+
// releases alike — echoes through the single --release flag. formatReleaseSpec
|
|
177
|
+
// produces a spec that parseReleaseSpec parses back into the same entry.
|
|
174
178
|
for (const entry of entries) {
|
|
175
|
-
|
|
176
|
-
// Named releases echo the --name form; description is not part of the
|
|
177
|
-
// flag and is set later via release-desc-edit / interactively.
|
|
178
|
-
commandEcho.addOption('--name', entry.id.name)
|
|
179
|
-
|
|
180
|
-
continue
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const spec = entry.description
|
|
184
|
-
? `${entry.id.raw}:${entry.type}:${entry.description}`
|
|
185
|
-
: `${entry.id.raw}:${entry.type}`
|
|
186
|
-
|
|
187
|
-
commandEcho.addOption('--release', spec)
|
|
179
|
+
commandEcho.addOption('--release', formatReleaseSpec(entry))
|
|
188
180
|
}
|
|
189
181
|
}
|
|
190
182
|
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type { CursorMode } from './worktrees-add'
|
|
1
|
+
export { worktreesAdd, worktreesAddMcpTool } from './worktrees-add'
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/* eslint-disable sonarjs/cognitive-complexity */
|
|
2
2
|
import checkbox from '@inquirer/checkbox'
|
|
3
3
|
import confirm from '@inquirer/confirm'
|
|
4
|
-
import select from '@inquirer/select'
|
|
5
4
|
import process from 'node:process'
|
|
6
5
|
import { z } from 'zod'
|
|
7
6
|
import { $ } from 'zx'
|
|
@@ -12,14 +11,15 @@ import {
|
|
|
12
11
|
listCmuxWorkspaceTitles,
|
|
13
12
|
openCmuxWorkspaceWithLayout,
|
|
14
13
|
} from 'src/integrations/cmux'
|
|
15
|
-
import { addFoldersToCursorWorkspace, resolveCursorWorkspacePath } from 'src/integrations/cursor'
|
|
16
14
|
import { getReleasePRsWithInfo } from 'src/integrations/gh'
|
|
15
|
+
import { IDE_MODES, addIdeWorktreeFolders } from 'src/integrations/ide'
|
|
16
|
+
import type { IdeMode } from 'src/integrations/ide'
|
|
17
17
|
import { commandEcho } from 'src/lib/command-echo'
|
|
18
18
|
import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
|
|
19
19
|
import { OperationError } from 'src/lib/errors/operation-error'
|
|
20
20
|
import { assertManagementContext } from 'src/lib/git-guard'
|
|
21
21
|
import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
|
|
22
|
-
import { getInfraKitConfig } from 'src/lib/infra-kit-config'
|
|
22
|
+
import { getInfraKitConfig, resolveConfiguredIdes } from 'src/lib/infra-kit-config'
|
|
23
23
|
import { logger } from 'src/lib/logger'
|
|
24
24
|
import { formatBranchName, isReleaseBranch, parseReleaseRef } from 'src/lib/release-id'
|
|
25
25
|
import { detectReleaseType, formatBranchChoices, getJiraDescriptions, releaseBranchLabels } from 'src/lib/release-utils'
|
|
@@ -31,13 +31,12 @@ import type { RequiredConfirmedOptionArg } from 'src/types'
|
|
|
31
31
|
const FEATURE_DIR = 'feature'
|
|
32
32
|
const RELEASE_DIR = 'release'
|
|
33
33
|
|
|
34
|
-
export const CURSOR_MODES = ['workspace', 'windows', 'none'] as const
|
|
35
|
-
export type CursorMode = (typeof CURSOR_MODES)[number]
|
|
36
|
-
|
|
37
34
|
interface WorktreeManagementArgs extends RequiredConfirmedOptionArg {
|
|
38
35
|
all?: boolean
|
|
39
36
|
versions?: string
|
|
40
|
-
|
|
37
|
+
ide?: IdeMode
|
|
38
|
+
/** @deprecated Alias for `ide`, kept for back-compat. Ignored when `ide` is set. */
|
|
39
|
+
cursor?: IdeMode
|
|
41
40
|
githubDesktop?: boolean
|
|
42
41
|
cmux?: boolean
|
|
43
42
|
}
|
|
@@ -47,7 +46,9 @@ interface WorktreeManagementArgs extends RequiredConfirmedOptionArg {
|
|
|
47
46
|
* Creates worktrees for active release branches and removes unused ones
|
|
48
47
|
*/
|
|
49
48
|
export const worktreesAdd = async (options: WorktreeManagementArgs) => {
|
|
50
|
-
const { confirmedCommand, all, versions,
|
|
49
|
+
const { confirmedCommand, all, versions, githubDesktop, cmux } = options
|
|
50
|
+
// `cursor` is the deprecated alias for `ide`; `ide` wins when both are present.
|
|
51
|
+
const ide = options.ide ?? options.cursor
|
|
51
52
|
|
|
52
53
|
commandEcho.start('worktrees-add')
|
|
53
54
|
|
|
@@ -136,34 +137,13 @@ export const worktreesAdd = async (options: WorktreeManagementArgs) => {
|
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
const config = await getInfraKitConfig()
|
|
139
|
-
const cursorConfig = config.ide?.provider === 'cursor' ? config.ide.config : undefined
|
|
140
|
-
|
|
141
|
-
const cursorMode: CursorMode =
|
|
142
|
-
cursor ??
|
|
143
|
-
cursorConfig?.mode ??
|
|
144
|
-
(await select<CursorMode>({
|
|
145
|
-
message: 'Cursor mode for created worktrees?',
|
|
146
|
-
default: 'workspace',
|
|
147
|
-
choices: [
|
|
148
|
-
{
|
|
149
|
-
name: 'Add to workspace file',
|
|
150
|
-
value: 'workspace',
|
|
151
|
-
description: 'Append each worktree as a folder in ide.config.workspaceConfigPath, then open the workspace',
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
name: 'Open separate windows',
|
|
155
|
-
value: 'windows',
|
|
156
|
-
description: 'Open each created worktree in its own Cursor window',
|
|
157
|
-
},
|
|
158
|
-
{ name: 'Skip', value: 'none', description: 'Do not open Cursor' },
|
|
159
|
-
],
|
|
160
|
-
}))
|
|
161
|
-
|
|
162
|
-
if (typeof cursor === 'undefined' && !cursorConfig?.mode) {
|
|
163
|
-
commandEcho.setInteractive()
|
|
164
|
-
}
|
|
165
140
|
|
|
166
|
-
|
|
141
|
+
// One attach style: every configured editor gets the worktrees added to its
|
|
142
|
+
// workspace. Per-run skip is `--ide none` / `--no-ide`; no editor configured
|
|
143
|
+
// means there's nothing to open.
|
|
144
|
+
const ideMode: IdeMode = ide ?? (resolveConfiguredIdes(config).length > 0 ? 'workspace' : 'none')
|
|
145
|
+
|
|
146
|
+
commandEcho.addOption('--ide', ideMode)
|
|
167
147
|
|
|
168
148
|
const openInGithubDesktop =
|
|
169
149
|
githubDesktop ??
|
|
@@ -202,28 +182,8 @@ export const worktreesAdd = async (options: WorktreeManagementArgs) => {
|
|
|
202
182
|
|
|
203
183
|
logResults(createdWorktrees)
|
|
204
184
|
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
logger.warn('⚠️ Skipping Cursor: ide.config.workspaceConfigPath is not set in infra-kit config')
|
|
208
|
-
} else {
|
|
209
|
-
const workspacePath = resolveCursorWorkspacePath(cursorConfig.workspaceConfigPath, projectRoot)
|
|
210
|
-
|
|
211
|
-
const folderPaths = createdWorktrees.map((branch) => {
|
|
212
|
-
return `${worktreeDir}/${branch}`
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
const { added, skipped } = await addFoldersToCursorWorkspace({ workspacePath, folderPaths })
|
|
216
|
-
|
|
217
|
-
const skippedSuffix = skipped.length > 0 ? ` (${skipped.length} already present)` : ''
|
|
218
|
-
|
|
219
|
-
logger.info(`✅ Added ${added.length} folder(s) to ${workspacePath}${skippedSuffix}`)
|
|
220
|
-
|
|
221
|
-
await $`cursor ${workspacePath}`
|
|
222
|
-
}
|
|
223
|
-
} else if (cursorMode === 'windows') {
|
|
224
|
-
for (const branch of createdWorktrees) {
|
|
225
|
-
await $`cursor ${worktreeDir}/${branch}`
|
|
226
|
-
}
|
|
185
|
+
if (ideMode === 'workspace') {
|
|
186
|
+
await addIdeWorktreeFolders({ projectRoot, worktreeDir, branches: createdWorktrees })
|
|
227
187
|
}
|
|
228
188
|
|
|
229
189
|
if (openInGithubDesktop) {
|
|
@@ -369,12 +329,16 @@ export const worktreesAddMcpTool = defineMcpTool({
|
|
|
369
329
|
.describe(
|
|
370
330
|
'Comma-separated release versions or names to target (e.g. "1.2.5, 1.2.6" or "checkout-redesign, 1.2.5"). Either "versions" or all=true must be provided for MCP calls. Overrides "all" when set.',
|
|
371
331
|
),
|
|
372
|
-
|
|
373
|
-
.enum(
|
|
332
|
+
ide: z
|
|
333
|
+
.enum(IDE_MODES)
|
|
374
334
|
.optional()
|
|
375
335
|
.describe(
|
|
376
|
-
'
|
|
336
|
+
'Editor open mode for created worktrees, applied to all configured editors (Cursor and/or Zed, per the "ide" config). "workspace" (the only attach style) adds each worktree to every configured editor workspace and opens it. "none" skips the editor. Resolution order: this flag → "workspace" when at least one "ide" is configured → "none" otherwise.',
|
|
377
337
|
),
|
|
338
|
+
cursor: z
|
|
339
|
+
.enum(IDE_MODES)
|
|
340
|
+
.optional()
|
|
341
|
+
.describe('Deprecated alias for "ide". Prefer "ide". Ignored when "ide" is provided.'),
|
|
378
342
|
githubDesktop: z
|
|
379
343
|
.boolean()
|
|
380
344
|
.optional()
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import { $ } from 'zx'
|
|
3
2
|
|
|
4
3
|
import {
|
|
5
4
|
buildCmuxWorkspaceTitle,
|
|
@@ -7,26 +6,27 @@ import {
|
|
|
7
6
|
listCmuxWorkspaceTitles,
|
|
8
7
|
openCmuxWorkspaceWithLayout,
|
|
9
8
|
} from 'src/integrations/cmux'
|
|
10
|
-
import {
|
|
9
|
+
import { ideProviderLabel, openIdeWorkspace } from 'src/integrations/ide'
|
|
10
|
+
import type { IdeProvider } from 'src/integrations/ide'
|
|
11
11
|
import { commandEcho } from 'src/lib/command-echo'
|
|
12
12
|
import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
|
|
13
13
|
import { OperationError } from 'src/lib/errors/operation-error'
|
|
14
14
|
import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
|
|
15
|
-
import { getInfraKitConfig } from 'src/lib/infra-kit-config'
|
|
16
15
|
import { logger } from 'src/lib/logger'
|
|
17
16
|
import { defineMcpTool, textContent } from 'src/types'
|
|
18
17
|
|
|
19
18
|
interface WorktreesOpenResult {
|
|
20
19
|
openedCmux: string[]
|
|
21
20
|
skippedCmux: string[]
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
ideProviders: IdeProvider[]
|
|
22
|
+
ideFoldersAdded: number
|
|
23
|
+
ideFoldersRemoved: number
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Cold-start restore command: reconciles
|
|
28
|
-
* of release worktrees on disk, opens
|
|
29
|
-
* workspace exists per worktree. Idempotent and additive — never removes
|
|
27
|
+
* Cold-start restore command: reconciles the configured editor workspace against
|
|
28
|
+
* the set of release worktrees on disk, opens the editor against it, and ensures
|
|
29
|
+
* one cmux workspace exists per worktree. Idempotent and additive — never removes
|
|
30
30
|
* worktrees, never recreates running cmux workspaces.
|
|
31
31
|
*/
|
|
32
32
|
export const worktreesOpen = async () => {
|
|
@@ -37,17 +37,38 @@ export const worktreesOpen = async () => {
|
|
|
37
37
|
const worktreeDir = `${projectRoot}${WORKTREES_DIR_SUFFIX}`
|
|
38
38
|
const currentBranches = await getCurrentWorktrees('release')
|
|
39
39
|
|
|
40
|
-
const
|
|
40
|
+
const ideOutcomes = await openIdeWorkspace({
|
|
41
|
+
projectRoot,
|
|
42
|
+
worktreeDir,
|
|
43
|
+
currentBranches,
|
|
44
|
+
skipRelaunchWhenEmpty: false,
|
|
45
|
+
})
|
|
41
46
|
const cmuxOutcome = await openCmux({ worktreeDir, currentBranches })
|
|
42
47
|
|
|
43
48
|
const result: WorktreesOpenResult = {
|
|
44
49
|
openedCmux: cmuxOutcome.opened,
|
|
45
50
|
skippedCmux: cmuxOutcome.skipped,
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
ideProviders: ideOutcomes
|
|
52
|
+
.filter((outcome) => {
|
|
53
|
+
return outcome.ran
|
|
54
|
+
})
|
|
55
|
+
.map((outcome) => {
|
|
56
|
+
return outcome.provider
|
|
57
|
+
}),
|
|
58
|
+
ideFoldersAdded: ideOutcomes.reduce((sum, outcome) => {
|
|
59
|
+
return sum + outcome.added
|
|
60
|
+
}, 0),
|
|
61
|
+
ideFoldersRemoved: ideOutcomes.reduce((sum, outcome) => {
|
|
62
|
+
return sum + outcome.removed
|
|
63
|
+
}, 0),
|
|
48
64
|
}
|
|
49
65
|
|
|
50
|
-
logResults(result, {
|
|
66
|
+
logResults(result, {
|
|
67
|
+
ideRan: ideOutcomes.some((outcome) => {
|
|
68
|
+
return outcome.ran
|
|
69
|
+
}),
|
|
70
|
+
cmuxRan: cmuxOutcome.ran,
|
|
71
|
+
})
|
|
51
72
|
|
|
52
73
|
commandEcho.print()
|
|
53
74
|
|
|
@@ -64,49 +85,6 @@ export const worktreesOpen = async () => {
|
|
|
64
85
|
}
|
|
65
86
|
}
|
|
66
87
|
|
|
67
|
-
interface OpenCursorArgs {
|
|
68
|
-
projectRoot: string
|
|
69
|
-
worktreeDir: string
|
|
70
|
-
currentBranches: string[]
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface OpenCursorOutcome {
|
|
74
|
-
ran: boolean
|
|
75
|
-
added: number
|
|
76
|
-
removed: number
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const openCursor = async (args: OpenCursorArgs): Promise<OpenCursorOutcome> => {
|
|
80
|
-
const { projectRoot, worktreeDir, currentBranches } = args
|
|
81
|
-
|
|
82
|
-
const config = await getInfraKitConfig()
|
|
83
|
-
const cursorConfig = config.ide?.provider === 'cursor' ? config.ide.config : undefined
|
|
84
|
-
|
|
85
|
-
if (!cursorConfig || cursorConfig.mode !== 'workspace' || !cursorConfig.workspaceConfigPath) {
|
|
86
|
-
logger.warn('⚠️ Skipping Cursor: ide.provider must be "cursor", mode "workspace", and workspaceConfigPath set.')
|
|
87
|
-
|
|
88
|
-
return { ran: false, added: 0, removed: 0 }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const workspacePath = resolveCursorWorkspacePath(cursorConfig.workspaceConfigPath, projectRoot)
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
const { added, removed } = await reconcileCursorWorkspaceFolders({
|
|
95
|
-
workspacePath,
|
|
96
|
-
worktreeDir,
|
|
97
|
-
currentBranches,
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
await $`cursor ${workspacePath}`
|
|
101
|
-
|
|
102
|
-
return { ran: true, added: added.length, removed: removed.length }
|
|
103
|
-
} catch (error) {
|
|
104
|
-
logger.warn({ error }, `⚠️ Failed to reconcile/open Cursor workspace at ${workspacePath}`)
|
|
105
|
-
|
|
106
|
-
return { ran: false, added: 0, removed: 0 }
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
88
|
interface OpenCmuxArgs {
|
|
111
89
|
worktreeDir: string
|
|
112
90
|
currentBranches: string[]
|
|
@@ -153,18 +131,24 @@ export const openCmux = async (args: OpenCmuxArgs): Promise<OpenCmuxOutcome> =>
|
|
|
153
131
|
}
|
|
154
132
|
|
|
155
133
|
interface LogResultsContext {
|
|
156
|
-
|
|
134
|
+
ideRan: boolean
|
|
157
135
|
cmuxRan: boolean
|
|
158
136
|
}
|
|
159
137
|
|
|
160
138
|
const logResults = (result: WorktreesOpenResult, context: LogResultsContext): void => {
|
|
161
|
-
if (context.
|
|
162
|
-
|
|
163
|
-
|
|
139
|
+
if (context.ideRan) {
|
|
140
|
+
const ideLabels = result.ideProviders
|
|
141
|
+
.map((provider) => {
|
|
142
|
+
return ideProviderLabel(provider)
|
|
143
|
+
})
|
|
144
|
+
.join(', ')
|
|
145
|
+
|
|
146
|
+
if (result.ideFoldersAdded > 0) {
|
|
147
|
+
logger.info(`✅ Added ${result.ideFoldersAdded} folder(s) to ${ideLabels} workspace(s)`)
|
|
164
148
|
}
|
|
165
149
|
|
|
166
|
-
if (result.
|
|
167
|
-
logger.info(`🧹 Removed ${result.
|
|
150
|
+
if (result.ideFoldersRemoved > 0) {
|
|
151
|
+
logger.info(`🧹 Removed ${result.ideFoldersRemoved} dangling folder(s) from ${ideLabels} workspace(s)`)
|
|
168
152
|
}
|
|
169
153
|
}
|
|
170
154
|
|
|
@@ -180,11 +164,11 @@ const logResults = (result: WorktreesOpenResult, context: LogResultsContext): vo
|
|
|
180
164
|
}
|
|
181
165
|
|
|
182
166
|
if (
|
|
183
|
-
!context.
|
|
167
|
+
!context.ideRan &&
|
|
184
168
|
result.openedCmux.length === 0 &&
|
|
185
169
|
result.skippedCmux.length === 0 &&
|
|
186
|
-
result.
|
|
187
|
-
result.
|
|
170
|
+
result.ideFoldersAdded === 0 &&
|
|
171
|
+
result.ideFoldersRemoved === 0
|
|
188
172
|
) {
|
|
189
173
|
logger.info('ℹ️ Nothing to open')
|
|
190
174
|
}
|
|
@@ -194,15 +178,18 @@ const logResults = (result: WorktreesOpenResult, context: LogResultsContext): vo
|
|
|
194
178
|
export const worktreesOpenMcpTool = defineMcpTool({
|
|
195
179
|
name: 'worktrees-open',
|
|
196
180
|
description:
|
|
197
|
-
'Open Cursor against
|
|
181
|
+
'Open every configured editor (Cursor and/or Zed) against its workspace and ensure a cmux workspace exists for each existing release worktree. Idempotent and additive — never removes worktrees, never recreates running cmux workspaces. Use after a cold start (editor + cmux closed). For stale-worktree cleanup, use worktrees-sync.',
|
|
198
182
|
inputSchema: {},
|
|
199
183
|
outputSchema: {
|
|
200
184
|
openedCmux: z.array(z.string()).describe('Titles of cmux workspaces opened during this run'),
|
|
201
185
|
skippedCmux: z.array(z.string()).describe('Titles of cmux workspaces that were already open'),
|
|
202
|
-
|
|
203
|
-
|
|
186
|
+
ideProviders: z
|
|
187
|
+
.array(z.string())
|
|
188
|
+
.describe('Configured IDE providers that were opened (cursor | zed); empty if none configured'),
|
|
189
|
+
ideFoldersAdded: z.number().describe('Total worktree folders added across all configured editor workspaces'),
|
|
190
|
+
ideFoldersRemoved: z
|
|
204
191
|
.number()
|
|
205
|
-
.describe('
|
|
192
|
+
.describe('Total dangling worktree folders removed across all configured editor workspaces'),
|
|
206
193
|
},
|
|
207
194
|
handler: worktreesOpen,
|
|
208
195
|
})
|
|
@@ -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'
|