@indigoai-us/hq-cli 5.50.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 +7 -2
- 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/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 +6 -0
- package/src/types.ts +9 -8
- package/src/utils/contribution-table.ts +83 -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
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
|
-
|
|
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
|
|
@@ -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
|
|
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
|
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
|
|
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. */
|
|
@@ -75,6 +120,37 @@ export interface UnwireResult {
|
|
|
75
120
|
* uninstall from leaving dangling symlinks behind.
|
|
76
121
|
*/
|
|
77
122
|
export declare function unwirePack(hqRoot: string, packDir: string, contributes: Partial<Record<PackContributeKey, string[]>>): UnwireResult;
|
|
123
|
+
/** The result of {@link unwirePackMcp}: the per-server un-registration outcomes (empty when no `mcp` keys). */
|
|
124
|
+
export interface UnwireMcpResult {
|
|
125
|
+
/** One {@link UnregisterServerResult} per declared `contributes.mcp` server, in declaration order. */
|
|
126
|
+
servers: UnregisterServerResult[];
|
|
127
|
+
}
|
|
128
|
+
/** Tuning passthrough for {@link unwirePackMcp} (tests inject a tmpdir `env.home` + tiny lock timeouts). */
|
|
129
|
+
export interface UnwirePackMcpOptions {
|
|
130
|
+
/** Injectable env — tests MUST pass a tmpdir `home` so the real ~/.claude.json / ~/.codex are untouched. */
|
|
131
|
+
env?: Partial<SafeWriteEnv>;
|
|
132
|
+
/** Lock tuning (tests use tiny timeouts). */
|
|
133
|
+
lock?: AcquireLockOptions;
|
|
134
|
+
/** Backup timestamp override (deterministic tests). */
|
|
135
|
+
stamp?: string;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Un-register the `wire: 'merge'` (MCP) servers a pack declares — the parallel to
|
|
139
|
+
* {@link unwirePack}'s symlink removal. For each name in `mergeKeys(contributes)` →
|
|
140
|
+
* `contributes.<mergeKey>` (today only `mcp`), this delegates to
|
|
141
|
+
* {@link unregisterMcpServers}, which removes ONLY entries stamped with THIS pack's
|
|
142
|
+
* `_hqPack` provenance and SKIPS-AND-WARNS on a foreign/unstamped same-named entry.
|
|
143
|
+
*
|
|
144
|
+
* No-ops cleanly when the pack declares no merge keys (returns `{ servers: [] }`),
|
|
145
|
+
* when the runtime config is absent (Codex-less host → first-class skip; missing
|
|
146
|
+
* ~/.claude.json → `absent`), and on a re-run (already-removed → `absent`). It does
|
|
147
|
+
* NOT throw on a foreign entry — uninstall of one pack must not abort on a server
|
|
148
|
+
* another pack (or the user) owns.
|
|
149
|
+
*
|
|
150
|
+
* @param packName the pack being uninstalled — the provenance to match
|
|
151
|
+
* @param contributes the pack's `contributes` block (its `mcp` list drives removal)
|
|
152
|
+
*/
|
|
153
|
+
export declare function unwirePackMcp(packName: string, contributes: Partial<Record<PackContributeKey, string[]>>, options?: UnwirePackMcpOptions): UnwireMcpResult;
|
|
78
154
|
export declare function readHqVersion(hqRoot: string): string | null;
|
|
79
155
|
export interface CatalogEntry {
|
|
80
156
|
source: string;
|
|
@@ -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.50.
|
|
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.12.
|
|
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": {
|