@indigoai-us/hq-cli 5.50.1 → 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/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 +12 -0
- package/dist/commands/pack-install.js +74 -3
- package/dist/commands/packs.js +17 -3
- package/dist/types.d.ts +18 -0
- package/dist/utils/pack-contributions.d.ts +7 -0
- package/dist/utils/pack-contributions.js +12 -2
- package/dist/utils/version-gate.d.ts +40 -1
- package/dist/utils/version-gate.js +91 -20
- package/package.json +1 -1
- 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 +144 -0
- package/src/commands/pack-install.ts +86 -1
- package/src/commands/packs.ts +19 -0
- package/src/types.ts +19 -1
- package/src/utils/pack-contributions.test.ts +53 -0
- package/src/utils/pack-contributions.ts +17 -0
- package/src/utils/version-gate.test.ts +122 -0
- package/src/utils/version-gate.ts +109 -13
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the static create-time warning surfaced by both `create-company` and
|
|
3
|
+
* `dry-run`, because users may accidentally create duplicate companies instead
|
|
4
|
+
* of joining the existing company they were invited to.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildCreateCompanyWarning(): string;
|
|
7
|
+
//# sourceMappingURL=onboard-warning.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the static create-time warning surfaced by both `create-company` and
|
|
3
|
+
* `dry-run`, because users may accidentally create duplicate companies instead
|
|
4
|
+
* of joining the existing company they were invited to.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
!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]="853f43ae-d902-54b9-9279-8a4b1d5f7693")}catch(e){}}();
|
|
8
|
+
export function buildCreateCompanyWarning() {
|
|
9
|
+
return (" This creates a NEW company that you own — it does NOT join an existing one.\n" +
|
|
10
|
+
" If your company already uses HQ, do NOT create it again: ask your admin or owner to send you an invite, then accept it and run `hq sync` to pull the existing company.\n" +
|
|
11
|
+
" Creating a same-named company makes a separate one you'd be alone in.\n");
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=onboard-warning.js.map
|
|
14
|
+
//# debugId=853f43ae-d902-54b9-9279-8a4b1d5f7693
|
package/dist/commands/onboard.js
CHANGED
|
@@ -19,13 +19,14 @@
|
|
|
19
19
|
* browser-OAuth flow opens automatically.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
!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]="
|
|
22
|
+
!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]="235a4623-0834-5428-957c-0c8bbe96d089")}catch(e){}}();
|
|
23
23
|
import chalk from "chalk";
|
|
24
24
|
import { runOnboardCli, readCheckpoint } from "@indigoai-us/hq-onboarding";
|
|
25
25
|
import { DEFAULT_HQ_ROOT, DEFAULT_VAULT_API_URL, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
|
|
26
26
|
import { createDefaultVaultClient } from "./cloud-provision.js";
|
|
27
27
|
import { detectOnboardingIdentityMismatch } from "./onboard-identity-guard.js";
|
|
28
28
|
import { planOnboardJoin } from "./onboard-join.js";
|
|
29
|
+
import { buildCreateCompanyWarning } from "./onboard-warning.js";
|
|
29
30
|
// ---------------------------------------------------------------------------
|
|
30
31
|
// Command registration
|
|
31
32
|
// ---------------------------------------------------------------------------
|
|
@@ -49,9 +50,7 @@ export function registerOnboardCommand(program) {
|
|
|
49
50
|
console.log(` Company: ${options.name} (${options.slug})`);
|
|
50
51
|
console.log(` Person: ${options.personName} <${options.email}>`);
|
|
51
52
|
console.log(` HQ root: ${options.hqRoot}\n`);
|
|
52
|
-
console.log(chalk.gray(
|
|
53
|
-
" company? Stop — accept your invite, then run `hq sync` to pull it.\n" +
|
|
54
|
-
" Creating a same-named company leaves you alone in a separate one.\n"));
|
|
53
|
+
console.log(chalk.gray(buildCreateCompanyWarning()));
|
|
55
54
|
const accessToken = await ensureCognitoToken();
|
|
56
55
|
const result = await runOnboardCli({
|
|
57
56
|
mode: "create-company",
|
|
@@ -179,6 +178,7 @@ export function registerOnboardCommand(program) {
|
|
|
179
178
|
.option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
|
|
180
179
|
.action(async (options) => {
|
|
181
180
|
try {
|
|
181
|
+
console.log(chalk.gray(buildCreateCompanyWarning()));
|
|
182
182
|
// No auth needed for dry-run — runOnboardCli handles this branch
|
|
183
183
|
// without touching the vault-service.
|
|
184
184
|
const result = await runOnboardCli({
|
|
@@ -198,4 +198,4 @@ export function registerOnboardCommand(program) {
|
|
|
198
198
|
});
|
|
199
199
|
}
|
|
200
200
|
//# sourceMappingURL=onboard.js.map
|
|
201
|
-
//# debugId=
|
|
201
|
+
//# debugId=235a4623-0834-5428-957c-0c8bbe96d089
|
|
@@ -259,6 +259,18 @@ export declare function verifyArtifact(input: VerifyArtifactInput): void;
|
|
|
259
259
|
*/
|
|
260
260
|
export declare function validateMcpManifest(payloadDir: string, item: string): void;
|
|
261
261
|
export declare function validateManifest(payloadDir: string, hqVersion: string | null): PackManifest;
|
|
262
|
+
/**
|
|
263
|
+
* Enforce a pack's `requires.packs`: every named dependency MUST already be
|
|
264
|
+
* installed (and satisfy its optional semver RANGE) before we write anything.
|
|
265
|
+
*
|
|
266
|
+
* Installed packs are discovered by FILESYSTEM PRESENCE via `listInstalledPacks`
|
|
267
|
+
* — deliberately NOT `modules.yaml`, which `installPack` no longer writes under
|
|
268
|
+
* the v12+ layout (a modules.yaml-based check would silently ignore every modern
|
|
269
|
+
* pack). Throws on the first unmet dependency so the install aborts with NO
|
|
270
|
+
* partial state (it is called before installToPackages). No-op when
|
|
271
|
+
* `requires.packs` is absent/empty, keeping legacy packs unaffected.
|
|
272
|
+
*/
|
|
273
|
+
export declare function assertPackDependencies(hqRoot: string, pkg: PackManifest): void;
|
|
262
274
|
/**
|
|
263
275
|
* Derive the safe, auto-generated "get started" line for a freshly installed
|
|
264
276
|
* pack from its `initialization.entrypoint` ONLY. PHASE 1 deliberately ignores
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* from each pack's package.yaml; rationale lives in the layout-fix PR.)
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
-
!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]="
|
|
37
|
+
!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]="0ec07d3a-ee86-51fd-adbd-2608c620ae99")}catch(e){}}();
|
|
38
38
|
import * as fs from 'fs';
|
|
39
39
|
import * as os from 'os';
|
|
40
40
|
import * as path from 'path';
|
|
@@ -48,7 +48,7 @@ import semverValid from 'semver/functions/valid.js';
|
|
|
48
48
|
import semverValidRange from 'semver/ranges/valid.js';
|
|
49
49
|
import semverGt from 'semver/functions/gt.js';
|
|
50
50
|
import { findHqRoot } from '../utils/manifest.js';
|
|
51
|
-
import { readHqVersion, routeContribution } from '../utils/pack-contributions.js';
|
|
51
|
+
import { readHqVersion, routeContribution, listInstalledPacks } from '../utils/pack-contributions.js';
|
|
52
52
|
import { CONTRIBUTION_TABLE, payloadFor } from '../utils/contribution-table.js';
|
|
53
53
|
import { safeExtractTarball } from './safe-extract.js';
|
|
54
54
|
import { vaultApiFetchPublic } from '../utils/vault-api.js';
|
|
@@ -907,6 +907,32 @@ export function validateManifest(payloadDir, hqVersion) {
|
|
|
907
907
|
if (hqVersion && !semverSatisfies(hqVersion, range, { includePrerelease: true })) {
|
|
908
908
|
throw new Error(`Host hqCore ${hqVersion} does not satisfy pack requirement ${range}`);
|
|
909
909
|
}
|
|
910
|
+
// 6b. requires.packs (M0) — OPTIONAL pack-to-pack dependencies. Absent → legacy
|
|
911
|
+
// behavior (hqCore is the only prerequisite). Present → a list of
|
|
912
|
+
// { name, version? } where `name` is a valid hq-pack name and `version` (if
|
|
913
|
+
// given) is a valid semver RANGE. SHAPE validation only (no filesystem) so a
|
|
914
|
+
// malformed dependency can't masquerade as valid; whether the named packs are
|
|
915
|
+
// actually INSTALLED is enforced at install time by assertPackDependencies
|
|
916
|
+
// (which needs hqRoot — a pure manifest validator doesn't have it).
|
|
917
|
+
const reqPacks = m.requires?.packs;
|
|
918
|
+
if (reqPacks !== undefined) {
|
|
919
|
+
if (!Array.isArray(reqPacks)) {
|
|
920
|
+
throw new Error('requires.packs must be a list of { name, version? } entries');
|
|
921
|
+
}
|
|
922
|
+
for (const dep of reqPacks) {
|
|
923
|
+
if (!dep || typeof dep !== 'object' || Array.isArray(dep)) {
|
|
924
|
+
throw new Error('requires.packs entries must be mappings with a name (and optional version)');
|
|
925
|
+
}
|
|
926
|
+
const d = dep;
|
|
927
|
+
if (typeof d.name !== 'string' || !/^hq-pack-[a-z0-9][a-z0-9-]*$/.test(d.name)) {
|
|
928
|
+
throw new Error(`requires.packs[].name "${d.name}" must match ^hq-pack-[a-z0-9][a-z0-9-]*$`);
|
|
929
|
+
}
|
|
930
|
+
if (d.version !== undefined &&
|
|
931
|
+
(typeof d.version !== 'string' || !semverValidRange(d.version))) {
|
|
932
|
+
throw new Error(`requires.packs entry for "${d.name}" has an invalid version range "${d.version}"`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
910
936
|
// 7. contributes has at least one non-empty subfield
|
|
911
937
|
const contributes = (m.contributes ?? {});
|
|
912
938
|
const nonEmpty = Object.values(contributes).some((v) => Array.isArray(v) && v.length > 0);
|
|
@@ -1007,6 +1033,46 @@ export function validateManifest(payloadDir, hqVersion) {
|
|
|
1007
1033
|
return m;
|
|
1008
1034
|
}
|
|
1009
1035
|
// ---------------------------------------------------------------------------
|
|
1036
|
+
// Pack-to-pack dependency pre-flight (M0)
|
|
1037
|
+
// ---------------------------------------------------------------------------
|
|
1038
|
+
/**
|
|
1039
|
+
* Enforce a pack's `requires.packs`: every named dependency MUST already be
|
|
1040
|
+
* installed (and satisfy its optional semver RANGE) before we write anything.
|
|
1041
|
+
*
|
|
1042
|
+
* Installed packs are discovered by FILESYSTEM PRESENCE via `listInstalledPacks`
|
|
1043
|
+
* — deliberately NOT `modules.yaml`, which `installPack` no longer writes under
|
|
1044
|
+
* the v12+ layout (a modules.yaml-based check would silently ignore every modern
|
|
1045
|
+
* pack). Throws on the first unmet dependency so the install aborts with NO
|
|
1046
|
+
* partial state (it is called before installToPackages). No-op when
|
|
1047
|
+
* `requires.packs` is absent/empty, keeping legacy packs unaffected.
|
|
1048
|
+
*/
|
|
1049
|
+
export function assertPackDependencies(hqRoot, pkg) {
|
|
1050
|
+
const deps = pkg.requires?.packs ?? [];
|
|
1051
|
+
if (deps.length === 0)
|
|
1052
|
+
return;
|
|
1053
|
+
const installed = new Map();
|
|
1054
|
+
for (const p of listInstalledPacks(hqRoot)) {
|
|
1055
|
+
// Key on the manifest name when readable, else the directory name.
|
|
1056
|
+
installed.set(p.manifest?.name ?? p.name, p.manifest?.version);
|
|
1057
|
+
}
|
|
1058
|
+
for (const dep of deps) {
|
|
1059
|
+
if (dep.name === pkg.name) {
|
|
1060
|
+
throw new Error(`Pack ${pkg.name} cannot list itself in requires.packs.`);
|
|
1061
|
+
}
|
|
1062
|
+
if (!installed.has(dep.name)) {
|
|
1063
|
+
throw new Error(`Pack ${pkg.name} requires ${dep.name}, which is not installed. ` +
|
|
1064
|
+
`Install it first, e.g.: hq install marketplace:${dep.name}`);
|
|
1065
|
+
}
|
|
1066
|
+
if (dep.version) {
|
|
1067
|
+
const have = installed.get(dep.name);
|
|
1068
|
+
if (!have || !semverSatisfies(have, dep.version, { includePrerelease: true })) {
|
|
1069
|
+
throw new Error(`Pack ${pkg.name} requires ${dep.name} ${dep.version}, but ` +
|
|
1070
|
+
`${dep.name}${have ? ` ${have}` : ' (version unknown)'} is installed.`);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
// ---------------------------------------------------------------------------
|
|
1010
1076
|
// Post-install get-started line (US-005)
|
|
1011
1077
|
// ---------------------------------------------------------------------------
|
|
1012
1078
|
/**
|
|
@@ -1482,6 +1548,11 @@ export async function installPack(source, opts = {}) {
|
|
|
1482
1548
|
break;
|
|
1483
1549
|
}
|
|
1484
1550
|
const pkg = validateManifest(fetched.payloadDir, hqVersion);
|
|
1551
|
+
// M0 — pack-to-pack dependency pre-flight. Runs BEFORE any prompts or writes
|
|
1552
|
+
// so a missing/unsatisfied `requires.packs` aborts with no partial state, and
|
|
1553
|
+
// before we bother the operator with hook/MCP trust prompts for a pack that
|
|
1554
|
+
// can't install anyway.
|
|
1555
|
+
assertPackDependencies(hqRoot, pkg);
|
|
1485
1556
|
if (pkg.conditional) {
|
|
1486
1557
|
const allowed = await confirmConditional(pkg, opts.allowHooks ?? false);
|
|
1487
1558
|
if (!allowed) {
|
|
@@ -1558,4 +1629,4 @@ export async function installPack(source, opts = {}) {
|
|
|
1558
1629
|
}
|
|
1559
1630
|
}
|
|
1560
1631
|
//# sourceMappingURL=pack-install.js.map
|
|
1561
|
-
//# debugId=
|
|
1632
|
+
//# debugId=0ec07d3a-ee86-51fd-adbd-2608c620ae99
|
package/dist/commands/packs.js
CHANGED
|
@@ -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]="
|
|
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, unwirePackMcp, 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
|
}
|
|
@@ -278,6 +278,19 @@ async function runUninstall(name, opts) {
|
|
|
278
278
|
if (!manifest) {
|
|
279
279
|
warnings.push('package.yaml unreadable -- host symlinks could not be computed precisely; ran a re-scan to reconcile.');
|
|
280
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
|
+
}
|
|
281
294
|
// 1. Un-wire only our symlinks.
|
|
282
295
|
const { unlinked, skipped } = unwirePack(hqRoot, packDir, contributes);
|
|
283
296
|
// 1b. Un-register the pack's MCP (`wire: 'merge'`) servers — invisible to the
|
|
@@ -417,6 +430,7 @@ export function registerPacksCommand(parent) {
|
|
|
417
430
|
.option('--hq-root <path>', 'HQ root (default: auto-detect)')
|
|
418
431
|
.option('-y, --yes', 'Skip confirmation')
|
|
419
432
|
.option('--no-archive', 'Delete instead of archiving')
|
|
433
|
+
.option('--force', 'Uninstall even if other installed packs require this one')
|
|
420
434
|
.action(async (name, opts) => {
|
|
421
435
|
try {
|
|
422
436
|
if (!opts.yes) {
|
|
@@ -449,4 +463,4 @@ export function registerPacksCommand(parent) {
|
|
|
449
463
|
});
|
|
450
464
|
}
|
|
451
465
|
//# sourceMappingURL=packs.js.map
|
|
452
|
-
//# debugId=
|
|
466
|
+
//# debugId=69746edd-f9d8-528e-8902-de799c58bd8a
|
package/dist/types.d.ts
CHANGED
|
@@ -75,13 +75,31 @@ export interface PackAuthor {
|
|
|
75
75
|
handle: string;
|
|
76
76
|
displayName: string;
|
|
77
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
|
+
}
|
|
78
90
|
export interface PackManifest {
|
|
79
91
|
name: string;
|
|
80
92
|
version: string;
|
|
81
93
|
publisher: string;
|
|
82
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
|
+
*/
|
|
83
100
|
requires: {
|
|
84
101
|
hqCore: string;
|
|
102
|
+
packs?: PackDependency[];
|
|
85
103
|
};
|
|
86
104
|
contributes: Partial<Record<PackContributeKey, string[]>>;
|
|
87
105
|
description?: string;
|
|
@@ -100,6 +100,13 @@ export declare function readPackManifest(packDir: string): {
|
|
|
100
100
|
* of truth for installed content packs.
|
|
101
101
|
*/
|
|
102
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[];
|
|
103
110
|
export interface UnwireResult {
|
|
104
111
|
unlinked: Array<{
|
|
105
112
|
key: PackContributeKey;
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* `merge` rows -- they never produce a symlink.
|
|
38
38
|
*/
|
|
39
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]="
|
|
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){}}();
|
|
41
41
|
import * as fs from 'fs';
|
|
42
42
|
import * as path from 'path';
|
|
43
43
|
import * as yaml from 'js-yaml';
|
|
@@ -213,6 +213,16 @@ export function listInstalledPacks(hqRoot) {
|
|
|
213
213
|
}
|
|
214
214
|
return out;
|
|
215
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
|
+
}
|
|
216
226
|
/**
|
|
217
227
|
* Remove only the host symlinks that resolve into THIS pack's directory
|
|
218
228
|
* (status `live` or `broken`). Foreign links and real files are left in place
|
|
@@ -318,4 +328,4 @@ export function readRecommendedPackages(hqRoot) {
|
|
|
318
328
|
}
|
|
319
329
|
}
|
|
320
330
|
//# sourceMappingURL=pack-contributions.js.map
|
|
321
|
-
//# debugId=
|
|
331
|
+
//# debugId=63e778df-65dd-51a4-8a43-3a7d2aa9a451
|
|
@@ -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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { buildCreateCompanyWarning } from "./onboard-warning.js";
|
|
4
|
+
|
|
5
|
+
describe("buildCreateCompanyWarning", () => {
|
|
6
|
+
it("documents the warning shown on the create-company path", () => {
|
|
7
|
+
const warning = buildCreateCompanyWarning();
|
|
8
|
+
expect(warning).toContain("NEW company");
|
|
9
|
+
expect(warning).toMatch(/does NOT join an existing one/i);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("advises existing-company users to ask an admin or owner for an invite", () => {
|
|
13
|
+
const warning = buildCreateCompanyWarning();
|
|
14
|
+
expect(warning).toMatch(/ask your admin or owner.*invite/i);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("tells invitees to run hq sync after accepting", () => {
|
|
18
|
+
const warning = buildCreateCompanyWarning();
|
|
19
|
+
expect(warning).toContain("accept it and run `hq sync`");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("warns that duplicate creation leaves the user alone in a separate company", () => {
|
|
23
|
+
const warning = buildCreateCompanyWarning();
|
|
24
|
+
expect(warning).toMatch(/separate one you'd be alone in/i);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the static create-time warning surfaced by both `create-company` and
|
|
3
|
+
* `dry-run`, because users may accidentally create duplicate companies instead
|
|
4
|
+
* of joining the existing company they were invited to.
|
|
5
|
+
*/
|
|
6
|
+
export function buildCreateCompanyWarning(): string {
|
|
7
|
+
return (
|
|
8
|
+
" This creates a NEW company that you own — it does NOT join an existing one.\n" +
|
|
9
|
+
" If your company already uses HQ, do NOT create it again: ask your admin or owner to send you an invite, then accept it and run `hq sync` to pull the existing company.\n" +
|
|
10
|
+
" Creating a same-named company makes a separate one you'd be alone in.\n"
|
|
11
|
+
);
|
|
12
|
+
}
|
package/src/commands/onboard.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import { createDefaultVaultClient } from "./cloud-provision.js";
|
|
33
33
|
import { detectOnboardingIdentityMismatch } from "./onboard-identity-guard.js";
|
|
34
34
|
import { planOnboardJoin } from "./onboard-join.js";
|
|
35
|
+
import { buildCreateCompanyWarning } from "./onboard-warning.js";
|
|
35
36
|
|
|
36
37
|
// ---------------------------------------------------------------------------
|
|
37
38
|
// Command registration
|
|
@@ -70,13 +71,7 @@ export function registerOnboardCommand(program: Command): void {
|
|
|
70
71
|
console.log(` Company: ${options.name} (${options.slug})`);
|
|
71
72
|
console.log(` Person: ${options.personName} <${options.email}>`);
|
|
72
73
|
console.log(` HQ root: ${options.hqRoot}\n`);
|
|
73
|
-
console.log(
|
|
74
|
-
chalk.gray(
|
|
75
|
-
" This creates a NEW company you own. Joining a teammate's existing\n" +
|
|
76
|
-
" company? Stop — accept your invite, then run `hq sync` to pull it.\n" +
|
|
77
|
-
" Creating a same-named company leaves you alone in a separate one.\n",
|
|
78
|
-
),
|
|
79
|
-
);
|
|
74
|
+
console.log(chalk.gray(buildCreateCompanyWarning()));
|
|
80
75
|
|
|
81
76
|
const accessToken = await ensureCognitoToken();
|
|
82
77
|
const result = await runOnboardCli({
|
|
@@ -248,6 +243,8 @@ export function registerOnboardCommand(program: Command): void {
|
|
|
248
243
|
)
|
|
249
244
|
.action(async (options: { hqRoot: string }) => {
|
|
250
245
|
try {
|
|
246
|
+
console.log(chalk.gray(buildCreateCompanyWarning()));
|
|
247
|
+
|
|
251
248
|
// No auth needed for dry-run — runOnboardCli handles this branch
|
|
252
249
|
// without touching the vault-service.
|
|
253
250
|
const result = await runOnboardCli({
|