infra-kit 0.1.113 → 0.1.115
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintcache +1 -1
- package/.omc/state/agent-replay-0d1515ce-4ebe-4ce8-a865-2897f68de168.jsonl +1 -0
- package/.omc/state/agent-replay-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-f99d8dad-81d1-4987-8183-62edf0843d0c.jsonl +2 -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/0d1515ce-4ebe-4ce8-a865-2897f68de168/pre-tool-advisory-throttle.json +10 -0
- 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/.omc/state/sessions/560ea519-574a-4713-b134-5bf6cfaf3c33/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4/pre-tool-advisory-throttle.json +14 -0
- 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/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +14 -0
- package/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/ralph-state.json +13 -0
- package/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/ultrawork-state.json +11 -0
- package/.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-test.log +92 -44
- package/dist/boot-K4QUAFSP.js +2 -0
- package/dist/boot-K4QUAFSP.js.map +7 -0
- package/dist/chunk-RTSP2FCA.js +2 -0
- package/dist/chunk-RTSP2FCA.js.map +7 -0
- package/dist/chunk-VPBZR2PY.js +166 -0
- package/dist/chunk-VPBZR2PY.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/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +14 -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 +110 -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/.omc/state/agent-replay-f99d8dad-81d1-4987-8183-62edf0843d0c.jsonl +2 -0
- package/src/integrations/cursor/.omc/state/idle-notif-cooldown.json +3 -0
- package/src/integrations/cursor/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +18 -0
- package/src/integrations/cursor/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/subagent-tracking-state.json +17 -0
- package/src/integrations/cursor/index.ts +1 -0
- package/src/integrations/cursor/launch-cursor.ts +12 -0
- package/src/integrations/cursor/open-cursor-workspace.ts +8 -14
- 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 +57 -10
- package/src/integrations/ide/add-ide-worktree-folders.ts +2 -4
- 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/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/lib/worktrees/remove-worktrees.ts +15 -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
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { vendorCopyItemSchema } from '../config-schema'
|
|
4
|
+
|
|
5
|
+
const base = { name: 'Configs', type: 'directory' as const }
|
|
6
|
+
|
|
7
|
+
describe('vendorCopyItemSchema — safeRelPath containment', () => {
|
|
8
|
+
it('accepts a normal repo-relative path', () => {
|
|
9
|
+
const result = vendorCopyItemSchema.safeParse({ ...base, source: 'vendor/configs', target: 'vendor/configs' })
|
|
10
|
+
|
|
11
|
+
expect(result.success).toBe(true)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('rejects a target that escapes the repo root with ".."', () => {
|
|
15
|
+
const result = vendorCopyItemSchema.safeParse({ ...base, source: 'vendor/configs', target: '../escape' })
|
|
16
|
+
|
|
17
|
+
expect(result.success).toBe(false)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('rejects an absolute source path', () => {
|
|
21
|
+
const result = vendorCopyItemSchema.safeParse({ ...base, source: '/etc/passwd', target: 'vendor/configs' })
|
|
22
|
+
|
|
23
|
+
expect(result.success).toBe(false)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('rejects an empty path', () => {
|
|
27
|
+
const result = vendorCopyItemSchema.safeParse({ ...base, source: '', target: 'vendor/configs' })
|
|
28
|
+
|
|
29
|
+
expect(result.success).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { defineVendorConfig, loadVendorConfig } from '../config'
|
|
7
|
+
|
|
8
|
+
let root: string
|
|
9
|
+
|
|
10
|
+
const writeConfig = (body: string): void => {
|
|
11
|
+
fs.writeFileSync(path.join(root, 'vendor.config.ts'), body, 'utf8')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-config-test-'))
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('defineVendorConfig', () => {
|
|
23
|
+
it('is an identity helper', () => {
|
|
24
|
+
const config = { copy: [] }
|
|
25
|
+
|
|
26
|
+
expect(defineVendorConfig(config)).toBe(config)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('loadVendorConfig', () => {
|
|
31
|
+
it('loads an object default export', async () => {
|
|
32
|
+
writeConfig(
|
|
33
|
+
`export default { copy: [{ name: 'Configs', source: 'vendor/configs', target: 'vendor/configs', type: 'directory', vendored: true }] }`,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const config = await loadVendorConfig(root)
|
|
37
|
+
|
|
38
|
+
expect(config.copy[0]?.name).toBe('Configs')
|
|
39
|
+
expect(config.copy[0]?.vendored).toBe(true)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('loads a factory (function) default export', async () => {
|
|
43
|
+
writeConfig(`export default () => ({ copy: [] })`)
|
|
44
|
+
|
|
45
|
+
const config = await loadVendorConfig(root)
|
|
46
|
+
|
|
47
|
+
expect(config.copy).toEqual([])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('throws on a missing config file', async () => {
|
|
51
|
+
await expect(loadVendorConfig(root)).rejects.toThrow(/vendor\.config\.ts not found/)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('throws on a schema-invalid config', async () => {
|
|
55
|
+
writeConfig(`export default { copy: 'not-an-array' }`)
|
|
56
|
+
await expect(loadVendorConfig(root)).rejects.toThrow(/Invalid vendor\.config\.ts/)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('rejects a stray `targets` key (now machine-local, not in the source config)', async () => {
|
|
60
|
+
writeConfig(`export default { targets: ['travelist-monorepo'], copy: [] }`)
|
|
61
|
+
await expect(loadVendorConfig(root)).rejects.toThrow(/Invalid vendor\.config\.ts/)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { expandTilde, getFactoryConfigPath, loadFactoryConfig } from '../factory-config'
|
|
7
|
+
|
|
8
|
+
let home: string
|
|
9
|
+
let configDir: string
|
|
10
|
+
let configPath: string
|
|
11
|
+
|
|
12
|
+
const writeConfig = (body: string): void => {
|
|
13
|
+
fs.mkdirSync(configDir, { recursive: true })
|
|
14
|
+
fs.writeFileSync(configPath, body, 'utf8')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
home = fs.mkdtempSync(path.join(os.tmpdir(), 'factory-config-test-'))
|
|
19
|
+
// getFactoryConfigPath() resolves under os.homedir(); point it at the fixture.
|
|
20
|
+
vi.spyOn(os, 'homedir').mockReturnValue(home)
|
|
21
|
+
configDir = path.join(home, '.infra-kit')
|
|
22
|
+
configPath = path.join(configDir, 'vendor.json')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.restoreAllMocks()
|
|
27
|
+
fs.rmSync(home, { recursive: true, force: true })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('expandTilde', () => {
|
|
31
|
+
it('expands a bare ~ to the home dir', () => {
|
|
32
|
+
expect(expandTilde('~')).toBe(home)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('expands ~/x to a home-relative path', () => {
|
|
36
|
+
expect(expandTilde('~/projects')).toBe(path.join(home, 'projects'))
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('passes an absolute path through unchanged', () => {
|
|
40
|
+
expect(expandTilde('/abs/dir')).toBe('/abs/dir')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('throws on a non-absolute, non-~ path', () => {
|
|
44
|
+
expect(() => {
|
|
45
|
+
return expandTilde('rel/dir')
|
|
46
|
+
}).toThrow(/must be absolute or ~-prefixed/)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('getFactoryConfigPath', () => {
|
|
51
|
+
it('points at ~/.infra-kit/vendor.json', () => {
|
|
52
|
+
expect(getFactoryConfigPath()).toBe(configPath)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('loadFactoryConfig', () => {
|
|
57
|
+
it('loads a JSON config', async () => {
|
|
58
|
+
writeConfig(JSON.stringify({ workspaceDir: '~/projects', targets: ['travelist-monorepo'] }))
|
|
59
|
+
|
|
60
|
+
const config = await loadFactoryConfig()
|
|
61
|
+
|
|
62
|
+
expect(config.workspaceDir).toBe('~/projects')
|
|
63
|
+
expect(config.targets).toEqual(['travelist-monorepo'])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('throws an actionable error when the file is absent', async () => {
|
|
67
|
+
await expect(loadFactoryConfig()).rejects.toThrow(/vendor-config --init/)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('throws a descriptive error on malformed JSON', async () => {
|
|
71
|
+
writeConfig('{ not valid json ')
|
|
72
|
+
await expect(loadFactoryConfig()).rejects.toThrow(/Invalid JSON in vendor\.json/)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('throws on a schema-invalid config (missing targets)', async () => {
|
|
76
|
+
writeConfig(JSON.stringify({ workspaceDir: '~/projects' }))
|
|
77
|
+
await expect(loadFactoryConfig()).rejects.toThrow(/Invalid factory config/)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('rejects a stray `copy` key (.strict())', async () => {
|
|
81
|
+
writeConfig(JSON.stringify({ workspaceDir: '~/projects', targets: ['a'], copy: [] }))
|
|
82
|
+
await expect(loadFactoryConfig()).rejects.toThrow(/Invalid factory config/)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('reads the current file on every call so an edit is picked up without a restart', async () => {
|
|
86
|
+
writeConfig(JSON.stringify({ workspaceDir: '~/projects', targets: ['first'] }))
|
|
87
|
+
expect((await loadFactoryConfig()).targets).toEqual(['first'])
|
|
88
|
+
|
|
89
|
+
writeConfig(JSON.stringify({ workspaceDir: '~/projects', targets: ['second'] }))
|
|
90
|
+
expect((await loadFactoryConfig()).targets).toEqual(['second'])
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
CURRENT_SCHEMA_VERSION,
|
|
8
|
+
MANIFEST_FILE,
|
|
9
|
+
buildFilesMap,
|
|
10
|
+
compareToManifest,
|
|
11
|
+
isUnknownSchema,
|
|
12
|
+
readManifest,
|
|
13
|
+
sha256,
|
|
14
|
+
walkVendorTree,
|
|
15
|
+
writeManifest,
|
|
16
|
+
} from 'src/lib/vendor'
|
|
17
|
+
|
|
18
|
+
let tmp: string
|
|
19
|
+
|
|
20
|
+
const write = (rel: string, content: string): void => {
|
|
21
|
+
const full = path.join(tmp, rel)
|
|
22
|
+
|
|
23
|
+
fs.mkdirSync(path.dirname(full), { recursive: true })
|
|
24
|
+
fs.writeFileSync(full, content, 'utf8')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-lib-test-'))
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
fs.rmSync(tmp, { recursive: true, force: true })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('walkVendorTree', () => {
|
|
36
|
+
it('returns POSIX relative paths sorted byte-wise', () => {
|
|
37
|
+
write('b.txt', 'b')
|
|
38
|
+
write('a/nested.txt', 'n')
|
|
39
|
+
write('a.txt', 'a')
|
|
40
|
+
|
|
41
|
+
expect(walkVendorTree(tmp)).toEqual(['a.txt', 'a/nested.txt', 'b.txt'])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('is deterministic regardless of creation order', () => {
|
|
45
|
+
write('z.txt', 'z')
|
|
46
|
+
write('m/x.txt', 'x')
|
|
47
|
+
write('a.txt', 'a')
|
|
48
|
+
|
|
49
|
+
const first = walkVendorTree(tmp)
|
|
50
|
+
// Re-create in a different order in a fresh dir.
|
|
51
|
+
const tmp2 = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-lib-test2-'))
|
|
52
|
+
|
|
53
|
+
fs.writeFileSync(path.join(tmp2, 'a.txt'), 'a')
|
|
54
|
+
fs.mkdirSync(path.join(tmp2, 'm'))
|
|
55
|
+
fs.writeFileSync(path.join(tmp2, 'm', 'x.txt'), 'x')
|
|
56
|
+
fs.writeFileSync(path.join(tmp2, 'z.txt'), 'z')
|
|
57
|
+
const second = walkVendorTree(tmp2)
|
|
58
|
+
|
|
59
|
+
fs.rmSync(tmp2, { recursive: true, force: true })
|
|
60
|
+
|
|
61
|
+
expect(first).toEqual(second)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('skips configured dirs, files, and suffixes (incl. .vitest-attachments union member)', () => {
|
|
65
|
+
write('keep.ts', 'keep')
|
|
66
|
+
write('node_modules/pkg/index.js', 'x')
|
|
67
|
+
write('dist/out.js', 'x')
|
|
68
|
+
write('.vitest-attachments/blob', 'x')
|
|
69
|
+
write('.sync-manifest.json', '{}')
|
|
70
|
+
write('.eslintcache', 'x')
|
|
71
|
+
write('log.txt', 'x')
|
|
72
|
+
write('build.tsbuildinfo', 'x')
|
|
73
|
+
|
|
74
|
+
expect(walkVendorTree(tmp)).toEqual(['keep.ts'])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('follows a symlink and hashes its target content (legacy semantics)', () => {
|
|
78
|
+
write('real.txt', 'payload')
|
|
79
|
+
fs.symlinkSync(path.join(tmp, 'real.txt'), path.join(tmp, 'link.txt'))
|
|
80
|
+
|
|
81
|
+
const files = buildFilesMap(tmp)
|
|
82
|
+
|
|
83
|
+
expect(Object.keys(files).sort()).toEqual(['link.txt', 'real.txt'])
|
|
84
|
+
// Followed → identical content → identical hash.
|
|
85
|
+
expect(files['link.txt']).toBe(files['real.txt'])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('surfaces an explicit error for a dangling symlink', () => {
|
|
89
|
+
fs.symlinkSync(path.join(tmp, 'does-not-exist'), path.join(tmp, 'dangling.txt'))
|
|
90
|
+
|
|
91
|
+
expect(() => {
|
|
92
|
+
return buildFilesMap(tmp)
|
|
93
|
+
}).toThrow(/broken symlink or unreadable file/)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('manifest read/write/compare', () => {
|
|
98
|
+
it('write-then-check parity: a freshly written manifest reports no drift', () => {
|
|
99
|
+
write('configs/a.js', 'aaa')
|
|
100
|
+
write('packages/p/b.ts', 'bbb')
|
|
101
|
+
|
|
102
|
+
const manifest = writeManifest(tmp, { source: 'starter-workspace', commit: 'abc123' })
|
|
103
|
+
|
|
104
|
+
expect(manifest.schemaVersion).toBe(CURRENT_SCHEMA_VERSION)
|
|
105
|
+
expect(manifest.fileCount).toBe(2)
|
|
106
|
+
|
|
107
|
+
const diff = compareToManifest(tmp, manifest)
|
|
108
|
+
|
|
109
|
+
expect(diff).toEqual({ modified: [], added: [], removed: [] })
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('detects modified / added / removed files', () => {
|
|
113
|
+
write('a.js', 'a')
|
|
114
|
+
write('b.js', 'b')
|
|
115
|
+
const manifest = writeManifest(tmp, { source: 's', commit: 'c' })
|
|
116
|
+
|
|
117
|
+
fs.writeFileSync(path.join(tmp, 'a.js'), 'CHANGED')
|
|
118
|
+
write('c.js', 'c')
|
|
119
|
+
fs.rmSync(path.join(tmp, 'b.js'))
|
|
120
|
+
|
|
121
|
+
const diff = compareToManifest(tmp, manifest)
|
|
122
|
+
|
|
123
|
+
expect(diff.modified).toEqual(['a.js'])
|
|
124
|
+
expect(diff.added).toEqual(['c.js'])
|
|
125
|
+
expect(diff.removed).toEqual(['b.js'])
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('reads a legacy manifest with no schemaVersion as compatible', () => {
|
|
129
|
+
write('a.js', 'a')
|
|
130
|
+
const legacy = {
|
|
131
|
+
source: 'starter-workspace',
|
|
132
|
+
commit: 'deadbeef',
|
|
133
|
+
syncedAt: new Date().toISOString(),
|
|
134
|
+
fileCount: 1,
|
|
135
|
+
files: { 'a.js': sha256(path.join(tmp, 'a.js')) },
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fs.writeFileSync(path.join(tmp, MANIFEST_FILE), JSON.stringify(legacy))
|
|
139
|
+
|
|
140
|
+
const manifest = readManifest(tmp)
|
|
141
|
+
|
|
142
|
+
expect(manifest.schemaVersion).toBeUndefined()
|
|
143
|
+
expect(isUnknownSchema(manifest)).toBe(false)
|
|
144
|
+
expect(compareToManifest(tmp, manifest)).toEqual({ modified: [], added: [], removed: [] })
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('flags a newer schemaVersion as unknown (fail-closed)', () => {
|
|
148
|
+
const manifest = { ...writeManifest(tmp, { source: 's', commit: 'c' }), schemaVersion: CURRENT_SCHEMA_VERSION + 1 }
|
|
149
|
+
|
|
150
|
+
expect(isUnknownSchema(manifest)).toBe(true)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('throws on a missing manifest', () => {
|
|
154
|
+
expect(() => {
|
|
155
|
+
return readManifest(tmp)
|
|
156
|
+
}).toThrow(/Missing \.sync-manifest\.json/)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared spy for every `zx` subprocess call in the vendor write-path tests.
|
|
5
|
+
* Tests assert against it, e.g. `expect(execMock).not.toHaveBeenCalled()`.
|
|
6
|
+
*/
|
|
7
|
+
export const execMock = vi.fn(() => {
|
|
8
|
+
return Promise.resolve({ stdout: '' })
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `vi.mock('zx')` factory body. Returns a `$` handling both the tagged-template
|
|
13
|
+
* `` $`cmd` `` and the `$({ cwd })` options form, routing every call through
|
|
14
|
+
* {@link execMock}. Referencing this imported function (rather than a top-level
|
|
15
|
+
* const) keeps the hoisted `vi.mock` factory hoist-safe.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* vi.mock('zx', () => zxModuleMock())
|
|
19
|
+
*/
|
|
20
|
+
export const zxModuleMock = () => {
|
|
21
|
+
const $ = (first: unknown) => {
|
|
22
|
+
if (Array.isArray(first)) {
|
|
23
|
+
return execMock()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
return execMock()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { $ }
|
|
32
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure (node-free) vendor config schema + authoring helper. Kept separate from
|
|
5
|
+
* `config.ts` (which imports node builtins for the runtime loader) so the public
|
|
6
|
+
* lib entry can re-export `defineVendorConfig` without dragging node types into
|
|
7
|
+
* the emitted `.d.ts`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Filename a source repo provides at its root to declare WHAT `vendor sync`
|
|
12
|
+
* copies (`copy[]`). Lives ONLY on the write path — `vendor check` never loads it.
|
|
13
|
+
* WHERE/WHICH to stamp (`workspaceDir` + `targets`) is machine-local and lives in
|
|
14
|
+
* the user-global factory config (`~/.infra-kit/vendor.json`).
|
|
15
|
+
*/
|
|
16
|
+
export const VENDOR_CONFIG_FILE = 'vendor.config.ts'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A non-empty, repo-relative path with no `..` segments and no absolute prefix
|
|
20
|
+
* (POSIX `/`, UNC/`\`, or a Windows drive like `C:`). Containment guard for
|
|
21
|
+
* vendor copy items so a malicious/typo config can't read or write outside the
|
|
22
|
+
* source/target repo roots. Kept as a string/regex check (no `node:path`) to
|
|
23
|
+
* respect this file's node-free constraint — `sync-ops.ts` does the resolved
|
|
24
|
+
* runtime containment assert as defense in depth.
|
|
25
|
+
*/
|
|
26
|
+
const safeRelPath = z.string().refine(
|
|
27
|
+
(p) => {
|
|
28
|
+
const isAbsolute = /^(?:[/\\]|[a-z]:)/i.test(p)
|
|
29
|
+
const hasDotDotSegment = p.split(/[\\/]/).includes('..')
|
|
30
|
+
|
|
31
|
+
return !isAbsolute && !hasDotDotSegment && p.trim().length > 0
|
|
32
|
+
},
|
|
33
|
+
{ message: 'must be a non-empty repo-relative path without ".." segments' },
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One item to sync from the source repo into each target. `vendored: true` marks
|
|
38
|
+
* workspace packages that must land under `vendor/` (the single-source-of-truth
|
|
39
|
+
* code); everything else is root-level tooling that stays at the repo root.
|
|
40
|
+
*/
|
|
41
|
+
export const vendorCopyItemSchema = z.object({
|
|
42
|
+
name: z.string(),
|
|
43
|
+
source: safeRelPath,
|
|
44
|
+
target: safeRelPath,
|
|
45
|
+
type: z.enum(['file', 'directory']),
|
|
46
|
+
vendored: z.boolean().optional(),
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export const vendorConfigSchema = z
|
|
50
|
+
.object({
|
|
51
|
+
/** Files/dirs to copy. Items with `vendored: true` land under `vendor/`. */
|
|
52
|
+
copy: z.array(vendorCopyItemSchema),
|
|
53
|
+
})
|
|
54
|
+
// Reject stray keys so a leftover `targets` (now machine-local, in
|
|
55
|
+
// ~/.infra-kit/vendor.json) yields a clear "unrecognized key" error rather
|
|
56
|
+
// than being silently ignored.
|
|
57
|
+
.strict()
|
|
58
|
+
|
|
59
|
+
export type VendorCopyItem = z.infer<typeof vendorCopyItemSchema>
|
|
60
|
+
export type VendorConfig = z.infer<typeof vendorConfigSchema>
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Identity helper for authoring a type-safe `vendor.config.ts` in a source repo.
|
|
64
|
+
* Re-exported from the public lib entry so a source repo can
|
|
65
|
+
* `import { defineVendorConfig } from 'infra-kit'`.
|
|
66
|
+
*
|
|
67
|
+
* NOTE: a `vendor.config.ts` must be type-strippable — Node's native type
|
|
68
|
+
* stripping (Node >= 24) loads it without a build step, which forbids `enum`,
|
|
69
|
+
* `namespace`, and parameter properties.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* export default defineVendorConfig({
|
|
73
|
+
* copy: [{ name: 'Configs', source: 'vendor/configs', target: 'vendor/configs', type: 'directory', vendored: true }],
|
|
74
|
+
* })
|
|
75
|
+
*/
|
|
76
|
+
export const defineVendorConfig = (config: VendorConfig): VendorConfig => {
|
|
77
|
+
return config
|
|
78
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
|
|
6
|
+
import { VENDOR_CONFIG_FILE, vendorConfigSchema } from './config-schema'
|
|
7
|
+
import type { VendorConfig } from './config-schema'
|
|
8
|
+
|
|
9
|
+
// Convenience re-export so importers of the loader can also grab the authoring
|
|
10
|
+
// helper from one module. The node-free definitions live in `./config-schema`
|
|
11
|
+
// (the public `.d.ts` emit imports from there to stay node-type-free).
|
|
12
|
+
export { defineVendorConfig } from './config-schema'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Load, resolve, and validate a source repo's `vendor.config.ts`.
|
|
16
|
+
*
|
|
17
|
+
* Reuses the established loader pattern from
|
|
18
|
+
* `package-validator/loader/config-loader.ts`: dynamic-import the `.ts` file via
|
|
19
|
+
* Node's native type stripping with an mtime cache-bust (so a long-running MCP
|
|
20
|
+
* server picks up edits), resolve a Vite-style factory or object default export,
|
|
21
|
+
* then validate against {@link vendorConfigSchema}. No new dependency (no
|
|
22
|
+
* jiti/tsx). Throws a descriptive error when the file is absent or malformed.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* await loadVendorConfig('/Users/me/projects/starter-workspace')
|
|
26
|
+
*/
|
|
27
|
+
export const loadVendorConfig = async (rootDir: string): Promise<VendorConfig> => {
|
|
28
|
+
const configPath = path.join(rootDir, VENDOR_CONFIG_FILE)
|
|
29
|
+
|
|
30
|
+
let stat
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
stat = await fs.stat(configPath)
|
|
34
|
+
} catch {
|
|
35
|
+
throw new Error(`${VENDOR_CONFIG_FILE} not found at ${configPath}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Cache-bust with the file mtime so repeated loads pick up edits without a
|
|
39
|
+
// process restart. `.ts` configs load via Node's native type stripping.
|
|
40
|
+
const moduleUrl = `${pathToFileURL(configPath).href}?mtime=${Number(stat.mtimeMs)}`
|
|
41
|
+
|
|
42
|
+
const imported = (await import(moduleUrl)) as { default?: unknown }
|
|
43
|
+
const rawExport = imported.default
|
|
44
|
+
|
|
45
|
+
if (rawExport === undefined) {
|
|
46
|
+
throw new Error(`${VENDOR_CONFIG_FILE} at ${configPath} has no default export`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const resolvedExport = typeof rawExport === 'function' ? await (rawExport as () => unknown)() : rawExport
|
|
50
|
+
|
|
51
|
+
const parsed = vendorConfigSchema.safeParse(resolvedExport)
|
|
52
|
+
|
|
53
|
+
if (!parsed.success) {
|
|
54
|
+
throw new Error(`Invalid ${VENDOR_CONFIG_FILE} at ${configPath}: ${z.prettifyError(parsed.error)}`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return parsed.data
|
|
58
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure (node-free) factory config schema. Kept separate from `factory-config.ts`
|
|
5
|
+
* (which imports node builtins for the runtime loader) so the public lib entry can
|
|
6
|
+
* re-export the `FactoryConfig` type without dragging node types into the emitted
|
|
7
|
+
* `.d.ts` — same split as `config-schema.ts` / `config.ts`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Basename of the machine-local factory config, which lives at
|
|
12
|
+
* `~/.infra-kit/vendor.json`. It is static JSON (no longer an executable `.ts`
|
|
13
|
+
* module): the loader reads it with `JSON.parse`. The committed SOURCE-repo config
|
|
14
|
+
* keeps the `vendor.config.ts` basename — always refer to this one as "the
|
|
15
|
+
* user-global factory config".
|
|
16
|
+
*/
|
|
17
|
+
export const FACTORY_CONFIG_FILE = 'vendor.json'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The machine-local factory registry. Answers "where do my project repos live"
|
|
21
|
+
* (`workspaceDir`) and "which ones does the factory stamp" (`targets`). The
|
|
22
|
+
* portable "what to vendor" definition (`copy[]`) lives in the committed SOURCE
|
|
23
|
+
* `vendor.config.ts`, never here. `.strict()` rejects a stray `copy` key so a
|
|
24
|
+
* misplaced source config produces a clear error.
|
|
25
|
+
*/
|
|
26
|
+
export const factoryConfigSchema = z
|
|
27
|
+
.object({
|
|
28
|
+
/** Absolute (or `~`-prefixed) dir where the target project repos are cloned. */
|
|
29
|
+
workspaceDir: z.string().min(1),
|
|
30
|
+
/** Target repo directory names, resolved under `workspaceDir`. */
|
|
31
|
+
targets: z.array(z.string()).min(1),
|
|
32
|
+
})
|
|
33
|
+
.strict()
|
|
34
|
+
|
|
35
|
+
export type FactoryConfig = z.infer<typeof factoryConfigSchema>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
|
|
6
|
+
import { USER_CONFIG_DIR_NAME } from 'src/lib/infra-kit-config'
|
|
7
|
+
|
|
8
|
+
import { FACTORY_CONFIG_FILE, factoryConfigSchema } from './factory-config-schema'
|
|
9
|
+
import type { FactoryConfig } from './factory-config-schema'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Absolute path to the machine-local factory config (`~/.infra-kit/vendor.json`).
|
|
13
|
+
* Always returned, even when the file does not exist, so callers can surface a
|
|
14
|
+
* "where would it go?" prompt.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* getFactoryConfigPath() // => '/Users/arthur/.infra-kit/vendor.json'
|
|
18
|
+
*/
|
|
19
|
+
export const getFactoryConfigPath = (): string => {
|
|
20
|
+
return path.join(os.homedir(), USER_CONFIG_DIR_NAME, FACTORY_CONFIG_FILE)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Expand a leading `~` to the home dir. Absolute paths pass through unchanged.
|
|
25
|
+
* A non-absolute, non-`~` value throws — cwd-relative resolution is ambiguous
|
|
26
|
+
* across CLI vs long-running MCP-server invocations, so we reject it loudly.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* expandTilde('~') // => '/Users/arthur'
|
|
30
|
+
* expandTilde('~/projects') // => '/Users/arthur/projects'
|
|
31
|
+
* expandTilde('/abs/dir') // => '/abs/dir'
|
|
32
|
+
* expandTilde('rel/dir') // throws
|
|
33
|
+
*/
|
|
34
|
+
export const expandTilde = (p: string): string => {
|
|
35
|
+
if (p === '~') return os.homedir()
|
|
36
|
+
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2))
|
|
37
|
+
if (path.isAbsolute(p)) return p
|
|
38
|
+
|
|
39
|
+
throw new Error(`workspaceDir must be absolute or ~-prefixed, got: ${p}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load and validate the user-global factory config at `~/.infra-kit/vendor.json`.
|
|
44
|
+
* Reads the static JSON with `JSON.parse` (an empty/whitespace-only file is treated
|
|
45
|
+
* as `{}`), then validates against {@link factoryConfigSchema}.
|
|
46
|
+
*
|
|
47
|
+
* Reads the file on every call (no module cache to bust — unlike a `.ts`
|
|
48
|
+
* `import()`, `JSON.parse` always sees the current bytes), so an absent → present
|
|
49
|
+
* transition works within one process: a `--init` scaffold followed by a load
|
|
50
|
+
* succeeds, and a long-running MCP server picks up edits without a restart.
|
|
51
|
+
* Throws an actionable error pointing at `infra-kit vendor-config --init` when the
|
|
52
|
+
* file is absent.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* const { workspaceDir, targets } = await loadFactoryConfig()
|
|
56
|
+
*/
|
|
57
|
+
export const loadFactoryConfig = async (): Promise<FactoryConfig> => {
|
|
58
|
+
const configPath = getFactoryConfigPath()
|
|
59
|
+
|
|
60
|
+
let raw: string
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
raw = await fs.readFile(configPath, 'utf-8')
|
|
64
|
+
} catch {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Factory config not found at ${configPath}. infra-kit needs a machine-local factory ` +
|
|
67
|
+
`registry to know where your project repos live and which to stamp. Run ` +
|
|
68
|
+
`\`infra-kit vendor-config --init\` to scaffold it, or create it manually as JSON:\n\n` +
|
|
69
|
+
` {\n` +
|
|
70
|
+
` "workspaceDir": "~/projects",\n` +
|
|
71
|
+
` "targets": ["my-repo"]\n` +
|
|
72
|
+
` }\n`,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let data: unknown
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
data = raw.trim() === '' ? {} : JSON.parse(raw)
|
|
80
|
+
} catch (err) {
|
|
81
|
+
throw new Error(`Invalid JSON in vendor.json at ${configPath}: ${(err as Error).message}`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const parsed = factoryConfigSchema.safeParse(data)
|
|
85
|
+
|
|
86
|
+
if (!parsed.success) {
|
|
87
|
+
throw new Error(`Invalid factory config at ${configPath}: ${z.prettifyError(parsed.error)}`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return parsed.data
|
|
91
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SHA-256 of a file's contents, hex-encoded. Matches the legacy scripts'
|
|
6
|
+
* `createHash('sha256').update(readFileSync(path)).digest('hex')`.
|
|
7
|
+
*
|
|
8
|
+
* `readFileSync` follows symlinks, so a symlinked file is hashed by its target
|
|
9
|
+
* content — the behavior both legacy scripts already relied on.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* sha256('/repo/vendor/configs/eslint-config/index.js') // => 'a1b2c3...'
|
|
13
|
+
*/
|
|
14
|
+
export const sha256 = (filePath: string): string => {
|
|
15
|
+
return createHash('sha256').update(readFileSync(filePath)).digest('hex')
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { sha256 } from './hash'
|
|
2
|
+
export {
|
|
3
|
+
buildFilesMap,
|
|
4
|
+
compareToManifest,
|
|
5
|
+
CURRENT_SCHEMA_VERSION,
|
|
6
|
+
isUnknownSchema,
|
|
7
|
+
MANIFEST_FILE,
|
|
8
|
+
readManifest,
|
|
9
|
+
writeManifest,
|
|
10
|
+
} from './manifest'
|
|
11
|
+
export type { ManifestDiff, VendorManifest } from './manifest'
|
|
12
|
+
/**
|
|
13
|
+
* Read-path barrel for the vendor integrity primitives. This is the SINGLE
|
|
14
|
+
* source of truth for walk/hash/manifest logic, consumed by `vendor check` and
|
|
15
|
+
* (via the same functions) the write-path commands.
|
|
16
|
+
*
|
|
17
|
+
* IMPORTANT: this barrel intentionally re-exports ONLY the read path. The config
|
|
18
|
+
* loader (`./config`) and rsync/`zx` (`./sync-ops`) live with the write commands;
|
|
19
|
+
* `vendor check` never imports them, so its runtime path stays free of them. The
|
|
20
|
+
* skip-set constants are an internal detail (`./skip-sets`) used by `walk`.
|
|
21
|
+
*/
|
|
22
|
+
export { VENDOR_DIR } from './skip-sets'
|
|
23
|
+
export { walkVendorTree } from './walk'
|