@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.
- package/CHANGELOG.md +39 -0
- package/dist/startup-registration.d.ts +21 -1
- package/dist/startup-registration.js +62 -9
- package/dist/utils/incomplete-install-error.d.ts +37 -10
- package/dist/utils/incomplete-install-error.js +126 -46
- package/dist/utils/install-tree-torn.d.ts +168 -14
- package/dist/utils/install-tree-torn.js +340 -26
- package/dist/utils/sentry-fingerprint.js +5 -5
- package/package.json +1 -1
|
@@ -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
|
|
35
|
-
* present at
|
|
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
|
-
*
|
|
165
|
-
* (ii) the missing target is present, and — when `packageRoot` is known —
|
|
166
|
-
* (iii) `<packageRoot>/package.json` parses with `name === CLI_NAME` and
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* bounded
|
|
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
|