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
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
import { sha256 } from './hash'
|
|
6
|
+
import { walkVendorTree } from './walk'
|
|
7
|
+
|
|
8
|
+
/** Filename of the integrity manifest inside a `vendor/` tree. */
|
|
9
|
+
export const MANIFEST_FILE = '.sync-manifest.json'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Manifest schema version the CLI understands. A manifest with a HIGHER version
|
|
13
|
+
* than this was written by a newer CLI and is treated as drift (fail-closed) —
|
|
14
|
+
* see {@link isUnknownSchema}. Legacy manifests carry no `schemaVersion` field
|
|
15
|
+
* and read as compatible.
|
|
16
|
+
*/
|
|
17
|
+
export const CURRENT_SCHEMA_VERSION = 1
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `schemaVersion` is OPTIONAL with no default: legacy manifests written by
|
|
21
|
+
* `writeVendorMeta` have no such field, and a required field would make
|
|
22
|
+
* `vendor check` fail-closed on every existing manifest. Absent is read as
|
|
23
|
+
* "compatible" (current). `writeManifest` always emits the current version.
|
|
24
|
+
*/
|
|
25
|
+
const manifestSchema = z.object({
|
|
26
|
+
schemaVersion: z.number().optional(),
|
|
27
|
+
source: z.string(),
|
|
28
|
+
commit: z.string(),
|
|
29
|
+
syncedAt: z.string(),
|
|
30
|
+
fileCount: z.number(),
|
|
31
|
+
files: z.record(z.string(), z.string()),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export type VendorManifest = z.infer<typeof manifestSchema>
|
|
35
|
+
|
|
36
|
+
export interface ManifestDiff {
|
|
37
|
+
modified: string[]
|
|
38
|
+
added: string[]
|
|
39
|
+
removed: string[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether a manifest's `schemaVersion` is newer than this CLI supports. Absent
|
|
44
|
+
* (legacy) reads as compatible; only a strictly-greater version is unknown.
|
|
45
|
+
*/
|
|
46
|
+
export const isUnknownSchema = (manifest: VendorManifest): boolean => {
|
|
47
|
+
return (manifest.schemaVersion ?? CURRENT_SCHEMA_VERSION) > CURRENT_SCHEMA_VERSION
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Walk the vendor tree and hash every file into a `{ relativePath: sha256 }`
|
|
52
|
+
* map (sorted by walk order → stable). A file that cannot be read (e.g. a
|
|
53
|
+
* dangling symlink) surfaces an explicit error rather than crashing with a raw
|
|
54
|
+
* fs stack.
|
|
55
|
+
*/
|
|
56
|
+
export const buildFilesMap = (vendorRoot: string): Record<string, string> => {
|
|
57
|
+
const files: Record<string, string> = {}
|
|
58
|
+
|
|
59
|
+
for (const rel of walkVendorTree(vendorRoot)) {
|
|
60
|
+
try {
|
|
61
|
+
files[rel] = sha256(path.join(vendorRoot, rel))
|
|
62
|
+
} catch (cause) {
|
|
63
|
+
const reason = cause instanceof Error ? cause.message : String(cause)
|
|
64
|
+
|
|
65
|
+
throw new Error(`Cannot hash vendor file "${rel}" (broken symlink or unreadable file): ${reason}`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return files
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Read and validate the manifest at `<vendorRoot>/.sync-manifest.json`. Throws a
|
|
74
|
+
* descriptive error if the file is missing or malformed.
|
|
75
|
+
*/
|
|
76
|
+
export const readManifest = (vendorRoot: string): VendorManifest => {
|
|
77
|
+
const manifestPath = path.join(vendorRoot, MANIFEST_FILE)
|
|
78
|
+
|
|
79
|
+
let raw: string
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
raw = readFileSync(manifestPath, 'utf8')
|
|
83
|
+
} catch {
|
|
84
|
+
throw new Error(`Missing ${MANIFEST_FILE} at ${manifestPath}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const parsed = manifestSchema.safeParse(JSON.parse(raw))
|
|
88
|
+
|
|
89
|
+
if (!parsed.success) {
|
|
90
|
+
throw new Error(`Invalid ${MANIFEST_FILE} at ${manifestPath}: ${z.prettifyError(parsed.error)}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return parsed.data
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build and write a fresh manifest for the current content of `vendorRoot`.
|
|
98
|
+
* Emits the CURRENT schema version, the supplied `source`/`commit`, an ISO
|
|
99
|
+
* `syncedAt`, and the per-file checksum map. Returns the written manifest.
|
|
100
|
+
*
|
|
101
|
+
* The `files` map and field set (minus `schemaVersion`) match legacy
|
|
102
|
+
* `writeVendorMeta`, so a regenerated manifest differs from a legacy one only by
|
|
103
|
+
* the added `schemaVersion` and the `syncedAt` timestamp.
|
|
104
|
+
*/
|
|
105
|
+
export const writeManifest = (vendorRoot: string, meta: { source: string; commit: string }): VendorManifest => {
|
|
106
|
+
const files = buildFilesMap(vendorRoot)
|
|
107
|
+
|
|
108
|
+
const manifest: VendorManifest = {
|
|
109
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
110
|
+
source: meta.source,
|
|
111
|
+
commit: meta.commit,
|
|
112
|
+
syncedAt: new Date().toISOString(),
|
|
113
|
+
fileCount: Object.keys(files).length,
|
|
114
|
+
files,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
writeFileSync(path.join(vendorRoot, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`)
|
|
118
|
+
|
|
119
|
+
return manifest
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Compare the live `vendorRoot` tree against a manifest's recorded checksums.
|
|
124
|
+
* Returns the modified / added (present, not in manifest) / removed (in
|
|
125
|
+
* manifest, now missing) relative paths.
|
|
126
|
+
*/
|
|
127
|
+
export const compareToManifest = (vendorRoot: string, manifest: VendorManifest): ManifestDiff => {
|
|
128
|
+
const expected = manifest.files
|
|
129
|
+
const actualFiles = walkVendorTree(vendorRoot)
|
|
130
|
+
|
|
131
|
+
const modified: string[] = []
|
|
132
|
+
const added: string[] = []
|
|
133
|
+
|
|
134
|
+
for (const rel of actualFiles) {
|
|
135
|
+
if (!(rel in expected)) {
|
|
136
|
+
added.push(rel)
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (sha256(path.join(vendorRoot, rel)) !== expected[rel]) {
|
|
141
|
+
modified.push(rel)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const actualSet = new Set(actualFiles)
|
|
146
|
+
const removed = Object.keys(expected).filter((rel) => {
|
|
147
|
+
return !actualSet.has(rel)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
return { modified, added, removed }
|
|
151
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skip rules for walking a `vendor/` tree to build or verify its integrity
|
|
3
|
+
* manifest. This is the SINGLE source of truth consumed by both the read path
|
|
4
|
+
* (`vendor check`) and the write path (`vendor sync`/`manifest`).
|
|
5
|
+
*
|
|
6
|
+
* The set below is a strict superset of the legacy read script's
|
|
7
|
+
* `MANIFEST_SKIP_DIRS` (`scripts/vendor-check.mjs`). It additionally skips
|
|
8
|
+
* `.vitest-attachments` — a git-ignored directory (see repo `.gitignore`) that
|
|
9
|
+
* therefore never appears in a committed vendor tree. Widening the read path's
|
|
10
|
+
* skip set this way is deliberate and behavior-preserving: no existing manifest
|
|
11
|
+
* changes, and the read and write paths share this single set so they cannot
|
|
12
|
+
* diverge.
|
|
13
|
+
*/
|
|
14
|
+
/** Root of the mirrored tree inside each repo. Single source for read + write paths. */
|
|
15
|
+
export const VENDOR_DIR = 'vendor'
|
|
16
|
+
|
|
17
|
+
export const MANIFEST_SKIP_DIRS: ReadonlySet<string> = new Set([
|
|
18
|
+
'node_modules',
|
|
19
|
+
'dist',
|
|
20
|
+
'.turbo',
|
|
21
|
+
'.omc',
|
|
22
|
+
'__screenshots__',
|
|
23
|
+
'.vitest-attachments',
|
|
24
|
+
'.output',
|
|
25
|
+
'.source',
|
|
26
|
+
'.nitro',
|
|
27
|
+
'.tanstack',
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
export const MANIFEST_SKIP_FILES: ReadonlySet<string> = new Set(['.sync-manifest.json', '.eslintcache', 'log.txt'])
|
|
31
|
+
|
|
32
|
+
export const MANIFEST_SKIP_SUFFIXES: readonly string[] = ['.tsbuildinfo']
|
|
33
|
+
|
|
34
|
+
/** Whether a directory name should be skipped when walking the vendor tree. */
|
|
35
|
+
export const isSkippedDir = (name: string): boolean => {
|
|
36
|
+
return MANIFEST_SKIP_DIRS.has(name)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Whether a file name should be skipped when walking the vendor tree. */
|
|
40
|
+
export const isSkippedFile = (name: string): boolean => {
|
|
41
|
+
if (MANIFEST_SKIP_FILES.has(name)) {
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return MANIFEST_SKIP_SUFFIXES.some((suffix) => {
|
|
46
|
+
return name.endsWith(suffix)
|
|
47
|
+
})
|
|
48
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { $ } from 'zx'
|
|
4
|
+
|
|
5
|
+
import { OperationError } from 'src/lib/errors/operation-error'
|
|
6
|
+
|
|
7
|
+
import type { VendorConfig } from './config-schema'
|
|
8
|
+
import { expandTilde } from './factory-config'
|
|
9
|
+
import { writeManifest } from './manifest'
|
|
10
|
+
import { VENDOR_DIR } from './skip-sets'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Write-path operations for `vendor sync`/`manifest`/`diff`. This module imports
|
|
14
|
+
* `zx` at the top level and is NEVER re-exported from the read-path barrel
|
|
15
|
+
* (`./index.ts`); `vendor check` never imports it, so its runtime path stays
|
|
16
|
+
* free of `zx`/rsync.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** rsync excludes mirrored from the legacy sync script. */
|
|
20
|
+
const EXCLUDE_PATTERNS = [
|
|
21
|
+
'node_modules',
|
|
22
|
+
'dist',
|
|
23
|
+
'*.tsbuildinfo',
|
|
24
|
+
'.turbo',
|
|
25
|
+
'.eslintcache',
|
|
26
|
+
'.omc',
|
|
27
|
+
'__screenshots__',
|
|
28
|
+
'.vitest-attachments',
|
|
29
|
+
'.output',
|
|
30
|
+
'.source',
|
|
31
|
+
'.nitro',
|
|
32
|
+
'.tanstack',
|
|
33
|
+
'log.txt',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const VENDOR_README = `# vendor/ — mirrored from the source repo
|
|
37
|
+
|
|
38
|
+
**DO NOT EDIT files in this folder here.**
|
|
39
|
+
|
|
40
|
+
Everything under \`vendor/\` is the single source of truth maintained in the source
|
|
41
|
+
repo and copied into this repo by \`infra-kit vendor sync\`. Local edits are
|
|
42
|
+
overwritten on the next sync and will fail \`infra-kit vendor check\` in CI.
|
|
43
|
+
|
|
44
|
+
To change a vendored package, edit it in the source repo and re-run the sync.
|
|
45
|
+
|
|
46
|
+
See \`.sync-manifest.json\` for the source commit and per-file checksums.
|
|
47
|
+
`
|
|
48
|
+
|
|
49
|
+
const excludeFlags = (): string[] => {
|
|
50
|
+
return EXCLUDE_PATTERNS.map((pattern) => {
|
|
51
|
+
return `--exclude=${pattern}`
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Resolve the source repo's current HEAD commit (or `unknown` if unavailable). */
|
|
56
|
+
export const getSourceCommit = async (sourceRoot: string): Promise<string> => {
|
|
57
|
+
try {
|
|
58
|
+
const result = await $({ cwd: sourceRoot })`git rev-parse HEAD`
|
|
59
|
+
|
|
60
|
+
return result.stdout.trim()
|
|
61
|
+
} catch {
|
|
62
|
+
return 'unknown'
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** rsync a directory source→target (target is replaced to avoid nested copies). */
|
|
67
|
+
export const copyDirectory = async (source: string, target: string): Promise<boolean> => {
|
|
68
|
+
if (!existsSync(source)) {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (existsSync(target)) {
|
|
73
|
+
await $`rm -rf ${target}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sourceSlash = `${source}/`
|
|
77
|
+
const targetSlash = `${target}/`
|
|
78
|
+
|
|
79
|
+
await $`rsync -a ${excludeFlags()} ${sourceSlash} ${targetSlash}`
|
|
80
|
+
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Copy a single file source→target, creating the target dir as needed. */
|
|
85
|
+
export const copyFile = async (source: string, target: string): Promise<boolean> => {
|
|
86
|
+
if (!existsSync(source)) {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const targetDir = path.dirname(target)
|
|
91
|
+
|
|
92
|
+
if (!existsSync(targetDir)) {
|
|
93
|
+
mkdirSync(targetDir, { recursive: true })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await $`cp ${source} ${target}`
|
|
97
|
+
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Dry-run rsync reporting whether `target` has drifted from `source`. Returns the
|
|
103
|
+
* itemized change list (empty = in sync). `--delete` surfaces consumer-only files.
|
|
104
|
+
*/
|
|
105
|
+
export const diffDirectory = async (source: string, target: string): Promise<string[]> => {
|
|
106
|
+
if (!existsSync(source)) {
|
|
107
|
+
return []
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!existsSync(target)) {
|
|
111
|
+
return [`missing target: ${target}`]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sourceSlash = `${source}/`
|
|
115
|
+
const targetSlash = `${target}/`
|
|
116
|
+
const result = await $`rsync -ai --dry-run --delete ${excludeFlags()} ${sourceSlash} ${targetSlash}`
|
|
117
|
+
|
|
118
|
+
return result.stdout
|
|
119
|
+
.split('\n')
|
|
120
|
+
.map((line) => {
|
|
121
|
+
return line.trim()
|
|
122
|
+
})
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Write `vendor/README.md` + `vendor/.sync-manifest.json` for a target repo,
|
|
128
|
+
* mirroring the legacy `writeVendorMeta`. The manifest `files` map matches legacy
|
|
129
|
+
* output; it differs only by the added `schemaVersion` and the `syncedAt` stamp.
|
|
130
|
+
*/
|
|
131
|
+
export const writeVendorMeta = (targetRoot: string, meta: { source: string; commit: string }): void => {
|
|
132
|
+
const vendorRoot = path.join(targetRoot, VENDOR_DIR)
|
|
133
|
+
|
|
134
|
+
if (!existsSync(vendorRoot)) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
writeFileSync(path.join(vendorRoot, 'README.md'), VENDOR_README)
|
|
139
|
+
writeManifest(vendorRoot, meta)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Resolve a target repo name to its absolute root under the machine-local
|
|
144
|
+
* `workspaceDir` (from `~/.infra-kit/vendor.json`). Replaces the old
|
|
145
|
+
* "siblings of the source repo" assumption — the working dir is now explicit.
|
|
146
|
+
*/
|
|
147
|
+
export const resolveTargetRoot = (workspaceDir: string, repo: string): string => {
|
|
148
|
+
return path.join(expandTilde(workspaceDir), repo)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Narrow the configured target list to an optional `--repos` allowlist. */
|
|
152
|
+
export const selectTargets = (allTargets: string[], repos?: string[]): string[] => {
|
|
153
|
+
if (!repos) {
|
|
154
|
+
return allTargets
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return allTargets.filter((repo) => {
|
|
158
|
+
return repos.includes(repo)
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface VendorSyncResult {
|
|
163
|
+
source: string
|
|
164
|
+
commit: string
|
|
165
|
+
repos: { repo: string; copied: number; total: number; skipped: boolean }[]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Defense-in-depth path-traversal guard: throw if `child` resolves outside
|
|
170
|
+
* `root`. The schema's `safeRelPath` already rejects `..`/absolute segments at
|
|
171
|
+
* load time; this re-checks the resolved absolute paths so a symlink or odd join
|
|
172
|
+
* can never escape the source/target repo root.
|
|
173
|
+
*/
|
|
174
|
+
const assertContained = (root: string, child: string, label: string, rel: string, itemName: string): void => {
|
|
175
|
+
const resolvedRoot = path.resolve(root)
|
|
176
|
+
const resolved = path.resolve(child)
|
|
177
|
+
|
|
178
|
+
if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + path.sep)) {
|
|
179
|
+
throw new OperationError(undefined, {
|
|
180
|
+
operation: `sync vendor item ${itemName}`,
|
|
181
|
+
stderrExcerpt: `${label} escapes repo root: ${rel}`,
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Copy every configured item into each existing target, then write its manifest. */
|
|
187
|
+
export const runSync = async (
|
|
188
|
+
config: VendorConfig,
|
|
189
|
+
sourceRoot: string,
|
|
190
|
+
workspaceDir: string,
|
|
191
|
+
targets: string[],
|
|
192
|
+
meta: { source: string; commit: string },
|
|
193
|
+
): Promise<VendorSyncResult> => {
|
|
194
|
+
const repos: VendorSyncResult['repos'] = []
|
|
195
|
+
|
|
196
|
+
for (const repo of targets) {
|
|
197
|
+
const targetRoot = resolveTargetRoot(workspaceDir, repo)
|
|
198
|
+
|
|
199
|
+
if (!existsSync(targetRoot)) {
|
|
200
|
+
repos.push({ repo, copied: 0, total: config.copy.length, skipped: true })
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let copied = 0
|
|
205
|
+
|
|
206
|
+
for (const item of config.copy) {
|
|
207
|
+
const source = path.join(sourceRoot, item.source)
|
|
208
|
+
const target = path.join(targetRoot, item.target)
|
|
209
|
+
|
|
210
|
+
assertContained(sourceRoot, source, 'source', item.source, item.name)
|
|
211
|
+
assertContained(targetRoot, target, 'target', item.target, item.name)
|
|
212
|
+
|
|
213
|
+
const ok = item.type === 'directory' ? await copyDirectory(source, target) : await copyFile(source, target)
|
|
214
|
+
|
|
215
|
+
if (ok) {
|
|
216
|
+
copied++
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
writeVendorMeta(targetRoot, meta)
|
|
221
|
+
repos.push({ repo, copied, total: config.copy.length, skipped: false })
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { source: meta.source, commit: meta.commit, repos }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Regenerate the manifest+README in each existing target without copying. Needs
|
|
229
|
+
* neither the source `config` (no `copy[]` involved) nor `sourceRoot` (it writes
|
|
230
|
+
* only into targets) — just where the targets live and which ones.
|
|
231
|
+
*/
|
|
232
|
+
export const runManifest = async (
|
|
233
|
+
workspaceDir: string,
|
|
234
|
+
targets: string[],
|
|
235
|
+
meta: { source: string; commit: string },
|
|
236
|
+
): Promise<{ repo: string; skipped: boolean }[]> => {
|
|
237
|
+
const repos: { repo: string; skipped: boolean }[] = []
|
|
238
|
+
|
|
239
|
+
for (const repo of targets) {
|
|
240
|
+
const targetRoot = resolveTargetRoot(workspaceDir, repo)
|
|
241
|
+
const skipped = !existsSync(targetRoot)
|
|
242
|
+
|
|
243
|
+
if (!skipped) {
|
|
244
|
+
writeVendorMeta(targetRoot, meta)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
repos.push({ repo, skipped })
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return repos
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface VendorDiffEntry {
|
|
254
|
+
repo: string
|
|
255
|
+
target: string
|
|
256
|
+
changes: string[]
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Source-aware drift across every `vendored:true` item in each target. */
|
|
260
|
+
export const runDiff = async (
|
|
261
|
+
config: VendorConfig,
|
|
262
|
+
sourceRoot: string,
|
|
263
|
+
workspaceDir: string,
|
|
264
|
+
targets: string[],
|
|
265
|
+
): Promise<VendorDiffEntry[]> => {
|
|
266
|
+
const entries: VendorDiffEntry[] = []
|
|
267
|
+
const vendoredItems = config.copy.filter((item) => {
|
|
268
|
+
return item.vendored === true
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
for (const repo of targets) {
|
|
272
|
+
const targetRoot = resolveTargetRoot(workspaceDir, repo)
|
|
273
|
+
|
|
274
|
+
if (!existsSync(targetRoot)) {
|
|
275
|
+
continue
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
for (const item of vendoredItems) {
|
|
279
|
+
const source = path.join(sourceRoot, item.source)
|
|
280
|
+
const target = path.join(targetRoot, item.target)
|
|
281
|
+
const changes = await diffDirectory(source, target)
|
|
282
|
+
|
|
283
|
+
if (changes.length > 0) {
|
|
284
|
+
entries.push({ repo, target: item.target, changes })
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return entries
|
|
290
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readdirSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { isSkippedDir, isSkippedFile } from './skip-sets'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Convert a (possibly Windows) relative path to a POSIX-normalized form so the
|
|
8
|
+
* manifest is byte-identical across operating systems.
|
|
9
|
+
*/
|
|
10
|
+
const toPosix = (relativePath: string): string => {
|
|
11
|
+
return relativePath.split(path.sep).join('/')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Walk a `vendor/` tree and return the relative paths of every non-skipped file,
|
|
16
|
+
* POSIX-normalized and sorted byte-wise. The sort makes the output independent
|
|
17
|
+
* of `readdirSync` order, which keeps generated manifests deterministic.
|
|
18
|
+
*
|
|
19
|
+
* Symlink handling matches the legacy scripts: only REAL directories
|
|
20
|
+
* (`entry.isDirectory()`) are recursed; a symlink is treated as a leaf and its
|
|
21
|
+
* relative path is emitted, to be hashed later by following it to target
|
|
22
|
+
* content (`sha256`/`readFileSync`). This preserves legacy behavior exactly —
|
|
23
|
+
* the manifest checksum reflects target content, never the link itself.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* walkVendorTree('/repo/vendor')
|
|
27
|
+
* // => ['README.md', 'configs/eslint-config/index.js', ...]
|
|
28
|
+
*/
|
|
29
|
+
export const walkVendorTree = (vendorRoot: string): string[] => {
|
|
30
|
+
const acc: string[] = []
|
|
31
|
+
|
|
32
|
+
const walk = (dir: string): void => {
|
|
33
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
34
|
+
const full = path.join(dir, entry.name)
|
|
35
|
+
|
|
36
|
+
if (entry.isDirectory()) {
|
|
37
|
+
if (isSkippedDir(entry.name)) {
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
walk(full)
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (isSkippedFile(entry.name)) {
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
acc.push(toPosix(path.relative(vendorRoot, full)))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
walk(vendorRoot)
|
|
54
|
+
|
|
55
|
+
return acc.sort()
|
|
56
|
+
}
|
package/src/mcp/tools/index.ts
CHANGED
|
@@ -1,50 +1,13 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { envClearMcpTool } from 'src/commands/env-clear'
|
|
5
|
-
import { envListMcpTool } from 'src/commands/env-list'
|
|
6
|
-
import { envLoadMcpTool } from 'src/commands/env-load'
|
|
7
|
-
import { envStatusMcpTool } from 'src/commands/env-status'
|
|
8
|
-
import { ghMergeDevMcpTool } from 'src/commands/gh-merge-dev'
|
|
9
|
-
import { ghReleaseDeliverMcpTool } from 'src/commands/gh-release-deliver'
|
|
10
|
-
import { ghReleaseDeployAllMcpTool } from 'src/commands/gh-release-deploy-all'
|
|
11
|
-
import { ghReleaseDeploySelectedMcpTool } from 'src/commands/gh-release-deploy-selected'
|
|
12
|
-
import { ghReleaseListMcpTool } from 'src/commands/gh-release-list'
|
|
13
|
-
import { releaseCreateMcpTool } from 'src/commands/release-create'
|
|
14
|
-
import { releaseDescEditMcpTool } from 'src/commands/release-desc-edit'
|
|
15
|
-
import { versionMcpTool } from 'src/commands/version'
|
|
16
|
-
import { worktreesAddMcpTool } from 'src/commands/worktrees-add'
|
|
17
|
-
import { worktreesListMcpTool } from 'src/commands/worktrees-list'
|
|
18
|
-
import { worktreesOpenMcpTool } from 'src/commands/worktrees-open'
|
|
19
|
-
import { worktreesReloadMcpTool } from 'src/commands/worktrees-reload'
|
|
20
|
-
import { worktreesRemoveMcpTool } from 'src/commands/worktrees-remove'
|
|
21
|
-
import { worktreesSyncMcpTool } from 'src/commands/worktrees-sync'
|
|
3
|
+
import { getExposedMcpTools } from 'src/lib/command-catalog'
|
|
22
4
|
import { createToolHandler } from 'src/lib/tool-handler'
|
|
23
5
|
|
|
24
|
-
const tools = [
|
|
25
|
-
envStatusMcpTool,
|
|
26
|
-
envListMcpTool,
|
|
27
|
-
envLoadMcpTool,
|
|
28
|
-
envClearMcpTool,
|
|
29
|
-
ghMergeDevMcpTool,
|
|
30
|
-
releaseCreateMcpTool,
|
|
31
|
-
releaseDescEditMcpTool,
|
|
32
|
-
ghReleaseDeliverMcpTool,
|
|
33
|
-
ghReleaseDeployAllMcpTool,
|
|
34
|
-
ghReleaseDeploySelectedMcpTool,
|
|
35
|
-
ghReleaseListMcpTool,
|
|
36
|
-
auditMcpTool,
|
|
37
|
-
versionMcpTool,
|
|
38
|
-
worktreesAddMcpTool,
|
|
39
|
-
worktreesListMcpTool,
|
|
40
|
-
worktreesOpenMcpTool,
|
|
41
|
-
worktreesReloadMcpTool,
|
|
42
|
-
worktreesRemoveMcpTool,
|
|
43
|
-
worktreesSyncMcpTool,
|
|
44
|
-
]
|
|
45
|
-
|
|
46
6
|
export const initializeTools = async (server: McpServer) => {
|
|
47
|
-
|
|
7
|
+
// The registered tool set is derived from the single command catalog, filtered
|
|
8
|
+
// by its explicit `mcpExposed` allowlist. doctor / vendor-sync / vendor-manifest
|
|
9
|
+
// are intentionally excluded there and must never be registered here.
|
|
10
|
+
for (const tool of getExposedMcpTools()) {
|
|
48
11
|
server.registerTool(
|
|
49
12
|
tool.name,
|
|
50
13
|
{
|
package/src/sanity.test.tsx
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
// Guard the "no React on machine paths" invariant at the source level (the
|
|
7
|
+
// ESLint no-restricted-imports rule enforces it during lint; this asserts it in
|
|
8
|
+
// the test suite too, and documents the boundary). Only STATIC imports are
|
|
9
|
+
// forbidden — the TTY branch reaches the TUI via a dynamic `import('src/tui/boot')`.
|
|
10
|
+
|
|
11
|
+
const SRC_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
|
|
12
|
+
|
|
13
|
+
const MACHINE_PATH_FILES = ['entry/cli.ts', 'entry/mcp.ts']
|
|
14
|
+
|
|
15
|
+
/** Extract the module specifier from each `import ... from '<spec>'` line. */
|
|
16
|
+
const staticImportSpecifiers = (source: string): string[] => {
|
|
17
|
+
return source.split('\n').flatMap((line) => {
|
|
18
|
+
const match = /^\s*import\s.+from\s+['"]([^'"]+)['"]/.exec(line)
|
|
19
|
+
|
|
20
|
+
return match?.[1] ? [match[1]] : []
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A specifier that would pull React/Ink onto a machine path if imported statically. */
|
|
25
|
+
const isForbidden = (specifier: string): boolean => {
|
|
26
|
+
if (specifier === 'ink' || specifier === 'react' || specifier.startsWith('react/')) {
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
specifier === 'tui' || specifier.endsWith('/tui') || specifier.includes('/tui/') || specifier.startsWith('src/tui')
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('no-React boundary', () => {
|
|
36
|
+
it.each(MACHINE_PATH_FILES)('%s does not statically import ink/react/tui', (relative) => {
|
|
37
|
+
const source = fs.readFileSync(path.join(SRC_DIR, relative), 'utf-8')
|
|
38
|
+
|
|
39
|
+
for (const specifier of staticImportSpecifiers(source)) {
|
|
40
|
+
expect(isForbidden(specifier), `forbidden static import in ${relative}: ${specifier}`).toBe(false)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('entry/cli.ts reaches the TUI only via a dynamic import', () => {
|
|
45
|
+
const source = fs.readFileSync(path.join(SRC_DIR, 'entry/cli.ts'), 'utf-8')
|
|
46
|
+
|
|
47
|
+
// The dynamic import is how the palette is loaded; it must be present and
|
|
48
|
+
// must be a dynamic import() call, never a static `import ... from`.
|
|
49
|
+
expect(source).toMatch(/import\(\s*['"]src\/tui\/boot['"]\s*\)/)
|
|
50
|
+
})
|
|
51
|
+
})
|