@indigoai-us/hq-cli 5.50.0 → 5.50.2

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.
Files changed (46) hide show
  1. package/dist/commands/mcp-registration.d.ts +905 -0
  2. package/dist/commands/mcp-registration.js +2001 -0
  3. package/dist/commands/mcp-status.d.ts +130 -0
  4. package/dist/commands/mcp-status.js +406 -0
  5. package/dist/commands/onboard-warning.d.ts +7 -0
  6. package/dist/commands/onboard-warning.js +14 -0
  7. package/dist/commands/onboard.js +5 -5
  8. package/dist/commands/pack-install.d.ts +74 -0
  9. package/dist/commands/pack-install.js +493 -14
  10. package/dist/commands/packs.js +42 -4
  11. package/dist/commands/pkg-install.js +5 -2
  12. package/dist/index.js +7 -2
  13. package/dist/types.d.ts +26 -1
  14. package/dist/utils/contribution-table.d.ts +103 -0
  15. package/dist/utils/contribution-table.js +65 -0
  16. package/dist/utils/pack-contributions.d.ts +93 -10
  17. package/dist/utils/pack-contributions.js +140 -48
  18. package/dist/utils/secrets-cache.d.ts +9 -0
  19. package/dist/utils/secrets-cache.js +24 -2
  20. package/dist/utils/version-gate.d.ts +40 -1
  21. package/dist/utils/version-gate.js +91 -20
  22. package/package.json +3 -2
  23. package/scripts/generate-scan-packages-table.mjs +113 -0
  24. package/src/commands/mcp-registration.test.ts +2787 -0
  25. package/src/commands/mcp-registration.ts +2612 -0
  26. package/src/commands/mcp-status.test.ts +483 -0
  27. package/src/commands/mcp-status.ts +575 -0
  28. package/src/commands/mcp-status.us011.test.ts +243 -0
  29. package/src/commands/onboard-warning.test.ts +26 -0
  30. package/src/commands/onboard-warning.ts +12 -0
  31. package/src/commands/onboard.ts +4 -7
  32. package/src/commands/pack-install.test.ts +733 -0
  33. package/src/commands/pack-install.ts +582 -13
  34. package/src/commands/packs.ts +45 -1
  35. package/src/commands/pkg-install.ts +4 -1
  36. package/src/index.ts +6 -0
  37. package/src/types.ts +28 -9
  38. package/src/utils/contribution-table.ts +83 -0
  39. package/src/utils/pack-contributions.test.ts +310 -25
  40. package/src/utils/pack-contributions.ts +194 -47
  41. package/src/utils/secrets-cache.ts +22 -0
  42. package/src/utils/version-gate.test.ts +122 -0
  43. package/src/utils/version-gate.ts +109 -13
  44. package/test/e2e/smoke-install-mcp.sh +113 -0
  45. package/test/fixtures/hq-pack-smoke-mcp/mcp/smoke-http.json +1 -0
  46. package/test/fixtures/hq-pack-smoke-mcp/package.yaml +11 -0
@@ -18,7 +18,7 @@
18
18
  * Spec: knowledge/public/hq-core/package-yaml-spec.md.
19
19
  */
20
20
 
21
- !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]="527d0e7d-2101-5438-8251-2579a40453e6")}catch(e){}}();
21
+ !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]="69746edd-f9d8-528e-8902-de799c58bd8a")}catch(e){}}();
22
22
  import * as fs from 'fs';
23
23
  import * as path from 'path';
24
24
  import * as readline from 'readline';
@@ -27,7 +27,7 @@ import chalk from 'chalk';
27
27
  import semverSatisfies from 'semver/functions/satisfies.js';
28
28
  import { findHqRoot } from '../utils/manifest.js';
29
29
  import { classify, resolveLatest, resolveLatestMarketplace, runScanPackages, installPack, } from './pack-install.js';
30
- import { contributionLinks, linkStatus, listInstalledPacks, readPackManifest, unwirePack, readHqVersion, readRecommendedPackages, packagesDir, } from '../utils/pack-contributions.js';
30
+ import { contributionLinks, linkStatus, listInstalledPacks, findDependentPacks, readPackManifest, unwirePack, unwirePackMcp, readHqVersion, readRecommendedPackages, packagesDir, } from '../utils/pack-contributions.js';
31
31
  function resolveRoot(opts) {
32
32
  return opts.hqRoot ? path.resolve(opts.hqRoot) : findHqRoot();
33
33
  }
@@ -246,6 +246,7 @@ async function runUpdate(name, opts) {
246
246
  try {
247
247
  await installPack(source, {
248
248
  allowHooks: opts.yes || opts.allowHooks,
249
+ allowMcp: opts.yes || opts.allowMcp,
249
250
  followBranch: opts.branch,
250
251
  quiet: wantsJson(opts),
251
252
  });
@@ -277,8 +278,43 @@ async function runUninstall(name, opts) {
277
278
  if (!manifest) {
278
279
  warnings.push('package.yaml unreadable -- host symlinks could not be computed precisely; ran a re-scan to reconcile.');
279
280
  }
281
+ // 0. Dependents guard (M0): refuse to remove a pack that another installed pack
282
+ // lists in its `requires.packs`, unless --force. Filesystem presence is the
283
+ // source of truth (listInstalledPacks), consistent with the install-time
284
+ // assertPackDependencies check. Runs BEFORE any un-wiring so a blocked uninstall
285
+ // leaves the pack fully intact.
286
+ if (!opts.force) {
287
+ const dependents = findDependentPacks(listInstalledPacks(hqRoot), name);
288
+ if (dependents.length > 0) {
289
+ const who = dependents.map((p) => p.manifest?.name ?? p.name).join(', ');
290
+ throw new Error(`Cannot uninstall "${name}": required by ${who}. ` +
291
+ `Uninstall the dependent pack(s) first, or pass --force to override.`);
292
+ }
293
+ }
280
294
  // 1. Un-wire only our symlinks.
281
295
  const { unlinked, skipped } = unwirePack(hqRoot, packDir, contributes);
296
+ // 1b. Un-register the pack's MCP (`wire: 'merge'`) servers — invisible to the
297
+ // symlink unwire above. Provenance-scoped: removes ONLY entries stamped with this
298
+ // pack's `_hqPack`, and skip-and-warns on any foreign/unstamped same-named entry.
299
+ // Tolerant of a Codex-less host / absent config; idempotent on re-run.
300
+ try {
301
+ const mcp = unwirePackMcp(name, contributes);
302
+ for (const server of mcp.servers) {
303
+ if ('skipped' in server.claude)
304
+ continue; // (claude is always inspected; never skipped)
305
+ if (server.claude.outcome === 'skipped-foreign' && server.claude.reason) {
306
+ warnings.push(server.claude.reason);
307
+ }
308
+ if (!('skipped' in server.codex) && server.codex.outcome === 'skipped-foreign' && server.codex.reason) {
309
+ warnings.push(server.codex.reason);
310
+ }
311
+ }
312
+ }
313
+ catch (e) {
314
+ // Never let an MCP un-registration failure abort the rest of the uninstall
315
+ // (symlink unwire already ran; the pack dir still gets archived). Surface it.
316
+ warnings.push(`MCP un-registration encountered an error: ${e.message}`);
317
+ }
282
318
  // 2. Archive (or delete) the pack dir -- BEFORE re-scan so it isn't re-wired.
283
319
  let archived = null;
284
320
  if (opts.archive === false) {
@@ -358,8 +394,9 @@ export function registerPacksCommand(parent) {
358
394
  .option('--json', 'Machine-readable JSON output')
359
395
  .option('--hq-root <path>', 'HQ root (default: auto-detect)')
360
396
  .option('--check-only', 'Report availability without installing')
361
- .option('-y, --yes', 'Non-interactive (implies --allow-hooks)')
397
+ .option('-y, --yes', 'Non-interactive (implies --allow-hooks and --allow-mcp)')
362
398
  .option('--allow-hooks', 'Install pack hooks without prompting')
399
+ .option('--allow-mcp', 'Register pack MCP servers without prompting')
363
400
  .option('--branch', 'Follow the source branch instead of SHA-pinning')
364
401
  .action(async (name, opts) => {
365
402
  try {
@@ -393,6 +430,7 @@ export function registerPacksCommand(parent) {
393
430
  .option('--hq-root <path>', 'HQ root (default: auto-detect)')
394
431
  .option('-y, --yes', 'Skip confirmation')
395
432
  .option('--no-archive', 'Delete instead of archiving')
433
+ .option('--force', 'Uninstall even if other installed packs require this one')
396
434
  .action(async (name, opts) => {
397
435
  try {
398
436
  if (!opts.yes) {
@@ -425,4 +463,4 @@ export function registerPacksCommand(parent) {
425
463
  });
426
464
  }
427
465
  //# sourceMappingURL=packs.js.map
428
- //# debugId=527d0e7d-2101-5438-8251-2579a40453e6
466
+ //# debugId=69746edd-f9d8-528e-8902-de799c58bd8a
@@ -13,7 +13,7 @@
13
13
  * 9. Print next-step message
14
14
  */
15
15
 
16
- !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]="ac6f26b7-5678-5b4e-b0d8-2f5901adf9fe")}catch(e){}}();
16
+ !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]="ccc2e70c-f577-5a47-9a29-31a206194392")}catch(e){}}();
17
17
  import * as fs from 'fs';
18
18
  import * as os from 'os';
19
19
  import * as path from 'path';
@@ -33,12 +33,14 @@ export function registerPackageInstallCommand(parent) {
33
33
  '@scope/name[@ver] (npm pack), git URL[#ref], or local path.')
34
34
  .option('--company <co>', 'Scope the package to a specific company (registry flow only)')
35
35
  .option('--allow-hooks', 'Skip the hooks confirmation prompt (content-pack flow)')
36
+ .option('--allow-mcp', 'Skip the MCP server confirmation prompt (content-pack flow)')
36
37
  .option('--branch', 'Follow a ref instead of SHA-pinning (git content-pack flow)')
37
38
  .action(async (source, opts) => {
38
39
  try {
39
40
  if (sourceMatchesPackPattern(source)) {
40
41
  await installPack(source, {
41
42
  allowHooks: opts.allowHooks,
43
+ allowMcp: opts.allowMcp,
42
44
  followBranch: opts.branch,
43
45
  });
44
46
  }
@@ -49,6 +51,7 @@ export function registerPackageInstallCommand(parent) {
49
51
  // listings transport — the live install path — instead.
50
52
  await installPack(`${MARKETPLACE_PREFIX}${source}`, {
51
53
  allowHooks: opts.allowHooks,
54
+ allowMcp: opts.allowMcp,
52
55
  followBranch: opts.branch,
53
56
  });
54
57
  }
@@ -163,4 +166,4 @@ async function installPackage(slug, company) {
163
166
  }
164
167
  }
165
168
  //# sourceMappingURL=pkg-install.js.map
166
- //# debugId=ac6f26b7-5678-5b4e-b0d8-2f5901adf9fe
169
+ //# debugId=ccc2e70c-f577-5a47-9a29-31a206194392
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * HQ CLI - Module management, package management, and cloud sync for HQ
4
4
  */
5
5
 
6
- !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]="d57c1cc3-6dd3-5c7f-8eef-c7692d41b836")}catch(e){}}();
6
+ !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]="34e9e978-4c0a-5113-82af-9a33e30686c6")}catch(e){}}();
7
7
  import { Command } from "commander";
8
8
  import { initSentry, Sentry } from "./sentry.js";
9
9
  import { registerAddCommand } from "./commands/add.js";
@@ -44,6 +44,7 @@ import { registerSourcesCommand } from "./commands/sources.js";
44
44
  import { registerSignalsCommand } from "./commands/signals.js";
45
45
  import { registerReindexCommand } from "./commands/reindex.js";
46
46
  import { registerRescueCommand } from "./commands/rescue.js";
47
+ import { registerMcpCommand } from "./commands/mcp-status.js";
47
48
  import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
48
49
  import { environmentalFsErrorMessage } from "./utils/environmental-error.js";
49
50
  import { maybeWarnNewVersion, refreshVersionCache, } from "./utils/version-check.js";
@@ -159,6 +160,10 @@ registerReindexCommand(program);
159
160
  // the HQ Sync app's "Update / Restore" pill; drives the same replace-rescue.sh
160
161
  // shipped from @indigoai-us/hq-cloud.
161
162
  registerRescueCommand(program);
163
+ // MCP pack observability (subcommand group — `hq mcp status`). Read-only
164
+ // provenance-based status across BOTH Claude + Codex runtimes (reads `_hqPack`
165
+ // off the configs, NOT linkStatus), with secret-redacted output + `--json`.
166
+ registerMcpCommand(program);
162
167
  (async () => {
163
168
  try {
164
169
  Sentry.addBreadcrumb({
@@ -198,4 +203,4 @@ registerRescueCommand(program);
198
203
  }
199
204
  })();
200
205
  //# sourceMappingURL=index.js.map
201
- //# debugId=d57c1cc3-6dd3-5c7f-8eef-c7692d41b836
206
+ //# debugId=34e9e978-4c0a-5113-82af-9a33e30686c6
package/dist/types.d.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  * (new in hq-core v12.0.0; see
8
8
  * knowledge/public/hq-core/package-yaml-spec.md)
9
9
  */
10
+ import type { ContributionKey } from './utils/contribution-table.js';
10
11
  export type LegacyStrategy = 'link' | 'merge' | 'copy';
11
12
  export type SyncStrategy = LegacyStrategy | 'package';
12
13
  export type AccessLevel = 'public' | 'team' | `role:${string}`;
@@ -56,7 +57,13 @@ export interface SyncResult {
56
57
  message?: string;
57
58
  filesChanged?: number;
58
59
  }
59
- export type PackContributeKey = 'workers' | 'knowledge' | 'skills' | 'commands' | 'hooks' | 'policies' | 'scripts';
60
+ /**
61
+ * The `contributes.*` keys a pack may declare. DERIVED (US-003) from the single
62
+ * declarative contribution registry in `utils/contribution-table.ts` -- adding
63
+ * a contribution type is one row there, and this union updates automatically.
64
+ * Do NOT restate the keys here.
65
+ */
66
+ export type PackContributeKey = ContributionKey;
60
67
  /**
61
68
  * Pack authorship attribution (US-001). OPTIONAL and backwards-compatible —
62
69
  * packs published before this field still validate. When present, install can
@@ -68,13 +75,31 @@ export interface PackAuthor {
68
75
  handle: string;
69
76
  displayName: string;
70
77
  }
78
+ /**
79
+ * A pack-to-pack dependency (M0). OPTIONAL and backwards-compatible — packs
80
+ * published before this field omit `requires.packs` and install unchanged. When
81
+ * present, each entry names another content pack that MUST already be installed
82
+ * before this one (enforced at install time by `assertPackDependencies`, which
83
+ * tracks installed packs by FILESYSTEM PRESENCE — not `modules.yaml`). `version`
84
+ * is an optional semver RANGE the installed dependency must satisfy.
85
+ */
86
+ export interface PackDependency {
87
+ name: string;
88
+ version?: string;
89
+ }
71
90
  export interface PackManifest {
72
91
  name: string;
73
92
  version: string;
74
93
  publisher: string;
75
94
  access: 'public' | 'private';
95
+ /**
96
+ * Host + pack prerequisites. `hqCore` is a required semver RANGE the host HQ
97
+ * must satisfy. `packs` (M0) is an OPTIONAL list of other content packs that
98
+ * must be installed first — see PackDependency.
99
+ */
76
100
  requires: {
77
101
  hqCore: string;
102
+ packs?: PackDependency[];
78
103
  };
79
104
  contributes: Partial<Record<PackContributeKey, string[]>>;
80
105
  description?: string;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The contribution registry -- the SINGLE declarative source of truth (US-003)
3
+ * for the `contributes.* -> host` mapping. Every other surface DERIVES from
4
+ * this table:
5
+ *
6
+ * - `PackContributeKey` (types.ts) = `keyof typeof CONTRIBUTION_TABLE`
7
+ * - `linkFor` / `contributionLinks` read `payload` + `host`
8
+ * - `validateManifest`'s payload check reads `payload`
9
+ * - `core/scripts/scan-packages.sh` reads a data block GENERATED
10
+ * from this table
11
+ * (scripts/generate-scan-packages-table.mjs)
12
+ *
13
+ * Adding a contribution type is ONE row here, not a five-site edit. The parity
14
+ * test (`pack-contributions.test.ts`) asserts every surface agrees on the full
15
+ * key-set, the payload suffix, and the wire mode.
16
+ *
17
+ * Row fields:
18
+ * - `payload`: path INSIDE the pack, with the literal token `{item}` for the
19
+ * declared name. The suffix after `{item}` (e.g. `.md`, `.json`, or none)
20
+ * IS the load-bearing per-key shape.
21
+ * - `host`: for `wire: 'symlink'`, the host DIRECTORY (relative to the HQ
22
+ * root) the symlink is created under -- the symlink dst is
23
+ * `<host>/<expanded-item-basename>`. For `wire: 'merge'`, a non-path
24
+ * SENTINEL describing the merge target (e.g. `merge:claude+codex`); it is
25
+ * NEVER used as a filesystem path.
26
+ * - `wire`: `symlink` (a single `ln -s` into `host`) or `merge` (merged into
27
+ * a shared host config; see US-004/US-005). Symlink-only readers MUST skip
28
+ * `merge` rows.
29
+ *
30
+ * NOTE: the `mcp` row is DECLARED here as data only. US-003 does NOT wire any
31
+ * MCP behavior -- the merge engine lands in US-004/US-005. Declaring it now
32
+ * makes the table the single source so adding the merge wiring is a code change
33
+ * against an already-present row, and the parity test guards it from day one.
34
+ */
35
+ export type WireMode = 'symlink' | 'merge';
36
+ export interface ContributionRow {
37
+ /** Path inside the pack, with `{item}` substituted for the declared name. */
38
+ payload: string;
39
+ /**
40
+ * `wire: 'symlink'` -> host directory the symlink lives under (HQ-root
41
+ * relative). `wire: 'merge'` -> a non-path sentinel for the merge target.
42
+ */
43
+ host: string;
44
+ wire: WireMode;
45
+ }
46
+ /**
47
+ * The 8-key contribution registry. `as const` so `keyof typeof` yields the
48
+ * exact literal key union consumed by `PackContributeKey`.
49
+ */
50
+ export declare const CONTRIBUTION_TABLE: {
51
+ readonly workers: {
52
+ readonly payload: "workers/{item}";
53
+ readonly host: "core/workers/public";
54
+ readonly wire: "symlink";
55
+ };
56
+ readonly knowledge: {
57
+ readonly payload: "knowledge/{item}";
58
+ readonly host: "core/knowledge/public";
59
+ readonly wire: "symlink";
60
+ };
61
+ readonly skills: {
62
+ readonly payload: "skills/{item}";
63
+ readonly host: ".claude/skills";
64
+ readonly wire: "symlink";
65
+ };
66
+ readonly commands: {
67
+ readonly payload: "commands/{item}.md";
68
+ readonly host: ".claude/commands";
69
+ readonly wire: "symlink";
70
+ };
71
+ readonly hooks: {
72
+ readonly payload: "hooks/{item}.sh";
73
+ readonly host: ".claude/hooks";
74
+ readonly wire: "symlink";
75
+ };
76
+ readonly policies: {
77
+ readonly payload: "policies/{item}.md";
78
+ readonly host: "core/policies";
79
+ readonly wire: "symlink";
80
+ };
81
+ readonly scripts: {
82
+ readonly payload: "scripts/{item}";
83
+ readonly host: "core/scripts";
84
+ readonly wire: "symlink";
85
+ };
86
+ readonly mcp: {
87
+ readonly payload: "mcp/{item}.json";
88
+ readonly host: "merge:claude+codex";
89
+ readonly wire: "merge";
90
+ };
91
+ };
92
+ /** The declared contribution keys, derived once from the table. */
93
+ export type ContributionKey = keyof typeof CONTRIBUTION_TABLE;
94
+ /** All contribution keys, in declaration order. */
95
+ export declare const CONTRIBUTION_KEYS: ContributionKey[];
96
+ /**
97
+ * Expand a row's `payload` template for a concrete item name. Returns the
98
+ * pack-relative path of the contribution's payload (e.g. `commands/foo.md`).
99
+ */
100
+ export declare function payloadFor(key: ContributionKey, item: string): string;
101
+ /** Keys whose contributions are wired by a host symlink (skip `merge` rows). */
102
+ export declare const SYMLINK_KEYS: ("workers" | "knowledge" | "skills" | "commands" | "hooks" | "policies" | "scripts" | "mcp")[];
103
+ //# sourceMappingURL=contribution-table.d.ts.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The contribution registry -- the SINGLE declarative source of truth (US-003)
3
+ * for the `contributes.* -> host` mapping. Every other surface DERIVES from
4
+ * this table:
5
+ *
6
+ * - `PackContributeKey` (types.ts) = `keyof typeof CONTRIBUTION_TABLE`
7
+ * - `linkFor` / `contributionLinks` read `payload` + `host`
8
+ * - `validateManifest`'s payload check reads `payload`
9
+ * - `core/scripts/scan-packages.sh` reads a data block GENERATED
10
+ * from this table
11
+ * (scripts/generate-scan-packages-table.mjs)
12
+ *
13
+ * Adding a contribution type is ONE row here, not a five-site edit. The parity
14
+ * test (`pack-contributions.test.ts`) asserts every surface agrees on the full
15
+ * key-set, the payload suffix, and the wire mode.
16
+ *
17
+ * Row fields:
18
+ * - `payload`: path INSIDE the pack, with the literal token `{item}` for the
19
+ * declared name. The suffix after `{item}` (e.g. `.md`, `.json`, or none)
20
+ * IS the load-bearing per-key shape.
21
+ * - `host`: for `wire: 'symlink'`, the host DIRECTORY (relative to the HQ
22
+ * root) the symlink is created under -- the symlink dst is
23
+ * `<host>/<expanded-item-basename>`. For `wire: 'merge'`, a non-path
24
+ * SENTINEL describing the merge target (e.g. `merge:claude+codex`); it is
25
+ * NEVER used as a filesystem path.
26
+ * - `wire`: `symlink` (a single `ln -s` into `host`) or `merge` (merged into
27
+ * a shared host config; see US-004/US-005). Symlink-only readers MUST skip
28
+ * `merge` rows.
29
+ *
30
+ * NOTE: the `mcp` row is DECLARED here as data only. US-003 does NOT wire any
31
+ * MCP behavior -- the merge engine lands in US-004/US-005. Declaring it now
32
+ * makes the table the single source so adding the merge wiring is a code change
33
+ * against an already-present row, and the parity test guards it from day one.
34
+ */
35
+ /**
36
+ * The 8-key contribution registry. `as const` so `keyof typeof` yields the
37
+ * exact literal key union consumed by `PackContributeKey`.
38
+ */
39
+
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]="154fb78d-60ba-508f-b6e7-6889858f1841")}catch(e){}}();
41
+ export const CONTRIBUTION_TABLE = {
42
+ workers: { payload: 'workers/{item}', host: 'core/workers/public', wire: 'symlink' },
43
+ knowledge: { payload: 'knowledge/{item}', host: 'core/knowledge/public', wire: 'symlink' },
44
+ skills: { payload: 'skills/{item}', host: '.claude/skills', wire: 'symlink' },
45
+ commands: { payload: 'commands/{item}.md', host: '.claude/commands', wire: 'symlink' },
46
+ hooks: { payload: 'hooks/{item}.sh', host: '.claude/hooks', wire: 'symlink' },
47
+ policies: { payload: 'policies/{item}.md', host: 'core/policies', wire: 'symlink' },
48
+ scripts: { payload: 'scripts/{item}', host: 'core/scripts', wire: 'symlink' },
49
+ // wire:merge -- DECLARED as data (US-003); the merge engine ships in
50
+ // US-004/US-005. Symlink readers skip this row.
51
+ mcp: { payload: 'mcp/{item}.json', host: 'merge:claude+codex', wire: 'merge' },
52
+ };
53
+ /** All contribution keys, in declaration order. */
54
+ export const CONTRIBUTION_KEYS = Object.keys(CONTRIBUTION_TABLE);
55
+ /**
56
+ * Expand a row's `payload` template for a concrete item name. Returns the
57
+ * pack-relative path of the contribution's payload (e.g. `commands/foo.md`).
58
+ */
59
+ export function payloadFor(key, item) {
60
+ return CONTRIBUTION_TABLE[key].payload.replace('{item}', item);
61
+ }
62
+ /** Keys whose contributions are wired by a host symlink (skip `merge` rows). */
63
+ export const SYMLINK_KEYS = CONTRIBUTION_KEYS.filter((k) => CONTRIBUTION_TABLE[k].wire === 'symlink');
64
+ //# sourceMappingURL=contribution-table.js.map
65
+ //# debugId=154fb78d-60ba-508f-b6e7-6889858f1841
@@ -1,22 +1,65 @@
1
1
  /**
2
- * Content-pack contribution helpers -- the single source of truth (in TS) for
3
- * the `contributes.* -> host-path` symlink mapping that `hq install` wires via
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. That mapping is duplicated today in two places:
10
+ * leaving dangling symlinks.
11
11
  *
12
- * - core/scripts/scan-packages.sh (bash `case`, the wiring authority)
13
- * - pack-install.ts validateManifest's `subpaths` record (payload validation)
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
- * This module re-encodes it once for TS callers. A parity test
16
- * (`pack-contributions.test.ts`) asserts it matches scan-packages.sh's `case`
17
- * arms so the three copies cannot drift.
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
  import type { PackManifest, PackContributeKey } from '../types.js';
40
+ import { CONTRIBUTION_TABLE, type WireMode } from './contribution-table.js';
41
+ import { type UnregisterServerResult, type SafeWriteEnv, type AcquireLockOptions } from '../commands/mcp-registration.js';
42
+ export { CONTRIBUTION_TABLE };
43
+ export type { WireMode };
44
+ /**
45
+ * Where a contributes key is ROUTED (US-005). Reads the single-source table's
46
+ * `wire` field — `'symlink'` keys go through `linkFor`/`contributionLinks` (a
47
+ * host `ln -s`); `'merge'` keys (e.g. `mcp`) go through the `registerMcpServers`
48
+ * seam in `commands/mcp-registration.ts` and are NEVER symlinked.
49
+ *
50
+ * This is the explicit, code-enforced discriminator behind the invariant that
51
+ * merge keys never reach the symlink path: callers fan out on the return value
52
+ * rather than relying on `linkFor` happening to return `null`.
53
+ */
54
+ export declare function routeContribution(key: PackContributeKey): WireMode;
55
+ /**
56
+ * The subset of a pack's declared `contributes` keys that are `wire: 'merge'`
57
+ * (e.g. `mcp`) — the keys that MUST be routed to `registerMcpServers` (US-006)
58
+ * and MUST NOT be symlinked. Unknown keys and non-array/empty values are
59
+ * ignored, mirroring `contributionLinks`. Returned in `contributes` iteration
60
+ * order, de-duplicated.
61
+ */
62
+ export declare function mergeKeys(contributes: Partial<Record<PackContributeKey, string[]>>): PackContributeKey[];
20
63
  /** A single symlink a pack contributes: dst (host path) -> src (inside pack). */
21
64
  export interface WiredLink {
22
65
  key: PackContributeKey;
@@ -26,8 +69,10 @@ export interface WiredLink {
26
69
  }
27
70
  export type LinkStatus = 'live' | 'broken' | 'missing' | 'foreign';
28
71
  /**
29
- * Every symlink a pack's `contributes` block declares. Empty subfields and
30
- * non-array values are ignored, mirroring scan-packages.sh.
72
+ * Every SYMLINK a pack's `contributes` block declares. Empty subfields and
73
+ * non-array values are ignored, mirroring scan-packages.sh. `wire: 'merge'`
74
+ * keys (e.g. `mcp`) produce no symlink and are skipped here -- their host
75
+ * effect (a config merge) is handled separately (US-004/US-005).
31
76
  */
32
77
  export declare function contributionLinks(hqRoot: string, packDir: string, contributes: Partial<Record<PackContributeKey, string[]>>): WiredLink[];
33
78
  /** Classify a host path against the link that should own it. */
@@ -55,6 +100,13 @@ export declare function readPackManifest(packDir: string): {
55
100
  * of truth for installed content packs.
56
101
  */
57
102
  export declare function listInstalledPacks(hqRoot: string): InstalledPack[];
103
+ /**
104
+ * Installed packs that declare `name` in their `requires.packs` (M0). Pure over
105
+ * the supplied list — the uninstall dependents guard calls this with
106
+ * `listInstalledPacks(hqRoot)`. Excludes the pack named `name` itself, so a
107
+ * self-reference never counts as its own dependent.
108
+ */
109
+ export declare function findDependentPacks(installed: InstalledPack[], name: string): InstalledPack[];
58
110
  export interface UnwireResult {
59
111
  unlinked: Array<{
60
112
  key: PackContributeKey;
@@ -75,6 +127,37 @@ export interface UnwireResult {
75
127
  * uninstall from leaving dangling symlinks behind.
76
128
  */
77
129
  export declare function unwirePack(hqRoot: string, packDir: string, contributes: Partial<Record<PackContributeKey, string[]>>): UnwireResult;
130
+ /** The result of {@link unwirePackMcp}: the per-server un-registration outcomes (empty when no `mcp` keys). */
131
+ export interface UnwireMcpResult {
132
+ /** One {@link UnregisterServerResult} per declared `contributes.mcp` server, in declaration order. */
133
+ servers: UnregisterServerResult[];
134
+ }
135
+ /** Tuning passthrough for {@link unwirePackMcp} (tests inject a tmpdir `env.home` + tiny lock timeouts). */
136
+ export interface UnwirePackMcpOptions {
137
+ /** Injectable env — tests MUST pass a tmpdir `home` so the real ~/.claude.json / ~/.codex are untouched. */
138
+ env?: Partial<SafeWriteEnv>;
139
+ /** Lock tuning (tests use tiny timeouts). */
140
+ lock?: AcquireLockOptions;
141
+ /** Backup timestamp override (deterministic tests). */
142
+ stamp?: string;
143
+ }
144
+ /**
145
+ * Un-register the `wire: 'merge'` (MCP) servers a pack declares — the parallel to
146
+ * {@link unwirePack}'s symlink removal. For each name in `mergeKeys(contributes)` →
147
+ * `contributes.<mergeKey>` (today only `mcp`), this delegates to
148
+ * {@link unregisterMcpServers}, which removes ONLY entries stamped with THIS pack's
149
+ * `_hqPack` provenance and SKIPS-AND-WARNS on a foreign/unstamped same-named entry.
150
+ *
151
+ * No-ops cleanly when the pack declares no merge keys (returns `{ servers: [] }`),
152
+ * when the runtime config is absent (Codex-less host → first-class skip; missing
153
+ * ~/.claude.json → `absent`), and on a re-run (already-removed → `absent`). It does
154
+ * NOT throw on a foreign entry — uninstall of one pack must not abort on a server
155
+ * another pack (or the user) owns.
156
+ *
157
+ * @param packName the pack being uninstalled — the provenance to match
158
+ * @param contributes the pack's `contributes` block (its `mcp` list drives removal)
159
+ */
160
+ export declare function unwirePackMcp(packName: string, contributes: Partial<Record<PackContributeKey, string[]>>, options?: UnwirePackMcpOptions): UnwireMcpResult;
78
161
  export declare function readHqVersion(hqRoot: string): string | null;
79
162
  export interface CatalogEntry {
80
163
  source: string;