@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.
- 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/onboard-warning.d.ts +7 -0
- package/dist/commands/onboard-warning.js +14 -0
- package/dist/commands/onboard.js +5 -5
- package/dist/commands/pack-install.d.ts +74 -0
- package/dist/commands/pack-install.js +493 -14
- package/dist/commands/packs.js +42 -4
- package/dist/commands/pkg-install.js +5 -2
- package/dist/index.js +7 -2
- package/dist/types.d.ts +26 -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 +93 -10
- package/dist/utils/pack-contributions.js +140 -48
- package/dist/utils/secrets-cache.d.ts +9 -0
- package/dist/utils/secrets-cache.js +24 -2
- package/dist/utils/version-gate.d.ts +40 -1
- package/dist/utils/version-gate.js +91 -20
- 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/onboard-warning.test.ts +26 -0
- package/src/commands/onboard-warning.ts +12 -0
- package/src/commands/onboard.ts +4 -7
- package/src/commands/pack-install.test.ts +733 -0
- package/src/commands/pack-install.ts +582 -13
- package/src/commands/packs.ts +45 -1
- package/src/commands/pkg-install.ts +4 -1
- package/src/index.ts +6 -0
- package/src/types.ts +28 -9
- package/src/utils/contribution-table.ts +83 -0
- package/src/utils/pack-contributions.test.ts +310 -25
- package/src/utils/pack-contributions.ts +194 -47
- package/src/utils/secrets-cache.ts +22 -0
- package/src/utils/version-gate.test.ts +122 -0
- package/src/utils/version-gate.ts +109 -13
- 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]="63e778df-65dd-51a4-8a43-3a7d2aa9a451")}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;
|
|
@@ -161,6 +213,16 @@ export function listInstalledPacks(hqRoot) {
|
|
|
161
213
|
}
|
|
162
214
|
return out;
|
|
163
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Installed packs that declare `name` in their `requires.packs` (M0). Pure over
|
|
218
|
+
* the supplied list — the uninstall dependents guard calls this with
|
|
219
|
+
* `listInstalledPacks(hqRoot)`. Excludes the pack named `name` itself, so a
|
|
220
|
+
* self-reference never counts as its own dependent.
|
|
221
|
+
*/
|
|
222
|
+
export function findDependentPacks(installed, name) {
|
|
223
|
+
return installed.filter((p) => p.name !== name &&
|
|
224
|
+
(p.manifest?.requires?.packs ?? []).some((d) => d.name === name));
|
|
225
|
+
}
|
|
164
226
|
/**
|
|
165
227
|
* Remove only the host symlinks that resolve into THIS pack's directory
|
|
166
228
|
* (status `live` or `broken`). Foreign links and real files are left in place
|
|
@@ -196,6 +258,36 @@ export function unwirePack(hqRoot, packDir, contributes) {
|
|
|
196
258
|
}
|
|
197
259
|
return result;
|
|
198
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Un-register the `wire: 'merge'` (MCP) servers a pack declares — the parallel to
|
|
263
|
+
* {@link unwirePack}'s symlink removal. For each name in `mergeKeys(contributes)` →
|
|
264
|
+
* `contributes.<mergeKey>` (today only `mcp`), this delegates to
|
|
265
|
+
* {@link unregisterMcpServers}, which removes ONLY entries stamped with THIS pack's
|
|
266
|
+
* `_hqPack` provenance and SKIPS-AND-WARNS on a foreign/unstamped same-named entry.
|
|
267
|
+
*
|
|
268
|
+
* No-ops cleanly when the pack declares no merge keys (returns `{ servers: [] }`),
|
|
269
|
+
* when the runtime config is absent (Codex-less host → first-class skip; missing
|
|
270
|
+
* ~/.claude.json → `absent`), and on a re-run (already-removed → `absent`). It does
|
|
271
|
+
* NOT throw on a foreign entry — uninstall of one pack must not abort on a server
|
|
272
|
+
* another pack (or the user) owns.
|
|
273
|
+
*
|
|
274
|
+
* @param packName the pack being uninstalled — the provenance to match
|
|
275
|
+
* @param contributes the pack's `contributes` block (its `mcp` list drives removal)
|
|
276
|
+
*/
|
|
277
|
+
export function unwirePackMcp(packName, contributes, options) {
|
|
278
|
+
const servers = [];
|
|
279
|
+
for (const key of mergeKeys(contributes)) {
|
|
280
|
+
const names = (contributes[key] ?? []).filter((n) => typeof n === 'string' && n.length > 0);
|
|
281
|
+
if (names.length === 0)
|
|
282
|
+
continue;
|
|
283
|
+
servers.push(...unregisterMcpServers(packName, names, {
|
|
284
|
+
env: options?.env,
|
|
285
|
+
lock: options?.lock,
|
|
286
|
+
stamp: options?.stamp,
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
return { servers };
|
|
290
|
+
}
|
|
199
291
|
// ---------------------------------------------------------------------------
|
|
200
292
|
// Host introspection: hqVersion + recommended_packages catalog
|
|
201
293
|
// ---------------------------------------------------------------------------
|
|
@@ -236,4 +328,4 @@ export function readRecommendedPackages(hqRoot) {
|
|
|
236
328
|
}
|
|
237
329
|
}
|
|
238
330
|
//# sourceMappingURL=pack-contributions.js.map
|
|
239
|
-
//# debugId=
|
|
331
|
+
//# debugId=63e778df-65dd-51a4-8a43-3a7d2aa9a451
|
|
@@ -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
|
|
@@ -27,16 +27,49 @@
|
|
|
27
27
|
* Opt-out: `HQ_NO_UPDATE_CHECK=1` (same env as `version-check.ts` — one knob
|
|
28
28
|
* to silence both check + gate).
|
|
29
29
|
*/
|
|
30
|
+
interface VersionCheckResponse {
|
|
31
|
+
clientId: string;
|
|
32
|
+
currentVersion: string;
|
|
33
|
+
minVersion: string;
|
|
34
|
+
latestVersion: string;
|
|
35
|
+
updateRequired: boolean;
|
|
36
|
+
updateRecommended: boolean;
|
|
37
|
+
updateCommand?: string;
|
|
38
|
+
downloadUrl?: string;
|
|
39
|
+
message?: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function npmPrefixFromPackageDir(pkgDir: string): string | null;
|
|
42
|
+
export declare function resolveRunningPrefix(): string | null;
|
|
43
|
+
export declare function buildPrefixedInstallArgv(prefix: string): string[];
|
|
30
44
|
/**
|
|
31
45
|
* Run the upgrade command in a blocking subprocess. Inherits stdio so the
|
|
32
46
|
* user sees the npm progress. We do NOT auto-rerun the CLI on completion —
|
|
33
47
|
* forcing a re-invocation would run twice on the same process and feel
|
|
34
48
|
* janky; instead we print a clear "rerun your command" message and exit.
|
|
35
49
|
*/
|
|
36
|
-
|
|
50
|
+
type UpdateResult = {
|
|
37
51
|
ok: boolean;
|
|
38
52
|
detail?: string;
|
|
39
53
|
};
|
|
54
|
+
type UpdateRunner = (cmd: string, args: string[]) => UpdateResult;
|
|
55
|
+
declare function runUpdateCommand(cmd: string, args: string[]): UpdateResult;
|
|
56
|
+
declare function performUpdateCommand(cmd: string, args: string[], runner?: UpdateRunner): UpdateResult;
|
|
57
|
+
declare function performUpdate(command: string, runner?: UpdateRunner): UpdateResult;
|
|
58
|
+
/**
|
|
59
|
+
* Hard enforcement when the server says we're below `minVersion`. Print a
|
|
60
|
+
* red banner, attempt the update, then exit so the user reruns against the
|
|
61
|
+
* fresh binary. Sequence chosen so a user with a broken `npm` global prefix
|
|
62
|
+
* still gets a clear error rather than an opaque silent failure.
|
|
63
|
+
*
|
|
64
|
+
* Exit codes:
|
|
65
|
+
* 0 — update succeeded; user must rerun their command
|
|
66
|
+
* 75 — update failed (EX_TEMPFAIL; common for sudo/EACCES on system npm)
|
|
67
|
+
*/
|
|
68
|
+
declare function enforceUpdateRequired(decision: VersionCheckResponse, deps?: {
|
|
69
|
+
performUpdateString?: (command: string) => UpdateResult;
|
|
70
|
+
resolvePrefix?: () => string | null;
|
|
71
|
+
runner?: UpdateRunner;
|
|
72
|
+
}): never;
|
|
40
73
|
/**
|
|
41
74
|
* Public entry point. Call before commander parses argv. Blocks the CLI on
|
|
42
75
|
* network IO for up to FETCH_TIMEOUT_MS — acceptable because the alternative
|
|
@@ -58,7 +91,13 @@ export declare const __test__: {
|
|
|
58
91
|
CLIENT_ID: string;
|
|
59
92
|
ENDPOINT_PATH: string;
|
|
60
93
|
FETCH_TIMEOUT_MS: number;
|
|
94
|
+
buildPrefixedInstallArgv: typeof buildPrefixedInstallArgv;
|
|
95
|
+
enforceUpdateRequired: typeof enforceUpdateRequired;
|
|
96
|
+
npmPrefixFromPackageDir: typeof npmPrefixFromPackageDir;
|
|
61
97
|
performUpdate: typeof performUpdate;
|
|
98
|
+
performUpdateCommand: typeof performUpdateCommand;
|
|
99
|
+
runUpdateCommand: typeof runUpdateCommand;
|
|
100
|
+
resolveRunningPrefix: typeof resolveRunningPrefix;
|
|
62
101
|
};
|
|
63
102
|
export {};
|
|
64
103
|
//# sourceMappingURL=version-gate.d.ts.map
|
|
@@ -28,17 +28,66 @@
|
|
|
28
28
|
* to silence both check + gate).
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
!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]="
|
|
31
|
+
!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]="0f613895-ad4a-5211-bf75-a2e843782c47")}catch(e){}}();
|
|
32
32
|
import { spawnSync } from "node:child_process";
|
|
33
|
+
import { readFileSync } from "node:fs";
|
|
34
|
+
import path from "node:path";
|
|
35
|
+
import { fileURLToPath } from "node:url";
|
|
33
36
|
import chalk from "chalk";
|
|
34
|
-
import { CLI_VERSION } from "../cli-version.js";
|
|
37
|
+
import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
|
|
35
38
|
import { DEFAULT_VAULT_API_URL } from "./cognito-session.js";
|
|
36
39
|
const CLIENT_ID = "hq-cli";
|
|
37
40
|
const ENDPOINT_PATH = "/v1/client-version/check";
|
|
38
41
|
const FETCH_TIMEOUT_MS = 3_000;
|
|
42
|
+
const LATEST_PACKAGE_SPEC = `${CLI_NAME}@latest`;
|
|
39
43
|
function isOptedOut() {
|
|
40
44
|
return process.env.HQ_NO_UPDATE_CHECK === "1";
|
|
41
45
|
}
|
|
46
|
+
function findRunningPackageRoot() {
|
|
47
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
while (true) {
|
|
49
|
+
try {
|
|
50
|
+
const pkg = JSON.parse(readFileSync(path.join(dir, "package.json"), "utf-8"));
|
|
51
|
+
if (pkg.name === CLI_NAME)
|
|
52
|
+
return dir;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Keep walking; compiled installs usually start under dist/.
|
|
56
|
+
}
|
|
57
|
+
const parent = path.dirname(dir);
|
|
58
|
+
if (parent === dir)
|
|
59
|
+
return null;
|
|
60
|
+
dir = parent;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function npmPrefixFromPackageDir(pkgDir) {
|
|
64
|
+
const normalized = pkgDir.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
65
|
+
const segments = normalized.split("/");
|
|
66
|
+
const nodeModulesIndex = segments.lastIndexOf("node_modules");
|
|
67
|
+
if (nodeModulesIndex === -1)
|
|
68
|
+
return null;
|
|
69
|
+
const prefixEnd = segments[nodeModulesIndex - 1] === "lib"
|
|
70
|
+
? nodeModulesIndex - 1
|
|
71
|
+
: nodeModulesIndex;
|
|
72
|
+
const prefix = segments.slice(0, prefixEnd).join("/");
|
|
73
|
+
if (prefix === "" && normalized.startsWith("/"))
|
|
74
|
+
return "/";
|
|
75
|
+
return prefix || null;
|
|
76
|
+
}
|
|
77
|
+
export function resolveRunningPrefix() {
|
|
78
|
+
try {
|
|
79
|
+
const pkgRoot = findRunningPackageRoot();
|
|
80
|
+
if (!pkgRoot)
|
|
81
|
+
return null;
|
|
82
|
+
return npmPrefixFromPackageDir(pkgRoot);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function buildPrefixedInstallArgv(prefix) {
|
|
89
|
+
return ["install", "-g", "--prefix", prefix, LATEST_PACKAGE_SPEC];
|
|
90
|
+
}
|
|
42
91
|
/**
|
|
43
92
|
* Hit POST /v1/client-version/check. Returns the parsed body on 200, or
|
|
44
93
|
* `null` on any failure (caller treats as "no gate"). Tight 3s timeout —
|
|
@@ -74,18 +123,7 @@ async function fetchVersionDecision() {
|
|
|
74
123
|
return null;
|
|
75
124
|
}
|
|
76
125
|
}
|
|
77
|
-
|
|
78
|
-
* Run the upgrade command in a blocking subprocess. Inherits stdio so the
|
|
79
|
-
* user sees the npm progress. We do NOT auto-rerun the CLI on completion —
|
|
80
|
-
* forcing a re-invocation would run twice on the same process and feel
|
|
81
|
-
* janky; instead we print a clear "rerun your command" message and exit.
|
|
82
|
-
*/
|
|
83
|
-
function performUpdate(command) {
|
|
84
|
-
const parts = command.split(/\s+/).filter(Boolean);
|
|
85
|
-
if (parts.length === 0)
|
|
86
|
-
return { ok: false, detail: "empty command" };
|
|
87
|
-
const cmd = parts[0];
|
|
88
|
-
const args = parts.slice(1);
|
|
126
|
+
function runUpdateCommand(cmd, args) {
|
|
89
127
|
try {
|
|
90
128
|
const result = spawnSync(cmd, args, { stdio: "inherit" });
|
|
91
129
|
if (result.status !== 0) {
|
|
@@ -100,6 +138,17 @@ function performUpdate(command) {
|
|
|
100
138
|
return { ok: false, detail: err instanceof Error ? err.message : String(err) };
|
|
101
139
|
}
|
|
102
140
|
}
|
|
141
|
+
function performUpdateCommand(cmd, args, runner = runUpdateCommand) {
|
|
142
|
+
return runner(cmd, args);
|
|
143
|
+
}
|
|
144
|
+
function performUpdate(command, runner = runUpdateCommand) {
|
|
145
|
+
const parts = command.split(/\s+/).filter(Boolean);
|
|
146
|
+
if (parts.length === 0)
|
|
147
|
+
return { ok: false, detail: "empty command" };
|
|
148
|
+
const cmd = parts[0];
|
|
149
|
+
const args = parts.slice(1);
|
|
150
|
+
return performUpdateCommand(cmd, args, runner);
|
|
151
|
+
}
|
|
103
152
|
/**
|
|
104
153
|
* Soft notify when the server says we're below `latestVersion` but still ≥
|
|
105
154
|
* `minVersion`. Single chalk-yellow line on stderr; never blocks.
|
|
@@ -121,24 +170,40 @@ function nudgeUpdateRecommended(decision) {
|
|
|
121
170
|
* 0 — update succeeded; user must rerun their command
|
|
122
171
|
* 75 — update failed (EX_TEMPFAIL; common for sudo/EACCES on system npm)
|
|
123
172
|
*/
|
|
124
|
-
function enforceUpdateRequired(decision) {
|
|
173
|
+
function enforceUpdateRequired(decision, deps = {}) {
|
|
125
174
|
const banner = chalk.red.bold(`✗ hq-cli ${decision.currentVersion} is below the minimum required version (${decision.minVersion}).`);
|
|
126
175
|
console.error(banner);
|
|
127
176
|
if (decision.message)
|
|
128
177
|
console.error(chalk.dim(` ${decision.message}`));
|
|
129
178
|
const command = decision.updateCommand;
|
|
130
|
-
|
|
179
|
+
const prefix = (deps.resolvePrefix ?? resolveRunningPrefix)();
|
|
180
|
+
if (!command && !prefix) {
|
|
131
181
|
console.error(chalk.red(" No updateCommand provided by hq-pro — see https://hq.indigo.ai/docs/cli-update for manual steps."));
|
|
132
182
|
if (decision.downloadUrl) {
|
|
133
183
|
console.error(chalk.dim(` Download: ${decision.downloadUrl}`));
|
|
134
184
|
}
|
|
135
185
|
process.exit(75);
|
|
136
186
|
}
|
|
137
|
-
|
|
138
|
-
const result =
|
|
187
|
+
const runner = deps.runner ?? runUpdateCommand;
|
|
188
|
+
const result = prefix
|
|
189
|
+
? (() => {
|
|
190
|
+
const args = buildPrefixedInstallArgv(prefix);
|
|
191
|
+
console.error(chalk.dim(` Installing into npm prefix: ${prefix}`));
|
|
192
|
+
console.error(chalk.dim(` Running: npm ${args.join(" ")}`));
|
|
193
|
+
return performUpdateCommand("npm", args, runner);
|
|
194
|
+
})()
|
|
195
|
+
: (() => {
|
|
196
|
+
console.error(chalk.dim(` Running: ${command}`));
|
|
197
|
+
return deps.performUpdateString
|
|
198
|
+
? deps.performUpdateString(command)
|
|
199
|
+
: performUpdate(command, runner);
|
|
200
|
+
})();
|
|
139
201
|
if (!result.ok) {
|
|
140
202
|
console.error(chalk.red(`✗ Update failed${result.detail ? `: ${result.detail}` : ""}.`));
|
|
141
|
-
|
|
203
|
+
const manual = prefix
|
|
204
|
+
? `npm ${buildPrefixedInstallArgv(prefix).join(" ")}`
|
|
205
|
+
: command;
|
|
206
|
+
console.error(chalk.dim(` Try manually: ${manual}`));
|
|
142
207
|
process.exit(75);
|
|
143
208
|
}
|
|
144
209
|
console.error(chalk.green(`✓ Updated to hq-cli ${decision.latestVersion}. Rerun your command.`));
|
|
@@ -179,7 +244,13 @@ export const __test__ = {
|
|
|
179
244
|
CLIENT_ID,
|
|
180
245
|
ENDPOINT_PATH,
|
|
181
246
|
FETCH_TIMEOUT_MS,
|
|
247
|
+
buildPrefixedInstallArgv,
|
|
248
|
+
enforceUpdateRequired,
|
|
249
|
+
npmPrefixFromPackageDir,
|
|
182
250
|
performUpdate,
|
|
251
|
+
performUpdateCommand,
|
|
252
|
+
runUpdateCommand,
|
|
253
|
+
resolveRunningPrefix,
|
|
183
254
|
};
|
|
184
255
|
//# sourceMappingURL=version-gate.js.map
|
|
185
|
-
//# debugId=
|
|
256
|
+
//# debugId=0f613895-ad4a-5211-bf75-a2e843782c47
|
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.2",
|
|
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": {
|