@indigoai-us/hq-cli 5.50.1 → 5.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/bin/hq-auth-refresh.d.ts +1 -1
  2. package/dist/bin/hq-auth-refresh.js +5 -2
  3. package/dist/commands/members.d.ts +17 -0
  4. package/dist/commands/members.js +65 -28
  5. package/dist/commands/onboard-warning.d.ts +7 -0
  6. package/dist/commands/onboard-warning.js +14 -0
  7. package/dist/commands/onboard.js +5 -5
  8. package/dist/commands/pack-install.d.ts +12 -0
  9. package/dist/commands/pack-install.js +74 -3
  10. package/dist/commands/packs.js +17 -3
  11. package/dist/commands/people.d.ts +26 -1
  12. package/dist/commands/people.js +70 -7
  13. package/dist/commands/secrets-scope.d.ts +20 -0
  14. package/dist/commands/secrets-scope.js +19 -0
  15. package/dist/commands/secrets.js +21 -6
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +44 -14
  18. package/dist/node-preflight.d.ts +39 -0
  19. package/dist/node-preflight.js +55 -0
  20. package/dist/sentry.d.ts +12 -0
  21. package/dist/sentry.js +19 -3
  22. package/dist/types.d.ts +18 -0
  23. package/dist/utils/epipe.d.ts +8 -0
  24. package/dist/utils/epipe.js +30 -0
  25. package/dist/utils/intercepted-process-exit.d.ts +7 -0
  26. package/dist/utils/intercepted-process-exit.js +38 -0
  27. package/dist/utils/pack-contributions.d.ts +7 -0
  28. package/dist/utils/pack-contributions.js +12 -2
  29. package/dist/utils/version-gate.d.ts +40 -1
  30. package/dist/utils/version-gate.js +91 -20
  31. package/e2e/cli.test.ts +35 -0
  32. package/package.json +1 -1
  33. package/src/bin/hq-auth-refresh.ts +3 -0
  34. package/src/commands/members.test.ts +176 -0
  35. package/src/commands/members.ts +113 -28
  36. package/src/commands/onboard-warning.test.ts +26 -0
  37. package/src/commands/onboard-warning.ts +12 -0
  38. package/src/commands/onboard.ts +4 -7
  39. package/src/commands/pack-install.test.ts +144 -0
  40. package/src/commands/pack-install.ts +86 -1
  41. package/src/commands/packs.ts +19 -0
  42. package/src/commands/people.test.ts +212 -5
  43. package/src/commands/people.ts +141 -5
  44. package/src/commands/secrets-scope.test.ts +56 -0
  45. package/src/commands/secrets-scope.ts +32 -0
  46. package/src/commands/secrets.ts +24 -10
  47. package/src/index.ts +40 -12
  48. package/src/node-preflight.test.ts +60 -0
  49. package/src/node-preflight.ts +67 -0
  50. package/src/sentry-epipe.test.ts +37 -0
  51. package/src/sentry-release.test.ts +54 -0
  52. package/src/sentry.ts +21 -1
  53. package/src/types.ts +19 -1
  54. package/src/utils/epipe.test.ts +28 -0
  55. package/src/utils/epipe.ts +29 -0
  56. package/src/utils/intercepted-process-exit.test.ts +37 -0
  57. package/src/utils/intercepted-process-exit.ts +36 -0
  58. package/src/utils/pack-contributions.test.ts +53 -0
  59. package/src/utils/pack-contributions.ts +17 -0
  60. package/src/utils/version-gate.test.ts +122 -0
  61. package/src/utils/version-gate.ts +109 -13
@@ -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]="3049e431-c5db-5430-90fe-82866c35e95b")}catch(e){}}();
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]="96461f1a-6d5b-5dc0-b00a-0a9fa30a0dad")}catch(e){}}();
3
3
  import chalk from "chalk";
4
4
  import * as readline from "node:readline";
5
5
  import { spawn } from "node:child_process";
@@ -8,6 +8,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
8
8
  import { DEFAULT_SECRETS_CACHE_TTL_MS, readCache, writeCache, removeCacheEntry, clearAllCache, } from "../utils/secrets-cache.js";
9
9
  import { computeSha256 } from "../utils/integrity.js";
10
10
  import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN } from "./_patterns.js";
11
+ import { describeSecretsScope, formatSecretSaved, formatSecretsListEmpty, formatSecretsListHeader, } from "./secrets-scope.js";
11
12
  import { vaultApiFetch, getCompanyUid, getEntityUid, } from "../utils/vault-api.js";
12
13
  export { vaultApiFetch, getCompanyUid, getEntityUid };
13
14
  function scopeOpts(opts) {
@@ -344,7 +345,13 @@ export function registerSecretsCommand(program) {
344
345
  process.exit(1);
345
346
  }
346
347
  const token = await ensureCognitoToken();
347
- const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
348
+ const scope = scopeOpts(secrets.opts());
349
+ const companyUid = await getEntityUid(token, scope);
350
+ const scopeLabel = describeSecretsScope({
351
+ personal: scope.personal,
352
+ companySlug: scope.companySlug,
353
+ companyUid,
354
+ });
348
355
  const res = await vaultApiFetch({
349
356
  token,
350
357
  path: `/secrets/${encodeURIComponent(companyUid)}`,
@@ -357,7 +364,7 @@ export function registerSecretsCommand(program) {
357
364
  process.exit(1);
358
365
  }
359
366
  removeCacheEntry(companyUid, name);
360
- console.log(chalk.green(`Secret '${name}' saved.`));
367
+ console.log(chalk.green(formatSecretSaved(name, scopeLabel)));
361
368
  }
362
369
  catch (err) {
363
370
  console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
@@ -487,7 +494,13 @@ export function registerSecretsCommand(program) {
487
494
  normalizedPrefix = normalized;
488
495
  }
489
496
  const token = await ensureCognitoToken();
490
- const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
497
+ const scope = scopeOpts(secrets.opts());
498
+ const companyUid = await getEntityUid(token, scope);
499
+ const scopeLabel = describeSecretsScope({
500
+ personal: scope.personal,
501
+ companySlug: scope.companySlug,
502
+ companyUid,
503
+ });
491
504
  const query = {};
492
505
  if (normalizedPrefix) {
493
506
  query.prefix = normalizedPrefix;
@@ -504,7 +517,7 @@ export function registerSecretsCommand(program) {
504
517
  }
505
518
  const data = (await res.json());
506
519
  if (data.secrets.length === 0) {
507
- console.log(chalk.dim("No secrets found."));
520
+ console.log(chalk.dim(formatSecretsListEmpty(scopeLabel)));
508
521
  return;
509
522
  }
510
523
  const nameWidth = Math.max(4, ...data.secrets.map((s) => s.name.length));
@@ -514,6 +527,7 @@ export function registerSecretsCommand(program) {
514
527
  if (hasPermission) {
515
528
  const accessWidth = Math.max(6, ...data.secrets.map((s) => (s.permission ?? "-").length));
516
529
  const header = `${"NAME".padEnd(nameWidth)} ${"ACCESS".padEnd(accessWidth)} ${"TIER".padEnd(tierWidth)} ${"SCRIPT LOCK".padEnd(scriptLockWidth)} LAST MODIFIED`;
530
+ console.log(chalk.dim(formatSecretsListHeader(scopeLabel)));
517
531
  console.log(chalk.bold(header));
518
532
  for (const s of data.secrets) {
519
533
  const access = s.permission ?? "-";
@@ -525,6 +539,7 @@ export function registerSecretsCommand(program) {
525
539
  }
526
540
  else {
527
541
  const header = `${"NAME".padEnd(nameWidth)} ${"TIER".padEnd(tierWidth)} ${"SCRIPT LOCK".padEnd(scriptLockWidth)} LAST MODIFIED`;
542
+ console.log(chalk.dim(formatSecretsListHeader(scopeLabel)));
528
543
  console.log(chalk.bold(header));
529
544
  for (const s of data.secrets) {
530
545
  const tier = normalizeSecretTier(s.tier);
@@ -1148,4 +1163,4 @@ export function registerSecretsCommand(program) {
1148
1163
  });
1149
1164
  }
1150
1165
  //# sourceMappingURL=secrets.js.map
1151
- //# debugId=3049e431-c5db-5430-90fe-82866c35e95b
1166
+ //# debugId=96461f1a-6d5b-5dc0-b00a-0a9fa30a0dad
package/dist/index.d.ts CHANGED
@@ -2,5 +2,5 @@
2
2
  /**
3
3
  * HQ CLI - Module management, package management, and cloud sync for HQ
4
4
  */
5
- export {};
5
+ import "./node-preflight.js";
6
6
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -2,8 +2,11 @@
2
2
  /**
3
3
  * HQ CLI - Module management, package management, and cloud sync for HQ
4
4
  */
5
+ // MUST be first: guard the Node version before any dependency that needs a
6
+ // Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
5
7
 
6
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="34e9e978-4c0a-5113-82af-9a33e30686c6")}catch(e){}}();
8
+ !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]="82c44c14-a953-5644-9bf2-dd2c6d487d63")}catch(e){}}();
9
+ import "./node-preflight.js";
7
10
  import { Command } from "commander";
8
11
  import { initSentry, Sentry } from "./sentry.js";
9
12
  import { registerAddCommand } from "./commands/add.js";
@@ -47,12 +50,18 @@ import { registerRescueCommand } from "./commands/rescue.js";
47
50
  import { registerMcpCommand } from "./commands/mcp-status.js";
48
51
  import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
49
52
  import { environmentalFsErrorMessage } from "./utils/environmental-error.js";
53
+ import { isEpipe } from "./utils/epipe.js";
54
+ import { isInterceptedProcessExit } from "./utils/intercepted-process-exit.js";
50
55
  import { maybeWarnNewVersion, refreshVersionCache, } from "./utils/version-check.js";
51
56
  import { enforceVersionGate, shouldSkipGate, } from "./utils/version-gate.js";
52
57
  import { CLI_VERSION } from "./cli-version.js";
53
- // Swallow EPIPE when a downstream reader (e.g. `source <(…)`, `| head`) closes the pipe early.
58
+ // Swallow EPIPE when a downstream reader (e.g. `source <(…)`, `| head`) closes
59
+ // the pipe early. This covers the ASYNC path — an 'error' event emitted on the
60
+ // stream. The SYNCHRONOUS path (a `write EPIPE` thrown straight out of
61
+ // console.log inside a command) is handled in the top-level catch below; both
62
+ // share `isEpipe` (HQ-6B).
54
63
  const onPipeError = (err) => {
55
- if (err.code === "EPIPE") {
64
+ if (isEpipe(err)) {
56
65
  process.exit(0);
57
66
  }
58
67
  throw err;
@@ -182,19 +191,40 @@ registerMcpCommand(program);
182
191
  await program.parseAsync();
183
192
  }
184
193
  catch (err) {
185
- // A full disk / exhausted quota / read-only filesystem is the user's
186
- // machine, not an HQ code defect. Surface a clear, actionable message and
187
- // skip Sentry capture so one full disk doesn't flood the tracker with
188
- // identical, unfixable crash reports (HQ-CLI-2). Genuine errors still go
189
- // to Sentry and still exit 1.
190
- const envMsg = environmentalFsErrorMessage(err);
191
- if (envMsg) {
192
- process.stderr.write(`hq: ${envMsg}\n`);
194
+ // A broken pipe (EPIPE) means the reader of `hq`'s output closed it early
195
+ // (`hq | head`, `source <(hq …)`, a parent that exited). That is normal
196
+ // Unix behavior with no user-facing degradation exit cleanly (0) and
197
+ // skip Sentry capture instead of shipping a fatal (HQ-6B). A synchronous
198
+ // `write EPIPE` thrown out of console.log lands here rather than on the
199
+ // stream 'error' listener above.
200
+ if (isEpipe(err)) {
201
+ process.exitCode = 0;
202
+ }
203
+ else if (isInterceptedProcessExit(err)) {
204
+ // A security/audit FUZZ harness replaced `process.exit` with a throw so it
205
+ // can keep exercising the binary. Commander calling `process.exit` for
206
+ // normal CLI control flow (e.g. an unknown command → exit 1) then surfaces
207
+ // here as that synthetic marker. It is a test-harness artifact, NOT an
208
+ // hq-cli defect — a real user's `process.exit` just exits, so nothing is
209
+ // thrown or captured. Skip Sentry capture (no signal, no user-facing
210
+ // degradation) and preserve the intended non-zero exit (HQ-CLI-3).
211
+ process.exitCode = 1;
193
212
  }
194
213
  else {
195
- Sentry.captureException(err);
214
+ // A full disk / exhausted quota / read-only filesystem is the user's
215
+ // machine, not an HQ code defect. Surface a clear, actionable message and
216
+ // skip Sentry capture so one full disk doesn't flood the tracker with
217
+ // identical, unfixable crash reports (HQ-CLI-2). Genuine errors still go
218
+ // to Sentry and still exit 1.
219
+ const envMsg = environmentalFsErrorMessage(err);
220
+ if (envMsg) {
221
+ process.stderr.write(`hq: ${envMsg}\n`);
222
+ }
223
+ else {
224
+ Sentry.captureException(err);
225
+ }
226
+ process.exitCode = 1;
196
227
  }
197
- process.exitCode = 1;
198
228
  }
199
229
  finally {
200
230
  // Release health: finalize the per-run session before the flush.
@@ -203,4 +233,4 @@ registerMcpCommand(program);
203
233
  }
204
234
  })();
205
235
  //# sourceMappingURL=index.js.map
206
- //# debugId=34e9e978-4c0a-5113-82af-9a33e30686c6
236
+ //# debugId=82c44c14-a953-5644-9bf2-dd2c6d487d63
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Runtime Node.js version guard for the hq CLI.
3
+ *
4
+ * HQ tooling requires Node.js 20 or newer. On older runtimes (notably Node 18)
5
+ * the CLI dies with cryptic failures long before reaching any of its own code:
6
+ * a native-module ABI mismatch from a prebuilt dependency, and a missing
7
+ * `util.styleText` (added in Node 20). Those errors give the user no hint that
8
+ * the real problem is just an old Node.
9
+ *
10
+ * This module exists to fail fast with an actionable message instead. It is
11
+ * imported FIRST by every CLI entry point (`index.ts`, `bin/hq-auth-refresh.ts`)
12
+ * so the check runs before commander, Sentry, or any dependency that needs a
13
+ * Node 20+ API or a newer native ABI is evaluated. ES modules evaluate their
14
+ * imports in source order, so as long as this is the first import in the entry
15
+ * module, the guard short-circuits an unsupported runtime cleanly.
16
+ *
17
+ * Keep this file dependency-free — it must not import anything that could itself
18
+ * fail to load on the very runtime it is trying to detect.
19
+ */
20
+ export declare const MIN_NODE_MAJOR = 20;
21
+ export interface NodeVersionCheck {
22
+ ok: boolean;
23
+ major: number;
24
+ message?: string;
25
+ }
26
+ /**
27
+ * Pure check: is the given Node version string (e.g. "18.19.0") supported?
28
+ * Defaults to the running runtime's version. An unparseable version is treated
29
+ * as supported so we never block a user on a version string we can't read.
30
+ */
31
+ export declare function checkNodeVersion(versionString?: string): NodeVersionCheck;
32
+ /**
33
+ * Side-effecting guard run on import: prints the upgrade message to stderr and
34
+ * exits 1 on an unsupported runtime. A no-op on Node 20+. Set
35
+ * `HQ_SKIP_NODE_PREFLIGHT=1` to bypass (used by the test runner, which already
36
+ * runs on a supported Node).
37
+ */
38
+ export declare function enforceNodeVersion(): void;
39
+ //# sourceMappingURL=node-preflight.d.ts.map
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Runtime Node.js version guard for the hq CLI.
3
+ *
4
+ * HQ tooling requires Node.js 20 or newer. On older runtimes (notably Node 18)
5
+ * the CLI dies with cryptic failures long before reaching any of its own code:
6
+ * a native-module ABI mismatch from a prebuilt dependency, and a missing
7
+ * `util.styleText` (added in Node 20). Those errors give the user no hint that
8
+ * the real problem is just an old Node.
9
+ *
10
+ * This module exists to fail fast with an actionable message instead. It is
11
+ * imported FIRST by every CLI entry point (`index.ts`, `bin/hq-auth-refresh.ts`)
12
+ * so the check runs before commander, Sentry, or any dependency that needs a
13
+ * Node 20+ API or a newer native ABI is evaluated. ES modules evaluate their
14
+ * imports in source order, so as long as this is the first import in the entry
15
+ * module, the guard short-circuits an unsupported runtime cleanly.
16
+ *
17
+ * Keep this file dependency-free — it must not import anything that could itself
18
+ * fail to load on the very runtime it is trying to detect.
19
+ */
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]="7d63229d-ff96-5511-9601-53c556487763")}catch(e){}}();
22
+ export const MIN_NODE_MAJOR = 20;
23
+ /**
24
+ * Pure check: is the given Node version string (e.g. "18.19.0") supported?
25
+ * Defaults to the running runtime's version. An unparseable version is treated
26
+ * as supported so we never block a user on a version string we can't read.
27
+ */
28
+ export function checkNodeVersion(versionString = process.versions.node) {
29
+ const major = Number.parseInt(String(versionString).split(".")[0] ?? "", 10);
30
+ if (!Number.isFinite(major) || major >= MIN_NODE_MAJOR) {
31
+ return { ok: true, major };
32
+ }
33
+ const message = `hq requires Node.js ${MIN_NODE_MAJOR} or newer — you are running Node ${versionString}.\n` +
34
+ `Older versions fail with native-module ABI mismatches and missing APIs.\n` +
35
+ `Please upgrade to Node ${MIN_NODE_MAJOR}+ (https://nodejs.org/) and run hq again.`;
36
+ return { ok: false, major, message };
37
+ }
38
+ /**
39
+ * Side-effecting guard run on import: prints the upgrade message to stderr and
40
+ * exits 1 on an unsupported runtime. A no-op on Node 20+. Set
41
+ * `HQ_SKIP_NODE_PREFLIGHT=1` to bypass (used by the test runner, which already
42
+ * runs on a supported Node).
43
+ */
44
+ export function enforceNodeVersion() {
45
+ if (process.env.HQ_SKIP_NODE_PREFLIGHT)
46
+ return;
47
+ const result = checkNodeVersion();
48
+ if (!result.ok && result.message) {
49
+ process.stderr.write(`${result.message}\n`);
50
+ process.exit(1);
51
+ }
52
+ }
53
+ enforceNodeVersion();
54
+ //# sourceMappingURL=node-preflight.js.map
55
+ //# debugId=7d63229d-ff96-5511-9601-53c556487763
package/dist/sentry.d.ts CHANGED
@@ -1,4 +1,16 @@
1
1
  import * as Sentry from "@sentry/node";
2
+ import type { ErrorEvent, EventHint } from "@sentry/node";
3
+ /**
4
+ * Drop broken-pipe (EPIPE) crashes before scrubbing/send. A closed downstream
5
+ * reader (`hq … | head`, `source <(hq …)`, a parent that exited) is normal
6
+ * Unix behavior with no user-facing degradation — never a defect to report.
7
+ * This is the path-independent catch-all for HQ-6B: whichever way an EPIPE
8
+ * surfaces (a synchronous throw into the command catch, an async stream
9
+ * 'error', or a write outside the top-level try), it never ships a fatal. The
10
+ * clean-exit handlers in index.ts still keep exit code 0 for the common paths.
11
+ * Consistent with the swallow-EPIPE posture established in #138.
12
+ */
13
+ export declare function epipeAwareBeforeSend(event: ErrorEvent, hint: EventHint): ErrorEvent | null;
2
14
  export declare function initSentry(): void;
3
15
  export { Sentry };
4
16
  //# sourceMappingURL=sentry.d.ts.map
package/dist/sentry.js CHANGED
@@ -1,11 +1,27 @@
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]="a260ea42-f6d8-52d0-90c6-6e586de5efd6")}catch(e){}}();
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]="a2ba6d5a-af8a-5512-9ce8-ced925ad5505")}catch(e){}}();
3
3
  import * as Sentry from "@sentry/node";
4
4
  import { BUNDLED_DSN } from "./sentry-dsn.generated.js";
5
5
  import { beforeSend } from "./sentry-before-send.js";
6
6
  import { beforeBreadcrumb } from "./utils/breadcrumb-buffer.js";
7
7
  import { CLI_VERSION } from "./cli-version.js";
8
8
  import { getCachedSentryUser } from "./utils/sentry-identity.js";
9
+ import { isEpipe } from "./utils/epipe.js";
10
+ /**
11
+ * Drop broken-pipe (EPIPE) crashes before scrubbing/send. A closed downstream
12
+ * reader (`hq … | head`, `source <(hq …)`, a parent that exited) is normal
13
+ * Unix behavior with no user-facing degradation — never a defect to report.
14
+ * This is the path-independent catch-all for HQ-6B: whichever way an EPIPE
15
+ * surfaces (a synchronous throw into the command catch, an async stream
16
+ * 'error', or a write outside the top-level try), it never ships a fatal. The
17
+ * clean-exit handlers in index.ts still keep exit code 0 for the common paths.
18
+ * Consistent with the swallow-EPIPE posture established in #138.
19
+ */
20
+ export function epipeAwareBeforeSend(event, hint) {
21
+ if (isEpipe(hint?.originalException))
22
+ return null;
23
+ return beforeSend(event, hint);
24
+ }
9
25
  export function initSentry() {
10
26
  const dsn = BUNDLED_DSN || process.env.SENTRY_DSN;
11
27
  if (!dsn)
@@ -20,7 +36,7 @@ export function initSentry() {
20
36
  initialScope: {
21
37
  tags: { repo: "hq-cli" },
22
38
  },
23
- beforeSend,
39
+ beforeSend: epipeAwareBeforeSend,
24
40
  beforeBreadcrumb,
25
41
  });
26
42
  // Attribute events to the logged-in HQ identity (best-effort; null when not
@@ -35,4 +51,4 @@ export function initSentry() {
35
51
  }
36
52
  export { Sentry };
37
53
  //# sourceMappingURL=sentry.js.map
38
- //# debugId=a260ea42-f6d8-52d0-90c6-6e586de5efd6
54
+ //# debugId=a2ba6d5a-af8a-5512-9ce8-ced925ad5505
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;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * True when `err` is a Node EPIPE error — the pipe `hq` was writing to was
3
+ * closed by its reader. Callers should treat this as a clean, expected exit
4
+ * (code 0) and SKIP Sentry capture: there is no defect and no user-facing
5
+ * degradation to fix.
6
+ */
7
+ export declare function isEpipe(err: unknown): boolean;
8
+ //# sourceMappingURL=epipe.d.ts.map
@@ -0,0 +1,30 @@
1
+ // src/utils/epipe.ts
2
+ //
3
+ // Classify the "broken pipe" (EPIPE) error: a downstream reader closed the
4
+ // pipe before the CLI finished writing to stdout/stderr. This is normal,
5
+ // expected Unix behavior — `hq … | head`, `source <(hq …)`, or a parent
6
+ // process that exits while `hq` is still printing — NOT an HQ code defect.
7
+ //
8
+ // HQ-6B: a synchronous `write EPIPE` thrown from `console.log` (the Sentry
9
+ // console-instrumentation wraps it) propagated out of an awaited command into
10
+ // the CLI's top-level `catch`, which captured it via `Sentry.captureException`
11
+ // and shipped a fatal — even though the only thing that happened is the
12
+ // consumer of `hq`'s output went away. The async path (an emitted stream
13
+ // 'error' event) was already handled by the stdout/stderr listeners in
14
+ // index.ts; this predicate closes the synchronous-throw path too, mirroring
15
+ // the environmental-error carve-out (HQ-CLI-2).
16
+ /**
17
+ * True when `err` is a Node EPIPE error — the pipe `hq` was writing to was
18
+ * closed by its reader. Callers should treat this as a clean, expected exit
19
+ * (code 0) and SKIP Sentry capture: there is no defect and no user-facing
20
+ * degradation to fix.
21
+ */
22
+
23
+ !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]="3305e402-0237-55ac-a6d4-11143bc69b83")}catch(e){}}();
24
+ export function isEpipe(err) {
25
+ return (typeof err === "object" &&
26
+ err !== null &&
27
+ err.code === "EPIPE");
28
+ }
29
+ //# sourceMappingURL=epipe.js.map
30
+ //# debugId=3305e402-0237-55ac-a6d4-11143bc69b83
@@ -0,0 +1,7 @@
1
+ /**
2
+ * True when `err` is a fuzz/audit harness's intercepted-`process.exit` marker
3
+ * rather than a genuine fault. Callers should SKIP Sentry capture (no defect, no
4
+ * user-facing degradation) while preserving the intended exit code.
5
+ */
6
+ export declare function isInterceptedProcessExit(err: unknown): boolean;
7
+ //# sourceMappingURL=intercepted-process-exit.d.ts.map
@@ -0,0 +1,38 @@
1
+ // src/utils/intercepted-process-exit.ts
2
+ //
3
+ // Classify the "intercepted process.exit" synthetic error (HQ-CLI-3).
4
+ //
5
+ // Security/audit FUZZ harnesses (e.g. `/opt/audit/fuzz-exports.js`) monkey-patch
6
+ // `process.exit` to THROW a marker instead of actually terminating, so the
7
+ // fuzzer can keep exercising a binary's entrypoints. When such a harness drives
8
+ // `hq` (e.g. `hq <unknown-command>`), commander legitimately calls
9
+ // `process.exit(1)` — and the patched exit turns that normal CLI control-flow
10
+ // into a thrown `Error: fuzz-intercepted-process-exit` that propagates out of
11
+ // `program.parseAsync()` into the CLI's top-level catch, which then shipped it to
12
+ // Sentry as a fatal.
13
+ //
14
+ // This is a test-harness artifact, NOT an hq-cli defect: a real user's
15
+ // `process.exit` exits the process and nothing is ever thrown or captured.
16
+ // There is no user-facing degradation to fix, so the top-level catch skips
17
+ // Sentry capture for it — the genuine "expected, non-actionable" carve-out,
18
+ // mirroring the EPIPE (HQ-6B) and environmental-FS (HQ-CLI-2) carve-outs.
19
+ /** The marker a fuzz/audit harness throws in place of a real `process.exit`. */
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]="55c7e3b5-a8bc-50fd-835b-127a19172aa1")}catch(e){}}();
22
+ const INTERCEPTED_PROCESS_EXIT_MARKER = "fuzz-intercepted-process-exit";
23
+ /**
24
+ * True when `err` is a fuzz/audit harness's intercepted-`process.exit` marker
25
+ * rather than a genuine fault. Callers should SKIP Sentry capture (no defect, no
26
+ * user-facing degradation) while preserving the intended exit code.
27
+ */
28
+ export function isInterceptedProcessExit(err) {
29
+ if (typeof err === "object" && err !== null) {
30
+ const message = err.message;
31
+ if (typeof message === "string" && message.includes(INTERCEPTED_PROCESS_EXIT_MARKER)) {
32
+ return true;
33
+ }
34
+ }
35
+ return typeof err === "string" && err.includes(INTERCEPTED_PROCESS_EXIT_MARKER);
36
+ }
37
+ //# sourceMappingURL=intercepted-process-exit.js.map
38
+ //# debugId=55c7e3b5-a8bc-50fd-835b-127a19172aa1
@@ -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