@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.
@@ -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
@@ -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]="05a49c9b-23bc-5b96-b5e1-cc2e573b11c3")}catch(e){}}();
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(" This creates a NEW company you own. Joining a teammate's existing\n" +
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=05a49c9b-23bc-5b96-b5e1-cc2e573b11c3
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]="fbfbcf69-d410-51cb-ae59-886c451b3a55")}catch(e){}}();
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=fbfbcf69-d410-51cb-ae59-886c451b3a55
1632
+ //# debugId=0ec07d3a-ee86-51fd-adbd-2608c620ae99
@@ -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]="570f7491-8d05-577c-8bae-6f6cf7dfb2ec")}catch(e){}}();
21
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="69746edd-f9d8-528e-8902-de799c58bd8a")}catch(e){}}();
22
22
  import * as fs from 'fs';
23
23
  import * as path from 'path';
24
24
  import * as readline from 'readline';
@@ -27,7 +27,7 @@ import chalk from 'chalk';
27
27
  import semverSatisfies from 'semver/functions/satisfies.js';
28
28
  import { findHqRoot } from '../utils/manifest.js';
29
29
  import { classify, resolveLatest, resolveLatestMarketplace, runScanPackages, installPack, } from './pack-install.js';
30
- import { contributionLinks, linkStatus, listInstalledPacks, readPackManifest, unwirePack, 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=570f7491-8d05-577c-8bae-6f6cf7dfb2ec
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]="6cc0fc56-5a1c-5ce8-96d4-652805e508f8")}catch(e){}}();
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=6cc0fc56-5a1c-5ce8-96d4-652805e508f8
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
- declare function performUpdate(command: string): {
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]="d7d58261-f0e4-56bc-b6eb-4117731744f9")}catch(e){}}();
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
- if (!command) {
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
- console.error(chalk.dim(` Running: ${command}`));
138
- const result = performUpdate(command);
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
- console.error(chalk.dim(` Try manually: ${command}`));
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=d7d58261-f0e4-56bc-b6eb-4117731744f9
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.1",
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": {
@@ -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
+ }
@@ -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({