@indigoai-us/hq-cli 5.108.16 → 5.108.18
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 +38 -2
- package/dist/lib/mesh/live/daemon/index.d.ts +2 -2
- package/dist/lib/mesh/live/daemon/index.js +1 -1
- package/dist/lib/mesh/live/daemon/run.js +13 -1
- package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +114 -7
- package/dist/lib/mesh/live/daemon/transcript-watch.js +458 -187
- package/dist/main.js +51 -7
- package/dist/startup-registration.d.ts +79 -0
- package/dist/startup-registration.js +134 -0
- package/dist/utils/install-tree-torn.d.ts +204 -0
- package/dist/utils/install-tree-torn.js +359 -0
- package/dist/utils/sentry-fingerprint.js +26 -0
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
|
|
7
7
|
import "./node-preflight.js";
|
|
8
8
|
import "./node-network-compat.js";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
9
11
|
import { Command } from "commander";
|
|
10
12
|
import { initSentry, Sentry } from "./sentry.js";
|
|
11
13
|
import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
|
|
@@ -46,10 +48,19 @@ import { settleWithin } from "./utils/settle-with-timeout.js";
|
|
|
46
48
|
import { emitPlanLimitNag } from "./lib/plan-limit-nag.js";
|
|
47
49
|
import { kickFlagRegistryReadiness } from "./lib/flag-registry.js";
|
|
48
50
|
import { isPackageRootResolutionError, packageRootCaptureContext, } from "./utils/package-root-diagnostics.js";
|
|
51
|
+
import { registerCommandsWithRecovery } from "./startup-registration.js";
|
|
52
|
+
import { installTreeTornCaptureContext, installTreeTornStderrLine, isInstallTreeTornError, } from "./utils/install-tree-torn.js";
|
|
49
53
|
import { isVaultAccessDeniedError, vaultAccessDeniedMessage, } from "./utils/vault-access-denied-error.js";
|
|
50
54
|
import { fallbackOperatorMessage, unexpectedCliErrorMessage } from "./utils/unexpected-cli-error.js";
|
|
51
55
|
/** Hard upper bound for non-user-visible release-health finalization. */
|
|
52
56
|
const RELEASE_HEALTH_SETTLE_TIMEOUT_MS = 3_000;
|
|
57
|
+
/**
|
|
58
|
+
* The RUNNING install's own entrypoint (`<pkg>/dist/index.js`), used as the
|
|
59
|
+
* torn-install recovery re-exec target. It must be this resolved path — never
|
|
60
|
+
* `hq` from PATH — because a global reinstall leaves `bin/hq` absent for much of
|
|
61
|
+
* the rewrite window, while this path now holds the settled tree.
|
|
62
|
+
*/
|
|
63
|
+
const RUNNING_ENTRY_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "index.js");
|
|
53
64
|
const defaultStreamErrorDependencies = {
|
|
54
65
|
stderr: process.stderr,
|
|
55
66
|
exit: (code) => process.exit(code),
|
|
@@ -160,13 +171,31 @@ export async function runCli() {
|
|
|
160
171
|
// `--help`, a bare `hq`, an unknown command, any command not on the
|
|
161
172
|
// manifest — falls back to the complete graph, so its behaviour is
|
|
162
173
|
// unchanged. See register-all.ts for the measurements that motivated this.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
174
|
+
// The same lazy/full registration as before, wrapped so it recovers ONCE if
|
|
175
|
+
// a global reinstall is tearing the install tree out from under these
|
|
176
|
+
// deferred imports (Sentry HQ-CLI-1G/1H/1J/1K). Which modules are imported,
|
|
177
|
+
// and in what order, is unchanged — only the failure handling is added.
|
|
178
|
+
const registration = await registerCommandsWithRecovery({
|
|
179
|
+
register: async () => {
|
|
180
|
+
const lazy = findLazyCommand(process.argv);
|
|
181
|
+
if (lazy) {
|
|
182
|
+
await lazy.register(program);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const { registerAllCommands } = await import("./register-all.js");
|
|
186
|
+
registerAllCommands(program);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
argv: process.argv,
|
|
190
|
+
env: process.env,
|
|
191
|
+
entryPath: RUNNING_ENTRY_PATH,
|
|
192
|
+
stderr: process.stderr,
|
|
193
|
+
});
|
|
194
|
+
if (registration.reexecStatus !== undefined) {
|
|
195
|
+
// The re-exec'd child ran the command on the settled tree; carry its exit
|
|
196
|
+
// status out (after the finally block), exactly as the self-update re-exec.
|
|
197
|
+
reexecStatus = registration.reexecStatus;
|
|
198
|
+
return;
|
|
170
199
|
}
|
|
171
200
|
await program.parseAsync();
|
|
172
201
|
}
|
|
@@ -337,6 +366,21 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
|
|
|
337
366
|
});
|
|
338
367
|
deps.setExitCode(1);
|
|
339
368
|
}
|
|
369
|
+
else if (isInstallTreeTornError(err)) {
|
|
370
|
+
// HQ-CLI-1G/1H/1J/1K: a global reinstall tore the install tree out from
|
|
371
|
+
// under a starting `hq` and the bounded settle-wait + single re-exec could
|
|
372
|
+
// not recover it (a still-incomplete tree, a foreign updater, or a failed
|
|
373
|
+
// re-exec). Print the fixed reinstall remedy naming the bounded missing
|
|
374
|
+
// specifier, and STILL capture WITH bounded diagnostics — a genuinely
|
|
375
|
+
// pruned or incomplete install must stay visible. Mirrors the package-root
|
|
376
|
+
// branch's print-and-capture shape; a class-instance check disjoint from
|
|
377
|
+
// every neighbour, so no existing ordering changes. A SUCCESSFUL recovery
|
|
378
|
+
// never reaches here (it returns the child's status), so this line and this
|
|
379
|
+
// capture only ever mark a real, unrecovered break.
|
|
380
|
+
deps.stderr.write(`hq: ${installTreeTornStderrLine(err)}\n`);
|
|
381
|
+
deps.sentry.captureException(err, installTreeTornCaptureContext(err));
|
|
382
|
+
deps.setExitCode(1);
|
|
383
|
+
}
|
|
340
384
|
else if (isVaultAccessDeniedError(err)) {
|
|
341
385
|
// ARM B (Sentry 7709408531): `hq skill create`'s post-register vault sync
|
|
342
386
|
// was DENIED by S3 — an AWS SDK v3 403 from an hq-pro IAM session-policy
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command-registration recovery seam for the "install tree torn out from
|
|
3
|
+
* under a starting `hq`" condition (Sentry HQ-CLI-1G/1H/1J/1K).
|
|
4
|
+
*
|
|
5
|
+
* `runCli` registers commands as its last step before `program.parseAsync()` —
|
|
6
|
+
* the lazy command import or `import("./register-all.js")` and everything those
|
|
7
|
+
* pull in. That is the ONE place a module-resolution failure is provably safe to
|
|
8
|
+
* recover from: it is inside hq-cli's own graph, it runs before `parseAsync` and
|
|
9
|
+
* before the preAction hook, so no command action, telemetry, or stdin read has
|
|
10
|
+
* happened yet, and a single re-exec can therefore never run a command twice.
|
|
11
|
+
*
|
|
12
|
+
* This wraps the existing `register()` call with exactly that recovery, driven by
|
|
13
|
+
* the classifier / probe / settle wait / carrier in ./utils/install-tree-torn.ts.
|
|
14
|
+
* It changes nothing about WHICH modules are imported or in what order — only the
|
|
15
|
+
* failure handling around the existing imports. Dependency-injected in the house
|
|
16
|
+
* style of handleTopLevelError so the whole seam is unit-testable without real
|
|
17
|
+
* spawns, waits, or a real install tree.
|
|
18
|
+
*/
|
|
19
|
+
import { type WaitForInstallTreeSettledArgs, type WaitForInstallTreeSettledResult } from "./utils/install-tree-torn.js";
|
|
20
|
+
/**
|
|
21
|
+
* Set on the re-exec'd child so it can NEVER wait or re-exec again — distinct
|
|
22
|
+
* from self-update's HQ_RESCUE_SELF_UPDATED so the two recovery paths cannot
|
|
23
|
+
* shadow each other.
|
|
24
|
+
*/
|
|
25
|
+
export declare const INSTALL_TREE_RECOVERY_GUARD_ENV = "HQ_INSTALL_TREE_RECOVERED";
|
|
26
|
+
/** Operator override for the settle wait's deadline. */
|
|
27
|
+
export declare const INSTALL_SETTLE_TIMEOUT_ENV = "HQ_INSTALL_SETTLE_TIMEOUT_MS";
|
|
28
|
+
/** Default settle deadline (ms). Overridable by {@link INSTALL_SETTLE_TIMEOUT_ENV}. */
|
|
29
|
+
export declare const DEFAULT_INSTALL_SETTLE_TIMEOUT_MS = 90000;
|
|
30
|
+
/**
|
|
31
|
+
* The single dim, informational line emitted when — and only when — a wait
|
|
32
|
+
* actually happens. It tells a human why the command paused and is the e2e's
|
|
33
|
+
* synchronization point; it is never a remedy and never sets an error exit code.
|
|
34
|
+
*/
|
|
35
|
+
export declare const INSTALL_TREE_WAIT_NOTICE = "hq: the hq-cli install is being updated underneath this command; waiting for it to finish\u2026";
|
|
36
|
+
/** Resolve the settle deadline from the environment (0 = evaluate once; default on invalid). */
|
|
37
|
+
export declare function resolveSettleTimeoutMs(env: NodeJS.ProcessEnv): number;
|
|
38
|
+
/** Minimal view of a `spawnSync` result the seam relies on. */
|
|
39
|
+
export interface RecoverySpawnResult {
|
|
40
|
+
status: number | null;
|
|
41
|
+
error?: Error;
|
|
42
|
+
}
|
|
43
|
+
export interface RegisterRecoveryDeps {
|
|
44
|
+
spawn?: (command: string, args: string[], options: {
|
|
45
|
+
stdio: "inherit";
|
|
46
|
+
env: NodeJS.ProcessEnv;
|
|
47
|
+
}) => RecoverySpawnResult;
|
|
48
|
+
resolveInstall?: () => {
|
|
49
|
+
packageRoot: string | null;
|
|
50
|
+
};
|
|
51
|
+
lockPath?: () => string;
|
|
52
|
+
waitForSettled?: (args: WaitForInstallTreeSettledArgs) => Promise<WaitForInstallTreeSettledResult>;
|
|
53
|
+
/** node flags to forward to the re-exec child (defaults to process.execArgv). */
|
|
54
|
+
execArgv?: readonly string[];
|
|
55
|
+
}
|
|
56
|
+
export interface RegisterCommandsWithRecoveryArgs {
|
|
57
|
+
/** Runs the existing registration (lazy command import or register-all import). */
|
|
58
|
+
register: () => Promise<void>;
|
|
59
|
+
argv: readonly string[];
|
|
60
|
+
env: NodeJS.ProcessEnv;
|
|
61
|
+
/** The RUNNING install's own `dist/index.js` — the safe re-exec target. */
|
|
62
|
+
entryPath: string;
|
|
63
|
+
stderr: Pick<typeof process.stderr, "write">;
|
|
64
|
+
deps?: RegisterRecoveryDeps;
|
|
65
|
+
}
|
|
66
|
+
export interface RegisterCommandsWithRecoveryResult {
|
|
67
|
+
/** Set only when a re-exec ran: its exit status becomes this invocation's. */
|
|
68
|
+
reexecStatus?: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Register commands, recovering once from a torn-install module-resolution
|
|
72
|
+
* failure. On a clean registration this returns `{}` and touches nothing else.
|
|
73
|
+
* On a classified failure it waits (bounded) for the install to settle and
|
|
74
|
+
* re-execs the running install's own entrypoint once; a child (guarded), an
|
|
75
|
+
* unsettled wait, or a failed spawn each throws an {@link InstallTreeTornError}
|
|
76
|
+
* carrying bounded diagnostics so a genuinely broken install stays visible.
|
|
77
|
+
*/
|
|
78
|
+
export declare function registerCommandsWithRecovery(args: RegisterCommandsWithRecoveryArgs): Promise<RegisterCommandsWithRecoveryResult>;
|
|
79
|
+
//# sourceMappingURL=startup-registration.d.ts.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command-registration recovery seam for the "install tree torn out from
|
|
3
|
+
* under a starting `hq`" condition (Sentry HQ-CLI-1G/1H/1J/1K).
|
|
4
|
+
*
|
|
5
|
+
* `runCli` registers commands as its last step before `program.parseAsync()` —
|
|
6
|
+
* the lazy command import or `import("./register-all.js")` and everything those
|
|
7
|
+
* pull in. That is the ONE place a module-resolution failure is provably safe to
|
|
8
|
+
* recover from: it is inside hq-cli's own graph, it runs before `parseAsync` and
|
|
9
|
+
* before the preAction hook, so no command action, telemetry, or stdin read has
|
|
10
|
+
* happened yet, and a single re-exec can therefore never run a command twice.
|
|
11
|
+
*
|
|
12
|
+
* This wraps the existing `register()` call with exactly that recovery, driven by
|
|
13
|
+
* the classifier / probe / settle wait / carrier in ./utils/install-tree-torn.ts.
|
|
14
|
+
* It changes nothing about WHICH modules are imported or in what order — only the
|
|
15
|
+
* failure handling around the existing imports. Dependency-injected in the house
|
|
16
|
+
* style of handleTopLevelError so the whole seam is unit-testable without real
|
|
17
|
+
* spawns, waits, or a real install tree.
|
|
18
|
+
*/
|
|
19
|
+
import { spawnSync } from "node:child_process";
|
|
20
|
+
import { resolveRunningInstall } from "./utils/version-gate.js";
|
|
21
|
+
import { updateLockPath } from "./utils/update-lock.js";
|
|
22
|
+
import { classifyModuleNotFound, InstallTreeTornError, waitForInstallTreeSettled, } from "./utils/install-tree-torn.js";
|
|
23
|
+
/**
|
|
24
|
+
* Set on the re-exec'd child so it can NEVER wait or re-exec again — distinct
|
|
25
|
+
* from self-update's HQ_RESCUE_SELF_UPDATED so the two recovery paths cannot
|
|
26
|
+
* shadow each other.
|
|
27
|
+
*/
|
|
28
|
+
export const INSTALL_TREE_RECOVERY_GUARD_ENV = "HQ_INSTALL_TREE_RECOVERED";
|
|
29
|
+
/** Operator override for the settle wait's deadline. */
|
|
30
|
+
export const INSTALL_SETTLE_TIMEOUT_ENV = "HQ_INSTALL_SETTLE_TIMEOUT_MS";
|
|
31
|
+
/** Default settle deadline (ms). Overridable by {@link INSTALL_SETTLE_TIMEOUT_ENV}. */
|
|
32
|
+
export const DEFAULT_INSTALL_SETTLE_TIMEOUT_MS = 90_000;
|
|
33
|
+
/**
|
|
34
|
+
* The single dim, informational line emitted when — and only when — a wait
|
|
35
|
+
* actually happens. It tells a human why the command paused and is the e2e's
|
|
36
|
+
* synchronization point; it is never a remedy and never sets an error exit code.
|
|
37
|
+
*/
|
|
38
|
+
export const INSTALL_TREE_WAIT_NOTICE = "hq: the hq-cli install is being updated underneath this command; waiting for it to finish…";
|
|
39
|
+
/** Resolve the settle deadline from the environment (0 = evaluate once; default on invalid). */
|
|
40
|
+
export function resolveSettleTimeoutMs(env) {
|
|
41
|
+
const raw = env[INSTALL_SETTLE_TIMEOUT_ENV];
|
|
42
|
+
if (typeof raw !== "string" || raw.trim() === "")
|
|
43
|
+
return DEFAULT_INSTALL_SETTLE_TIMEOUT_MS;
|
|
44
|
+
if (!/^\d+$/.test(raw.trim()))
|
|
45
|
+
return DEFAULT_INSTALL_SETTLE_TIMEOUT_MS;
|
|
46
|
+
return Number.parseInt(raw.trim(), 10);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Register commands, recovering once from a torn-install module-resolution
|
|
50
|
+
* failure. On a clean registration this returns `{}` and touches nothing else.
|
|
51
|
+
* On a classified failure it waits (bounded) for the install to settle and
|
|
52
|
+
* re-execs the running install's own entrypoint once; a child (guarded), an
|
|
53
|
+
* unsettled wait, or a failed spawn each throws an {@link InstallTreeTornError}
|
|
54
|
+
* carrying bounded diagnostics so a genuinely broken install stays visible.
|
|
55
|
+
*/
|
|
56
|
+
export async function registerCommandsWithRecovery(args) {
|
|
57
|
+
const { register, argv, env, entryPath, stderr } = args;
|
|
58
|
+
const deps = args.deps ?? {};
|
|
59
|
+
try {
|
|
60
|
+
await register();
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const classified = classifyModuleNotFound(err);
|
|
65
|
+
// Not a module-resolution failure — rethrow the SAME object to the existing
|
|
66
|
+
// boundary, preserving today's behaviour exactly.
|
|
67
|
+
if (!classified)
|
|
68
|
+
throw err;
|
|
69
|
+
const packageRoot = resolvePackageRoot(deps);
|
|
70
|
+
// A child that already recovered must never wait or re-exec again: report
|
|
71
|
+
// once and let the boundary capture it.
|
|
72
|
+
if (env[INSTALL_TREE_RECOVERY_GUARD_ENV] === "1") {
|
|
73
|
+
throw new InstallTreeTornError({
|
|
74
|
+
cause: err,
|
|
75
|
+
classified,
|
|
76
|
+
packageRoot,
|
|
77
|
+
outcome: "guarded",
|
|
78
|
+
attempt: 2,
|
|
79
|
+
waitedMs: 0,
|
|
80
|
+
sawLock: false,
|
|
81
|
+
sawRetired: false,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
stderr.write(`${INSTALL_TREE_WAIT_NOTICE}\n`);
|
|
85
|
+
const lockPath = (deps.lockPath ?? updateLockPath)();
|
|
86
|
+
const waitFor = deps.waitForSettled ?? waitForInstallTreeSettled;
|
|
87
|
+
const result = await waitFor({
|
|
88
|
+
target: classified.target,
|
|
89
|
+
packageRoot,
|
|
90
|
+
lockPath,
|
|
91
|
+
deadlineMs: resolveSettleTimeoutMs(env),
|
|
92
|
+
});
|
|
93
|
+
if (!result.settled) {
|
|
94
|
+
throw new InstallTreeTornError({
|
|
95
|
+
cause: err,
|
|
96
|
+
classified,
|
|
97
|
+
packageRoot,
|
|
98
|
+
outcome: "unsettled",
|
|
99
|
+
attempt: 1,
|
|
100
|
+
waitedMs: result.waitedMs,
|
|
101
|
+
sawLock: result.sawLock,
|
|
102
|
+
sawRetired: result.sawRetired,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const spawn = deps.spawn ?? spawnSync;
|
|
106
|
+
const execArgv = deps.execArgv ?? process.execArgv;
|
|
107
|
+
const child = spawn(process.execPath, [...execArgv, entryPath, ...argv.slice(2)], { stdio: "inherit", env: { ...env, [INSTALL_TREE_RECOVERY_GUARD_ENV]: "1" } });
|
|
108
|
+
if (child.error) {
|
|
109
|
+
throw new InstallTreeTornError({
|
|
110
|
+
cause: err,
|
|
111
|
+
classified,
|
|
112
|
+
packageRoot,
|
|
113
|
+
outcome: "reexec-failed",
|
|
114
|
+
attempt: 1,
|
|
115
|
+
waitedMs: result.waitedMs,
|
|
116
|
+
sawLock: result.sawLock,
|
|
117
|
+
sawRetired: result.sawRetired,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// A signal-killed child has status null; surface it as a failure exit rather
|
|
121
|
+
// than pretending the command completed (mirrors self-update's reexecHq).
|
|
122
|
+
return { reexecStatus: child.status ?? 1 };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Resolve the running install's package dir, tolerating any resolver failure. */
|
|
126
|
+
function resolvePackageRoot(deps) {
|
|
127
|
+
try {
|
|
128
|
+
return (deps.resolveInstall ?? resolveRunningInstall)().packageRoot;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=startup-registration.js.map
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection and bounded, recovery-safe reporting for the "install tree torn out
|
|
3
|
+
* from under a starting `hq`" condition (Sentry HQ-CLI-1G/1H/1J/1K —
|
|
4
|
+
* 7713386011, 7713482655, 7713783135, 7714501738).
|
|
5
|
+
*
|
|
6
|
+
* hq-cli resolves most of its module graph lazily INSIDE `runCli` (the lazy
|
|
7
|
+
* command module or `import("./register-all.js")`, plus everything those pull in
|
|
8
|
+
* — nested `@aws-sdk/*`, `js-yaml`, `@indigoai-us/hq-cloud`). An EXTERNAL global
|
|
9
|
+
* reinstall — the outpost `hq-cli-update` systemd timer every 6h, the agent
|
|
10
|
+
* boxes' `self_update`, the desktop app — renames the installed package dir
|
|
11
|
+
* aside (`.hq-cli-<rand>`), recreates it empty, and re-extracts it file by file
|
|
12
|
+
* over ~16s. A process that started just before that rename resolves its
|
|
13
|
+
* deferred imports against a directory that no longer holds them and dies with
|
|
14
|
+
* ERR_MODULE_NOT_FOUND / MODULE_NOT_FOUND. None of those writers is the process's
|
|
15
|
+
* OWN updater (the version-gate and self-updater install synchronously under the
|
|
16
|
+
* shared lock and never overlap their own imports), so hq-cli cannot stop them —
|
|
17
|
+
* but it CAN detect the condition at the one seam where it is provably
|
|
18
|
+
* side-effect free (the registration phase, before `parseAsync` and before the
|
|
19
|
+
* preAction hook), wait (bounded) for the install to settle, and re-run itself
|
|
20
|
+
* once on the settled tree.
|
|
21
|
+
*
|
|
22
|
+
* This module is the reusable core of that recovery: a STRUCTURAL, closed
|
|
23
|
+
* classifier for the loader-error dialects Node raises; a readiness probe that
|
|
24
|
+
* re-resolves the missing target exactly as Node's resolver does; a bounded
|
|
25
|
+
* settle wait keyed on the same signals the writer leaves behind; and the typed
|
|
26
|
+
* carrier + boundary helpers (house style of package-root-diagnostics.ts) that
|
|
27
|
+
* keep a genuinely-broken install VISIBLE in Sentry with bounded, hq-derived
|
|
28
|
+
* diagnostics. The recovery seam that drives them lives in
|
|
29
|
+
* ../startup-registration.ts; version-gate.ts / self-update.ts / update-lock.ts
|
|
30
|
+
* are only READ (their exported symbols), never modified.
|
|
31
|
+
*/
|
|
32
|
+
/** The two loader-error `code`s that mean "a module could not be resolved". */
|
|
33
|
+
export type ModuleNotFoundCode = "ERR_MODULE_NOT_FOUND" | "MODULE_NOT_FOUND";
|
|
34
|
+
/**
|
|
35
|
+
* The closed set of resolution-failure dialects, verified on Node v22.23.1 (the
|
|
36
|
+
* @sentry/node import-in-the-middle hook does not change the shapes):
|
|
37
|
+
* - `esm-path`: ESM importer, an absolute file (or legacyMainResolve dir)
|
|
38
|
+
* target — `err.url` set, or `Cannot find package '<abs>'…`.
|
|
39
|
+
* - `esm-package`: ESM importer, a bare specifier — `Cannot find package
|
|
40
|
+
* '<name>' imported from <importer>`, no `err.url`.
|
|
41
|
+
* - `cjs-path`: CJS require of an absolute path — `Cannot find module
|
|
42
|
+
* '<abs>'` + `requireStack`.
|
|
43
|
+
* - `cjs-package`: CJS require of a bare specifier — `Cannot find module
|
|
44
|
+
* '<name>'` + `requireStack`.
|
|
45
|
+
* - `unknown`: a module-not-found whose message did not parse; recovery
|
|
46
|
+
* still waits on the lock / retired-dir / quiet signals.
|
|
47
|
+
*/
|
|
48
|
+
export type ModuleErrorDialect = "esm-path" | "esm-package" | "cjs-path" | "cjs-package" | "unknown";
|
|
49
|
+
/**
|
|
50
|
+
* The missing thing, re-resolvable by the readiness probe:
|
|
51
|
+
* - `path`: an absolute filesystem path (a `.js` file, or a package dir).
|
|
52
|
+
* - `package`: a bare package `name` resolvable from directory `from` upward.
|
|
53
|
+
* - `unknown`: the message did not parse; treated as "present" by the probe so
|
|
54
|
+
* readiness turns only on the lock / retired-dir / quiet signals.
|
|
55
|
+
*/
|
|
56
|
+
export type ModuleErrorTarget = {
|
|
57
|
+
kind: "path";
|
|
58
|
+
path: string;
|
|
59
|
+
} | {
|
|
60
|
+
kind: "package";
|
|
61
|
+
name: string;
|
|
62
|
+
from: string;
|
|
63
|
+
} | {
|
|
64
|
+
kind: "unknown";
|
|
65
|
+
};
|
|
66
|
+
export interface ClassifiedModuleError {
|
|
67
|
+
code: ModuleNotFoundCode;
|
|
68
|
+
dialect: ModuleErrorDialect;
|
|
69
|
+
/** The raw specifier the message named (absolute path or bare name), or "". */
|
|
70
|
+
specifier: string;
|
|
71
|
+
/** The importer the message named (from-clause or requireStack[0]), or "". */
|
|
72
|
+
importer: string;
|
|
73
|
+
target: ModuleErrorTarget;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Reduce a bare specifier to its PACKAGE name: `@scope/name/sub` → `@scope/name`,
|
|
77
|
+
* `name/sub` → `name`, `name` → `name`. This is the unit the readiness probe
|
|
78
|
+
* re-resolves (Node looks up `node_modules/<package>/package.json`, never the
|
|
79
|
+
* subpath, when it raises the bare-specifier dialects).
|
|
80
|
+
*/
|
|
81
|
+
export declare function packageNameOf(specifier: string): string;
|
|
82
|
+
/**
|
|
83
|
+
* Classify a thrown value as a module-resolution failure and extract the missing
|
|
84
|
+
* target, or return `null` for anything that is not one. The decision is
|
|
85
|
+
* STRUCTURAL: `err.code` must be exactly `ERR_MODULE_NOT_FOUND` or
|
|
86
|
+
* `MODULE_NOT_FOUND` — no argv, env, or free text is ever consulted, and any
|
|
87
|
+
* other error (including an import-time throw of another class) returns `null`
|
|
88
|
+
* so it is rethrown to the existing boundary unchanged.
|
|
89
|
+
*/
|
|
90
|
+
export declare function classifyModuleNotFound(err: unknown): ClassifiedModuleError | null;
|
|
91
|
+
/** The filesystem surface the probe and wait use, injectable for hermetic tests. */
|
|
92
|
+
export interface InstallTreeFs {
|
|
93
|
+
existsSync(p: string): boolean;
|
|
94
|
+
statSync(p: string): {
|
|
95
|
+
isDirectory(): boolean;
|
|
96
|
+
};
|
|
97
|
+
readFileSync(p: string, encoding: "utf-8"): string;
|
|
98
|
+
readdirSync(p: string): string[];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Whether the missing target is now present on disk — the readiness signal the
|
|
102
|
+
* settle wait polls. It re-resolves the SAME step Node's resolver took:
|
|
103
|
+
* - `path`: the file must exist; a directory target must additionally hold
|
|
104
|
+
* a `package.json` (the legacyMainResolve case).
|
|
105
|
+
* - `package`: walk from `from` toward the filesystem root and return true as
|
|
106
|
+
* soon as `<dir>/node_modules/<name>/package.json` exists — the
|
|
107
|
+
* exact ancestor walk `getPackageJSONURL` performs, done WITHOUT
|
|
108
|
+
* `createRequire` so an ESM-only / export-conditioned package
|
|
109
|
+
* cannot false-negative.
|
|
110
|
+
* - `unknown`: true (readiness turns on the other signals).
|
|
111
|
+
* Any filesystem error reads as "not present" rather than throwing.
|
|
112
|
+
*/
|
|
113
|
+
export declare function installTargetPresent(target: ModuleErrorTarget, fs?: InstallTreeFs): boolean;
|
|
114
|
+
export interface WaitForInstallTreeSettledArgs {
|
|
115
|
+
target: ModuleErrorTarget;
|
|
116
|
+
/** The running install's package dir (from resolveRunningInstall), or null. */
|
|
117
|
+
packageRoot: string | null;
|
|
118
|
+
/** The shared update-lock path ($HOME/.hq/locks/cli-update.lock). */
|
|
119
|
+
lockPath: string;
|
|
120
|
+
now?: () => number;
|
|
121
|
+
sleep?: (ms: number) => Promise<void>;
|
|
122
|
+
fs?: InstallTreeFs;
|
|
123
|
+
isPidAlive?: (pid: number) => boolean;
|
|
124
|
+
pollMs?: number;
|
|
125
|
+
quietMs?: number;
|
|
126
|
+
deadlineMs: number;
|
|
127
|
+
}
|
|
128
|
+
export interface WaitForInstallTreeSettledResult {
|
|
129
|
+
settled: boolean;
|
|
130
|
+
waitedMs: number;
|
|
131
|
+
/** A FRESH update lock held by another pid was observed at least once. */
|
|
132
|
+
sawLock: boolean;
|
|
133
|
+
/** An npm retired/staging sibling (`.hq-cli-<rand>`) was observed at least once. */
|
|
134
|
+
sawRetired: boolean;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Poll until the install tree has been continuously READY for `quietMs`, or the
|
|
138
|
+
* `deadlineMs` passes. READY means: (i) no FRESH update lock held by another pid,
|
|
139
|
+
* (ii) the missing target is present, and — when `packageRoot` is known —
|
|
140
|
+
* (iii) `<packageRoot>/package.json` parses with `name === CLI_NAME` and
|
|
141
|
+
* (iv) no `.<leaf>-<rand>` retired sibling remains beside it. Every filesystem
|
|
142
|
+
* error makes its condition "not ready" rather than throwing. `deadlineMs === 0`
|
|
143
|
+
* evaluates readiness exactly once (no waiting); otherwise the whole wait is
|
|
144
|
+
* bounded, so the caller can never hang.
|
|
145
|
+
*/
|
|
146
|
+
export declare function waitForInstallTreeSettled(args: WaitForInstallTreeSettledArgs): Promise<WaitForInstallTreeSettledResult>;
|
|
147
|
+
/** The recovery OUTCOME, which — not the dialect — decides capture. */
|
|
148
|
+
export type InstallTreeTornOutcome = "guarded" | "unsettled" | "reexec-failed";
|
|
149
|
+
export type InstallTreeTornDiagnostics = {
|
|
150
|
+
dialect: ModuleErrorDialect;
|
|
151
|
+
code: ModuleNotFoundCode;
|
|
152
|
+
specifier: string;
|
|
153
|
+
importer: string;
|
|
154
|
+
packageRoot: string;
|
|
155
|
+
outcome: InstallTreeTornOutcome;
|
|
156
|
+
attempt: 1 | 2;
|
|
157
|
+
waitedMs: number;
|
|
158
|
+
sawLock: boolean;
|
|
159
|
+
sawRetired: boolean;
|
|
160
|
+
node: string;
|
|
161
|
+
};
|
|
162
|
+
export interface InstallTreeTornInit {
|
|
163
|
+
/** The original loader error, carried unchanged as `cause`. */
|
|
164
|
+
cause: unknown;
|
|
165
|
+
classified: ClassifiedModuleError;
|
|
166
|
+
packageRoot: string | null;
|
|
167
|
+
outcome: InstallTreeTornOutcome;
|
|
168
|
+
attempt: 1 | 2;
|
|
169
|
+
waitedMs: number;
|
|
170
|
+
sawLock: boolean;
|
|
171
|
+
sawRetired: boolean;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* A torn-install failure that stays VISIBLE in Sentry with bounded, hq-derived
|
|
175
|
+
* diagnostics. Fixed `name`/`message` (no argv, path, or free text in either),
|
|
176
|
+
* `cause` = the original loader error, and a bounded `diagnostics` object whose
|
|
177
|
+
* only interpolated strings are the hq-derived specifier/importer/packageRoot,
|
|
178
|
+
* each capped. Mirrors PackageRootResolutionError's construction discipline.
|
|
179
|
+
*/
|
|
180
|
+
export declare class InstallTreeTornError extends Error {
|
|
181
|
+
readonly diagnostics: InstallTreeTornDiagnostics;
|
|
182
|
+
constructor(init: InstallTreeTornInit);
|
|
183
|
+
}
|
|
184
|
+
export declare function isInstallTreeTornError(err: unknown): err is InstallTreeTornError;
|
|
185
|
+
/** The exact Sentry `contexts` payload for a captured torn-install failure. */
|
|
186
|
+
export declare function installTreeTornCaptureContext(err: InstallTreeTornError): {
|
|
187
|
+
contexts: {
|
|
188
|
+
install_tree_torn: InstallTreeTornDiagnostics;
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* The fixed, input-free operator remedy. It names the likely writers (so a
|
|
193
|
+
* human knows this was not their command's fault) and the reinstall commands,
|
|
194
|
+
* and interpolates NOTHING — the only per-event value is the bounded specifier
|
|
195
|
+
* appended by {@link installTreeTornStderrLine}.
|
|
196
|
+
*/
|
|
197
|
+
export declare const INSTALL_TREE_TORN_REMEDY: string;
|
|
198
|
+
/**
|
|
199
|
+
* The single actionable stderr line for a captured torn-install failure: the
|
|
200
|
+
* fixed remedy plus the bounded, hq-derived missing specifier — the only
|
|
201
|
+
* interpolated value, never argv.
|
|
202
|
+
*/
|
|
203
|
+
export declare function installTreeTornStderrLine(err: InstallTreeTornError): string;
|
|
204
|
+
//# sourceMappingURL=install-tree-torn.d.ts.map
|