@indigoai-us/hq-cli 5.49.0 → 5.50.1
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/dist/commands/mcp-registration.d.ts +905 -0
- package/dist/commands/mcp-registration.js +2001 -0
- package/dist/commands/mcp-status.d.ts +130 -0
- package/dist/commands/mcp-status.js +406 -0
- package/dist/commands/pack-install.d.ts +62 -0
- package/dist/commands/pack-install.js +422 -14
- package/dist/commands/packs.js +28 -4
- package/dist/commands/pkg-install.js +5 -2
- package/dist/index.js +20 -3
- package/dist/types.d.ts +8 -1
- package/dist/utils/contribution-table.d.ts +103 -0
- package/dist/utils/contribution-table.js +65 -0
- package/dist/utils/environmental-error.d.ts +10 -0
- package/dist/utils/environmental-error.js +40 -0
- package/dist/utils/pack-contributions.d.ts +86 -10
- package/dist/utils/pack-contributions.js +130 -48
- package/dist/utils/secrets-cache.d.ts +9 -0
- package/dist/utils/secrets-cache.js +24 -2
- package/package.json +3 -2
- package/scripts/generate-scan-packages-table.mjs +113 -0
- package/src/commands/mcp-registration.test.ts +2787 -0
- package/src/commands/mcp-registration.ts +2612 -0
- package/src/commands/mcp-status.test.ts +483 -0
- package/src/commands/mcp-status.ts +575 -0
- package/src/commands/mcp-status.us011.test.ts +243 -0
- package/src/commands/pack-install.test.ts +589 -0
- package/src/commands/pack-install.ts +497 -13
- package/src/commands/packs.ts +26 -1
- package/src/commands/pkg-install.ts +4 -1
- package/src/index.ts +18 -1
- package/src/types.ts +9 -8
- package/src/utils/contribution-table.ts +83 -0
- package/src/utils/environmental-error.test.ts +45 -0
- package/src/utils/environmental-error.ts +39 -0
- package/src/utils/pack-contributions.test.ts +257 -25
- package/src/utils/pack-contributions.ts +177 -47
- package/src/utils/secrets-cache.ts +22 -0
- package/test/e2e/smoke-install-mcp.sh +113 -0
- package/test/fixtures/hq-pack-smoke-mcp/mcp/smoke-http.json +1 -0
- package/test/fixtures/hq-pack-smoke-mcp/package.yaml +11 -0
|
@@ -1,83 +1,135 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Content-pack contribution helpers -- the
|
|
3
|
-
* the `contributes.* -> host
|
|
2
|
+
* Content-pack contribution helpers -- the SINGLE SOURCE OF TRUTH (US-003) for
|
|
3
|
+
* the `contributes.* -> host` mapping that `hq install` wires via
|
|
4
4
|
* `core/scripts/scan-packages.sh`.
|
|
5
5
|
*
|
|
6
6
|
* `pack-install.ts` only INSTALLS content packs (into `core/packages/<name>/`,
|
|
7
7
|
* tracked by filesystem presence -- there is no registry file). The list /
|
|
8
8
|
* update / uninstall lifecycle in `commands/packs.ts` needs to reason about the
|
|
9
9
|
* SAME mapping so it can report link health and cleanly un-wire a pack without
|
|
10
|
-
* leaving dangling symlinks.
|
|
10
|
+
* leaving dangling symlinks.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Historically that mapping was RESTATED in five places (the scan-packages.sh
|
|
13
|
+
* bash `case`, pack-install.ts:validateManifest's `subpaths` record,
|
|
14
|
+
* `linkFor`'s switch, `contributionLinks`, and the `PackContributeKey` union),
|
|
15
|
+
* drift-guarded only by a substring check. US-003 collapses it to ONE
|
|
16
|
+
* declarative table -- `CONTRIBUTION_TABLE` below -- with a row per key
|
|
17
|
+
* `{ payload, host, wire }`:
|
|
14
18
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
19
|
+
* - `PackContributeKey` is DERIVED from the table keys (see types.ts).
|
|
20
|
+
* - `linkFor` / `contributionLinks` / the `subpaths` validator READ the table.
|
|
21
|
+
* - `core/scripts/scan-packages.sh` reads a generated data block emitted from
|
|
22
|
+
* this same table (regenerate via `scripts/generate-scan-packages-table.mjs`).
|
|
23
|
+
*
|
|
24
|
+
* A parity test (`pack-contributions.test.ts`) asserts FULL key-set +
|
|
25
|
+
* payload-suffix + wire-mode equivalence across every surface, so the copies
|
|
26
|
+
* cannot drift.
|
|
27
|
+
*
|
|
28
|
+
* `wire` discriminates HOW a contribution reaches the host:
|
|
29
|
+
* - `symlink`: a single `ln -s` into a well-known host directory (every
|
|
30
|
+
* contribution type that shipped before MCP).
|
|
31
|
+
* - `merge`: the contribution is MERGED into a shared host config rather than
|
|
32
|
+
* symlinked (the `mcp` row -- registered into the Claude + Codex agent
|
|
33
|
+
* configs). The merge WIRING is delivered in US-004/US-005; US-003 only
|
|
34
|
+
* DECLARES the row as data so the table is the single source and the parity
|
|
35
|
+
* test guards the real invariant. `symlink`-only readers (linkFor,
|
|
36
|
+
* contributionLinks, the symlink validator, scan-packages) must SKIP
|
|
37
|
+
* `merge` rows -- they never produce a symlink.
|
|
18
38
|
*/
|
|
19
39
|
|
|
20
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
40
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="6cc0fc56-5a1c-5ce8-96d4-652805e508f8")}catch(e){}}();
|
|
21
41
|
import * as fs from 'fs';
|
|
22
42
|
import * as path from 'path';
|
|
23
43
|
import * as yaml from 'js-yaml';
|
|
44
|
+
import { CONTRIBUTION_TABLE, payloadFor } from './contribution-table.js';
|
|
45
|
+
import { unregisterMcpServers, } from '../commands/mcp-registration.js';
|
|
46
|
+
// NOTE on dependency direction: this module imports from ../commands/mcp-registration.js
|
|
47
|
+
// to un-register `wire:merge` (mcp) servers on uninstall. mcp-registration.ts imports
|
|
48
|
+
// ONLY fs/os/path/smol-toml (it does NOT import from pack-contributions.ts), so there
|
|
49
|
+
// is NO import cycle. If that ever changes, invert via a callback injected by packs.ts.
|
|
50
|
+
export { CONTRIBUTION_TABLE };
|
|
24
51
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
52
|
+
* Where a contributes key is ROUTED (US-005). Reads the single-source table's
|
|
53
|
+
* `wire` field — `'symlink'` keys go through `linkFor`/`contributionLinks` (a
|
|
54
|
+
* host `ln -s`); `'merge'` keys (e.g. `mcp`) go through the `registerMcpServers`
|
|
55
|
+
* seam in `commands/mcp-registration.ts` and are NEVER symlinked.
|
|
56
|
+
*
|
|
57
|
+
* This is the explicit, code-enforced discriminator behind the invariant that
|
|
58
|
+
* merge keys never reach the symlink path: callers fan out on the return value
|
|
59
|
+
* rather than relying on `linkFor` happening to return `null`.
|
|
27
60
|
*/
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
break;
|
|
48
|
-
case 'hooks':
|
|
49
|
-
srcRel = path.join('hooks', `${item}.sh`);
|
|
50
|
-
dstRel = path.join('.claude', 'hooks', `${item}.sh`);
|
|
51
|
-
break;
|
|
52
|
-
case 'policies':
|
|
53
|
-
srcRel = path.join('policies', `${item}.md`);
|
|
54
|
-
dstRel = path.join('core', 'policies', `${item}.md`);
|
|
55
|
-
break;
|
|
56
|
-
case 'scripts':
|
|
57
|
-
srcRel = path.join('scripts', item);
|
|
58
|
-
dstRel = path.join('core', 'scripts', item);
|
|
59
|
-
break;
|
|
61
|
+
export function routeContribution(key) {
|
|
62
|
+
return CONTRIBUTION_TABLE[key].wire;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The subset of a pack's declared `contributes` keys that are `wire: 'merge'`
|
|
66
|
+
* (e.g. `mcp`) — the keys that MUST be routed to `registerMcpServers` (US-006)
|
|
67
|
+
* and MUST NOT be symlinked. Unknown keys and non-array/empty values are
|
|
68
|
+
* ignored, mirroring `contributionLinks`. Returned in `contributes` iteration
|
|
69
|
+
* order, de-duplicated.
|
|
70
|
+
*/
|
|
71
|
+
export function mergeKeys(contributes) {
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const [key, items] of Object.entries(contributes)) {
|
|
74
|
+
if (!(key in CONTRIBUTION_TABLE))
|
|
75
|
+
continue; // unknown key -> ignore
|
|
76
|
+
if (!Array.isArray(items) || items.length === 0)
|
|
77
|
+
continue;
|
|
78
|
+
if (routeContribution(key) === 'merge' && !out.includes(key))
|
|
79
|
+
out.push(key);
|
|
60
80
|
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Map one SYMLINK contributes entry to its source/host paths, READING the
|
|
85
|
+
* single-source `CONTRIBUTION_TABLE` (US-003). The src is the expanded pack
|
|
86
|
+
* payload; the dst is `<host-dir>/<payload-basename>` so per-key suffixes
|
|
87
|
+
* (`.md`, `.sh`, none) carry through.
|
|
88
|
+
*
|
|
89
|
+
* Returns `null` for a `wire: 'merge'` key (e.g. `mcp`): a merge contribution
|
|
90
|
+
* is not a symlink and has no `WiredLink`. Callers that only handle symlinks
|
|
91
|
+
* (contributionLinks/unwirePack) skip those keys.
|
|
92
|
+
*/
|
|
93
|
+
function linkFor(hqRoot, packDir, key, item) {
|
|
94
|
+
const row = CONTRIBUTION_TABLE[key];
|
|
95
|
+
if (row.wire !== 'symlink')
|
|
96
|
+
return null;
|
|
97
|
+
const payloadRel = payloadFor(key, item); // e.g. commands/foo.md
|
|
98
|
+
// dst keeps the payload's basename (so the suffix carries through) under the
|
|
99
|
+
// host directory declared in the table.
|
|
100
|
+
const dstRel = path.join(row.host, path.basename(payloadRel));
|
|
61
101
|
return {
|
|
62
102
|
key,
|
|
63
103
|
item,
|
|
64
|
-
src: path.join(packDir,
|
|
104
|
+
src: path.join(packDir, payloadRel),
|
|
65
105
|
dst: path.join(hqRoot, dstRel),
|
|
66
106
|
};
|
|
67
107
|
}
|
|
68
108
|
/**
|
|
69
|
-
* Every
|
|
70
|
-
* non-array values are ignored, mirroring scan-packages.sh.
|
|
109
|
+
* Every SYMLINK a pack's `contributes` block declares. Empty subfields and
|
|
110
|
+
* non-array values are ignored, mirroring scan-packages.sh. `wire: 'merge'`
|
|
111
|
+
* keys (e.g. `mcp`) produce no symlink and are skipped here -- their host
|
|
112
|
+
* effect (a config merge) is handled separately (US-004/US-005).
|
|
71
113
|
*/
|
|
72
114
|
export function contributionLinks(hqRoot, packDir, contributes) {
|
|
73
115
|
const links = [];
|
|
74
116
|
for (const [key, items] of Object.entries(contributes)) {
|
|
117
|
+
if (!(key in CONTRIBUTION_TABLE))
|
|
118
|
+
continue; // unknown key -> ignore
|
|
75
119
|
if (!Array.isArray(items))
|
|
76
120
|
continue;
|
|
121
|
+
// INVARIANT (US-005): merge keys are NEVER symlinked. They route to
|
|
122
|
+
// registerMcpServers (commands/mcp-registration.ts) instead. Skip them
|
|
123
|
+
// explicitly here -- not just via linkFor returning null -- so the symlink
|
|
124
|
+
// path provably excludes them.
|
|
125
|
+
if (routeContribution(key) === 'merge')
|
|
126
|
+
continue;
|
|
77
127
|
for (const item of items) {
|
|
78
128
|
if (typeof item !== 'string' || item.length === 0)
|
|
79
129
|
continue;
|
|
80
|
-
|
|
130
|
+
const link = linkFor(hqRoot, packDir, key, item);
|
|
131
|
+
if (link)
|
|
132
|
+
links.push(link); // null for merge keys -- skip
|
|
81
133
|
}
|
|
82
134
|
}
|
|
83
135
|
return links;
|
|
@@ -196,6 +248,36 @@ export function unwirePack(hqRoot, packDir, contributes) {
|
|
|
196
248
|
}
|
|
197
249
|
return result;
|
|
198
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Un-register the `wire: 'merge'` (MCP) servers a pack declares — the parallel to
|
|
253
|
+
* {@link unwirePack}'s symlink removal. For each name in `mergeKeys(contributes)` →
|
|
254
|
+
* `contributes.<mergeKey>` (today only `mcp`), this delegates to
|
|
255
|
+
* {@link unregisterMcpServers}, which removes ONLY entries stamped with THIS pack's
|
|
256
|
+
* `_hqPack` provenance and SKIPS-AND-WARNS on a foreign/unstamped same-named entry.
|
|
257
|
+
*
|
|
258
|
+
* No-ops cleanly when the pack declares no merge keys (returns `{ servers: [] }`),
|
|
259
|
+
* when the runtime config is absent (Codex-less host → first-class skip; missing
|
|
260
|
+
* ~/.claude.json → `absent`), and on a re-run (already-removed → `absent`). It does
|
|
261
|
+
* NOT throw on a foreign entry — uninstall of one pack must not abort on a server
|
|
262
|
+
* another pack (or the user) owns.
|
|
263
|
+
*
|
|
264
|
+
* @param packName the pack being uninstalled — the provenance to match
|
|
265
|
+
* @param contributes the pack's `contributes` block (its `mcp` list drives removal)
|
|
266
|
+
*/
|
|
267
|
+
export function unwirePackMcp(packName, contributes, options) {
|
|
268
|
+
const servers = [];
|
|
269
|
+
for (const key of mergeKeys(contributes)) {
|
|
270
|
+
const names = (contributes[key] ?? []).filter((n) => typeof n === 'string' && n.length > 0);
|
|
271
|
+
if (names.length === 0)
|
|
272
|
+
continue;
|
|
273
|
+
servers.push(...unregisterMcpServers(packName, names, {
|
|
274
|
+
env: options?.env,
|
|
275
|
+
lock: options?.lock,
|
|
276
|
+
stamp: options?.stamp,
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
return { servers };
|
|
280
|
+
}
|
|
199
281
|
// ---------------------------------------------------------------------------
|
|
200
282
|
// Host introspection: hqVersion + recommended_packages catalog
|
|
201
283
|
// ---------------------------------------------------------------------------
|
|
@@ -236,4 +318,4 @@ export function readRecommendedPackages(hqRoot) {
|
|
|
236
318
|
}
|
|
237
319
|
}
|
|
238
320
|
//# sourceMappingURL=pack-contributions.js.map
|
|
239
|
-
//# debugId=
|
|
321
|
+
//# debugId=6cc0fc56-5a1c-5ce8-96d4-652805e508f8
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
export declare const DEFAULT_SECRETS_CACHE_TTL_MS: number;
|
|
2
2
|
export declare function readCache(companyUid: string, name: string): string | null;
|
|
3
3
|
export declare function writeCache(companyUid: string, name: string, value: string, ttlMs?: number): void;
|
|
4
|
+
/**
|
|
5
|
+
* List the scope UIDs (`cmp_*` / `prs_*` subdirectories) that currently have a
|
|
6
|
+
* secrets-cache directory on disk. Used by offline callers (e.g. install-time MCP
|
|
7
|
+
* registration) that have no `--company` flag and no network token and so cannot
|
|
8
|
+
* resolve a single active company UID up front: they instead probe every cached
|
|
9
|
+
* scope for a given secret name. Returns `[]` when the cache root is absent or
|
|
10
|
+
* unreadable (the desired graceful-deferral behavior — no scopes, no hits).
|
|
11
|
+
*/
|
|
12
|
+
export declare function listSecretCacheScopes(): string[];
|
|
4
13
|
export declare function removeCacheEntry(companyUid: string, name: string): void;
|
|
5
14
|
export declare function clearAllCache(): {
|
|
6
15
|
removed: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="68c3f1d3-dac4-57d6-8e2b-650c10a807ba")}catch(e){}}();
|
|
3
3
|
import * as crypto from "node:crypto";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import * as path from "node:path";
|
|
@@ -140,6 +140,28 @@ export function writeCache(companyUid, name, value, ttlMs = DEFAULT_SECRETS_CACH
|
|
|
140
140
|
// Cache write failure is non-fatal
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* List the scope UIDs (`cmp_*` / `prs_*` subdirectories) that currently have a
|
|
145
|
+
* secrets-cache directory on disk. Used by offline callers (e.g. install-time MCP
|
|
146
|
+
* registration) that have no `--company` flag and no network token and so cannot
|
|
147
|
+
* resolve a single active company UID up front: they instead probe every cached
|
|
148
|
+
* scope for a given secret name. Returns `[]` when the cache root is absent or
|
|
149
|
+
* unreadable (the desired graceful-deferral behavior — no scopes, no hits).
|
|
150
|
+
*/
|
|
151
|
+
export function listSecretCacheScopes() {
|
|
152
|
+
try {
|
|
153
|
+
return fs
|
|
154
|
+
.readdirSync(CACHE_DIR, { withFileTypes: true })
|
|
155
|
+
.filter((e) => e.isDirectory())
|
|
156
|
+
.map((e) => e.name)
|
|
157
|
+
// Only real entity scopes (cmp_*/prs_*); validateInputs in readCache also
|
|
158
|
+
// rejects anything with `/` or `..`, so this is belt-and-suspenders.
|
|
159
|
+
.filter((name) => !name.startsWith("."));
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
143
165
|
export function removeCacheEntry(companyUid, name) {
|
|
144
166
|
if (!validateInputs(companyUid, name))
|
|
145
167
|
return;
|
|
@@ -163,4 +185,4 @@ export function clearAllCache() {
|
|
|
163
185
|
return { removed };
|
|
164
186
|
}
|
|
165
187
|
//# sourceMappingURL=secrets-cache.js.map
|
|
166
|
-
//# debugId=
|
|
188
|
+
//# debugId=68c3f1d3-dac4-57d6-8e2b-650c10a807ba
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.50.1",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"clean": "rm -rf dist"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@indigoai-us/hq-cloud": "^6.
|
|
21
|
+
"@indigoai-us/hq-cloud": "^6.12.1",
|
|
22
22
|
"@indigoai-us/hq-onboarding": "^0.1.0",
|
|
23
23
|
"@sentry/node": "^10.49.0",
|
|
24
24
|
"chalk": "^5.3.0",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"open": "^10.1.0",
|
|
28
28
|
"semver": "^7.6.3",
|
|
29
29
|
"simple-git": "^3.27.0",
|
|
30
|
+
"smol-toml": "1.6.1",
|
|
30
31
|
"varlock": "1.0.0"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* generate-scan-packages-table.mjs (US-003)
|
|
4
|
+
*
|
|
5
|
+
* Emits the bash data block that `core/scripts/scan-packages.sh` reads, from the
|
|
6
|
+
* single-source `src/utils/contribution-table.ts`. This keeps the bash wirer
|
|
7
|
+
* GENERATED FROM the table rather than restating the mapping -- the US-003
|
|
8
|
+
* single-source-of-truth invariant.
|
|
9
|
+
*
|
|
10
|
+
* The TS table is the authority; this script transcribes it to bash. It does
|
|
11
|
+
* NOT parse TypeScript -- it imports the same literal data by re-declaring the
|
|
12
|
+
* rows is avoided by reading the table via a tiny regex extraction of the
|
|
13
|
+
* `CONTRIBUTION_TABLE` object literal, so the table file stays the only place
|
|
14
|
+
* the rows live.
|
|
15
|
+
*
|
|
16
|
+
* Output: a bash snippet (to stdout, or written in place into scan-packages.sh
|
|
17
|
+
* between the BEGIN/END GENERATED markers when --write <path> is passed).
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* node scripts/generate-scan-packages-table.mjs # print block
|
|
21
|
+
* node scripts/generate-scan-packages-table.mjs --check <path> # verify in-sync
|
|
22
|
+
* node scripts/generate-scan-packages-table.mjs --write <path> # rewrite in place
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import * as fs from 'fs';
|
|
26
|
+
import * as path from 'path';
|
|
27
|
+
import { fileURLToPath } from 'url';
|
|
28
|
+
|
|
29
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const TABLE_TS = path.resolve(__dirname, '../src/utils/contribution-table.ts');
|
|
31
|
+
|
|
32
|
+
const BEGIN = '# >>> BEGIN GENERATED contribution table (US-003) — do not edit by hand';
|
|
33
|
+
const END = '# <<< END GENERATED contribution table';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extract the rows from the `CONTRIBUTION_TABLE` object literal in the TS file.
|
|
37
|
+
* Each row line looks like:
|
|
38
|
+
* key: { payload: 'p/{item}', host: 'h', wire: 'symlink' },
|
|
39
|
+
*/
|
|
40
|
+
function readTable() {
|
|
41
|
+
const ts = fs.readFileSync(TABLE_TS, 'utf-8');
|
|
42
|
+
const open = ts.indexOf('export const CONTRIBUTION_TABLE');
|
|
43
|
+
if (open < 0) throw new Error('CONTRIBUTION_TABLE not found in ' + TABLE_TS);
|
|
44
|
+
const body = ts.slice(open, ts.indexOf('} as const', open));
|
|
45
|
+
const rowRe =
|
|
46
|
+
/^\s*([A-Za-z_][A-Za-z0-9_]*):\s*\{\s*payload:\s*'([^']+)'\s*,\s*host:\s*'([^']+)'\s*,\s*wire:\s*'([^']+)'\s*\}/gm;
|
|
47
|
+
const rows = [];
|
|
48
|
+
let m;
|
|
49
|
+
while ((m = rowRe.exec(body)) !== null) {
|
|
50
|
+
rows.push({ key: m[1], payload: m[2], host: m[3], wire: m[4] });
|
|
51
|
+
}
|
|
52
|
+
if (rows.length === 0) throw new Error('No rows parsed from CONTRIBUTION_TABLE');
|
|
53
|
+
return rows;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderBlock(rows) {
|
|
57
|
+
const lines = [];
|
|
58
|
+
lines.push(BEGIN);
|
|
59
|
+
lines.push('# Generated from hq-cli/src/utils/contribution-table.ts by');
|
|
60
|
+
lines.push('# hq-cli/scripts/generate-scan-packages-table.mjs. Regenerate with:');
|
|
61
|
+
lines.push('# node scripts/generate-scan-packages-table.mjs --write core/scripts/scan-packages.sh');
|
|
62
|
+
lines.push('# Each row is "payload|host|wire" (payload uses {item} for the name).');
|
|
63
|
+
lines.push('# bash 3.2-compatible (macOS default bash has no associative arrays):');
|
|
64
|
+
lines.push('# a plain indexed key list + a generated case-lookup function.');
|
|
65
|
+
lines.push('CONTRIB_KEYS=(' + rows.map((r) => r.key).join(' ') + ')');
|
|
66
|
+
lines.push('# contrib_row <key> -> echoes "payload|host|wire", empty if unknown.');
|
|
67
|
+
lines.push('contrib_row() {');
|
|
68
|
+
lines.push(' case "$1" in');
|
|
69
|
+
for (const r of rows) {
|
|
70
|
+
lines.push(` ${r.key}) printf '%s' '${r.payload}|${r.host}|${r.wire}' ;;`);
|
|
71
|
+
}
|
|
72
|
+
lines.push(" *) return 0 ;;");
|
|
73
|
+
lines.push(' esac');
|
|
74
|
+
lines.push('}');
|
|
75
|
+
lines.push(END);
|
|
76
|
+
return lines.join('\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function splice(scriptText, block) {
|
|
80
|
+
const b = scriptText.indexOf(BEGIN);
|
|
81
|
+
const e = scriptText.indexOf(END);
|
|
82
|
+
if (b < 0 || e < 0) {
|
|
83
|
+
throw new Error('BEGIN/END GENERATED markers not found in target script');
|
|
84
|
+
}
|
|
85
|
+
const before = scriptText.slice(0, b);
|
|
86
|
+
const after = scriptText.slice(e + END.length);
|
|
87
|
+
return before + block + after;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const rows = readTable();
|
|
91
|
+
const block = renderBlock(rows);
|
|
92
|
+
|
|
93
|
+
const arg = process.argv[2];
|
|
94
|
+
const target = process.argv[3];
|
|
95
|
+
|
|
96
|
+
if (arg === '--write') {
|
|
97
|
+
const text = fs.readFileSync(target, 'utf-8');
|
|
98
|
+
fs.writeFileSync(target, splice(text, block));
|
|
99
|
+
console.error(`Wrote generated contribution table into ${target}`);
|
|
100
|
+
} else if (arg === '--check') {
|
|
101
|
+
const text = fs.readFileSync(target, 'utf-8');
|
|
102
|
+
const expected = splice(text, block);
|
|
103
|
+
if (expected !== text) {
|
|
104
|
+
console.error(
|
|
105
|
+
`OUT OF SYNC: ${target} differs from contribution-table.ts. ` +
|
|
106
|
+
`Run: node scripts/generate-scan-packages-table.mjs --write ${target}`,
|
|
107
|
+
);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
console.error(`${target} is in sync with the contribution table.`);
|
|
111
|
+
} else {
|
|
112
|
+
process.stdout.write(block + '\n');
|
|
113
|
+
}
|