@indigoai-us/hq-cli 5.109.10 → 5.109.12

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.
@@ -31,10 +31,12 @@
31
31
  */
32
32
  /**
33
33
  * The loader-error `code`s recovery classifies. The first two mean "a module
34
- * could not be resolved"; `ENOENT` is the esm-loader dialect where a module was
35
- * present at resolve and gone at read (HQ-CLI-1M) see {@link ModuleErrorDialect}.
34
+ * could not be resolved"; `ENOENT` is the loader/resolver dialect where a module
35
+ * was present at one step and gone at the next (HQ-CLI-1M esm-load open, HQ-CLI-1S
36
+ * resolve-time lstat); `EVAL_THROW` is the synthetic code for a module-EVALUATION
37
+ * throw that carries NO real `code` (HQ-CLI-1T) — see {@link ModuleErrorDialect}.
36
38
  */
37
- export type ModuleNotFoundCode = "ERR_MODULE_NOT_FOUND" | "MODULE_NOT_FOUND" | "ENOENT";
39
+ export type ModuleNotFoundCode = "ERR_MODULE_NOT_FOUND" | "MODULE_NOT_FOUND" | "ENOENT" | "EVAL_THROW";
38
40
  /**
39
41
  * The closed set of resolution-failure dialects, verified on Node v22.23.1 (the
40
42
  * @sentry/node import-in-the-middle hook does not change the shapes):
@@ -54,10 +56,20 @@ export type ModuleNotFoundCode = "ERR_MODULE_NOT_FOUND" | "MODULE_NOT_FOUND" | "
54
56
  * - `esm-enoent`: ESM loader ENOENT (HQ-CLI-1M) — a module present at RESOLVE
55
57
  * and gone at READ, so getSourceSync/openSync raises ENOENT
56
58
  * (not ERR_MODULE_NOT_FOUND). `err.path` is the vanished file.
59
+ * - `resolve-enoent`: ESM/CJS RESOLVE-time ENOENT (HQ-CLI-1S) — a path component
60
+ * stat'ed OK, then vanished before realpathSync's lstat, so the
61
+ * resolver (finalizeResolution / Module._findPath→toRealPath)
62
+ * raises ENOENT with syscall `lstat`. `err.path` is the vanished
63
+ * component (which may be a package/scope directory).
64
+ * - `eval-throw`: a module-EVALUATION throw (HQ-CLI-1T) — a dependency file was
65
+ * found but still half-written, so require() returned an empty
66
+ * module and its top-level code threw a code-LESS TypeError /
67
+ * ReferenceError / SyntaxError under Module._compile /
68
+ * esm/module_job. The specifier is the bounded throw-site path.
57
69
  * - `unknown`: a module-not-found whose message did not parse; recovery
58
70
  * still waits on the lock / retired-dir / quiet signals.
59
71
  */
60
- export type ModuleErrorDialect = "esm-path" | "esm-package" | "cjs-path" | "cjs-package" | "cjs-relative" | "esm-enoent" | "unknown";
72
+ export type ModuleErrorDialect = "esm-path" | "esm-package" | "cjs-path" | "cjs-package" | "cjs-relative" | "esm-enoent" | "resolve-enoent" | "eval-throw" | "unknown";
61
73
  /**
62
74
  * The missing thing, re-resolvable by the readiness probe:
63
75
  * - `path`: an absolute filesystem path (a `.js` file, or a package dir).
@@ -66,6 +78,12 @@ export type ModuleErrorDialect = "esm-path" | "esm-package" | "cjs-path" | "cjs-
66
78
  * Node's CJS resolver does from directory `from` —
67
79
  * `path.resolve(from, specifier)` plus the `.js`/`.json`/`.node`
68
80
  * and directory forms.
81
+ * - `exists`: an absolute path whose bare EXISTENCE is the readiness signal
82
+ * (HQ-CLI-1S) — the resolver lstat'ed this component, which may be
83
+ * a package/scope DIRECTORY whose contents precede a `package.json`,
84
+ * so the `path` kind's directory rule could stay false forever.
85
+ * Readiness still rides the lock / manifest-health / retired-sibling
86
+ * / quiet guards, exactly as for the loader-race dialects.
69
87
  * - `unknown`: the message did not parse; treated as "present" by the probe so
70
88
  * readiness turns only on the lock / retired-dir / quiet signals.
71
89
  */
@@ -80,6 +98,9 @@ export type ModuleErrorTarget = {
80
98
  kind: "relative";
81
99
  specifier: string;
82
100
  from: string;
101
+ } | {
102
+ kind: "exists";
103
+ path: string;
83
104
  } | {
84
105
  kind: "unknown";
85
106
  };
@@ -91,6 +112,8 @@ export interface ClassifiedModuleError {
91
112
  /** The importer the message named (from-clause or requireStack[0]), or "". */
92
113
  importer: string;
93
114
  target: ModuleErrorTarget;
115
+ /** The thrown error's `name` for the eval-throw dialect (HQ-CLI-1T), else absent. */
116
+ errorName?: string;
94
117
  }
95
118
  /**
96
119
  * Reduce a bare specifier to its PACKAGE name: `@scope/name/sub` → `@scope/name`,
@@ -109,6 +132,39 @@ export declare function packageNameOf(specifier: string): string;
109
132
  * `null` so it is rethrown to the existing boundary unchanged.
110
133
  */
111
134
  export declare function classifyModuleNotFound(err: unknown): ClassifiedModuleError | null;
135
+ /** The structural fingerprint of a module-EVALUATION throw (HQ-CLI-1T). */
136
+ export interface EvalThrowShape {
137
+ /** The thrown error's `name`, in {@link EVAL_THROW_NAMES}. */
138
+ errorName: string;
139
+ /** The absolute throw-site path (first path token in the stack). */
140
+ throwSite: string;
141
+ }
142
+ /**
143
+ * Detect a module-EVALUATION throw STRUCTURALLY, WITHOUT the node_modules
144
+ * boundary gate: an error with NO `code`, a `name` in the closed set
145
+ * {TypeError, ReferenceError, SyntaxError}, a CJS/ESM module-evaluation loader
146
+ * frame, and a resolvable throw-site path. This is the shape a half-written
147
+ * dependency file produces — require() read an empty module and returned `{}`, so
148
+ * calling one of its exports throws a code-less TypeError while the module's own
149
+ * top-level code runs. No message text is ever parsed. The caller applies its own
150
+ * packageRoot gate ({@link classifyEvalThrow} for recovery, the boundary for
151
+ * capture context), so this stays pure and reusable. Returns null for anything
152
+ * that is not this shape — a coded error (ERR_REQUIRE_ESM etc.), an Error /
153
+ * RangeError name, a throw with no loader frame, or no absolute throw site.
154
+ */
155
+ export declare function evalThrowShape(err: unknown): EvalThrowShape | null;
156
+ /**
157
+ * Classify a module-EVALUATION throw as a torn-install recovery candidate
158
+ * (HQ-CLI-1T) — the {@link evalThrowShape} fingerprint AND a throw site under
159
+ * `<packageRoot>/node_modules/`. A throw under the install's own `dist/` or
160
+ * `assets/`, outside the root, or with an unresolved root is NOT classified, so a
161
+ * genuine hq-cli code defect keeps its raw capture. Unlike the loader-race
162
+ * dialects the recovery seam gates this on a live writer signal before waiting.
163
+ * `packageRoot` comes from the seam's resolvePackageRoot (running-install → derived
164
+ * fallback). The target is `unknown` — an eval-throw carries no re-resolvable
165
+ * missing file, so recovery keys entirely on the writer signal + quiet window.
166
+ */
167
+ export declare function classifyEvalThrow(err: unknown, packageRoot: string | null): ClassifiedModuleError | null;
112
168
  /** The filesystem surface the probe and wait use, injectable for hermetic tests. */
113
169
  export interface InstallTreeFs {
114
170
  existsSync(p: string): boolean;
@@ -133,10 +189,21 @@ export interface InstallTreeFs {
133
189
  * or a directory whose package.json `main` (or that main's index)
134
190
  * or own `index.*` is a real file. A bare or partially-extracted
135
191
  * directory is NOT loadable and reads as not-present.
192
+ * - `exists`: the path exists at all (any file type) — presence only, because
193
+ * the vanished component may be a package/scope directory.
136
194
  * - `unknown`: true (readiness turns on the other signals).
137
195
  * Any filesystem error reads as "not present" rather than throwing.
138
196
  */
139
197
  export declare function installTargetPresent(target: ModuleErrorTarget, fs?: InstallTreeFs): boolean;
198
+ /**
199
+ * Default ceiling on the CUMULATIVE time the wait may spend observing a FRESH
200
+ * foreign lock held by a live pid. Equal to the shared update-lock contract's own
201
+ * staleness window ({@link UPDATE_LOCK_STALE_MS} — "no healthy npm install -g of
202
+ * this package runs 10 minutes"), so the wait trusts a live cooperating writer for
203
+ * exactly as long as the contract legitimises holding the lock, and never longer.
204
+ * Overridable by the seam (env HQ_INSTALL_SETTLE_LOCK_TIMEOUT_MS).
205
+ */
206
+ export declare const DEFAULT_INSTALL_SETTLE_LOCK_WAIT_MS: number;
140
207
  export interface WaitForInstallTreeSettledArgs {
141
208
  target: ModuleErrorTarget;
142
209
  /** The running install's package dir (from resolveRunningInstall), or null. */
@@ -150,6 +217,22 @@ export interface WaitForInstallTreeSettledArgs {
150
217
  pollMs?: number;
151
218
  quietMs?: number;
152
219
  deadlineMs: number;
220
+ /**
221
+ * Ceiling on the CUMULATIVE time observed under a FRESH foreign lock held by a
222
+ * live pid. Locked time does NOT consume `deadlineMs` (the budget for the tree
223
+ * to settle once no cooperating writer is visible); it is bounded separately by
224
+ * this ceiling so the wait can never hang. Defaults to
225
+ * {@link DEFAULT_INSTALL_SETTLE_LOCK_WAIT_MS}.
226
+ */
227
+ lockWaitMs?: number;
228
+ /**
229
+ * Invoked exactly once — at the first poll where the total wait has reached
230
+ * `deadlineMs` while a fresh foreign lock is still held (the moment the
231
+ * pre-lock-aware wait would have given up). Lets the seam print a one-line
232
+ * notice that the pause is a live updater, not a hang. Never invoked when no
233
+ * lock is seen or the lock clears before the deadline.
234
+ */
235
+ onLockExtended?: () => void;
153
236
  }
154
237
  export interface WaitForInstallTreeSettledResult {
155
238
  settled: boolean;
@@ -158,20 +241,67 @@ export interface WaitForInstallTreeSettledResult {
158
241
  sawLock: boolean;
159
242
  /** An npm retired/staging sibling (`.hq-cli-<rand>`) was observed at least once. */
160
243
  sawRetired: boolean;
244
+ /** A fresh foreign lock was held on the FINAL poll of an unsettled wait. */
245
+ lockHeldAtEnd: boolean;
246
+ /** Cumulative wall time observed under a fresh foreign lock held by a live pid. */
247
+ lockedMs: number;
248
+ /** The bounded `tool` of the last fresh foreign lock seen, or "" if none. */
249
+ lockTool: string;
161
250
  }
162
251
  /**
163
252
  * Poll until the install tree has been continuously READY for `quietMs`, or the
164
- * `deadlineMs` passes. READY means: (i) no FRESH update lock held by another pid,
165
- * (ii) the missing target is present, and — when `packageRoot` is known —
166
- * (iii) `<packageRoot>/package.json` parses with `name === CLI_NAME` and
167
- * (iv) no `.<leaf>-<rand>` retired sibling remains beside it. Every filesystem
168
- * error makes its condition "not ready" rather than throwing. `deadlineMs === 0`
169
- * evaluates readiness exactly once (no waiting); otherwise the whole wait is
170
- * bounded, so the caller can never hang.
253
+ * wait exhausts its bounds. READY means: (i) no FRESH update lock held by another
254
+ * pid, (ii) the missing target is present, and — when `packageRoot` is known —
255
+ * (iii) `<packageRoot>/package.json` parses with `name === CLI_NAME` and (iv) no
256
+ * `.<leaf>-<rand>` retired sibling remains beside it. Every filesystem error makes
257
+ * its condition "not ready" rather than throwing.
258
+ *
259
+ * The wait is LOCK-AWARE and bounded on TWO independent axes, so a live
260
+ * cooperating writer (the desktop app, the box's update timer, another `hq`)
261
+ * cannot make a legitimately-in-progress reinstall look like a torn install:
262
+ * - UNLOCKED time — wall time observed with no fresh foreign lock — is bounded
263
+ * by `deadlineMs`. That is the budget for the TREE to settle once no writer is
264
+ * visible; with no lock ever seen, unlocked time equals total wait time and
265
+ * the behaviour is byte-for-byte the pre-lock-aware one (unsettled at exactly
266
+ * `deadlineMs`, and `deadlineMs === 0` evaluates readiness exactly once).
267
+ * - LOCKED time — wall time observed under a fresh foreign lock held by a live
268
+ * pid — is bounded SEPARATELY by `lockWaitMs` (default the lock contract's own
269
+ * 10-minute staleness window) and does NOT consume the settle budget. When
270
+ * that ceiling is reached with the lock still held, the result is unsettled
271
+ * with `lockHeldAtEnd` true so the seam can report a distinct `lock-held`
272
+ * outcome and remedy rather than telling the user to reinstall over a live
273
+ * writer.
274
+ * Each poll's REAL elapsed interval (now() deltas) is attributed to the locked or
275
+ * unlocked bucket by whether a fresh foreign lock was held when the interval
276
+ * began, so a starved event loop only makes the wait coarser, never unbounded. A
277
+ * stale lock (dead pid, older than the staleness window, unparseable) or our own
278
+ * pid never extends anything. The whole wait is bounded (≤ `deadlineMs +
279
+ * lockWaitMs`), so the caller can never hang.
171
280
  */
172
281
  export declare function waitForInstallTreeSettled(args: WaitForInstallTreeSettledArgs): Promise<WaitForInstallTreeSettledResult>;
282
+ export interface InstallRewriteSignalArgs {
283
+ /** The running install's package dir (from resolveRunningInstall), or null. */
284
+ packageRoot: string | null;
285
+ /** The shared update-lock path ($HOME/.hq/locks/cli-update.lock). */
286
+ lockPath: string;
287
+ now?: () => number;
288
+ fs?: InstallTreeFs;
289
+ isPidAlive?: (pid: number) => boolean;
290
+ }
291
+ /**
292
+ * Whether a global reinstall is PROVABLY rewriting the install tree right now —
293
+ * the positive writer signal the eval-throw gate requires before it will pay a
294
+ * settle wait. True iff (i) a fresh foreign update lock is held, (ii) packageRoot's
295
+ * manifest is absent or name-mismatched (mid-extraction), or (iii) a
296
+ * `.<leaf>-<hash>` retired sibling sits beside it. Crucially it is POSITIVE: a
297
+ * benign filesystem error that merely prevents READING the manifest or LISTING the
298
+ * parent (EACCES/EPERM) is NOT a signal, so a persistent third-party load-time
299
+ * defect on an otherwise-usable install never pays a spurious 90s wait. Every
300
+ * dependency is injectable so the recovery seam stays hermetic.
301
+ */
302
+ export declare function installRewriteInProgress(args: InstallRewriteSignalArgs): boolean;
173
303
  /** The recovery OUTCOME, which — not the dialect — decides capture. */
174
- export type InstallTreeTornOutcome = "guarded" | "unsettled" | "reexec-failed";
304
+ export type InstallTreeTornOutcome = "guarded" | "unsettled" | "reexec-failed" | "lock-held";
175
305
  export type InstallTreeTornDiagnostics = {
176
306
  dialect: ModuleErrorDialect;
177
307
  code: ModuleNotFoundCode;
@@ -183,7 +313,15 @@ export type InstallTreeTornDiagnostics = {
183
313
  waitedMs: number;
184
314
  sawLock: boolean;
185
315
  sawRetired: boolean;
316
+ /** A fresh foreign lock was still held when the wait gave up (→ 'lock-held'). */
317
+ lockHeldAtEnd: boolean;
318
+ /** Cumulative wall time the wait spent under a fresh foreign lock. */
319
+ lockedMs: number;
320
+ /** The bounded `tool` of the last fresh foreign lock seen, or "". */
321
+ lockTool: string;
186
322
  node: string;
323
+ /** The thrown error's `name` for the eval-throw dialect (HQ-CLI-1T), else "". */
324
+ errorName: string;
187
325
  };
188
326
  export interface InstallTreeTornInit {
189
327
  /** The original loader error, carried unchanged as `cause`. */
@@ -195,6 +333,12 @@ export interface InstallTreeTornInit {
195
333
  waitedMs: number;
196
334
  sawLock: boolean;
197
335
  sawRetired: boolean;
336
+ /** Whether a fresh foreign lock was held when the wait gave up (default false). */
337
+ lockHeldAtEnd?: boolean;
338
+ /** Cumulative time under a fresh foreign lock (default 0). */
339
+ lockedMs?: number;
340
+ /** The last fresh foreign lock's `tool`, bounded on construction (default ""). */
341
+ lockTool?: string;
198
342
  }
199
343
  /**
200
344
  * A torn-install failure that stays VISIBLE in Sentry with bounded, hq-derived
@@ -221,10 +365,20 @@ export declare function installTreeTornCaptureContext(err: InstallTreeTornError)
221
365
  * appended by {@link installTreeTornStderrLine}.
222
366
  */
223
367
  export declare const INSTALL_TREE_TORN_REMEDY: string;
368
+ /**
369
+ * The fixed, input-free remedy for the 'lock-held' outcome: a cooperating writer
370
+ * held the shared update lock past its own staleness ceiling. Telling the user to
371
+ * `npm i -g` here would collide with a live installer mid-rename — the exact race
372
+ * update-lock.ts exists to prevent — so this remedy tells them to let the running
373
+ * update finish and NEVER to reinstall over it. Interpolates NOTHING.
374
+ */
375
+ export declare const INSTALL_TREE_LOCK_HELD_REMEDY: string;
224
376
  /**
225
377
  * The single actionable stderr line for a captured torn-install failure: the
226
- * fixed remedy plus the bounded, hq-derived missing specifier — the only
227
- * interpolated value, never argv.
378
+ * outcome's fixed remedy plus the bounded, hq-derived missing specifier — the only
379
+ * interpolated value, never argv. A 'lock-held' outcome selects the "let the
380
+ * update finish" remedy (never the reinstall advice); every other outcome keeps
381
+ * {@link INSTALL_TREE_TORN_REMEDY} byte-for-byte.
228
382
  */
229
383
  export declare function installTreeTornStderrLine(err: InstallTreeTornError): string;
230
384
  //# sourceMappingURL=install-tree-torn.d.ts.map