infra-kit 0.1.114 → 0.1.117
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-1fcfe0ec-7567-41c3-9bfd-501e99e86819.jsonl +19 -0
- package/.omc/state/agent-replay-38382db5-3088-4b5a-aff7-2561a60827c7.jsonl +17 -0
- package/.omc/state/agent-replay-50c36be9-a3a2-42b9-9c85-5a06d3e22f6a.jsonl +5 -0
- package/.omc/state/agent-replay-8198b3e5-fad6-4ee0-9c59-ef773ad89af4.jsonl +1 -0
- package/.omc/state/agent-replay-8cd7652a-b8b5-49cf-9b1f-bee8e5972458.jsonl +3 -0
- package/.omc/state/agent-replay-9bea518f-a9ee-4d14-bb53-a936ebaf647c.jsonl +7 -0
- package/.omc/state/agent-replay-b52d89fb-0e24-4901-bc66-27e3c1697126.jsonl +2 -0
- package/.omc/state/agent-replay-f581a1ae-0ee4-48a3-96f3-ce31a0633bb0.jsonl +1 -0
- package/.omc/state/agent-replay-fe1057b4-9a25-4e8e-8c3f-3e11652ee697.jsonl +5 -0
- package/.omc/state/idle-notif-cooldown.json +1 -1
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/mission-state.json +109 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/pre-tool-advisory-throttle.json +42 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ralph-state.json +13 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/skill-active-state.json +15 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/subagent-tracking-state.json +62 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ultrawork-state.json +11 -0
- package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/mission-state.json +145 -0
- package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/pre-tool-advisory-throttle.json +38 -0
- package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/ralplan-state.json +11 -0
- package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/subagent-tracking-state.json +98 -0
- package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/mission-state.json +61 -0
- package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/pre-tool-advisory-throttle.json +22 -0
- package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/skill-active-state.json +15 -0
- package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/subagent-tracking-state.json +26 -0
- package/{src/lib/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2 → .omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4}/pre-tool-advisory-throttle.json +3 -3
- package/.omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4/subagent-tracking-state.json +17 -0
- package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/ralplan-state.json +11 -0
- package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/subagent-tracking-state.json +35 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/mission-state.json +79 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/pre-tool-advisory-throttle.json +22 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/progress.txt +7 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/skill-active-state.json +15 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/subagent-tracking-state.json +26 -0
- package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/pre-tool-advisory-throttle.json +14 -0
- package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/subagent-tracking-state.json +17 -0
- package/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/subagent-tracking-state.json +17 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/mission-state.json +61 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/prd.json +22 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/pre-tool-advisory-throttle.json +22 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/progress.txt +22 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/skill-active-state.json +15 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/subagent-tracking-state.json +26 -0
- package/.turbo/turbo-build.log +3 -3
- package/.turbo/turbo-eslint-check.log +2 -0
- package/.turbo/turbo-test.log +90 -42
- package/dist/boot-K4QUAFSP.js +2 -0
- package/dist/boot-K4QUAFSP.js.map +7 -0
- package/dist/chunk-FEGOOVZE.js +166 -0
- package/dist/chunk-FEGOOVZE.js.map +7 -0
- package/dist/chunk-WA4BQRDC.js +2 -0
- package/dist/chunk-WA4BQRDC.js.map +7 -0
- package/dist/cli.js +11 -107
- package/dist/cli.js.map +4 -4
- package/dist/entry/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +4 -4
- package/dist/lib/vendor/config-schema.d.ts +58 -0
- package/dist/mcp.js +1 -54
- package/dist/mcp.js.map +4 -4
- package/eslint.config.js +33 -1
- package/package.json +5 -1
- package/scripts/build.js +11 -1
- package/src/.omc/state/agent-replay-c513ef38-cc3e-4a29-9c4c-1e0137245dd5.jsonl +4 -0
- package/src/.omc/state/agent-replay-ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a.jsonl +2 -0
- package/src/.omc/state/idle-notif-cooldown.json +1 -1
- package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/mission-state.json +79 -0
- package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +22 -0
- package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/subagent-tracking-state.json +26 -0
- package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
- package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ralph-state.json +13 -0
- package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ultrawork-state.json +11 -0
- package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
- package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
- package/src/commands/config/config.ts +7 -37
- package/src/commands/doctor/__tests__/agent-files.test.ts +8 -30
- package/src/commands/doctor/__tests__/check-legacy-user-global-config.test.ts +110 -0
- package/src/commands/doctor/doctor.ts +104 -44
- package/src/commands/env-autoload/env-autoload.ts +12 -0
- package/src/commands/env-autoload/index.ts +1 -0
- package/src/commands/env-clear/__tests__/env-clear.test.ts +27 -0
- package/src/commands/env-clear/env-clear.ts +22 -8
- package/src/commands/env-load/__tests__/env-load.test.ts +36 -1
- package/src/commands/env-load/env-load.ts +152 -35
- package/src/commands/env-load/index.ts +2 -1
- package/src/commands/env-status/__tests__/env-status.test.ts +78 -0
- package/src/commands/env-status/env-status.ts +19 -7
- package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +45 -1
- package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +46 -1
- package/src/commands/init/__tests__/agent-files.test.ts +145 -27
- package/src/commands/init/__tests__/migrate-config.test.ts +136 -5
- package/src/commands/init/__tests__/migrate-factory-config.test.ts +93 -0
- package/src/commands/init/__tests__/seed-user-global-config.test.ts +64 -0
- package/src/commands/init/__tests__/shell-body.test.ts +30 -0
- package/src/commands/init/agent-files.ts +65 -45
- package/src/commands/init/init.ts +140 -31
- package/src/commands/init/migrate-config.ts +160 -32
- package/src/commands/vendor-check/__tests__/runtime-isolation.test.ts +71 -0
- package/src/commands/vendor-check/__tests__/vendor-check.test.ts +87 -0
- package/src/commands/vendor-check/index.ts +1 -0
- package/src/commands/vendor-check/vendor-check.ts +127 -0
- package/src/commands/vendor-config/__tests__/vendor-config.test.ts +103 -0
- package/src/commands/vendor-config/index.ts +1 -0
- package/src/commands/vendor-config/vendor-config.ts +162 -0
- package/src/commands/vendor-diff/__tests__/vendor-diff.test.ts +74 -0
- package/src/commands/vendor-diff/index.ts +1 -0
- package/src/commands/vendor-diff/vendor-diff.ts +77 -0
- package/src/commands/vendor-manifest/index.ts +1 -0
- package/src/commands/vendor-manifest/vendor-manifest.ts +60 -0
- package/src/commands/vendor-sync/__tests__/vendor-write.test.ts +86 -0
- package/src/commands/vendor-sync/index.ts +1 -0
- package/src/commands/vendor-sync/vendor-sync.ts +90 -0
- package/src/commands/worktrees-add/worktrees-add.ts +1 -1
- package/src/commands/worktrees-reload/worktrees-reload.ts +6 -5
- package/src/commands/worktrees-remove/worktrees-remove.ts +10 -1
- package/src/commands/worktrees-sync/worktrees-sync.ts +9 -1
- package/src/entry/cli.ts +417 -210
- package/src/entry/index.ts +7 -1
- package/src/integrations/cmux/__tests__/open-workspace-with-layout.test.ts +127 -0
- package/src/integrations/cmux/close-workspace-by-title.ts +2 -2
- package/src/integrations/cmux/list-workspace-titles.ts +1 -1
- package/src/integrations/cmux/open-workspace-with-layout.ts +15 -4
- package/src/integrations/cursor/open-cursor-workspace.ts +6 -11
- package/src/integrations/doppler/__tests__/doppler-errors.test.ts +76 -0
- package/src/integrations/doppler/doppler-cli-auth.ts +12 -4
- package/src/integrations/doppler/doppler-errors.ts +88 -0
- package/src/integrations/doppler/doppler-project.ts +80 -0
- package/src/integrations/doppler/index.ts +3 -1
- package/src/integrations/gh/gh-cli-auth/gh-cli-auth.ts +9 -5
- package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +16 -8
- package/src/integrations/ide/__tests__/ide-facade.test.ts +49 -10
- package/src/integrations/ide/open-ide-workspace.ts +6 -6
- package/src/integrations/ide/remove-ide-worktree-folders.ts +95 -30
- package/src/integrations/ide/types.ts +10 -1
- package/src/integrations/zed/__tests__/open-zed-workspace.test.ts +1 -17
- package/src/integrations/zed/__tests__/reuse-zed-workspace.test.ts +86 -0
- package/src/integrations/zed/index.ts +1 -0
- package/src/integrations/zed/open-zed-workspace.ts +4 -10
- package/src/integrations/zed/reuse-zed-workspace.ts +57 -0
- package/src/lib/command-catalog/__tests__/__snapshots__/command-catalog.test.ts.snap +212 -0
- package/src/lib/command-catalog/__tests__/command-catalog.test.ts +193 -0
- package/src/lib/command-catalog/command-catalog.ts +141 -0
- package/src/lib/command-catalog/index.ts +2 -0
- package/src/lib/command-echo/__tests__/command-echo.test.ts +127 -0
- package/src/lib/constants/constants.ts +13 -0
- package/src/lib/constants/index.ts +2 -0
- package/src/lib/env-autoload/__tests__/env-autoload.test.ts +288 -0
- package/src/lib/env-autoload/env-autoload.ts +294 -0
- package/src/lib/env-autoload/index.ts +9 -0
- package/src/lib/errors/operation-error.ts +1 -1
- package/src/lib/infra-kit-config/__tests__/env-autoload-schema.test.ts +71 -0
- package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +77 -6
- package/src/lib/infra-kit-config/index.ts +9 -1
- package/src/lib/infra-kit-config/infra-kit-config.ts +71 -11
- package/src/lib/json-output/__tests__/json-output.test.ts +139 -0
- package/src/lib/json-output/index.ts +1 -0
- package/src/lib/json-output/json-output.ts +47 -0
- package/src/lib/managed-block/managed-block.ts +4 -4
- package/src/lib/package-config/package-config.ts +1 -3
- package/src/lib/path-display/index.ts +1 -0
- package/src/lib/path-display/path-display.ts +42 -0
- package/src/lib/render/__tests__/render.test.ts +48 -0
- package/src/lib/render/index.ts +1 -0
- package/src/lib/render/render.ts +24 -0
- package/src/lib/vendor/__tests__/config-schema.test.ts +31 -0
- package/src/lib/vendor/__tests__/config.test.ts +63 -0
- package/src/lib/vendor/__tests__/factory-config.test.ts +92 -0
- package/src/lib/vendor/__tests__/vendor-lib.test.ts +158 -0
- package/src/lib/vendor/__tests__/zx-mock.ts +32 -0
- package/src/lib/vendor/config-schema.ts +78 -0
- package/src/lib/vendor/config.ts +58 -0
- package/src/lib/vendor/factory-config-schema.ts +35 -0
- package/src/lib/vendor/factory-config.ts +91 -0
- package/src/lib/vendor/hash.ts +16 -0
- package/src/lib/vendor/index.ts +23 -0
- package/src/lib/vendor/manifest.ts +151 -0
- package/src/lib/vendor/skip-sets.ts +48 -0
- package/src/lib/vendor/sync-ops.ts +290 -0
- package/src/lib/vendor/walk.ts +56 -0
- package/src/mcp/tools/index.ts +5 -42
- package/src/sanity.test.tsx +1 -0
- package/src/tui/__tests__/no-react-boundary.test.ts +51 -0
- package/src/tui/boot.tsx +45 -0
- package/src/tui/screens/__tests__/command-palette.test.tsx +136 -0
- package/src/tui/screens/command-palette.tsx +159 -0
- package/src/tui/types.ts +8 -0
- package/tsconfig.json +1 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +8 -0
- package/src/commands/worktrees-open/__tests__/open-cmux.test.ts +0 -96
- package/src/commands/worktrees-open/index.ts +0 -1
- package/src/commands/worktrees-open/worktrees-open.ts +0 -195
package/src/entry/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// Public library entry for `import { defineConfig } from 'infra-kit'`. Uses
|
|
2
2
|
// relative imports (no `src/*` alias) so the emitted .d.ts stays portable for
|
|
3
|
-
// external consumers. Keep this surface minimal
|
|
3
|
+
// external consumers. Keep this surface minimal.
|
|
4
4
|
export { defineConfig } from '../lib/package-config/package-config'
|
|
5
5
|
export type { InfraKitPackageConfig, InfraKitPackageConfigInput } from '../lib/package-config/package-config'
|
|
6
|
+
// Vendor sync config authoring (`import { defineVendorConfig } from 'infra-kit'`).
|
|
7
|
+
// Imported from the node-free schema module so the emitted .d.ts needs no node types.
|
|
8
|
+
export { defineVendorConfig } from '../lib/vendor/config-schema'
|
|
9
|
+
export type { VendorConfig, VendorCopyItem } from '../lib/vendor/config-schema'
|
|
10
|
+
// The machine-local factory registry is now static JSON (~/.infra-kit/vendor.json),
|
|
11
|
+
// not authored in TypeScript — so there is no `defineFactoryConfig` to export.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { $ } from 'zx'
|
|
3
|
+
|
|
4
|
+
import { getInfraKitConfig } from 'src/lib/infra-kit-config'
|
|
5
|
+
import type { CmuxLayout } from 'src/lib/infra-kit-config'
|
|
6
|
+
|
|
7
|
+
import { openCmuxWorkspaceWithLayout } from '../open-workspace-with-layout'
|
|
8
|
+
|
|
9
|
+
vi.mock('src/lib/infra-kit-config', async (importOriginal) => {
|
|
10
|
+
const actual = await importOriginal<typeof import('src/lib/infra-kit-config')>()
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
getInfraKitConfig: vi.fn(),
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
vi.mock('zx', () => {
|
|
19
|
+
function makeResult(stdout: string) {
|
|
20
|
+
return Object.assign(Promise.resolve({ stdout }), {
|
|
21
|
+
quiet: () => {
|
|
22
|
+
return Promise.resolve({ stdout })
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
$: vi.fn((strings: TemplateStringsArray) => {
|
|
29
|
+
const cmd = strings.join('')
|
|
30
|
+
|
|
31
|
+
if (cmd.includes('workspace create')) return makeResult('created workspace:7\n')
|
|
32
|
+
if (cmd.includes('list-pane-surfaces')) return makeResult('surface:12 (active)\n')
|
|
33
|
+
|
|
34
|
+
return makeResult('')
|
|
35
|
+
}),
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// A worktree cwd that is not under a publicly writable directory (avoids the
|
|
40
|
+
// sonarjs/publicly-writable-directories lint that flags /tmp paths).
|
|
41
|
+
const WORKTREE_CWD = '/home/dev/repo/wt'
|
|
42
|
+
|
|
43
|
+
type CmuxCall = [TemplateStringsArray, ...string[]]
|
|
44
|
+
|
|
45
|
+
const allCommands = (): string[] => {
|
|
46
|
+
const calls = vi.mocked($).mock.calls as unknown as CmuxCall[]
|
|
47
|
+
|
|
48
|
+
return calls.map((call) => {
|
|
49
|
+
return call[0].join('')
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const splitCommands = (): string[] => {
|
|
54
|
+
return allCommands().filter((cmd) => {
|
|
55
|
+
return cmd.includes('new-split')
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const mockLayout = (layout: CmuxLayout): void => {
|
|
60
|
+
vi.mocked(getInfraKitConfig).mockResolvedValue({
|
|
61
|
+
environments: ['dev'],
|
|
62
|
+
envManagement: { provider: 'doppler', config: { name: 'p' } },
|
|
63
|
+
worktrees: { cmux: { layout } },
|
|
64
|
+
} as Awaited<ReturnType<typeof getInfraKitConfig>>)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('openCmuxWorkspaceWithLayout', () => {
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
vi.clearAllMocks()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('two-columns layout issues a single right split (no down split)', async () => {
|
|
73
|
+
mockLayout('two-columns')
|
|
74
|
+
|
|
75
|
+
await openCmuxWorkspaceWithLayout({ cwd: WORKTREE_CWD })
|
|
76
|
+
|
|
77
|
+
const splits = splitCommands()
|
|
78
|
+
|
|
79
|
+
expect(
|
|
80
|
+
splits.some((cmd) => {
|
|
81
|
+
return cmd.includes('new-split right')
|
|
82
|
+
}),
|
|
83
|
+
).toBe(true)
|
|
84
|
+
expect(
|
|
85
|
+
splits.some((cmd) => {
|
|
86
|
+
return cmd.includes('new-split down')
|
|
87
|
+
}),
|
|
88
|
+
).toBe(false)
|
|
89
|
+
expect(splits).toHaveLength(1)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('three-pane layout issues both a right and a down split', async () => {
|
|
93
|
+
mockLayout('three-pane')
|
|
94
|
+
|
|
95
|
+
await openCmuxWorkspaceWithLayout({ cwd: WORKTREE_CWD })
|
|
96
|
+
|
|
97
|
+
const splits = splitCommands()
|
|
98
|
+
|
|
99
|
+
expect(
|
|
100
|
+
splits.some((cmd) => {
|
|
101
|
+
return cmd.includes('new-split right')
|
|
102
|
+
}),
|
|
103
|
+
).toBe(true)
|
|
104
|
+
expect(
|
|
105
|
+
splits.some((cmd) => {
|
|
106
|
+
return cmd.includes('new-split down')
|
|
107
|
+
}),
|
|
108
|
+
).toBe(true)
|
|
109
|
+
expect(splits).toHaveLength(2)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('renames the workspace when a title is provided', async () => {
|
|
113
|
+
mockLayout('two-columns')
|
|
114
|
+
|
|
115
|
+
await openCmuxWorkspaceWithLayout({ cwd: WORKTREE_CWD, title: 'my-title' })
|
|
116
|
+
|
|
117
|
+
const calls = vi.mocked($).mock.calls as unknown as CmuxCall[]
|
|
118
|
+
const renameCall = calls.find((call) => {
|
|
119
|
+
return call[0].join('').includes('workspace rename')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
expect(renameCall).toBeDefined()
|
|
123
|
+
// The title is the last interpolated value of `cmux workspace rename … --title ${title}`,
|
|
124
|
+
// so it lands in the final tuple slot — assert on that value, not tuple membership.
|
|
125
|
+
expect(renameCall?.at(-1)).toBe('my-title')
|
|
126
|
+
})
|
|
127
|
+
})
|
|
@@ -30,7 +30,7 @@ export const closeCmuxWorkspaceByTitle = async (title: string): Promise<void> =>
|
|
|
30
30
|
* title matches `title`, or undefined if no match. Both sides are compared via
|
|
31
31
|
* {@link canonicalizeCmuxTitle} so a workspace stored under a drifted title
|
|
32
32
|
* (whitespace, or an older CLI's `v`-prefixed semver) is still found — keeping
|
|
33
|
-
* close symmetric with the dedup in `worktrees-
|
|
33
|
+
* close symmetric with the cmux open dedup in `worktrees-reload`.
|
|
34
34
|
*
|
|
35
35
|
* Each line looks like:
|
|
36
36
|
* " workspace:8 hulyo-monorepo 1.48.0"
|
|
@@ -40,7 +40,7 @@ const findWorkspaceRefByTitle = (output: string, title: string): string | undefi
|
|
|
40
40
|
const target = canonicalizeCmuxTitle(title)
|
|
41
41
|
|
|
42
42
|
for (const rawLine of output.split('\n')) {
|
|
43
|
-
// eslint-disable-next-line sonarjs/
|
|
43
|
+
// eslint-disable-next-line sonarjs/super-linear-regex, regexp/no-super-linear-backtracking
|
|
44
44
|
const match = rawLine.match(/^[* ]\s*(workspace:\d+)\s+(.+?)(?:\s+\[selected\])?\s*$/)
|
|
45
45
|
|
|
46
46
|
if (!match) {
|
|
@@ -24,7 +24,7 @@ export const listCmuxWorkspaceTitles = async (): Promise<Set<string>> => {
|
|
|
24
24
|
const titles = new Set<string>()
|
|
25
25
|
|
|
26
26
|
for (const rawLine of output.split('\n')) {
|
|
27
|
-
// eslint-disable-next-line sonarjs/
|
|
27
|
+
// eslint-disable-next-line sonarjs/super-linear-regex, regexp/no-super-linear-backtracking
|
|
28
28
|
const match = rawLine.match(/^[* ]\s*workspace:\d+\s+(.+?)(?:\s+\[selected\])?\s*$/)
|
|
29
29
|
|
|
30
30
|
if (!match) {
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import { $ } from 'zx'
|
|
2
2
|
|
|
3
|
+
import { getInfraKitConfig, resolveCmuxLayout } from 'src/lib/infra-kit-config'
|
|
4
|
+
|
|
3
5
|
interface OpenCmuxWorkspaceArgs {
|
|
4
6
|
cwd: string
|
|
5
7
|
title?: string
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
|
-
* Opens a new cmux workspace rooted at `cwd
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* Opens a new cmux workspace rooted at `cwd`, with panes arranged per the
|
|
12
|
+
* configured `worktrees.cmux.layout` (resolved via {@link resolveCmuxLayout},
|
|
13
|
+
* default `two-columns`):
|
|
14
|
+
* two-columns — left | right, both full-height (two panes)
|
|
15
|
+
* three-pane — left-top / left-bottom | full-height right (three panes)
|
|
12
16
|
* All panes inherit `cwd` from the workspace.
|
|
13
17
|
*/
|
|
14
18
|
export const openCmuxWorkspaceWithLayout = async (args: OpenCmuxWorkspaceArgs): Promise<void> => {
|
|
15
19
|
const { cwd, title } = args
|
|
16
20
|
|
|
21
|
+
const layout = resolveCmuxLayout(await getInfraKitConfig())
|
|
22
|
+
|
|
17
23
|
const newWorkspaceOutput = (await $`cmux workspace create --cwd ${cwd}`).stdout
|
|
18
24
|
|
|
19
25
|
const workspaceRef = parseWorkspaceRef(newWorkspaceOutput)
|
|
@@ -22,8 +28,13 @@ export const openCmuxWorkspaceWithLayout = async (args: OpenCmuxWorkspaceArgs):
|
|
|
22
28
|
|
|
23
29
|
const leftTopRef = parseFirstSurfaceRef(surfacesOutput)
|
|
24
30
|
|
|
31
|
+
// Both layouts share the vertical split into left | right columns; only the
|
|
32
|
+
// legacy three-pane layout additionally splits the left column top/bottom.
|
|
25
33
|
await $`cmux new-split right --workspace ${workspaceRef} --surface ${leftTopRef}`
|
|
26
|
-
|
|
34
|
+
|
|
35
|
+
if (layout === 'three-pane') {
|
|
36
|
+
await $`cmux new-split down --workspace ${workspaceRef} --surface ${leftTopRef}`
|
|
37
|
+
}
|
|
27
38
|
|
|
28
39
|
if (title) {
|
|
29
40
|
await $`cmux workspace rename --workspace ${workspaceRef} --title ${title}`
|
|
@@ -8,13 +8,6 @@ interface OpenCursorWorkspaceArgs {
|
|
|
8
8
|
projectRoot: string
|
|
9
9
|
worktreeDir: string
|
|
10
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
11
|
/**
|
|
19
12
|
* The Cursor entry's config, selected by the facade from the (possibly
|
|
20
13
|
* multi-IDE) infra-kit config. Passed in rather than re-read here so this
|
|
@@ -32,14 +25,16 @@ interface OpenCursorWorkspaceOutcome {
|
|
|
32
25
|
|
|
33
26
|
/**
|
|
34
27
|
* Reconciles the configured Cursor `.code-workspace` `folders` array against the
|
|
35
|
-
* set of release worktrees on disk, then launches Cursor against it
|
|
36
|
-
*
|
|
28
|
+
* set of release worktrees on disk, then launches Cursor against it (the folder
|
|
29
|
+
* reconcile still runs when there are no worktrees, but the relaunch is skipped
|
|
30
|
+
* so `worktrees-reload` never pops an empty Cursor window). Used by
|
|
31
|
+
* `worktrees-reload`.
|
|
37
32
|
*
|
|
38
33
|
* No-ops (returns `ran: false`) when the Cursor entry has no `workspaceConfigPath`.
|
|
39
34
|
* All failures are swallowed into a warning — opening Cursor is best-effort.
|
|
40
35
|
*/
|
|
41
36
|
export const openCursorWorkspace = async (args: OpenCursorWorkspaceArgs): Promise<OpenCursorWorkspaceOutcome> => {
|
|
42
|
-
const { projectRoot, worktreeDir, currentBranches,
|
|
37
|
+
const { projectRoot, worktreeDir, currentBranches, cursorConfig } = args
|
|
43
38
|
|
|
44
39
|
if (!cursorConfig.workspaceConfigPath) {
|
|
45
40
|
logger.warn('⚠️ Skipping Cursor: workspaceConfigPath is not set.')
|
|
@@ -56,7 +51,7 @@ export const openCursorWorkspace = async (args: OpenCursorWorkspaceArgs): Promis
|
|
|
56
51
|
currentBranches,
|
|
57
52
|
})
|
|
58
53
|
|
|
59
|
-
if (
|
|
54
|
+
if (currentBranches.length > 0) {
|
|
60
55
|
await launchCursor(workspacePath)
|
|
61
56
|
}
|
|
62
57
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { buildDopplerNotFoundMessage, classifyDopplerDownloadError } from '../doppler-errors'
|
|
4
|
+
|
|
5
|
+
// Verbatim Doppler stderr observed on a not-found download (doppler v3.76.0,
|
|
6
|
+
// 2026-06-27). The "[31m" prefix is the real ANSI color on "Doppler Error:".
|
|
7
|
+
const PROJECT_NOT_FOUND_STDERR =
|
|
8
|
+
"Unable to download secrets\n[31mDoppler Error:[0m Could not find requested project 'infra-kit'\n"
|
|
9
|
+
const CONFIG_NOT_FOUND_STDERR =
|
|
10
|
+
"Unable to download secrets\n[31mDoppler Error:[0m Could not find requested config 'bogus-cfg'\n"
|
|
11
|
+
|
|
12
|
+
describe('classifyDopplerDownloadError', () => {
|
|
13
|
+
it('classifies a project-not-found stderr as "project"', () => {
|
|
14
|
+
expect(classifyDopplerDownloadError(PROJECT_NOT_FOUND_STDERR)).toBe('project')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('classifies a config-not-found stderr as "config"', () => {
|
|
18
|
+
expect(classifyDopplerDownloadError(CONFIG_NOT_FOUND_STDERR)).toBe('config')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('is color-safe — the markers are matched despite ANSI codes', () => {
|
|
22
|
+
expect(classifyDopplerDownloadError('[31mDoppler Error:[0m Could not find requested project')).toBe('project')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('returns "unknown" for unrelated failures (auth, network, timeout)', () => {
|
|
26
|
+
expect(classifyDopplerDownloadError('Doppler Error: you must be logged in')).toBe('unknown')
|
|
27
|
+
expect(classifyDopplerDownloadError('connect ETIMEDOUT')).toBe('unknown')
|
|
28
|
+
expect(classifyDopplerDownloadError('')).toBe('unknown')
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('buildDopplerNotFoundMessage', () => {
|
|
33
|
+
it('names the missing project and points at envManagement.config.name', () => {
|
|
34
|
+
const msg = buildDopplerNotFoundMessage({ kind: 'project', project: 'infra-kit', config: 'dev', available: null })
|
|
35
|
+
|
|
36
|
+
expect(msg).toContain('Doppler project "infra-kit" not found')
|
|
37
|
+
expect(msg).toContain('envManagement.config.name')
|
|
38
|
+
expect(msg).toContain('Fix:')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('lists available projects when the enrichment list is non-empty', () => {
|
|
42
|
+
const msg = buildDopplerNotFoundMessage({
|
|
43
|
+
kind: 'project',
|
|
44
|
+
project: 'infra-kit',
|
|
45
|
+
config: 'dev',
|
|
46
|
+
available: ['example-project', 'nomadream'],
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
expect(msg).toContain('Available projects: example-project, nomadream.')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('omits the available line when the list is empty (told: none exist)', () => {
|
|
53
|
+
const msg = buildDopplerNotFoundMessage({ kind: 'project', project: 'infra-kit', config: 'dev', available: [] })
|
|
54
|
+
|
|
55
|
+
expect(msg).not.toContain('Available projects')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('omits the available line when the list is null (lookup failed)', () => {
|
|
59
|
+
const msg = buildDopplerNotFoundMessage({ kind: 'project', project: 'infra-kit', config: 'dev', available: null })
|
|
60
|
+
|
|
61
|
+
expect(msg).not.toContain('Available projects')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('names the missing config, its project, and points at environments', () => {
|
|
65
|
+
const msg = buildDopplerNotFoundMessage({
|
|
66
|
+
kind: 'config',
|
|
67
|
+
project: 'nomadream',
|
|
68
|
+
config: 'staging',
|
|
69
|
+
available: ['dev', 'arthur'],
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
expect(msg).toContain('Doppler config "staging" not found in project "nomadream"')
|
|
73
|
+
expect(msg).toContain('infra-kit.json → environments')
|
|
74
|
+
expect(msg).toContain('Available configs in "nomadream": dev, arthur.')
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { $ } from 'zx'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Hard upper bound for the Doppler auth probes. `doppler me` is a network call;
|
|
5
|
+
* without a timeout a hung VPN/connection would stall every caller indefinitely
|
|
6
|
+
* (notably the env auto-load preAction hook, which runs before each CLI command).
|
|
7
|
+
*/
|
|
8
|
+
const DOPPLER_AUTH_TIMEOUT_MS = 5_000
|
|
9
|
+
|
|
3
10
|
/**
|
|
4
11
|
* Validate Doppler CLI installation and authentication status. Throws on failure
|
|
5
12
|
* so callers (CLI entry, MCP tool handler) can translate to the right surface —
|
|
6
13
|
* CLI exits non-zero; MCP returns a structured tool error instead of tearing
|
|
7
|
-
* down the server.
|
|
14
|
+
* down the server. Both probes are time-bounded so a hung Doppler/network never
|
|
15
|
+
* blocks the caller (a timeout surfaces as the same "not authenticated" path).
|
|
8
16
|
*/
|
|
9
17
|
export const validateDopplerCliAndAuth = async (): Promise<void> => {
|
|
10
18
|
try {
|
|
11
|
-
await $`doppler --version
|
|
19
|
+
await $`doppler --version`.timeout(DOPPLER_AUTH_TIMEOUT_MS)
|
|
12
20
|
} catch (error: unknown) {
|
|
13
21
|
throw new Error('Doppler CLI is not installed. Install it from: https://docs.doppler.com/docs/install-cli', {
|
|
14
22
|
cause: error,
|
|
@@ -16,8 +24,8 @@ export const validateDopplerCliAndAuth = async (): Promise<void> => {
|
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
try {
|
|
19
|
-
await $`doppler me
|
|
27
|
+
await $`doppler me`.timeout(DOPPLER_AUTH_TIMEOUT_MS)
|
|
20
28
|
} catch (error: unknown) {
|
|
21
|
-
throw new Error('Doppler CLI is not authenticated. Run: doppler login', { cause: error })
|
|
29
|
+
throw new Error('Doppler CLI is not authenticated (or timed out). Run: doppler login', { cause: error })
|
|
22
30
|
}
|
|
23
31
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translate Doppler `secrets download` not-found failures into actionable,
|
|
3
|
+
* point-at-the-fix guidance. Pure (no I/O) so the classification + message policy
|
|
4
|
+
* is unit-testable without spawning the Doppler CLI — mirroring the pure-helper
|
|
5
|
+
* pattern in `commands/env-load/env-load.ts`.
|
|
6
|
+
*
|
|
7
|
+
* Observed Doppler output on a not-found download (verified 2026-06-27, doppler
|
|
8
|
+
* v3.76.0), exit 1, written to STDERR (stdout empty):
|
|
9
|
+
* Unable to download secrets
|
|
10
|
+
* Doppler Error: Could not find requested project 'name'
|
|
11
|
+
* Doppler Error: Could not find requested config 'name'
|
|
12
|
+
* Only the "Doppler Error:" prefix is ANSI-colored; the "Could not find requested
|
|
13
|
+
* ..." text is plain, so substring matching is color-safe. zx's
|
|
14
|
+
* `ProcessOutput.message` also concatenates stdout+stderr, so the same markers are
|
|
15
|
+
* matchable off `.message` when `.stderr` is unavailable.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Which kind of Doppler not-found a download failure represents (`unknown` = not a not-found error). */
|
|
19
|
+
export type DopplerNotFoundKind = 'project' | 'config' | 'unknown'
|
|
20
|
+
|
|
21
|
+
const PROJECT_NOT_FOUND_MARKER = 'Could not find requested project'
|
|
22
|
+
const CONFIG_NOT_FOUND_MARKER = 'Could not find requested config'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Classify a Doppler download failure from its stderr/message text. Returns
|
|
26
|
+
* `'unknown'` for anything that isn't a recognized not-found error so the caller
|
|
27
|
+
* rethrows the original untouched (degrades to status quo, never worse).
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* classifyDopplerDownloadError("Doppler Error: Could not find requested project 'x'") // => 'project'
|
|
31
|
+
* classifyDopplerDownloadError("Doppler Error: Could not find requested config 'dev'") // => 'config'
|
|
32
|
+
* classifyDopplerDownloadError('network unreachable') // => 'unknown'
|
|
33
|
+
*/
|
|
34
|
+
export const classifyDopplerDownloadError = (stderr: string): DopplerNotFoundKind => {
|
|
35
|
+
if (stderr.includes(PROJECT_NOT_FOUND_MARKER)) return 'project'
|
|
36
|
+
|
|
37
|
+
if (stderr.includes(CONFIG_NOT_FOUND_MARKER)) return 'config'
|
|
38
|
+
|
|
39
|
+
return 'unknown'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface DopplerNotFoundMessageArgs {
|
|
43
|
+
/** Whether the missing entity is the project or the config within it. */
|
|
44
|
+
kind: 'project' | 'config'
|
|
45
|
+
project: string
|
|
46
|
+
config: string
|
|
47
|
+
/**
|
|
48
|
+
* Names to suggest. `null` = lookup failed/unavailable, `[]` = none exist; both
|
|
49
|
+
* omit the suggestion line so a failed enrichment probe is never misreported as
|
|
50
|
+
* a definitive "none available".
|
|
51
|
+
*/
|
|
52
|
+
available?: string[] | null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build the actionable, multi-line error body for a Doppler not-found failure:
|
|
57
|
+
* states what's missing, points at the exact infra-kit.json field, lists available
|
|
58
|
+
* names when known, and gives a concrete fix.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* buildDopplerNotFoundMessage({ kind: 'project', project: 'infra-kit', config: 'dev', available: ['nomadream'] })
|
|
62
|
+
* // => 'Doppler project "infra-kit" not found (set in infra-kit.json → envManagement.config.name).\nAvailable projects: nomadream.\nFix: ...'
|
|
63
|
+
*/
|
|
64
|
+
export const buildDopplerNotFoundMessage = ({
|
|
65
|
+
kind,
|
|
66
|
+
project,
|
|
67
|
+
config,
|
|
68
|
+
available,
|
|
69
|
+
}: DopplerNotFoundMessageArgs): string => {
|
|
70
|
+
const lines: string[] =
|
|
71
|
+
kind === 'project'
|
|
72
|
+
? [`Doppler project "${project}" not found (set in infra-kit.json → envManagement.config.name).`]
|
|
73
|
+
: [`Doppler config "${config}" not found in project "${project}" (set in infra-kit.json → environments).`]
|
|
74
|
+
|
|
75
|
+
if (available && available.length > 0) {
|
|
76
|
+
const label = kind === 'project' ? 'Available projects' : `Available configs in "${project}"`
|
|
77
|
+
|
|
78
|
+
lines.push(`${label}: ${available.join(', ')}.`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
lines.push(
|
|
82
|
+
kind === 'project'
|
|
83
|
+
? 'Fix: update envManagement.config.name to an existing project, or create it in Doppler.'
|
|
84
|
+
: 'Fix: update environments to an existing config, or create it in Doppler.',
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return lines.join('\n')
|
|
88
|
+
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
import { $ } from 'zx'
|
|
2
|
+
import type { ProcessPromise } from 'zx'
|
|
3
|
+
|
|
1
4
|
import { getInfraKitConfig } from 'src/lib/infra-kit-config'
|
|
2
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Hard upper bound for the Doppler list probes. These run only on error/diagnostic
|
|
8
|
+
* paths (download-failure enrichment, `doctor`), but a hung VPN/connection must not
|
|
9
|
+
* stall them — mirrors the auth-probe budget in `doppler-cli-auth.ts`.
|
|
10
|
+
*/
|
|
11
|
+
const DOPPLER_LIST_TIMEOUT_MS = 5_000
|
|
12
|
+
|
|
3
13
|
/**
|
|
4
14
|
* Resolve Doppler project name from infra-kit.json at the project root
|
|
5
15
|
*/
|
|
@@ -8,3 +18,73 @@ export const getDopplerProject = async (): Promise<string> => {
|
|
|
8
18
|
|
|
9
19
|
return envManagement.config.name
|
|
10
20
|
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* List the Doppler project names for the authenticated account, or `null` when the
|
|
24
|
+
* lookup itself fails (CLI missing, unauthenticated, network/timeout, malformed
|
|
25
|
+
* JSON). NEVER throws. Callers MUST distinguish `null` ("couldn't tell") from `[]`
|
|
26
|
+
* ("told: none exist") so an enrichment or diagnostic message never misreports a
|
|
27
|
+
* failed probe as "no projects available".
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* await listDopplerProjects() // => ['example-project', 'nomadream'] (or null on failure)
|
|
31
|
+
*/
|
|
32
|
+
export const listDopplerProjects = async (): Promise<string[] | null> => {
|
|
33
|
+
return runDopplerNameList(() => {
|
|
34
|
+
return $`doppler projects --json`
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* List the Doppler config names for a project, or `null` when the lookup itself
|
|
40
|
+
* fails (see {@link listDopplerProjects} for the `null` vs `[]` contract).
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* await listDopplerConfigs('nomadream') // => ['dev', 'oleg', 'prod', 'arthur'] (or null on failure)
|
|
44
|
+
*/
|
|
45
|
+
export const listDopplerConfigs = async (project: string): Promise<string[] | null> => {
|
|
46
|
+
return runDopplerNameList(() => {
|
|
47
|
+
return $`doppler configs --project ${project} --json`
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run a `doppler ... --json` listing quietly + time-bounded and extract the `name`
|
|
53
|
+
* field of each entry. Returns `null` on any failure (subprocess error, timeout,
|
|
54
|
+
* non-array/malformed JSON) so the caller can tell a failed probe from an empty set.
|
|
55
|
+
*/
|
|
56
|
+
const runDopplerNameList = async (run: () => ProcessPromise): Promise<string[] | null> => {
|
|
57
|
+
const prevQuiet = $.quiet
|
|
58
|
+
|
|
59
|
+
$.quiet = true
|
|
60
|
+
try {
|
|
61
|
+
const result = await run().timeout(DOPPLER_LIST_TIMEOUT_MS)
|
|
62
|
+
|
|
63
|
+
return parseDopplerNames(result.stdout)
|
|
64
|
+
} catch {
|
|
65
|
+
return null
|
|
66
|
+
} finally {
|
|
67
|
+
$.quiet = prevQuiet
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Parse a Doppler `--json` array into its `name` strings, or `null` if the shape is unexpected. */
|
|
72
|
+
const parseDopplerNames = (stdout: string): string[] | null => {
|
|
73
|
+
let parsed: unknown
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(stdout)
|
|
77
|
+
} catch {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!Array.isArray(parsed)) return null
|
|
82
|
+
|
|
83
|
+
return parsed
|
|
84
|
+
.map((entry) => {
|
|
85
|
+
return entry !== null && typeof entry === 'object' ? (entry as { name?: unknown }).name : undefined
|
|
86
|
+
})
|
|
87
|
+
.filter((name): name is string => {
|
|
88
|
+
return typeof name === 'string'
|
|
89
|
+
})
|
|
90
|
+
}
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { validateDopplerCliAndAuth } from './doppler-cli-auth'
|
|
2
|
-
export {
|
|
2
|
+
export { buildDopplerNotFoundMessage, classifyDopplerDownloadError } from './doppler-errors'
|
|
3
|
+
export type { DopplerNotFoundKind } from './doppler-errors'
|
|
4
|
+
export { getDopplerProject, listDopplerConfigs, listDopplerProjects } from './doppler-project'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import process from 'node:process'
|
|
2
1
|
import { $ } from 'zx'
|
|
3
2
|
|
|
3
|
+
import { OperationError } from 'src/lib/errors/operation-error'
|
|
4
4
|
import { logger } from 'src/lib/logger'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -11,15 +11,19 @@ export const validateGitHubCliAndAuth = async () => {
|
|
|
11
11
|
await $`gh --version`
|
|
12
12
|
} catch (error: unknown) {
|
|
13
13
|
logger.error({ error }, 'Error: GitHub CLI (gh) is not installed.')
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
throw new OperationError(error, {
|
|
15
|
+
operation: 'verify GitHub CLI is installed',
|
|
16
|
+
remediation: 'install gh from https://cli.github.com/',
|
|
17
|
+
})
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
try {
|
|
19
21
|
await $`gh auth status`
|
|
20
22
|
} catch (error: unknown) {
|
|
21
23
|
logger.error({ error }, 'Error: GitHub CLI (gh) is not authenticated.')
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
throw new OperationError(error, {
|
|
25
|
+
operation: 'verify GitHub CLI authentication',
|
|
26
|
+
remediation: 'run "gh auth login" (https://cli.github.com/manual/gh_auth_login)',
|
|
27
|
+
})
|
|
24
28
|
}
|
|
25
29
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import process from 'node:process'
|
|
2
1
|
import { $ } from 'zx'
|
|
3
2
|
|
|
3
|
+
import { OperationError } from 'src/lib/errors/operation-error'
|
|
4
4
|
import { logger } from 'src/lib/logger'
|
|
5
5
|
import { compareReleaseIds, formatBranchName, formatPrTitle, parseBranchName } from 'src/lib/release-id'
|
|
6
6
|
import type { ReleaseId } from 'src/lib/release-id'
|
|
@@ -84,18 +84,21 @@ export const getReleasePRs = async (): Promise<string[]> => {
|
|
|
84
84
|
const prs = await fetchAllReleasePRs()
|
|
85
85
|
|
|
86
86
|
if (prs.length === 0) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
throw new OperationError(undefined, {
|
|
88
|
+
operation: 'find open release PRs',
|
|
89
|
+
remediation: 'open a release PR first, or check you are in the right repo',
|
|
90
|
+
})
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
return sortReleasePRs(prs).map((pr) => {
|
|
93
94
|
return pr.headRefName
|
|
94
95
|
})
|
|
95
96
|
} catch (error) {
|
|
97
|
+
if (error instanceof OperationError) throw error
|
|
98
|
+
|
|
96
99
|
logger.error({ error }, '❌ Error fetching release PRs')
|
|
97
100
|
|
|
98
|
-
|
|
101
|
+
throw new OperationError(error, { operation: 'fetch release PRs' })
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
|
|
@@ -110,8 +113,10 @@ export const getReleasePRsWithInfo = async (): Promise<ReleasePRInfo[]> => {
|
|
|
110
113
|
const prs = await fetchAllReleasePRs()
|
|
111
114
|
|
|
112
115
|
if (prs.length === 0) {
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
throw new OperationError(undefined, {
|
|
117
|
+
operation: 'find open release PRs',
|
|
118
|
+
remediation: 'open a release PR first, or check you are in the right repo',
|
|
119
|
+
})
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
return sortReleasePRs(prs).map((pr) => {
|
|
@@ -122,8 +127,11 @@ export const getReleasePRsWithInfo = async (): Promise<ReleasePRInfo[]> => {
|
|
|
122
127
|
}
|
|
123
128
|
})
|
|
124
129
|
} catch (error) {
|
|
130
|
+
if (error instanceof OperationError) throw error
|
|
131
|
+
|
|
125
132
|
logger.error({ error }, '❌ Error fetching release PRs')
|
|
126
|
-
|
|
133
|
+
|
|
134
|
+
throw new OperationError(error, { operation: 'fetch release PRs' })
|
|
127
135
|
}
|
|
128
136
|
}
|
|
129
137
|
|