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
|
@@ -6,7 +6,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
6
6
|
// Import AFTER the mock is declared so the module picks up the mocked dep.
|
|
7
7
|
import { getProjectRoot, getRepoName } from 'src/lib/git-utils'
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
getInfraKitConfig,
|
|
11
|
+
resetInfraKitConfigCache,
|
|
12
|
+
resolveCmuxLayout,
|
|
13
|
+
resolveConfiguredIdes,
|
|
14
|
+
} from '../infra-kit-config'
|
|
10
15
|
import type { InfraKitConfig } from '../infra-kit-config'
|
|
11
16
|
|
|
12
17
|
vi.mock('src/lib/git-utils', () => {
|
|
@@ -32,7 +37,7 @@ const withTmpRepo = async (fn: (tmp: string) => Promise<void>): Promise<void> =>
|
|
|
32
37
|
vi.mocked(getProjectRoot).mockResolvedValue(tmp)
|
|
33
38
|
vi.mocked(getRepoName).mockResolvedValue(path.basename(tmp))
|
|
34
39
|
// Point os.homedir() at the tmp dir so user-scope override layers
|
|
35
|
-
// (~/.infra-kit/
|
|
40
|
+
// (~/.infra-kit/infra-kit.json, ~/.infra-kit/projects/<repo>/infra-kit.json)
|
|
36
41
|
// can't leak the developer's real config into the test.
|
|
37
42
|
const homedirSpy = vi.spyOn(os, 'homedir').mockReturnValue(tmp)
|
|
38
43
|
|
|
@@ -240,7 +245,7 @@ describe('getInfraKitConfig', () => {
|
|
|
240
245
|
|
|
241
246
|
fs.mkdirSync(userGlobalDir, { recursive: true })
|
|
242
247
|
fs.writeFileSync(
|
|
243
|
-
path.join(userGlobalDir, '
|
|
248
|
+
path.join(userGlobalDir, 'infra-kit.json'),
|
|
244
249
|
JSON.stringify({ worktrees: { openInGithubDesktop: false, openInCmux: true } }),
|
|
245
250
|
)
|
|
246
251
|
|
|
@@ -258,7 +263,7 @@ describe('getInfraKitConfig', () => {
|
|
|
258
263
|
const userGlobalDir = path.join(tmp, '.infra-kit')
|
|
259
264
|
|
|
260
265
|
fs.mkdirSync(userGlobalDir, { recursive: true })
|
|
261
|
-
fs.writeFileSync(path.join(userGlobalDir, '
|
|
266
|
+
fs.writeFileSync(path.join(userGlobalDir, 'infra-kit.json'), ' \n')
|
|
262
267
|
|
|
263
268
|
const cfg = await getInfraKitConfig()
|
|
264
269
|
|
|
@@ -320,7 +325,7 @@ describe('getInfraKitConfig', () => {
|
|
|
320
325
|
})
|
|
321
326
|
})
|
|
322
327
|
|
|
323
|
-
it('ignores a non-loaded
|
|
328
|
+
it('ignores a non-loaded infra-kit.example.jsonc in the user-global layer', async () => {
|
|
324
329
|
await withTmpRepo(async (tmp) => {
|
|
325
330
|
fs.writeFileSync(path.join(tmp, 'infra-kit.json'), VALID_JSON)
|
|
326
331
|
|
|
@@ -328,7 +333,10 @@ describe('getInfraKitConfig', () => {
|
|
|
328
333
|
|
|
329
334
|
fs.mkdirSync(userGlobalDir, { recursive: true })
|
|
330
335
|
// Schema-failing content that must never be merged.
|
|
331
|
-
fs.writeFileSync(
|
|
336
|
+
fs.writeFileSync(
|
|
337
|
+
path.join(userGlobalDir, 'infra-kit.example.jsonc'),
|
|
338
|
+
'{\n // comment\n "environments": []\n}\n',
|
|
339
|
+
)
|
|
332
340
|
|
|
333
341
|
const cfg = await getInfraKitConfig()
|
|
334
342
|
|
|
@@ -431,3 +439,66 @@ describe('resolveConfiguredIdes', () => {
|
|
|
431
439
|
expect(resolveConfiguredIdes(base)).toEqual([])
|
|
432
440
|
})
|
|
433
441
|
})
|
|
442
|
+
|
|
443
|
+
describe('resolveCmuxLayout', () => {
|
|
444
|
+
const base = {
|
|
445
|
+
environments: ['dev'],
|
|
446
|
+
envManagement: { provider: 'doppler', config: { name: 'p' } },
|
|
447
|
+
} as InfraKitConfig
|
|
448
|
+
|
|
449
|
+
it('defaults to two-columns when worktrees.cmux is unset', () => {
|
|
450
|
+
expect(resolveCmuxLayout(base)).toBe('two-columns')
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it('defaults to two-columns when worktrees is set but cmux.layout is unset', () => {
|
|
454
|
+
const cfg = { ...base, worktrees: { openInCmux: true } } as InfraKitConfig
|
|
455
|
+
|
|
456
|
+
expect(resolveCmuxLayout(cfg)).toBe('two-columns')
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it('returns the explicit two-columns layout', () => {
|
|
460
|
+
const cfg = { ...base, worktrees: { cmux: { layout: 'two-columns' } } } as InfraKitConfig
|
|
461
|
+
|
|
462
|
+
expect(resolveCmuxLayout(cfg)).toBe('two-columns')
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('returns the explicit three-pane layout', () => {
|
|
466
|
+
const cfg = { ...base, worktrees: { cmux: { layout: 'three-pane' } } } as InfraKitConfig
|
|
467
|
+
|
|
468
|
+
expect(resolveCmuxLayout(cfg)).toBe('three-pane')
|
|
469
|
+
})
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
describe('worktrees.cmux schema validation', () => {
|
|
473
|
+
it('accepts a valid cmux.layout and round-trips it through getInfraKitConfig', async () => {
|
|
474
|
+
await withTmpRepo(async (tmp) => {
|
|
475
|
+
fs.writeFileSync(
|
|
476
|
+
path.join(tmp, 'infra-kit.json'),
|
|
477
|
+
JSON.stringify({
|
|
478
|
+
environments: ['dev'],
|
|
479
|
+
envManagement: { provider: 'doppler', config: { name: 'p' } },
|
|
480
|
+
worktrees: { openInCmux: true, cmux: { layout: 'three-pane' } },
|
|
481
|
+
}),
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
const cfg = await getInfraKitConfig()
|
|
485
|
+
|
|
486
|
+
expect(resolveCmuxLayout(cfg)).toBe('three-pane')
|
|
487
|
+
})
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('rejects an unknown cmux.layout value', async () => {
|
|
491
|
+
await withTmpRepo(async (tmp) => {
|
|
492
|
+
fs.writeFileSync(
|
|
493
|
+
path.join(tmp, 'infra-kit.json'),
|
|
494
|
+
JSON.stringify({
|
|
495
|
+
environments: ['dev'],
|
|
496
|
+
envManagement: { provider: 'doppler', config: { name: 'p' } },
|
|
497
|
+
worktrees: { cmux: { layout: 'four-pane' } },
|
|
498
|
+
}),
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
await expect(getInfraKitConfig()).rejects.toThrow(/Invalid.*infra-kit/i)
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
})
|
|
@@ -4,6 +4,14 @@ export {
|
|
|
4
4
|
infraKitConfigSchema,
|
|
5
5
|
infraKitOverrideConfigSchema,
|
|
6
6
|
resetInfraKitConfigCache,
|
|
7
|
+
resolveCmuxLayout,
|
|
7
8
|
resolveConfiguredIdes,
|
|
9
|
+
USER_CONFIG_DIR_NAME,
|
|
10
|
+
} from './infra-kit-config'
|
|
11
|
+
export type {
|
|
12
|
+
CmuxLayout,
|
|
13
|
+
ConfiguredIde,
|
|
14
|
+
EnvAutoLoadConfig,
|
|
15
|
+
InfraKitConfig,
|
|
16
|
+
InfraKitConfigPaths,
|
|
8
17
|
} from './infra-kit-config'
|
|
9
|
-
export type { ConfiguredIde, InfraKitConfig, InfraKitConfigPaths } from './infra-kit-config'
|
|
@@ -3,12 +3,19 @@ import os from 'node:os'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { z } from 'zod'
|
|
5
5
|
|
|
6
|
-
import { getProjectRoot
|
|
6
|
+
import { getProjectRoot } from 'src/lib/git-utils'
|
|
7
7
|
|
|
8
8
|
const INFRA_KIT_CONFIG_FILE = 'infra-kit.json'
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Directory under the user's home that holds all machine-local infra-kit config:
|
|
12
|
+
* the runtime `infra-kit.json` merge layer, per-project overrides, and the factory
|
|
13
|
+
* registry (`vendor.json`). Single source of truth for `.infra-kit`, reused
|
|
14
|
+
* by the vendor factory-config loader (no import cycle — this module imports no
|
|
15
|
+
* `lib/vendor` code).
|
|
16
|
+
*/
|
|
17
|
+
export const USER_CONFIG_DIR_NAME = '.infra-kit'
|
|
18
|
+
const USER_GLOBAL_CONFIG_FILE = 'infra-kit.json'
|
|
12
19
|
const USER_PROJECTS_DIR = 'projects'
|
|
13
20
|
|
|
14
21
|
// envManagement
|
|
@@ -64,12 +71,40 @@ const jiraTaskManagerSchema = z.object({
|
|
|
64
71
|
|
|
65
72
|
const taskManagerSchema = z.discriminatedUnion('provider', [jiraTaskManagerSchema])
|
|
66
73
|
|
|
74
|
+
// cmux pane layout for opened worktree workspaces. Named presets keep the config
|
|
75
|
+
// typo-proof (an enum, not a free string) and let the opener switch on a single
|
|
76
|
+
// value; extend the enum + the opener's switch to add a layout.
|
|
77
|
+
// two-columns — left | right, both full-height (default)
|
|
78
|
+
// three-pane — left split top/bottom + full-height right (legacy layout)
|
|
79
|
+
const cmuxLayouts = ['two-columns', 'three-pane'] as const
|
|
80
|
+
|
|
81
|
+
const cmuxConfigSchema = z.object({
|
|
82
|
+
layout: z.enum(cmuxLayouts).optional(),
|
|
83
|
+
})
|
|
84
|
+
|
|
67
85
|
// worktrees prompt defaults
|
|
68
86
|
const worktreesConfigSchema = z.object({
|
|
69
87
|
openInGithubDesktop: z.boolean().optional(),
|
|
70
88
|
openInCmux: z.boolean().optional(),
|
|
89
|
+
cmux: cmuxConfigSchema.optional(),
|
|
71
90
|
})
|
|
72
91
|
|
|
92
|
+
// env auto-load: opt-in convenience that primes Doppler env when you work inside
|
|
93
|
+
// this project / a worktree. Absent => disabled. `trigger` selects the moment
|
|
94
|
+
// (pick one):
|
|
95
|
+
// shell-startup — when a new shell opens inside the project
|
|
96
|
+
// cli-invocation — before each `infra-kit` command (primes SUBSEQUENT commands)
|
|
97
|
+
// `config` names which environment to load. It is intentionally NOT validated
|
|
98
|
+
// against `environments` here: an invalid name must DISABLE the feature at
|
|
99
|
+
// resolve time (lib/env-autoload), never throw inside the merged-config parse and
|
|
100
|
+
// brick every command.
|
|
101
|
+
const envAutoLoadSchema = z
|
|
102
|
+
.object({
|
|
103
|
+
trigger: z.enum(['shell-startup', 'cli-invocation']),
|
|
104
|
+
config: z.string().min(1),
|
|
105
|
+
})
|
|
106
|
+
.strict()
|
|
107
|
+
|
|
73
108
|
// Base object shape, kept separate so `.partial()` (which only works on a plain
|
|
74
109
|
// ZodObject, not the `.superRefine`-wrapped full schema) can derive the override
|
|
75
110
|
// schema from it.
|
|
@@ -79,6 +114,7 @@ const infraKitConfigObject = z.object({
|
|
|
79
114
|
ide: idesSchema.optional(),
|
|
80
115
|
taskManager: taskManagerSchema.optional(),
|
|
81
116
|
worktrees: worktreesConfigSchema.optional(),
|
|
117
|
+
envAutoLoad: envAutoLoadSchema.optional(),
|
|
82
118
|
})
|
|
83
119
|
|
|
84
120
|
// Full schema = base object + a parse-time uniqueness check on the `ide` array.
|
|
@@ -110,6 +146,9 @@ export const infraKitOverrideConfigSchema = infraKitConfigObject.partial()
|
|
|
110
146
|
|
|
111
147
|
export type InfraKitConfig = z.infer<typeof infraKitConfigSchema>
|
|
112
148
|
|
|
149
|
+
/** Resolved env auto-load config (`{ trigger, config }`), or `undefined` when off. */
|
|
150
|
+
export type EnvAutoLoadConfig = z.infer<typeof envAutoLoadSchema>
|
|
151
|
+
|
|
113
152
|
/** A single resolved IDE entry (`{ provider, config }`). */
|
|
114
153
|
export type ConfiguredIde = z.infer<typeof ideSchema>
|
|
115
154
|
|
|
@@ -131,6 +170,25 @@ export const resolveConfiguredIdes = (config: InfraKitConfig): ConfiguredIde[] =
|
|
|
131
170
|
return Array.isArray(ide) ? ide : [ide]
|
|
132
171
|
}
|
|
133
172
|
|
|
173
|
+
/** A cmux pane layout preset (see {@link cmuxLayouts}). */
|
|
174
|
+
export type CmuxLayout = (typeof cmuxLayouts)[number]
|
|
175
|
+
|
|
176
|
+
/** The layout applied when `worktrees.cmux.layout` is left unset. */
|
|
177
|
+
export const DEFAULT_CMUX_LAYOUT: CmuxLayout = 'two-columns'
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Resolve the cmux pane layout for opened worktree workspaces, falling back to
|
|
181
|
+
* {@link DEFAULT_CMUX_LAYOUT} when unconfigured. The one source of truth for
|
|
182
|
+
* "which layout should the cmux opener build."
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* resolveCmuxLayout({ worktrees: { cmux: { layout: 'three-pane' } } }) // => 'three-pane'
|
|
186
|
+
* resolveCmuxLayout({}) // => 'two-columns'
|
|
187
|
+
*/
|
|
188
|
+
export const resolveCmuxLayout = (config: InfraKitConfig): CmuxLayout => {
|
|
189
|
+
return config.worktrees?.cmux?.layout ?? DEFAULT_CMUX_LAYOUT
|
|
190
|
+
}
|
|
191
|
+
|
|
134
192
|
export interface InfraKitConfigPaths {
|
|
135
193
|
/** Committed project config (required). */
|
|
136
194
|
main: string
|
|
@@ -158,14 +216,16 @@ let cached: CacheEntry | null = null
|
|
|
158
216
|
* const paths = await getInfraKitConfigPaths()
|
|
159
217
|
* // {
|
|
160
218
|
* // main: '/Users/arthur/projects/api/infra-kit.json',
|
|
161
|
-
* // userGlobal: '/Users/arthur/.infra-kit/
|
|
219
|
+
* // userGlobal: '/Users/arthur/.infra-kit/infra-kit.json',
|
|
162
220
|
* // userProject: '/Users/arthur/.infra-kit/projects/api/infra-kit.json',
|
|
163
221
|
* // projectName: 'api',
|
|
164
222
|
* // }
|
|
165
223
|
*/
|
|
166
224
|
export const getInfraKitConfigPaths = async (): Promise<InfraKitConfigPaths> => {
|
|
167
225
|
const projectRoot = await getProjectRoot()
|
|
168
|
-
|
|
226
|
+
// Repo name is the project-root basename (same as getRepoName), derived here to
|
|
227
|
+
// avoid a second `git rev-parse` — getRepoName would re-resolve the root.
|
|
228
|
+
const projectName = path.basename(projectRoot)
|
|
169
229
|
const userConfigDir = path.join(os.homedir(), USER_CONFIG_DIR_NAME)
|
|
170
230
|
|
|
171
231
|
return {
|
|
@@ -180,7 +240,7 @@ export const getInfraKitConfigPaths = async (): Promise<InfraKitConfigPaths> =>
|
|
|
180
240
|
* Read and validate `infra-kit.json`, with optional override layers shallow-merged
|
|
181
241
|
* on top in this order (later wins):
|
|
182
242
|
* 1. project `infra-kit.json` — committed source of truth
|
|
183
|
-
* 2. `~/.infra-kit/
|
|
243
|
+
* 2. `~/.infra-kit/infra-kit.json` — user-global defaults
|
|
184
244
|
* 3. `~/.infra-kit/projects/<repo-name>/infra-kit.json` — user-scope per-project overrides
|
|
185
245
|
*
|
|
186
246
|
* Top-level keys (entire capability sections like `ide`, `envManagement`)
|
|
@@ -189,7 +249,7 @@ export const getInfraKitConfigPaths = async (): Promise<InfraKitConfigPaths> =>
|
|
|
189
249
|
*
|
|
190
250
|
* @example
|
|
191
251
|
* // infra-kit.json: { "environments": ["dev"], "envManagement": { "provider": "doppler", "config": { "name": "p" } } }
|
|
192
|
-
* // ~/.infra-kit/
|
|
252
|
+
* // ~/.infra-kit/infra-kit.json: { "ide": { "provider": "cursor", "config": { "workspaceConfigPath": "./ws.code-workspace" } } }
|
|
193
253
|
* const cfg = await getInfraKitConfig()
|
|
194
254
|
* // => { environments: ['dev'], envManagement: {...}, ide: { provider: 'cursor', config: { workspaceConfigPath: './ws.code-workspace' } } }
|
|
195
255
|
*/
|
|
@@ -233,7 +293,7 @@ export const getInfraKitConfig = async (): Promise<InfraKitConfig> => {
|
|
|
233
293
|
|
|
234
294
|
const layers: ConfigLayer[] = [
|
|
235
295
|
{ label: 'infra-kit.json', path: paths.main, required: true },
|
|
236
|
-
{ label: '~/.infra-kit/
|
|
296
|
+
{ label: '~/.infra-kit/infra-kit.json', path: paths.userGlobal, required: false },
|
|
237
297
|
{
|
|
238
298
|
label: `~/.infra-kit/projects/${paths.projectName}/infra-kit.json`,
|
|
239
299
|
path: paths.userProject,
|
|
@@ -335,12 +395,12 @@ interface ConfigLayer {
|
|
|
335
395
|
* An empty/whitespace-only file is treated as `{}` (JSON.parse would throw).
|
|
336
396
|
*
|
|
337
397
|
* @example
|
|
338
|
-
* await loadLayer({ label: '~/.infra-kit/
|
|
398
|
+
* await loadLayer({ label: '~/.infra-kit/infra-kit.json', path: '/missing.json', required: false })
|
|
339
399
|
* // => null
|
|
340
400
|
*
|
|
341
401
|
* @example
|
|
342
|
-
* // /home/me/.infra-kit/
|
|
343
|
-
* await loadLayer({ label: '~/.infra-kit/
|
|
402
|
+
* // /home/me/.infra-kit/infra-kit.json: '{ "ide": { "provider": "cursor", "config": { "workspaceConfigPath": "./ws.code-workspace" } } }'
|
|
403
|
+
* await loadLayer({ label: '~/.infra-kit/infra-kit.json', path: '/home/me/.infra-kit/infra-kit.json', required: false })
|
|
344
404
|
* // => { ide: { provider: 'cursor', config: { workspaceConfigPath: './ws.code-workspace' } } }
|
|
345
405
|
*/
|
|
346
406
|
const loadLayer = async (layer: ConfigLayer): Promise<Record<string, unknown> | null> => {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { addJsonOption, emit, jsonOutput } from '../json-output'
|
|
5
|
+
|
|
6
|
+
describe('json-output', () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
jsonOutput.enabled = false
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe('emit', () => {
|
|
12
|
+
it('writes structuredContent as pretty JSON when enabled', () => {
|
|
13
|
+
jsonOutput.enabled = true
|
|
14
|
+
const writes: string[] = []
|
|
15
|
+
|
|
16
|
+
const result = emit({ structuredContent: { version: '1.2.3' } }, (text) => {
|
|
17
|
+
writes.push(text)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
expect(writes).toHaveLength(1)
|
|
21
|
+
const [payload = ''] = writes
|
|
22
|
+
|
|
23
|
+
expect(JSON.parse(payload)).toEqual({ version: '1.2.3' })
|
|
24
|
+
// returns the result unchanged for pass-through use (emit(await handler()))
|
|
25
|
+
expect(result).toEqual({ structuredContent: { version: '1.2.3' } })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('is a no-op when json mode is disabled', () => {
|
|
29
|
+
jsonOutput.enabled = false
|
|
30
|
+
const writes: string[] = []
|
|
31
|
+
|
|
32
|
+
emit({ structuredContent: { a: 1 } }, (text) => {
|
|
33
|
+
writes.push(text)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expect(writes).toHaveLength(0)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('writes nothing when structuredContent is undefined or null, even if enabled', () => {
|
|
40
|
+
jsonOutput.enabled = true
|
|
41
|
+
const writes: string[] = []
|
|
42
|
+
|
|
43
|
+
emit(undefined, (text) => {
|
|
44
|
+
writes.push(text)
|
|
45
|
+
})
|
|
46
|
+
emit({}, (text) => {
|
|
47
|
+
writes.push(text)
|
|
48
|
+
})
|
|
49
|
+
emit({ structuredContent: null }, (text) => {
|
|
50
|
+
writes.push(text)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
expect(writes).toHaveLength(0)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Regression: `--json` must activate on grouped/nested subcommands, not just
|
|
58
|
+
// flat top-level commands. Commander binds a post-subcommand `--json` to the
|
|
59
|
+
// parent group, so the preAction hook must read `optsWithGlobals()`, not
|
|
60
|
+
// `opts()`. This drives the real Commander parse path end-to-end.
|
|
61
|
+
describe('grouped subcommand --json (regression)', () => {
|
|
62
|
+
const buildProgram = (writes: string[]): Command => {
|
|
63
|
+
const program = new Command()
|
|
64
|
+
const group = program.command('group')
|
|
65
|
+
|
|
66
|
+
group.command('leaf').action(() => {
|
|
67
|
+
emit({ structuredContent: { ok: true } }, (text) => {
|
|
68
|
+
writes.push(text)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
program.commands.forEach(addJsonOption)
|
|
73
|
+
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
74
|
+
jsonOutput.enabled = Boolean(actionCommand.optsWithGlobals().json)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
return program
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
it('emits JSON for `group leaf --json` (post-subcommand flag on a nested command)', async () => {
|
|
81
|
+
const writes: string[] = []
|
|
82
|
+
|
|
83
|
+
await buildProgram(writes).parseAsync(['node', 'cli', 'group', 'leaf', '--json'])
|
|
84
|
+
|
|
85
|
+
expect(writes).toHaveLength(1)
|
|
86
|
+
const [payload = ''] = writes
|
|
87
|
+
|
|
88
|
+
expect(JSON.parse(payload)).toEqual({ ok: true })
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('emits nothing for `group leaf` without --json', async () => {
|
|
92
|
+
const writes: string[] = []
|
|
93
|
+
|
|
94
|
+
await buildProgram(writes).parseAsync(['node', 'cli', 'group', 'leaf'])
|
|
95
|
+
|
|
96
|
+
expect(writes).toHaveLength(0)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('addJsonOption', () => {
|
|
101
|
+
it('registers --json on a command', () => {
|
|
102
|
+
const cmd = new Command('demo')
|
|
103
|
+
|
|
104
|
+
addJsonOption(cmd)
|
|
105
|
+
|
|
106
|
+
expect(
|
|
107
|
+
cmd.options.some((option) => {
|
|
108
|
+
return option.long === '--json'
|
|
109
|
+
}),
|
|
110
|
+
).toBe(true)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('is idempotent (does not double-register)', () => {
|
|
114
|
+
const cmd = new Command('demo')
|
|
115
|
+
|
|
116
|
+
addJsonOption(cmd)
|
|
117
|
+
addJsonOption(cmd)
|
|
118
|
+
|
|
119
|
+
const jsonOptions = cmd.options.filter((option) => {
|
|
120
|
+
return option.long === '--json'
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
expect(jsonOptions).toHaveLength(1)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('registers --json on nested subcommands too', () => {
|
|
127
|
+
const parent = new Command('parent')
|
|
128
|
+
const child = parent.command('child')
|
|
129
|
+
|
|
130
|
+
addJsonOption(parent)
|
|
131
|
+
|
|
132
|
+
expect(
|
|
133
|
+
child.options.some((option) => {
|
|
134
|
+
return option.long === '--json'
|
|
135
|
+
}),
|
|
136
|
+
).toBe(true)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { addJsonOption, emit, jsonOutput } from './json-output'
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `--json` output mode for the CLI. This is a presentation concern only: every
|
|
6
|
+
* command handler already returns a `structuredContent` payload (the same one
|
|
7
|
+
* the MCP surface consumes). Human/log output goes to stderr (see lib/logger),
|
|
8
|
+
* so writing the structured payload to stdout never collides with it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface CommandResult {
|
|
12
|
+
structuredContent?: unknown
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Mutable holder (object so `prefer-const` holds while the flag toggles per run). */
|
|
16
|
+
export const jsonOutput = { enabled: false }
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* In `--json` mode, write a command result's `structuredContent` to stdout as
|
|
20
|
+
* pretty JSON and return the result unchanged. Outside `--json` mode it is a
|
|
21
|
+
* no-op pass-through. The writer is injectable for testing.
|
|
22
|
+
*/
|
|
23
|
+
export const emit = <T extends CommandResult | void>(
|
|
24
|
+
result: T,
|
|
25
|
+
write: (text: string) => void = (text) => {
|
|
26
|
+
process.stdout.write(text)
|
|
27
|
+
},
|
|
28
|
+
): T => {
|
|
29
|
+
if (jsonOutput.enabled && result && result.structuredContent != null) {
|
|
30
|
+
write(`${JSON.stringify(result.structuredContent, null, 2)}\n`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return result
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Register `--json` on a command and every nested subcommand (idempotent). */
|
|
37
|
+
export const addJsonOption = (cmd: Command): void => {
|
|
38
|
+
const alreadyHasJson = cmd.options.some((option) => {
|
|
39
|
+
return option.long === '--json'
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
if (!alreadyHasJson) {
|
|
43
|
+
cmd.option('--json', 'Output the structured result as JSON on stdout (human logs stay on stderr)')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
cmd.commands.forEach(addJsonOption)
|
|
47
|
+
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* This is the same mechanism `infra-kit init` already uses for the `~/.zshrc`
|
|
6
6
|
* shell block (`# -- infra-kit:begin -- … # -- infra-kit:end --`), lifted here so
|
|
7
|
-
* it can be reused for the repo agent-instruction
|
|
8
|
-
*
|
|
7
|
+
* it can be reused for the repo agent-instruction guidance block in `CLAUDE.md`.
|
|
8
|
+
* It mirrors the design of OMC's
|
|
9
9
|
* `<!-- OMC:START --> … <!-- OMC:END -->` CLAUDE.md installer.
|
|
10
10
|
*/
|
|
11
11
|
|
|
@@ -64,7 +64,7 @@ export const removeManagedBlock = (content: string, startMarker: string, endMark
|
|
|
64
64
|
|
|
65
65
|
if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) return null
|
|
66
66
|
|
|
67
|
-
// eslint-disable-next-line sonarjs/
|
|
67
|
+
// eslint-disable-next-line sonarjs/super-linear-regex
|
|
68
68
|
const before = content.slice(0, startIdx).replace(/\n+$/, '')
|
|
69
69
|
const after = content.slice(endIdx + endMarker.length).replace(/^\n+/, '')
|
|
70
70
|
|
|
@@ -138,7 +138,7 @@ export const upsertManagedBlock = ({
|
|
|
138
138
|
// append-end, or replace-in-place with no existing block: drop any stale block
|
|
139
139
|
// then append the fresh one at end-of-file.
|
|
140
140
|
const stripped = present ? (removeManagedBlock(content, startMarker, endMarker) ?? content) : content
|
|
141
|
-
// eslint-disable-next-line sonarjs/
|
|
141
|
+
// eslint-disable-next-line sonarjs/super-linear-regex
|
|
142
142
|
const base = stripped.replace(/\n+$/, '')
|
|
143
143
|
|
|
144
144
|
return base.length > 0 ? `${base}\n${block}\n` : `${block}\n`
|
|
@@ -31,9 +31,7 @@ export interface InfraKitPackageConfig {
|
|
|
31
31
|
* `defineConfig` input: a plain object, a sync factory, or an async factory.
|
|
32
32
|
*/
|
|
33
33
|
export type InfraKitPackageConfigInput =
|
|
34
|
-
| InfraKitPackageConfig
|
|
35
|
-
| (() => InfraKitPackageConfig)
|
|
36
|
-
| (() => Promise<InfraKitPackageConfig>)
|
|
34
|
+
InfraKitPackageConfig | (() => InfraKitPackageConfig) | (() => Promise<InfraKitPackageConfig>)
|
|
37
35
|
|
|
38
36
|
/**
|
|
39
37
|
* Identity helper that gives `infra-kit.config.js` authors full type inference
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { fileExists, tildify } from './path-display'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared path-display helpers for CLI output. Extracted from `commands/config`
|
|
6
|
+
* and `commands/init/migrate-config` (which each had private copies) so config
|
|
7
|
+
* surfaces print paths consistently. Keep this dependency-light (node builtins
|
|
8
|
+
* only) — it is imported by several commands.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve whether a file is reachable, suppressing ENOENT (and any access error)
|
|
13
|
+
* into a boolean. Used to render `[✓]`/`[ ]` existence markers.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* await fileExists('/etc/hosts') // => true
|
|
17
|
+
* await fileExists('/nope.txt') // => false
|
|
18
|
+
*/
|
|
19
|
+
export const fileExists = async (filePath: string): Promise<boolean> => {
|
|
20
|
+
try {
|
|
21
|
+
await fs.access(filePath)
|
|
22
|
+
|
|
23
|
+
return true
|
|
24
|
+
} catch {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Replace the user's home prefix with `~` so logged paths stay short and portable
|
|
31
|
+
* across machines. Leaves non-home paths untouched.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // os.homedir() === '/Users/arthur'
|
|
35
|
+
* tildify('/Users/arthur/.infra-kit/infra-kit.json') // => '~/.infra-kit/infra-kit.json'
|
|
36
|
+
* tildify('/etc/hosts') // => '/etc/hosts'
|
|
37
|
+
*/
|
|
38
|
+
export const tildify = (filePath: string): string => {
|
|
39
|
+
const home = os.homedir()
|
|
40
|
+
|
|
41
|
+
return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath
|
|
42
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { formatAlignedRows } from '../render'
|
|
4
|
+
|
|
5
|
+
describe('formatAlignedRows', () => {
|
|
6
|
+
it('pads every left cell to the widest left cell and joins with the default 2-space gap', () => {
|
|
7
|
+
expect(
|
|
8
|
+
formatAlignedRows([
|
|
9
|
+
['a', 'x'],
|
|
10
|
+
['bbb', 'y'],
|
|
11
|
+
]),
|
|
12
|
+
).toEqual(['a x', 'bbb y'])
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('honours a custom gap', () => {
|
|
16
|
+
expect(
|
|
17
|
+
formatAlignedRows(
|
|
18
|
+
[
|
|
19
|
+
['a', 'x'],
|
|
20
|
+
['bb', 'y'],
|
|
21
|
+
],
|
|
22
|
+
' | ',
|
|
23
|
+
),
|
|
24
|
+
).toEqual(['a | x', 'bb | y'])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('reproduces the menu label format (name padded + 2 spaces + description)', () => {
|
|
28
|
+
const rows: ReadonlyArray<readonly [string, string]> = [
|
|
29
|
+
['merge-dev', 'Merge dev branch into every release branch'],
|
|
30
|
+
['release-list', 'List all release branches'],
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
const width = Math.max(
|
|
34
|
+
...rows.map(([left]) => {
|
|
35
|
+
return left.length
|
|
36
|
+
}),
|
|
37
|
+
)
|
|
38
|
+
const expected = rows.map(([left, right]) => {
|
|
39
|
+
return `${left.padEnd(width)} ${right}`
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
expect(formatAlignedRows(rows)).toEqual(expected)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('returns an empty array for no rows', () => {
|
|
46
|
+
expect(formatAlignedRows([])).toEqual([])
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { formatAlignedRows } from './render'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small shared rendering helpers for aligned terminal output. These replace
|
|
3
|
+
* the ad-hoc "compute the max width, then padEnd each row" pattern that the
|
|
4
|
+
* interactive menu (and similar list views) hand-rolled. Pure string helpers —
|
|
5
|
+
* no I/O, no color — so they are trivially testable and reusable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Align a list of two-column rows: pad every left cell to the widest left cell,
|
|
10
|
+
* then join the two cells with `gap`. Returns one rendered line per row.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* formatAlignedRows([['a', 'x'], ['bbb', 'y']])
|
|
14
|
+
* // ['a x', 'bbb y'] (left padded to width 3, default 2-space gap)
|
|
15
|
+
*/
|
|
16
|
+
export const formatAlignedRows = (rows: ReadonlyArray<readonly [string, string]>, gap = ' '): string[] => {
|
|
17
|
+
const width = rows.reduce((max, [left]) => {
|
|
18
|
+
return Math.max(max, left.length)
|
|
19
|
+
}, 0)
|
|
20
|
+
|
|
21
|
+
return rows.map(([left, right]) => {
|
|
22
|
+
return `${left.padEnd(width)}${gap}${right}`
|
|
23
|
+
})
|
|
24
|
+
}
|