infra-kit 0.1.114 → 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-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/.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/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/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/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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises'
|
|
2
|
-
import os from 'node:os'
|
|
3
2
|
import path from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
4
|
import yaml from 'yaml'
|
|
5
5
|
import { z } from 'zod'
|
|
6
6
|
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
resetInfraKitConfigCache,
|
|
12
12
|
} from 'src/lib/infra-kit-config'
|
|
13
13
|
import { logger } from 'src/lib/logger'
|
|
14
|
+
import { fileExists, tildify } from 'src/lib/path-display'
|
|
15
|
+
import { getFactoryConfigPath } from 'src/lib/vendor/factory-config'
|
|
16
|
+
import { factoryConfigSchema } from 'src/lib/vendor/factory-config-schema'
|
|
14
17
|
|
|
15
18
|
interface MigrateLayer {
|
|
16
19
|
label: string
|
|
@@ -22,35 +25,6 @@ interface MigrateLayer {
|
|
|
22
25
|
schema: z.ZodType
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
/**
|
|
26
|
-
* Replace the user's home prefix with `~` so logged paths stay short.
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* // os.homedir() === '/Users/arthur'
|
|
30
|
-
* tildify('/Users/arthur/.infra-kit/config.yml') // => '~/.infra-kit/config.yml'
|
|
31
|
-
*/
|
|
32
|
-
const tildify = (filePath: string): string => {
|
|
33
|
-
const home = os.homedir()
|
|
34
|
-
|
|
35
|
-
return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* `fs.access` reduced to a boolean, swallowing ENOENT.
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* await fileExists('/etc/hosts') // => true
|
|
43
|
-
*/
|
|
44
|
-
const fileExists = async (filePath: string): Promise<boolean> => {
|
|
45
|
-
try {
|
|
46
|
-
await fs.access(filePath)
|
|
47
|
-
|
|
48
|
-
return true
|
|
49
|
-
} catch {
|
|
50
|
-
return false
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
28
|
/**
|
|
55
29
|
* Swap a resolved `.json` config path back to its legacy `.yml` sibling.
|
|
56
30
|
*
|
|
@@ -87,8 +61,13 @@ export const migrateLegacyConfig = async (): Promise<void> => {
|
|
|
87
61
|
const layers: MigrateLayer[] = [
|
|
88
62
|
{ label: 'infra-kit.json', yml: legacyYmlPath(paths.main), json: paths.main, schema: infraKitConfigSchema },
|
|
89
63
|
{
|
|
90
|
-
label: '~/.infra-kit/
|
|
91
|
-
|
|
64
|
+
label: '~/.infra-kit/infra-kit.json',
|
|
65
|
+
// The user-global JSON filename changed (config.json → infra-kit.json), but
|
|
66
|
+
// the legacy YAML it converts from kept the old `config.yml` name. Pin it
|
|
67
|
+
// here so it does not drift to `infra-kit.yml` via legacyYmlPath(); the
|
|
68
|
+
// main/userProject layers keep deriving their `.yml` sibling normally
|
|
69
|
+
// because their `.json` filename never changed.
|
|
70
|
+
yml: path.join(path.dirname(paths.userGlobal), 'config.yml'),
|
|
92
71
|
json: paths.userGlobal,
|
|
93
72
|
schema: infraKitOverrideConfigSchema,
|
|
94
73
|
},
|
|
@@ -145,6 +124,155 @@ export const migrateLegacyConfig = async (): Promise<void> => {
|
|
|
145
124
|
}
|
|
146
125
|
}
|
|
147
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Rename the user-global config from the legacy `config.json` filename to the
|
|
129
|
+
* canonical `infra-kit.json` (and its `config.example.jsonc` reference to
|
|
130
|
+
* `infra-kit.example.jsonc`), so every merge-chain layer shares one filename.
|
|
131
|
+
* Run by `infra-kit init` before the user-global config is seeded, so a user's
|
|
132
|
+
* existing overrides are carried forward instead of being shadowed by a fresh
|
|
133
|
+
* `{}` stub written under the new name.
|
|
134
|
+
*
|
|
135
|
+
* Best-effort and non-fatal per file: a missing source is skipped silently
|
|
136
|
+
* (idempotent — nothing to rename on a second run), and an already-present
|
|
137
|
+
* target is never overwritten (the stale `config.json` is left in place so no
|
|
138
|
+
* bytes are lost — the user reconciles it manually). Resets the config cache
|
|
139
|
+
* when anything was renamed.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* await migrateUserGlobalConfigFilename()
|
|
143
|
+
* // ✓ Renamed ~/.infra-kit/config.json → ~/.infra-kit/infra-kit.json
|
|
144
|
+
* // (no output when there is nothing legacy to rename)
|
|
145
|
+
*/
|
|
146
|
+
export const migrateUserGlobalConfigFilename = async (): Promise<void> => {
|
|
147
|
+
let paths: Awaited<ReturnType<typeof getInfraKitConfigPaths>>
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
paths = await getInfraKitConfigPaths()
|
|
151
|
+
} catch {
|
|
152
|
+
// No resolvable project (e.g. init run outside a repo) — nothing to rename.
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const dir = path.dirname(paths.userGlobal)
|
|
157
|
+
// The config.json → infra-kit.json rename carries forward a user's REAL config and
|
|
158
|
+
// is load-bearing. The example-file rename is now largely redundant: seedUserGlobalConfig
|
|
159
|
+
// rewrites infra-kit.example.jsonc unconditionally moments later, so a renamed legacy
|
|
160
|
+
// example is overwritten anyway (and if infra-kit.example.jsonc already exists, the
|
|
161
|
+
// no-overwrite guard simply leaves an orphaned config.example.jsonc behind — harmless).
|
|
162
|
+
const pairs: { from: string; to: string }[] = [
|
|
163
|
+
{ from: path.join(dir, 'config.json'), to: paths.userGlobal },
|
|
164
|
+
{ from: path.join(dir, 'config.example.jsonc'), to: path.join(dir, 'infra-kit.example.jsonc') },
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
let renamed = 0
|
|
168
|
+
|
|
169
|
+
for (const { from, to } of pairs) {
|
|
170
|
+
const [fromExists, toExists] = await Promise.all([fileExists(from), fileExists(to)])
|
|
171
|
+
|
|
172
|
+
if (!fromExists) continue
|
|
173
|
+
|
|
174
|
+
if (toExists) {
|
|
175
|
+
logger.info(`⚠ Skipped ${tildify(from)} — ${tildify(to)} already exists (remove the stale file manually)`)
|
|
176
|
+
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Same-directory rename is atomic; per-file try/catch keeps a TOCTOU race or
|
|
181
|
+
// I/O error (EACCES, read-only FS) non-fatal so one bad file never aborts init.
|
|
182
|
+
try {
|
|
183
|
+
await fs.mkdir(path.dirname(to), { recursive: true })
|
|
184
|
+
await fs.rename(from, to)
|
|
185
|
+
|
|
186
|
+
logger.info(`✓ Renamed ${tildify(from)} → ${tildify(to)}`)
|
|
187
|
+
renamed++
|
|
188
|
+
} catch (err) {
|
|
189
|
+
logger.info(`⚠ Skipped ${tildify(from)} — ${(err as Error).message}`)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (renamed > 0) {
|
|
194
|
+
resetInfraKitConfigCache()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Convert a legacy machine-local factory config from executable TypeScript
|
|
200
|
+
* (`~/.infra-kit/vendor.config.ts`) to static JSON (`~/.infra-kit/vendor.json`) as
|
|
201
|
+
* part of `infra-kit init`. The factory config used to be a `.ts` module loaded via
|
|
202
|
+
* dynamic `import()`; it is now strict JSON read with `JSON.parse`. This one-shot
|
|
203
|
+
* migration dynamic-imports the old `.ts` (the ONLY remaining dynamic import of the
|
|
204
|
+
* factory file), resolves a factory-function or object default export, validates it,
|
|
205
|
+
* writes `vendor.json`, and removes the old `.ts`.
|
|
206
|
+
*
|
|
207
|
+
* Best-effort, non-fatal, idempotent (no old `.ts` → no-op), and never overwrites an
|
|
208
|
+
* existing `vendor.json`. When the old export was a FUNCTION, its resolved output is
|
|
209
|
+
* frozen into static JSON — so a DISTINCT warning is emitted (not a plain
|
|
210
|
+
* `✓ Migrated`) because dynamic re-evaluation (env vars, directory globbing) no
|
|
211
|
+
* longer happens. No `resetInfraKitConfigCache()` — the factory loader shares no
|
|
212
|
+
* cache with the `infra-kit.json` layers.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* await migrateFactoryConfigToJson()
|
|
216
|
+
* // ✓ Migrated ~/.infra-kit/vendor.config.ts → ~/.infra-kit/vendor.json
|
|
217
|
+
* // (no output when there is nothing legacy to convert)
|
|
218
|
+
*/
|
|
219
|
+
export const migrateFactoryConfigToJson = async (): Promise<void> => {
|
|
220
|
+
const newJson = getFactoryConfigPath() // ~/.infra-kit/vendor.json
|
|
221
|
+
const dir = path.dirname(newJson)
|
|
222
|
+
const oldTs = path.join(dir, 'vendor.config.ts')
|
|
223
|
+
|
|
224
|
+
const [oldExists, newExists] = await Promise.all([fileExists(oldTs), fileExists(newJson)])
|
|
225
|
+
|
|
226
|
+
if (!oldExists) return // idempotent: nothing to migrate (incl. after a prior successful run)
|
|
227
|
+
|
|
228
|
+
if (newExists) {
|
|
229
|
+
logger.info(
|
|
230
|
+
`⚠ Skipped ${tildify(oldTs)} — ${tildify(newJson)} already exists (remove the stale vendor.config.ts manually)`,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// The factory `.ts` is executable; read it once via dynamic import (Node native
|
|
237
|
+
// type stripping) with an mtime cache-bust, resolving a factory-function or object
|
|
238
|
+
// default export. Per-file try/catch keeps a malformed module or I/O error
|
|
239
|
+
// non-fatal so one bad file never aborts init.
|
|
240
|
+
try {
|
|
241
|
+
const stat = await fs.stat(oldTs)
|
|
242
|
+
const moduleUrl = `${pathToFileURL(oldTs).href}?mtime=${Number(stat.mtimeMs)}`
|
|
243
|
+
const imported = (await import(moduleUrl)) as { default?: unknown }
|
|
244
|
+
const raw = imported.default
|
|
245
|
+
const wasFunction = typeof raw === 'function'
|
|
246
|
+
const resolved = wasFunction ? await (raw as () => unknown)() : raw
|
|
247
|
+
|
|
248
|
+
const result = factoryConfigSchema.safeParse(resolved)
|
|
249
|
+
|
|
250
|
+
if (!result.success) {
|
|
251
|
+
logger.info(`⚠ Skipped ${tildify(oldTs)} — invalid factory config: ${z.prettifyError(result.error)}`)
|
|
252
|
+
|
|
253
|
+
return // leave the old .ts in place so the user can fix and re-run; do not delete unconverted data
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await fs.mkdir(dir, { recursive: true })
|
|
257
|
+
await fs.writeFile(newJson, `${JSON.stringify(result.data, null, 2)}\n`, 'utf-8')
|
|
258
|
+
await fs.rm(oldTs, { force: true })
|
|
259
|
+
|
|
260
|
+
if (wasFunction) {
|
|
261
|
+
// The old export was a function: its values were resolved ONCE and frozen into
|
|
262
|
+
// static JSON. Disclose the lost dynamism instead of reporting a clean success.
|
|
263
|
+
logger.info(
|
|
264
|
+
`⚠ Migrated ${tildify(oldTs)} → ${tildify(newJson)} as a STATIC SNAPSHOT — the old file was a ` +
|
|
265
|
+
`function (export default () => ({...})); computed values (env vars, directory globbing) were ` +
|
|
266
|
+
`frozen at their current values and no longer re-evaluate. Edit ${tildify(newJson)} directly to change them.`,
|
|
267
|
+
)
|
|
268
|
+
} else {
|
|
269
|
+
logger.info(`✓ Migrated ${tildify(oldTs)} → ${tildify(newJson)}`)
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
logger.info(`⚠ Skipped ${tildify(oldTs)} — ${(err as Error).message}`)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
148
276
|
/**
|
|
149
277
|
* Surgically strip the removed `ide.config.mode` field from a parsed config
|
|
150
278
|
* object (single-provider or array form). The windows-removal made `mode` a dead
|
|
@@ -0,0 +1,71 @@
|
|
|
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 { writeManifest } from 'src/lib/vendor'
|
|
7
|
+
import { execMock, zxModuleMock } from 'src/lib/vendor/__tests__/zx-mock'
|
|
8
|
+
|
|
9
|
+
import { vendorCheck } from '../vendor-check'
|
|
10
|
+
|
|
11
|
+
// Spy on every subprocess-spawning and config-loading collaborator. The
|
|
12
|
+
// invariant under test is RUNTIME: invoking `vendor check` must spawn zero
|
|
13
|
+
// subprocess and load zero vendor.config.ts — regardless of what the single
|
|
14
|
+
// bundled CLI artifact happens to contain.
|
|
15
|
+
vi.mock('zx', () => {
|
|
16
|
+
return zxModuleMock()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const loadVendorConfigMock = vi.fn()
|
|
20
|
+
|
|
21
|
+
vi.mock('src/lib/vendor/config', async (importActual) => {
|
|
22
|
+
const actual = await importActual<typeof import('src/lib/vendor/config')>()
|
|
23
|
+
|
|
24
|
+
return { ...actual, loadVendorConfig: loadVendorConfigMock }
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const loadFactoryConfigMock = vi.fn()
|
|
28
|
+
|
|
29
|
+
vi.mock('src/lib/vendor/factory-config', async (importActual) => {
|
|
30
|
+
const actual = await importActual<typeof import('src/lib/vendor/factory-config')>()
|
|
31
|
+
|
|
32
|
+
return { ...actual, loadFactoryConfig: loadFactoryConfigMock }
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
let root: string
|
|
36
|
+
let vendorRoot: string
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
vi.clearAllMocks()
|
|
40
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-isolation-test-'))
|
|
41
|
+
vendorRoot = path.join(root, 'vendor')
|
|
42
|
+
fs.mkdirSync(path.join(vendorRoot, 'configs'), { recursive: true })
|
|
43
|
+
fs.writeFileSync(path.join(vendorRoot, 'configs', 'a.js'), 'aaa')
|
|
44
|
+
writeManifest(vendorRoot, { source: 's', commit: 'c' })
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('vendor check runtime isolation', () => {
|
|
52
|
+
it('spawns no subprocess and loads no vendor.config.ts on a clean check', async () => {
|
|
53
|
+
const { structuredContent } = await vendorCheck({ cwd: root })
|
|
54
|
+
|
|
55
|
+
expect(structuredContent.ok).toBe(true)
|
|
56
|
+
expect(execMock).not.toHaveBeenCalled()
|
|
57
|
+
expect(loadVendorConfigMock).not.toHaveBeenCalled()
|
|
58
|
+
expect(loadFactoryConfigMock).not.toHaveBeenCalled()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('spawns no subprocess even when reporting drift', async () => {
|
|
62
|
+
fs.writeFileSync(path.join(vendorRoot, 'configs', 'a.js'), 'CHANGED')
|
|
63
|
+
|
|
64
|
+
const { structuredContent } = await vendorCheck({ cwd: root })
|
|
65
|
+
|
|
66
|
+
expect(structuredContent.ok).toBe(false)
|
|
67
|
+
expect(execMock).not.toHaveBeenCalled()
|
|
68
|
+
expect(loadVendorConfigMock).not.toHaveBeenCalled()
|
|
69
|
+
expect(loadFactoryConfigMock).not.toHaveBeenCalled()
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,87 @@
|
|
|
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 { writeManifest } from 'src/lib/vendor'
|
|
7
|
+
|
|
8
|
+
import { vendorCheck } from '../vendor-check'
|
|
9
|
+
|
|
10
|
+
let root: string
|
|
11
|
+
let vendorRoot: string
|
|
12
|
+
|
|
13
|
+
const writeVendor = (rel: string, content: string): void => {
|
|
14
|
+
const full = path.join(vendorRoot, rel)
|
|
15
|
+
|
|
16
|
+
fs.mkdirSync(path.dirname(full), { recursive: true })
|
|
17
|
+
fs.writeFileSync(full, content, 'utf8')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-check-test-'))
|
|
22
|
+
vendorRoot = path.join(root, 'vendor')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('vendorCheck exit-code matrix', () => {
|
|
30
|
+
it('missing vendor/ → ok (skip)', async () => {
|
|
31
|
+
const { structuredContent } = await vendorCheck({ cwd: root })
|
|
32
|
+
|
|
33
|
+
expect(structuredContent.status).toBe('skipped')
|
|
34
|
+
expect(structuredContent.ok).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('missing manifest → not ok', async () => {
|
|
38
|
+
writeVendor('a.js', 'a')
|
|
39
|
+
const { structuredContent } = await vendorCheck({ cwd: root })
|
|
40
|
+
|
|
41
|
+
expect(structuredContent.status).toBe('missing-manifest')
|
|
42
|
+
expect(structuredContent.ok).toBe(false)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('clean tree → ok', async () => {
|
|
46
|
+
writeVendor('configs/a.js', 'a')
|
|
47
|
+
writeVendor('packages/p/b.ts', 'b')
|
|
48
|
+
writeManifest(vendorRoot, { source: 's', commit: 'c' })
|
|
49
|
+
|
|
50
|
+
const { structuredContent } = await vendorCheck({ cwd: root })
|
|
51
|
+
|
|
52
|
+
expect(structuredContent.status).toBe('clean')
|
|
53
|
+
expect(structuredContent.ok).toBe(true)
|
|
54
|
+
expect(structuredContent.fileCount).toBe(2)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('drifted tree → not ok with categorized paths', async () => {
|
|
58
|
+
writeVendor('a.js', 'a')
|
|
59
|
+
writeVendor('b.js', 'b')
|
|
60
|
+
writeManifest(vendorRoot, { source: 's', commit: 'c' })
|
|
61
|
+
|
|
62
|
+
fs.writeFileSync(path.join(vendorRoot, 'a.js'), 'CHANGED')
|
|
63
|
+
writeVendor('c.js', 'c')
|
|
64
|
+
fs.rmSync(path.join(vendorRoot, 'b.js'))
|
|
65
|
+
|
|
66
|
+
const { structuredContent } = await vendorCheck({ cwd: root })
|
|
67
|
+
|
|
68
|
+
expect(structuredContent.status).toBe('drift')
|
|
69
|
+
expect(structuredContent.ok).toBe(false)
|
|
70
|
+
expect(structuredContent.modified).toEqual(['a.js'])
|
|
71
|
+
expect(structuredContent.added).toEqual(['c.js'])
|
|
72
|
+
expect(structuredContent.removed).toEqual(['b.js'])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('unknown future schemaVersion → not ok (fail-closed)', async () => {
|
|
76
|
+
writeVendor('a.js', 'a')
|
|
77
|
+
const manifest = writeManifest(vendorRoot, { source: 's', commit: 'c' })
|
|
78
|
+
const bumped = { ...manifest, schemaVersion: (manifest.schemaVersion ?? 1) + 1 }
|
|
79
|
+
|
|
80
|
+
fs.writeFileSync(path.join(vendorRoot, '.sync-manifest.json'), JSON.stringify(bumped))
|
|
81
|
+
|
|
82
|
+
const { structuredContent } = await vendorCheck({ cwd: root })
|
|
83
|
+
|
|
84
|
+
expect(structuredContent.status).toBe('unknown-schema')
|
|
85
|
+
expect(structuredContent.ok).toBe(false)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { vendorCheck, vendorCheckMcpTool } from './vendor-check'
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
|
|
6
|
+
import { getProjectRoot } from 'src/lib/git-utils'
|
|
7
|
+
import { logger } from 'src/lib/logger'
|
|
8
|
+
import { MANIFEST_FILE, VENDOR_DIR, compareToManifest, isUnknownSchema, readManifest } from 'src/lib/vendor'
|
|
9
|
+
import { defineMcpTool, textContent } from 'src/types'
|
|
10
|
+
|
|
11
|
+
/** Max drifted paths to print per category before truncating. */
|
|
12
|
+
const MAX_REPORTED = 30
|
|
13
|
+
|
|
14
|
+
type VendorCheckStatus = 'clean' | 'drift' | 'skipped' | 'missing-manifest' | 'unknown-schema'
|
|
15
|
+
|
|
16
|
+
interface VendorCheckOptions {
|
|
17
|
+
/** Repo root to check. Defaults to the git toplevel. */
|
|
18
|
+
cwd?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const report = (label: string, list: string[]): void => {
|
|
22
|
+
if (list.length === 0) {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
logger.info(`\n${label} (${list.length}):`)
|
|
27
|
+
|
|
28
|
+
for (const rel of list.slice(0, MAX_REPORTED)) {
|
|
29
|
+
logger.info(` ${rel}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (list.length > MAX_REPORTED) {
|
|
33
|
+
logger.info(` …and ${list.length - MAX_REPORTED} more`)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Verify that files under `vendor/` still match the checksums recorded in
|
|
39
|
+
* `vendor/.sync-manifest.json`. Config-free and self-contained (no source repo,
|
|
40
|
+
* no `vendor.config.ts`, no rsync) so it runs in any consumer's CI. Read-only —
|
|
41
|
+
* never calls `process.exit`; the CLI action maps `structuredContent.ok` to the
|
|
42
|
+
* exit code, and the MCP tool reuses the same handler.
|
|
43
|
+
*
|
|
44
|
+
* Exit-code contract (preserved from the legacy `vendor-check.mjs`):
|
|
45
|
+
* clean → ok; drift → not ok; missing `vendor/` → ok (skip);
|
|
46
|
+
* missing manifest → not ok; unknown (newer) schemaVersion → not ok (fail-closed).
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* await vendorCheck() // checks the current repo's vendor/
|
|
50
|
+
* await vendorCheck({ cwd: '/repo' }) // checks a specific root (tests)
|
|
51
|
+
*/
|
|
52
|
+
export const vendorCheck = async (options: VendorCheckOptions = {}) => {
|
|
53
|
+
const root = options.cwd ?? (await getProjectRoot())
|
|
54
|
+
const vendorRoot = path.join(root, VENDOR_DIR)
|
|
55
|
+
|
|
56
|
+
const base = { modified: [] as string[], added: [] as string[], removed: [] as string[], fileCount: 0 }
|
|
57
|
+
|
|
58
|
+
if (!existsSync(vendorRoot)) {
|
|
59
|
+
logger.info(`ℹ️ No ${VENDOR_DIR}/ folder found — nothing to check.`)
|
|
60
|
+
const structuredContent = { status: 'skipped' as VendorCheckStatus, ok: true, ...base }
|
|
61
|
+
|
|
62
|
+
return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!existsSync(path.join(vendorRoot, MANIFEST_FILE))) {
|
|
66
|
+
logger.error(`❌ Missing ${VENDOR_DIR}/${MANIFEST_FILE}. Re-run the vendor sync to generate it.`)
|
|
67
|
+
const structuredContent = { status: 'missing-manifest' as VendorCheckStatus, ok: false, ...base }
|
|
68
|
+
|
|
69
|
+
return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const manifest = readManifest(vendorRoot)
|
|
73
|
+
|
|
74
|
+
if (isUnknownSchema(manifest)) {
|
|
75
|
+
logger.error(
|
|
76
|
+
`❌ ${VENDOR_DIR}/${MANIFEST_FILE} has a newer schemaVersion (${String(manifest.schemaVersion)}) than this CLI supports. Upgrade infra-kit.`,
|
|
77
|
+
)
|
|
78
|
+
const structuredContent = { status: 'unknown-schema' as VendorCheckStatus, ok: false, ...base }
|
|
79
|
+
|
|
80
|
+
return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { modified, added, removed } = compareToManifest(vendorRoot, manifest)
|
|
84
|
+
const drifted = modified.length > 0 || added.length > 0 || removed.length > 0
|
|
85
|
+
|
|
86
|
+
if (drifted) {
|
|
87
|
+
logger.error(`❌ ${VENDOR_DIR}/ has drifted from ${VENDOR_DIR}/${MANIFEST_FILE}.`)
|
|
88
|
+
logger.error(' These files are mirrored from the source repo — edit them upstream, not here.')
|
|
89
|
+
report('Modified', modified)
|
|
90
|
+
report('Added (not in manifest)', added)
|
|
91
|
+
report('Removed (in manifest, now missing)', removed)
|
|
92
|
+
} else {
|
|
93
|
+
logger.info(`✅ ${VENDOR_DIR}/ matches manifest (${manifest.fileCount} files).`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const structuredContent = {
|
|
97
|
+
status: (drifted ? 'drift' : 'clean') as VendorCheckStatus,
|
|
98
|
+
ok: !drifted,
|
|
99
|
+
fileCount: manifest.fileCount,
|
|
100
|
+
modified,
|
|
101
|
+
added,
|
|
102
|
+
removed,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const vendorCheckOutputSchema = {
|
|
109
|
+
status: z.enum(['clean', 'drift', 'skipped', 'missing-manifest', 'unknown-schema']),
|
|
110
|
+
ok: z.boolean().describe('Whether the vendor tree matches its manifest (false → CI should fail)'),
|
|
111
|
+
fileCount: z.number(),
|
|
112
|
+
modified: z.array(z.string()),
|
|
113
|
+
added: z.array(z.string()),
|
|
114
|
+
removed: z.array(z.string()),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// MCP Tool Registration (read-only; safe to expose).
|
|
118
|
+
export const vendorCheckMcpTool = defineMcpTool({
|
|
119
|
+
name: 'vendor-check',
|
|
120
|
+
description:
|
|
121
|
+
'Verify that files under vendor/ match the checksums in vendor/.sync-manifest.json. Self-contained (no source repo or config needed). Returns ok=false on drift, missing manifest, or an unknown future schemaVersion; ok=true when clean or when no vendor/ folder exists.',
|
|
122
|
+
inputSchema: {},
|
|
123
|
+
outputSchema: vendorCheckOutputSchema,
|
|
124
|
+
handler: () => {
|
|
125
|
+
return vendorCheck({ cwd: process.cwd() })
|
|
126
|
+
},
|
|
127
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { getFactoryConfigPath } from 'src/lib/vendor/factory-config'
|
|
8
|
+
|
|
9
|
+
import { vendorConfig } from '../vendor-config'
|
|
10
|
+
|
|
11
|
+
let home: string
|
|
12
|
+
let source: string
|
|
13
|
+
let factoryPath: string
|
|
14
|
+
|
|
15
|
+
const writeFactory = (workspaceDir: string, targets: string[]): void => {
|
|
16
|
+
fs.mkdirSync(path.dirname(factoryPath), { recursive: true })
|
|
17
|
+
fs.writeFileSync(factoryPath, JSON.stringify({ workspaceDir, targets }), 'utf8')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
home = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-config-home-'))
|
|
22
|
+
source = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-config-src-'))
|
|
23
|
+
vi.spyOn(os, 'homedir').mockReturnValue(home)
|
|
24
|
+
factoryPath = getFactoryConfigPath()
|
|
25
|
+
process.exitCode = 0
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.restoreAllMocks()
|
|
30
|
+
fs.rmSync(home, { recursive: true, force: true })
|
|
31
|
+
fs.rmSync(source, { recursive: true, force: true })
|
|
32
|
+
process.exitCode = 0
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('vendorConfig (print)', () => {
|
|
36
|
+
it('exits non-zero when the factory config is absent', async () => {
|
|
37
|
+
await vendorConfig()
|
|
38
|
+
|
|
39
|
+
expect(process.exitCode).toBe(1)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('exits zero when workspaceDir and every target are reachable', async () => {
|
|
43
|
+
const workspaceDir = path.join(home, 'projects')
|
|
44
|
+
|
|
45
|
+
fs.mkdirSync(path.join(workspaceDir, 'repo-a'), { recursive: true })
|
|
46
|
+
writeFactory(workspaceDir, ['repo-a'])
|
|
47
|
+
|
|
48
|
+
await vendorConfig()
|
|
49
|
+
|
|
50
|
+
expect(process.exitCode).toBe(0)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('exits non-zero when a target is missing', async () => {
|
|
54
|
+
const workspaceDir = path.join(home, 'projects')
|
|
55
|
+
|
|
56
|
+
fs.mkdirSync(workspaceDir, { recursive: true })
|
|
57
|
+
writeFactory(workspaceDir, ['missing-repo'])
|
|
58
|
+
|
|
59
|
+
await vendorConfig()
|
|
60
|
+
|
|
61
|
+
expect(process.exitCode).toBe(1)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('vendorConfig --init', () => {
|
|
66
|
+
it('scaffolds the factory config as strict JSON with a placeholder workspaceDir', async () => {
|
|
67
|
+
await vendorConfig({ init: true, cwd: source })
|
|
68
|
+
|
|
69
|
+
const written = fs.readFileSync(factoryPath, 'utf8')
|
|
70
|
+
const parsed = JSON.parse(written)
|
|
71
|
+
|
|
72
|
+
// Strict JSON — no executable export, no `infra-kit` import.
|
|
73
|
+
expect(written).not.toContain('export default')
|
|
74
|
+
expect(written).not.toContain(`from 'infra-kit'`)
|
|
75
|
+
expect(parsed.workspaceDir).toBe('~/projects')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('seeds targets from a legacy source vendor.config.ts', async () => {
|
|
79
|
+
// The SOURCE config stays TypeScript (only the machine-local factory file moved to JSON).
|
|
80
|
+
fs.writeFileSync(
|
|
81
|
+
path.join(source, 'vendor.config.ts'),
|
|
82
|
+
`export default { targets: ['travelist-monorepo', 'hulyo-monorepo'], copy: [] }`,
|
|
83
|
+
'utf8',
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
await vendorConfig({ init: true, cwd: source })
|
|
87
|
+
|
|
88
|
+
const parsed = JSON.parse(fs.readFileSync(factoryPath, 'utf8'))
|
|
89
|
+
|
|
90
|
+
expect(parsed.targets).toEqual(['travelist-monorepo', 'hulyo-monorepo'])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('leaves an existing factory config untouched', async () => {
|
|
94
|
+
const workspaceDir = path.join(home, 'projects')
|
|
95
|
+
|
|
96
|
+
writeFactory(workspaceDir, ['original-repo'])
|
|
97
|
+
const before = fs.readFileSync(factoryPath, 'utf8')
|
|
98
|
+
|
|
99
|
+
await vendorConfig({ init: true, cwd: source })
|
|
100
|
+
|
|
101
|
+
expect(fs.readFileSync(factoryPath, 'utf8')).toBe(before)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { vendorConfig } from './vendor-config'
|