@indigoai-us/hq-cli 5.106.2 → 5.106.3

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/CHANGELOG.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.106.3] — 2026-09-02
6
+
5
7
  ## [5.106.2] — 2026-09-02
6
8
 
7
9
  ### Fixed
@@ -8,6 +8,8 @@ import { computeSha256 } from '../utils/integrity.js';
8
8
  import { vaultApiFetch, getCompanyUid } from '../utils/vault-api.js';
9
9
  import { discoverSchemas } from '../run/discover-schemas.js';
10
10
  import { installHqPlugin, prewarmHqSecrets } from '../run/hq-plugin.js';
11
+ import { assertEnvGraphLoaded, resolveEnvValuesOrThrow, EnvGraphLoadError, EnvResolutionError, } from '../run/env-graph-guard.js';
12
+ import { hadFatalRejection } from '../unhandled-rejection-boundary.js';
11
13
  const SECRET_LOAD_TIMEOUT_MS = 30_000;
12
14
  export async function buildRunUsage(scriptPath, scriptId) {
13
15
  if (scriptId && !scriptPath) {
@@ -119,8 +121,18 @@ export function registerRunCommand(program) {
119
121
  entryFilePaths: paths,
120
122
  afterInit: async (g) => { state = installHqPlugin(g, pluginOpts); },
121
123
  });
124
+ // Refuse to resolve a graph varlock never finished loading (a malformed
125
+ // .env.schema / .env.local): resolving an unprocessed item whose key is
126
+ // also in process.env reaches varlock's `expected dataType to be set`
127
+ // invariant as a FLOATED, never-settling rejection — the CLI then exits
128
+ // 0 with the command never run (HQ-CLI-W). Run this BEFORE prewarm so a
129
+ // bad file can never trigger a secret fetch.
130
+ assertEnvGraphLoaded(graph);
122
131
  await prewarmHqSecrets(graph, pluginOpts, state);
123
- await graph.resolveEnvValues();
132
+ // Defence in depth: convert a floated resolution rejection into a thrown
133
+ // error instead of hanging on varlock's never-settling promise, in case
134
+ // the shape-dependent guard above ever silently degrades.
135
+ await resolveEnvValuesOrThrow(graph);
124
136
  const schemaErrors = Object.entries(graph.configSchema)
125
137
  .filter(([, item]) => item.errors?.length > 0);
126
138
  if (schemaErrors.length > 0) {
@@ -147,10 +159,21 @@ export function registerRunCommand(program) {
147
159
  if (signal) {
148
160
  process.kill(process.pid, signal);
149
161
  }
150
- process.exit(code ?? 1);
162
+ // A fatal floated rejection during the child run set process.exitCode,
163
+ // which this explicit exit would otherwise erase. Force a non-zero
164
+ // code so such a rejection is never masked by a child that exited 0,
165
+ // while still propagating a genuine non-zero child status.
166
+ process.exit(hadFatalRejection() ? (code || 1) : (code ?? 1));
151
167
  });
152
168
  }
153
169
  catch (err) {
170
+ // Typed varlock env-schema failures are the user's file, not an hq-cli
171
+ // defect. Re-throw them so main.ts's handleTopLevelError prints one
172
+ // actionable `hq:` line and skips Sentry capture; everything else keeps
173
+ // run.ts's existing `Error:`-prefixed exit-1 behaviour.
174
+ if (err instanceof EnvGraphLoadError || err instanceof EnvResolutionError) {
175
+ throw err;
176
+ }
154
177
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
155
178
  process.exit(1);
156
179
  }
package/dist/index.js CHANGED
@@ -7,10 +7,20 @@ import { CLI_VERSION } from "./cli-version.js";
7
7
  // Dependency-light: a pure parser + table, no command graph. Safe to load on
8
8
  // every path (including --version) without reintroducing the heavy startup.
9
9
  import { isFastCoreRequest } from "./commands/scaffold-fast.js";
10
+ // Also dependency-light: registers a process-level rejection boundary; its only
11
+ // heavy import (main.js) is lazy and reached only when a rejection fires.
12
+ import { installProcessRejectionBoundary } from "./unhandled-rejection-boundary.js";
10
13
  function isVersionRequest(argv) {
11
14
  const args = argv.slice(2);
12
15
  return args.length === 1 && (args[0] === "--version" || args[0] === "-V" || args[0] === "-v");
13
16
  }
17
+ // Install BEFORE any dispatch so a floated rejection on ANY path (the command
18
+ // graph or the fast-core forwarder) becomes a non-zero exit. @sentry/node's
19
+ // onUnhandledRejection integration defaults to 'warn' — it captures but does
20
+ // NOT rethrow — so without this the event loop drains and Node exits 0 with the
21
+ // command never run (HQ-CLI-W). Sets process.exitCode only, never process.exit,
22
+ // so runCli's finally still finalizes release health and flushes Sentry.
23
+ installProcessRejectionBoundary();
14
24
  if (isVersionRequest(process.argv)) {
15
25
  process.stdout.write(`${CLI_VERSION}\n`);
16
26
  }
@@ -25,7 +35,8 @@ else {
25
35
  // before the fast --version split. Some best-effort teardown work uses
26
36
  // unref'd handles; top-level-awaiting runCli() makes Node turn an otherwise
27
37
  // successful command into exit 13 when that teardown promise remains pending.
28
- // Rejections still become unhandled and preserve a genuine non-zero failure.
38
+ // A floated rejection here no longer exits 0 silently: the boundary installed
39
+ // above turns it into a non-zero exit (HQ-CLI-W).
29
40
  void import("./main.js").then(({ runCli }) => runCli());
30
41
  }
31
42
  export const __test__ = { isVersionRequest, isFastCoreRequest };
package/dist/main.js CHANGED
@@ -75,6 +75,7 @@ import { qmdStoreMissingMessage } from "./utils/qmd-store-missing-error.js";
75
75
  import { qmdStoreUnopenableMessage } from "./utils/qmd-store-unopenable-error.js";
76
76
  import { hqStateWriteErrorMessage } from "./utils/hq-state-write-error.js";
77
77
  import { isExpectedUserError } from "./utils/expected-cli-error.js";
78
+ import { isVarlockEnvError } from "./run/env-graph-guard.js";
78
79
  import { isEpipe } from "./utils/epipe.js";
79
80
  import { isInterceptedProcessExit } from "./utils/intercepted-process-exit.js";
80
81
  import { isAuthError } from "./utils/auth-error.js";
@@ -463,6 +464,17 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
463
464
  deps.stderr.write(`hq: ${err.message}\n`);
464
465
  deps.setExitCode(1);
465
466
  }
467
+ else if (isVarlockEnvError(err)) {
468
+ // HQ-CLI-W: `hq run` could not load or resolve a .env.schema/.env.local
469
+ // because the file itself is malformed. The typed carrier
470
+ // (EnvGraphLoadError / EnvResolutionError) already names the offending
471
+ // file and the pinned varlock version, so print that one actionable line
472
+ // and skip Sentry capture — a user's un-parseable env file is their file,
473
+ // not an hq-cli defect. A genuinely unclassified floated rejection is not
474
+ // one of these carriers, so it keeps falling through to the capture arm.
475
+ deps.stderr.write(`hq: ${err.message}\n`);
476
+ deps.setExitCode(1);
477
+ }
466
478
  else if (isPackageRootResolutionError(err)) {
467
479
  deps.stderr.write(`hq: ${err.message}\n`);
468
480
  deps.sentry.captureException(err, {
@@ -0,0 +1,83 @@
1
+ /** The varlock version hq-cli is pinned to (package.json `dependencies`). */
2
+ export declare const VARLOCK_PINNED_VERSION = "1.0.0";
3
+ /** A schema/plugin source varlock could not finish loading. */
4
+ interface GuardDataSource {
5
+ readonly isValid: boolean;
6
+ readonly loadingError?: {
7
+ readonly message?: string;
8
+ } | null;
9
+ readonly schemaErrors?: ReadonlyArray<{
10
+ readonly message?: string;
11
+ readonly isWarning?: boolean;
12
+ }>;
13
+ readonly resolutionErrors?: ReadonlyArray<{
14
+ readonly message?: string;
15
+ }>;
16
+ readonly label?: string;
17
+ /** Present on file-based sources (.env.schema / .env.local). */
18
+ readonly fullPath?: string;
19
+ }
20
+ interface GuardPlugin {
21
+ readonly loadingError?: {
22
+ readonly message?: string;
23
+ } | null;
24
+ readonly localPath?: string;
25
+ }
26
+ /**
27
+ * The structural subset of varlock's EnvGraph this module reads. varlock's
28
+ * concrete EnvGraph is assignable to it, so `hq run` passes its graph with no
29
+ * cast. Everything the guard reads is optional so a varlock shape change
30
+ * degrades to "no problems found" rather than a crash — the resolve-time layer
31
+ * and the varlock-shape smoke test (src/run/varlock-shape.test.ts) are what
32
+ * make such a degradation fail loudly.
33
+ */
34
+ export interface LoadedEnvGraph {
35
+ readonly sortedDataSources?: ReadonlyArray<GuardDataSource>;
36
+ readonly plugins?: ReadonlyArray<GuardPlugin>;
37
+ resolveEnvValues(keys?: Array<string>): Promise<void>;
38
+ }
39
+ /**
40
+ * A schema/entry file could not be loaded, so the env graph never finished
41
+ * processing. Carries an actionable, file-naming message; classified by
42
+ * handleTopLevelError as a user-file error (printed, not reported to Sentry).
43
+ */
44
+ export declare class EnvGraphLoadError extends Error {
45
+ constructor(message: string);
46
+ }
47
+ /**
48
+ * Resolving the env graph floated a rejection (varlock's un-awaited resolver).
49
+ * Converted from the process-level unhandled rejection by resolveEnvValuesOrThrow
50
+ * so the promise `hq run` awaits can never hang. Classified as a user-file error.
51
+ */
52
+ export declare class EnvResolutionError extends Error {
53
+ readonly reason: unknown;
54
+ constructor(reason: unknown);
55
+ }
56
+ /** True for either typed carrier this module raises. */
57
+ export declare function isVarlockEnvError(err: unknown): boolean;
58
+ /**
59
+ * Throw a typed, actionable EnvGraphLoadError when varlock did not finish
60
+ * loading the graph — i.e. when it would have bailed out of finishLoad() before
61
+ * processing items, leaving them unresolvable. Mirrors finishLoad's two
62
+ * early-return conditions exactly: any data source with `isValid === false`, or
63
+ * any plugin with a `loadingError`. A fully-valid graph passes silently.
64
+ */
65
+ export declare function assertEnvGraphLoaded(graph: LoadedEnvGraph): void;
66
+ /**
67
+ * Await `graph.resolveEnvValues()`, but convert varlock's floated resolution
68
+ * rejection into a thrown EnvResolutionError instead of hanging on the promise
69
+ * that never settles.
70
+ *
71
+ * varlock@1.0.0's resolveEnvValues dispatches its per-item resolver un-awaited
72
+ * and with a dead `_reject`, so a throw inside it becomes a process-level
73
+ * unhandled rejection AND leaves the returned promise permanently pending. We
74
+ * race the resolve against a scoped `unhandledRejection` listener: whichever
75
+ * settles first wins. The listener is prepended (so the prior listeners still
76
+ * run — nothing is swallowed) and removed in `finally` (restoring the exact
77
+ * prior listener set, including the no-prior-listener case). If resolve settles
78
+ * normally the listener never fires; a stray unrelated rejection during the
79
+ * window is still delivered to the pre-existing listeners.
80
+ */
81
+ export declare function resolveEnvValuesOrThrow(graph: LoadedEnvGraph): Promise<void>;
82
+ export {};
83
+ //# sourceMappingURL=env-graph-guard.d.ts.map
@@ -0,0 +1,155 @@
1
+ // src/run/env-graph-guard.ts
2
+ //
3
+ // Guards for `hq run`'s use of varlock's env graph. Two independent layers
4
+ // close the same class of failure that produced Sentry HQ-CLI-W
5
+ // ("expected dataType to be set", varlock ConfigItem.resolve):
6
+ //
7
+ // 1. assertEnvGraphLoaded(graph) — refuse to resolve a graph varlock never
8
+ // finished loading. When ANY discovered entry file (.env.schema /
9
+ // .env.local) fails to parse, varlock's EnvGraph.finishLoad() returns
10
+ // early — BEFORE it processes items — so every ConfigItem is left with
11
+ // `dataType === undefined` and NO error attached. Resolving such a graph
12
+ // reaches varlock's internal invariant `expected dataType to be set` for
13
+ // any item whose key also exists in process.env (varlock treats the whole
14
+ // process env as overrides), and — because varlock fires that resolution
15
+ // as an un-awaited, un-caught promise — the throw becomes a process-level
16
+ // unhandled rejection while the promise `hq run` awaits NEVER settles.
17
+ // finishLoad's two early-return conditions are: any data source with
18
+ // `isValid === false`, or any plugin with a `loadingError`. This guard
19
+ // mirrors exactly those conditions and throws a typed, actionable error
20
+ // first, so resolution is never reached with an unprocessed graph.
21
+ //
22
+ // 2. resolveEnvValuesOrThrow(graph) — defence in depth for the case the
23
+ // shape-dependent guard above cannot see (a future varlock could change
24
+ // the members it reads). It wraps `graph.resolveEnvValues()` in a
25
+ // narrowly-scoped `unhandledRejection` window: if varlock floats the
26
+ // resolution rejection, this converts it into a thrown EnvResolutionError
27
+ // instead of hanging forever on the never-settling promise. The window is
28
+ // restored to the exact prior listener set afterwards, and every rejection
29
+ // it observes is still forwarded to the pre-existing listeners, so it never
30
+ // swallows an unrelated rejection or permanently alters process semantics.
31
+ //
32
+ // Both failures are a user's malformed schema file, not an hq-cli defect, so
33
+ // main.ts's handleTopLevelError classifies these typed carriers as an
34
+ // actionable `hq: …` line WITHOUT a Sentry capture (see isVarlockEnvError).
35
+ import { claimRejection, releaseRejection, } from "../unhandled-rejection-boundary.js";
36
+ /** The varlock version hq-cli is pinned to (package.json `dependencies`). */
37
+ export const VARLOCK_PINNED_VERSION = "1.0.0";
38
+ /**
39
+ * A schema/entry file could not be loaded, so the env graph never finished
40
+ * processing. Carries an actionable, file-naming message; classified by
41
+ * handleTopLevelError as a user-file error (printed, not reported to Sentry).
42
+ */
43
+ export class EnvGraphLoadError extends Error {
44
+ constructor(message) {
45
+ super(message);
46
+ this.name = "EnvGraphLoadError";
47
+ }
48
+ }
49
+ /**
50
+ * Resolving the env graph floated a rejection (varlock's un-awaited resolver).
51
+ * Converted from the process-level unhandled rejection by resolveEnvValuesOrThrow
52
+ * so the promise `hq run` awaits can never hang. Classified as a user-file error.
53
+ */
54
+ export class EnvResolutionError extends Error {
55
+ reason;
56
+ constructor(reason) {
57
+ const underlying = reason instanceof Error ? reason.message : String(reason);
58
+ super(`could not resolve your env schema (varlock ${VARLOCK_PINNED_VERSION}): ${underlying}. ` +
59
+ `This usually means a .env.schema or .env.local file could not be parsed — ` +
60
+ `check them for syntax errors and try again.`);
61
+ this.name = "EnvResolutionError";
62
+ this.reason = reason;
63
+ }
64
+ }
65
+ /** True for either typed carrier this module raises. */
66
+ export function isVarlockEnvError(err) {
67
+ return err instanceof EnvGraphLoadError || err instanceof EnvResolutionError;
68
+ }
69
+ /** Collect the human-readable reason(s) a source is invalid, mirroring varlock. */
70
+ function describeSourceFailure(source) {
71
+ const parts = [];
72
+ const loadingMessage = source.loadingError?.message;
73
+ if (loadingMessage)
74
+ parts.push(loadingMessage);
75
+ for (const err of source.schemaErrors ?? []) {
76
+ if (!err.isWarning && err.message)
77
+ parts.push(err.message);
78
+ }
79
+ for (const err of source.resolutionErrors ?? []) {
80
+ if (err.message)
81
+ parts.push(err.message);
82
+ }
83
+ return parts.length > 0 ? parts.join("; ") : "failed to load";
84
+ }
85
+ /**
86
+ * Throw a typed, actionable EnvGraphLoadError when varlock did not finish
87
+ * loading the graph — i.e. when it would have bailed out of finishLoad() before
88
+ * processing items, leaving them unresolvable. Mirrors finishLoad's two
89
+ * early-return conditions exactly: any data source with `isValid === false`, or
90
+ * any plugin with a `loadingError`. A fully-valid graph passes silently.
91
+ */
92
+ export function assertEnvGraphLoaded(graph) {
93
+ const problems = [];
94
+ for (const source of graph.sortedDataSources ?? []) {
95
+ if (source.isValid === false) {
96
+ const where = source.fullPath ?? source.label ?? "unknown source";
97
+ problems.push(`${where}: ${describeSourceFailure(source)}`);
98
+ }
99
+ }
100
+ for (const plugin of graph.plugins ?? []) {
101
+ if (plugin.loadingError) {
102
+ const where = plugin.localPath ?? "plugin";
103
+ problems.push(`${where}: ${plugin.loadingError.message ?? String(plugin.loadingError)}`);
104
+ }
105
+ }
106
+ if (problems.length === 0)
107
+ return;
108
+ throw new EnvGraphLoadError(`hq run could not load your env schema (varlock ${VARLOCK_PINNED_VERSION}). ` +
109
+ `Fix the following and re-run:\n` +
110
+ problems.map((p) => ` - ${p}`).join("\n"));
111
+ }
112
+ /**
113
+ * Await `graph.resolveEnvValues()`, but convert varlock's floated resolution
114
+ * rejection into a thrown EnvResolutionError instead of hanging on the promise
115
+ * that never settles.
116
+ *
117
+ * varlock@1.0.0's resolveEnvValues dispatches its per-item resolver un-awaited
118
+ * and with a dead `_reject`, so a throw inside it becomes a process-level
119
+ * unhandled rejection AND leaves the returned promise permanently pending. We
120
+ * race the resolve against a scoped `unhandledRejection` listener: whichever
121
+ * settles first wins. The listener is prepended (so the prior listeners still
122
+ * run — nothing is swallowed) and removed in `finally` (restoring the exact
123
+ * prior listener set, including the no-prior-listener case). If resolve settles
124
+ * normally the listener never fires; a stray unrelated rejection during the
125
+ * window is still delivered to the pre-existing listeners.
126
+ */
127
+ export async function resolveEnvValuesOrThrow(graph) {
128
+ let claimedReason;
129
+ let didClaim = false;
130
+ const onRejection = (reason) => {
131
+ // Claim this rejection so the process-wide boundary (index.ts) defers to us
132
+ // and does not ALSO report it — one failure, one diagnostic. The
133
+ // pre-existing listeners still fire (this is a prependOnceListener), so the
134
+ // rejection is never swallowed from the process's normal handling.
135
+ claimedReason = reason;
136
+ didClaim = true;
137
+ claimRejection(reason);
138
+ rejectRace(new EnvResolutionError(reason));
139
+ };
140
+ let rejectRace = () => { };
141
+ const rejection = new Promise((_resolve, reject) => {
142
+ rejectRace = reject;
143
+ });
144
+ process.prependOnceListener("unhandledRejection", onRejection);
145
+ try {
146
+ await Promise.race([graph.resolveEnvValues(), rejection]);
147
+ }
148
+ finally {
149
+ // No-op if the once-listener already fired; restores the prior set exactly.
150
+ process.removeListener("unhandledRejection", onRejection);
151
+ if (didClaim)
152
+ releaseRejection(claimedReason);
153
+ }
154
+ }
155
+ //# sourceMappingURL=env-graph-guard.js.map
@@ -0,0 +1,30 @@
1
+ export declare function hadFatalRejection(): boolean;
2
+ export declare function claimRejection(reason: unknown): void;
3
+ export declare function releaseRejection(reason: unknown): void;
4
+ /** Reset the process-level rejection state. Test-only. */
5
+ export declare function __resetRejectionStateForTests(): void;
6
+ export interface RejectionBoundaryDependencies {
7
+ /** Reads the current process exit code (undefined when still unset). */
8
+ readonly getExitCode: () => number | undefined;
9
+ readonly setExitCode: (code: number) => void;
10
+ /** Best-effort actionable report; may be async and may not finish before exit. */
11
+ readonly report: (reason: unknown) => void;
12
+ }
13
+ /**
14
+ * Handle one floated rejection. Synchronously fixes the exit code — a broken
15
+ * pipe (EPIPE) is a clean close (0), anything else is a failure (1) — WITHOUT
16
+ * downgrading a non-zero code a command path already set, then kicks off a
17
+ * best-effort actionable report. Never calls process.exit, so the command's
18
+ * finally block (release-health session end + bounded Sentry flush) still runs.
19
+ */
20
+ export declare function handleFatalRejection(reason: unknown, deps: RejectionBoundaryDependencies): void;
21
+ /**
22
+ * Register the boundary on `process`, once, before index.ts dispatches into the
23
+ * command graph. Idempotent so repeated imports/installs add a single listener.
24
+ * The actionable report routes through main.ts's existing handleTopLevelError
25
+ * with a NO-OP capture: @sentry/node's integration has already captured the
26
+ * rejection, so this only adds the classified `hq:` line (and its EPIPE / typed
27
+ * carve-outs) without a second Sentry event.
28
+ */
29
+ export declare function installProcessRejectionBoundary(): void;
30
+ //# sourceMappingURL=unhandled-rejection-boundary.d.ts.map
@@ -0,0 +1,106 @@
1
+ // src/unhandled-rejection-boundary.ts
2
+ //
3
+ // A process-level unhandledRejection boundary for the CLI entrypoint.
4
+ //
5
+ // @sentry/node's onUnhandledRejection integration defaults to mode 'warn': it
6
+ // CAPTURES a floated rejection but does NOT rethrow it. hq-cli installs no
7
+ // rejection handler of its own, so without this boundary ANY floated rejection
8
+ // (the class that produced Sentry HQ-CLI-W) leaves process.exitCode unset, the
9
+ // event loop drains, and Node exits 0 — a silent success in which the command
10
+ // never ran. index.ts's own comment ("Rejections still become unhandled and
11
+ // preserve a genuine non-zero failure") described the pre-Sentry world and is
12
+ // no longer true; this boundary restores it.
13
+ //
14
+ // The logic is factored out of index.ts so it is unit-testable: importing
15
+ // index.ts for a test would trigger the real command dispatch.
16
+ import { isEpipe } from "./utils/epipe.js";
17
+ // Sticky once a non-EPIPE floated rejection has been handled. `process.exitCode`
18
+ // alone is not enough: a command path that later calls `process.exit(0)` — e.g.
19
+ // `hq run`'s child-`close` handler after a child that exited 0 — would erase the
20
+ // failure. Exit paths that propagate their own status consult this to force a
21
+ // non-zero code instead (see src/commands/run.ts).
22
+ let fatalRejectionSeen = false;
23
+ export function hadFatalRejection() {
24
+ return fatalRejectionSeen;
25
+ }
26
+ // Rejections a scoped handler (e.g. resolveEnvValuesOrThrow) has already claimed
27
+ // and will surface through the normal command path. The process-wide boundary
28
+ // defers to the claimer so one failure yields exactly one diagnostic rather than
29
+ // both a scoped throw AND a boundary report of the same rejection.
30
+ const claimedRejections = new Set();
31
+ export function claimRejection(reason) {
32
+ claimedRejections.add(reason);
33
+ }
34
+ export function releaseRejection(reason) {
35
+ claimedRejections.delete(reason);
36
+ }
37
+ /** Reset the process-level rejection state. Test-only. */
38
+ export function __resetRejectionStateForTests() {
39
+ fatalRejectionSeen = false;
40
+ claimedRejections.clear();
41
+ }
42
+ /**
43
+ * Handle one floated rejection. Synchronously fixes the exit code — a broken
44
+ * pipe (EPIPE) is a clean close (0), anything else is a failure (1) — WITHOUT
45
+ * downgrading a non-zero code a command path already set, then kicks off a
46
+ * best-effort actionable report. Never calls process.exit, so the command's
47
+ * finally block (release-health session end + bounded Sentry flush) still runs.
48
+ */
49
+ export function handleFatalRejection(reason, deps) {
50
+ // A scoped handler already owns this exact rejection and will surface it
51
+ // through the normal command path; do not exit-code or report it twice.
52
+ if (claimedRejections.has(reason))
53
+ return;
54
+ if (isEpipe(reason)) {
55
+ // A clean downstream close. Only assert 0 when nothing has already failed.
56
+ if (deps.getExitCode() === undefined)
57
+ deps.setExitCode(0);
58
+ }
59
+ else {
60
+ // A genuine fatal rejection: mark it sticky, and mark the run failed unless
61
+ // a command already set a non-zero code (e.g. a child's exit status).
62
+ fatalRejectionSeen = true;
63
+ if (!deps.getExitCode())
64
+ deps.setExitCode(1);
65
+ }
66
+ deps.report(reason);
67
+ }
68
+ let installed = false;
69
+ /**
70
+ * Register the boundary on `process`, once, before index.ts dispatches into the
71
+ * command graph. Idempotent so repeated imports/installs add a single listener.
72
+ * The actionable report routes through main.ts's existing handleTopLevelError
73
+ * with a NO-OP capture: @sentry/node's integration has already captured the
74
+ * rejection, so this only adds the classified `hq:` line (and its EPIPE / typed
75
+ * carve-outs) without a second Sentry event.
76
+ */
77
+ export function installProcessRejectionBoundary() {
78
+ if (installed)
79
+ return;
80
+ installed = true;
81
+ process.on("unhandledRejection", (reason) => {
82
+ handleFatalRejection(reason, {
83
+ getExitCode: () => process.exitCode,
84
+ setExitCode: (code) => {
85
+ process.exitCode = code;
86
+ },
87
+ report: (r) => {
88
+ void import("./main.js")
89
+ .then(({ handleTopLevelError }) => handleTopLevelError(r, {
90
+ sentry: { captureException: () => undefined },
91
+ stderr: process.stderr,
92
+ // Print-only. handleFatalRejection already set the authoritative,
93
+ // downgrade-guarded exit code synchronously; letting the async
94
+ // report re-set it would let handleTopLevelError's EPIPE branch
95
+ // (setExitCode(0)) reset an earlier failure to success.
96
+ setExitCode: () => { },
97
+ }))
98
+ .catch(() => {
99
+ // main.js could not be loaded (a rejection on a path that never
100
+ // imported it). The synchronous exit code above still stands.
101
+ });
102
+ },
103
+ });
104
+ });
105
+ }
106
+ //# sourceMappingURL=unhandled-rejection-boundary.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.106.2",
3
+ "version": "5.106.3",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -30,7 +30,7 @@
30
30
  "dependencies": {
31
31
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
32
32
  "@aws-sdk/client-s3": "^3.1049.0",
33
- "@indigoai-us/hq-cloud": "~6.16.3",
33
+ "@indigoai-us/hq-cloud": "~6.16.6",
34
34
  "@indigoai-us/hq-onboarding": "^0.1.0",
35
35
  "@sentry/node": "^10.49.0",
36
36
  "@tobilu/qmd": "2.5.3",