@indigoai-us/hq-cli 5.299.0 → 5.301.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/assets/scaffold/core/scripts/token-usage-report.sh +9 -0
  3. package/dist/command-catalog.generated.d.ts +32 -6
  4. package/dist/command-catalog.generated.js +38 -6
  5. package/dist/commands/__fixtures__/access-vault.d.ts +2 -0
  6. package/dist/commands/__fixtures__/access-vault.js +1 -1
  7. package/dist/commands/access.d.ts +2 -1
  8. package/dist/commands/access.js +58 -49
  9. package/dist/commands/cloud-provision.js +51 -2
  10. package/dist/commands/cloud.js +12 -7
  11. package/dist/commands/core-company-index-scope-flag.d.ts +3 -0
  12. package/dist/commands/core-company-index-scope-flag.js +6 -0
  13. package/dist/commands/core-job-native-utils.d.ts +6 -0
  14. package/dist/commands/core-job-native-utils.js +80 -0
  15. package/dist/commands/core-job-native.d.ts +2 -4
  16. package/dist/commands/core-job-native.js +6 -10
  17. package/dist/commands/core-job-notify-native.d.ts +36 -0
  18. package/dist/commands/core-job-notify-native.js +359 -0
  19. package/dist/commands/core-job-remediate-native.d.ts +26 -0
  20. package/dist/commands/core-job-remediate-native.js +374 -0
  21. package/dist/commands/core.js +26 -8
  22. package/dist/commands/files.d.ts +1 -0
  23. package/dist/commands/files.js +90 -14
  24. package/dist/commands/integrations-api.d.ts +6 -6
  25. package/dist/commands/integrations-api.js +4 -13
  26. package/dist/commands/integrations-manage.js +1 -4
  27. package/dist/commands/lanes-message.js +78 -37
  28. package/dist/commands/lanes-resume.js +247 -210
  29. package/dist/commands/lanes-shared.d.ts +5 -1
  30. package/dist/commands/lanes.d.ts +4 -0
  31. package/dist/commands/lanes.js +7 -4
  32. package/dist/commands/meetings.js +35 -16
  33. package/dist/commands/safe-extract.d.ts +4 -4
  34. package/dist/commands/safe-extract.js +6 -7
  35. package/dist/lib/core-utils/token-usage-report.js +17 -3
  36. package/dist/lib/index-render/companies.d.ts +4 -1
  37. package/dist/lib/index-render/companies.js +66 -3
  38. package/dist/lib/index-render/index.d.ts +1 -0
  39. package/dist/lib/index-render/index.js +2 -1
  40. package/dist/lib/index-render/shared.d.ts +1 -1
  41. package/dist/lib/index-render/shared.js +4 -2
  42. package/dist/lib/lanes/reconcile.js +22 -4
  43. package/dist/lib/lanes/story-switch.d.ts +21 -0
  44. package/dist/lib/lanes/story-switch.js +75 -0
  45. package/dist/lib/lanes/types.d.ts +25 -0
  46. package/dist/lib/lanes/types.js +1 -1
  47. package/dist/lib/lanes/watcher.js +15 -4
  48. package/dist/lib/mesh/live/daemon/mqtt-events.d.ts +7 -0
  49. package/dist/lib/mesh/live/daemon/mqtt-events.js +2 -0
  50. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.d.ts +2 -0
  51. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.js +10 -0
  52. package/dist/lib/mesh/live/daemon/presence.d.ts +2 -2
  53. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +2 -2
  54. package/dist/utils/self-update.js +51 -4
  55. package/dist/utils/staged-npm-install.d.ts +16 -0
  56. package/dist/utils/staged-npm-install.js +264 -0
  57. package/dist/utils/update-command-supervisor.cjs +53 -1
  58. package/dist/utils/version-gate.d.ts +8 -0
  59. package/dist/utils/version-gate.js +82 -15
  60. package/package.json +2 -2
  61. package/dist/lib/install-path-compatibility-flag.d.ts +0 -9
  62. package/dist/lib/install-path-compatibility-flag.js +0 -12
@@ -3,9 +3,9 @@
3
3
  * Owns live-cache.json writes. Presence publish stays on PresenceClient.
4
4
  */
5
5
  import type { IClientOptions } from "mqtt";
6
+ import type { MeshMqttEventClient } from "./mqtt-events.js";
6
7
  import { type PersonalCredentialsFetcher } from "./credentials.js";
7
- export interface WorkCacheMqtt {
8
- on(event: string, handler: (...args: any[]) => void): unknown;
8
+ export interface WorkCacheMqtt extends MeshMqttEventClient {
9
9
  subscribe(topic: string, opts: {
10
10
  qos: 0 | 1;
11
11
  }, cb?: (err?: Error | null) => void): unknown;
@@ -748,7 +748,7 @@ async function updateAndReexec(argv, flavor, known, deps) {
748
748
  console.error(chalk.dim(` Or repair the install: hq rescue`));
749
749
  }
750
750
  console.error(chalk.dim(` Continuing the ${flavor.noun} on ${current}.`));
751
- await captureSelfUpdateOutcome("update-failed", current, latestValid, install.manager, result.code, env, flavor.noun === "rescue");
751
+ await captureSelfUpdateOutcome("update-failed", current, latestValid, install.manager, result, env, flavor.noun === "rescue");
752
752
  return { action: "update-failed", latest };
753
753
  }
754
754
  if (installSnapshot)
@@ -801,10 +801,52 @@ async function updateAndReexec(argv, flavor, known, deps) {
801
801
  console.error(chalk.dim(`✓ hq-cli updated to ${latest}.`));
802
802
  return { action: "reexec", reexecStatus: status, latest };
803
803
  }
804
- const SELF_UPDATE_ERROR_CLASSES = new Set(["MODULE_NOT_FOUND", "EACCES", "EEXIST"]);
805
- async function captureSelfUpdateOutcome(action, fromVersion, targetVersion, installFlavor, code, env, flushBeforeExit) {
804
+ const SELF_UPDATE_ERROR_CLASSES = new Set([
805
+ "MODULE_NOT_FOUND",
806
+ "ENOENT",
807
+ "EACCES",
808
+ "EEXIST",
809
+ "E404",
810
+ "EAI_AGAIN",
811
+ "ECONNREFUSED",
812
+ "ECONNRESET",
813
+ "ENOTFOUND",
814
+ "EPERM",
815
+ "ETARGET",
816
+ "ETIMEDOUT",
817
+ ]);
818
+ function selfUpdateErrorReason(code, failureKind) {
819
+ switch (code) {
820
+ case "MODULE_NOT_FOUND":
821
+ return "module_missing";
822
+ case "ENOENT":
823
+ return "program_not_found";
824
+ case "EACCES":
825
+ case "EPERM":
826
+ return "permission_denied";
827
+ case "EEXIST":
828
+ return "file_collision";
829
+ case "E404":
830
+ return "npm_package_not_found";
831
+ case "ETARGET":
832
+ return "npm_target_unavailable";
833
+ case "EAI_AGAIN":
834
+ case "ECONNREFUSED":
835
+ case "ECONNRESET":
836
+ case "ENOTFOUND":
837
+ return "registry_network_error";
838
+ case "ETIMEDOUT":
839
+ return "timed_out";
840
+ default:
841
+ if (failureKind === "package_manager_exit")
842
+ return "package_manager_exit";
843
+ return code ? "other_error" : "unknown";
844
+ }
845
+ }
846
+ async function captureSelfUpdateOutcome(action, fromVersion, targetVersion, installFlavor, failure, env, flushBeforeExit) {
806
847
  if (isCliTelemetryDisabled(env))
807
848
  return;
849
+ const code = failure?.diagnosticCode ?? failure?.code;
808
850
  const error = new Error(action === "update-failed" ? "Self-update failed" : "Self-update ineffective");
809
851
  // This event needs no stack: frames would expose the machine's install path.
810
852
  error.stack = undefined;
@@ -814,7 +856,12 @@ async function captureSelfUpdateOutcome(action, fromVersion, targetVersion, inst
814
856
  from_version: fromVersion,
815
857
  target_version: targetVersion,
816
858
  install_flavor: installFlavor,
817
- error_class: code && SELF_UPDATE_ERROR_CLASSES.has(code) ? code : "other",
859
+ error_class: code && SELF_UPDATE_ERROR_CLASSES.has(code)
860
+ ? code
861
+ : failure?.failureKind === "package_manager_exit"
862
+ ? "package_manager_exit"
863
+ : "other",
864
+ error_reason: selfUpdateErrorReason(code, failure?.failureKind),
818
865
  },
819
866
  });
820
867
  if (flushBeforeExit) {
@@ -0,0 +1,16 @@
1
+ export type StagedInstallResult = {
2
+ ok: boolean;
3
+ detail?: string;
4
+ code?: string;
5
+ };
6
+ export type StagedInstallRunner = (command: string, args: string[], env?: NodeJS.ProcessEnv) => StagedInstallResult;
7
+ /**
8
+ * Build hq-cli in a space-free temporary npm prefix, then replace the package
9
+ * and its executable links under the real prefix only after npm succeeds.
10
+ * Existing files are held beside their destinations until activation completes,
11
+ * so failed installs and failed activation restore the prior working tree.
12
+ */
13
+ export declare function installNpmPackageStaged(prefix: string, packageSpec: string, runner: StagedInstallRunner, platform?: NodeJS.Platform, activate?: typeof activateStagedNpmPackage): StagedInstallResult;
14
+ /** Atomically replace a package already built under a temporary npm prefix. */
15
+ export declare function activateStagedNpmPackage(prefix: string, stagePrefix: string, platform?: NodeJS.Platform): StagedInstallResult;
16
+ //# sourceMappingURL=staged-npm-install.d.ts.map
@@ -0,0 +1,264 @@
1
+ import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync, } from "node:fs";
2
+ import { randomBytes } from "node:crypto";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ const PACKAGE_NAME = "@indigoai-us/hq-cli";
7
+ const RANDOM_HEX_BYTES = 6;
8
+ function packageRoot(prefix, platform) {
9
+ const modules = platform === "win32"
10
+ ? path.join(prefix, "node_modules")
11
+ : path.join(prefix, "lib", "node_modules");
12
+ return path.join(modules, "@indigoai-us", "hq-cli");
13
+ }
14
+ function binsFromPackageJson(packageDir) {
15
+ const parsed = JSON.parse(readFileSync(path.join(packageDir, "package.json"), "utf8"));
16
+ if (parsed.name !== PACKAGE_NAME)
17
+ throw new Error("staged npm package name did not match hq-cli");
18
+ const bins = typeof parsed.bin === "string"
19
+ ? { hq: parsed.bin }
20
+ : parsed.bin && typeof parsed.bin === "object" && !Array.isArray(parsed.bin)
21
+ ? parsed.bin
22
+ : {};
23
+ for (const [name, target] of Object.entries(bins)) {
24
+ if (!name || name === "." || name === ".." || /[\\/]/.test(name)) {
25
+ throw new Error(`invalid executable name in staged package: ${name}`);
26
+ }
27
+ if (typeof target !== "string" || path.isAbsolute(target)) {
28
+ throw new Error(`invalid executable target in staged package: ${name}`);
29
+ }
30
+ const resolved = path.resolve(packageDir, target);
31
+ if (resolved !== packageDir && !resolved.startsWith(`${packageDir}${path.sep}`)) {
32
+ throw new Error(`executable target escapes staged package: ${name}`);
33
+ }
34
+ }
35
+ return bins;
36
+ }
37
+ function oldBinNames(packageDir) {
38
+ try {
39
+ return Object.keys(binsFromPackageJson(packageDir));
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ }
45
+ function temporaryRoot() {
46
+ const candidates = [os.tmpdir(), process.platform === "win32" ? `${process.env.SystemDrive ?? "C:"}\\Temp` : "/tmp"];
47
+ const root = candidates.find((candidate) => !/\s/.test(candidate) && existsSync(candidate));
48
+ if (!root)
49
+ throw new Error("no writable temporary directory without spaces is available");
50
+ return mkdtempSync(path.join(root, "hq-cli-update-"));
51
+ }
52
+ function randomSuffix() {
53
+ return randomBytes(RANDOM_HEX_BYTES).toString("hex");
54
+ }
55
+ function bestEffortRemove(target) {
56
+ if (!target)
57
+ return;
58
+ try {
59
+ rmSync(target, { recursive: true, force: true });
60
+ }
61
+ catch (error) {
62
+ console.warn("[hq-cli-update] could not remove staged install artifact", {
63
+ path: target,
64
+ error: error instanceof Error ? error.message : String(error),
65
+ });
66
+ }
67
+ }
68
+ /**
69
+ * Build hq-cli in a space-free temporary npm prefix, then replace the package
70
+ * and its executable links under the real prefix only after npm succeeds.
71
+ * Existing files are held beside their destinations until activation completes,
72
+ * so failed installs and failed activation restore the prior working tree.
73
+ */
74
+ export function installNpmPackageStaged(prefix, packageSpec, runner, platform = process.platform, activate = activateStagedNpmPackage) {
75
+ let stagePrefix;
76
+ try {
77
+ stagePrefix = temporaryRoot();
78
+ const npmResult = runner("npm", ["install", "-g", "--prefix", stagePrefix, packageSpec], { ...process.env, HQ_STAGED_INSTALL: "1" });
79
+ if (!npmResult.ok)
80
+ return npmResult;
81
+ const activation = activate(prefix, stagePrefix, platform);
82
+ if (activation.ok || activation.code !== "EACCES" || platform === "win32")
83
+ return activation;
84
+ // A spaced, root-owned npm prefix must still be buildable without passing
85
+ // that prefix to npm/node-gyp. Re-run only the filesystem activation as
86
+ // root; the package build remains in the space-free user-owned temp path.
87
+ const elevated = runner("sudo", [
88
+ "-n",
89
+ "npm",
90
+ "exec",
91
+ "--",
92
+ process.execPath,
93
+ fileURLToPath(import.meta.url),
94
+ "--activate",
95
+ prefix,
96
+ stagePrefix,
97
+ platform,
98
+ ]);
99
+ return elevated.ok
100
+ ? { ok: true }
101
+ : {
102
+ ...elevated,
103
+ detail: elevated.detail ?? "could not activate the staged npm install with sudo",
104
+ };
105
+ }
106
+ catch (error) {
107
+ return {
108
+ ok: false,
109
+ detail: error instanceof Error ? error.message : String(error),
110
+ code: error?.code,
111
+ };
112
+ }
113
+ finally {
114
+ bestEffortRemove(stagePrefix);
115
+ }
116
+ }
117
+ /** Atomically replace a package already built under a temporary npm prefix. */
118
+ export function activateStagedNpmPackage(prefix, stagePrefix, platform = process.platform) {
119
+ let candidatePackage;
120
+ let stagedBins;
121
+ let backupBins;
122
+ let backupPackage;
123
+ let packageBackedUp = false;
124
+ let packageActivated = false;
125
+ const backedUpBins = [];
126
+ const activatedBins = [];
127
+ try {
128
+ const stagedPackage = packageRoot(stagePrefix, platform);
129
+ const targetPackage = packageRoot(prefix, platform);
130
+ const stageBinDir = platform === "win32" ? stagePrefix : path.join(stagePrefix, "bin");
131
+ const targetBinDir = platform === "win32" ? prefix : path.join(prefix, "bin");
132
+ const bins = binsFromPackageJson(stagedPackage);
133
+ const suffix = randomSuffix();
134
+ const targetScopeDir = path.dirname(targetPackage);
135
+ mkdirSync(targetScopeDir, { recursive: true });
136
+ mkdirSync(targetBinDir, { recursive: true });
137
+ candidatePackage = path.join(targetScopeDir, `.hq-cli-next-${suffix}`);
138
+ cpSync(stagedPackage, candidatePackage, { recursive: true, verbatimSymlinks: true });
139
+ stagedBins = path.join(targetBinDir, `.hq-cli-bins-next-${suffix}`);
140
+ backupBins = path.join(targetBinDir, `.hq-cli-bins-backup-${suffix}`);
141
+ mkdirSync(stagedBins);
142
+ mkdirSync(backupBins);
143
+ backupPackage = path.join(targetScopeDir, `.hq-cli-backup-${suffix}`);
144
+ const names = new Set([...Object.keys(bins), ...oldBinNames(targetPackage)]);
145
+ for (const name of names) {
146
+ const entry = bins[name];
147
+ if (entry === undefined)
148
+ continue;
149
+ const executable = path.join(targetPackage, entry);
150
+ if (platform === "win32") {
151
+ for (const extension of ["", ".cmd", ".ps1"]) {
152
+ const stagedFile = path.join(stageBinDir, `${name}${extension}`);
153
+ if (!existsSync(stagedFile))
154
+ continue;
155
+ const stagedText = readFileSync(stagedFile, "utf8");
156
+ const relocated = stagedText
157
+ .split(stagePrefix).join(prefix)
158
+ .split(stagePrefix.replace(/\\/g, "/")).join(prefix.replace(/\\/g, "/"));
159
+ writeFileSync(path.join(stagedBins, `${name}${extension}`), relocated, {
160
+ mode: extension === ".cmd" || extension === "" ? 0o755 : 0o644,
161
+ });
162
+ }
163
+ }
164
+ else {
165
+ const stagedFile = path.join(stageBinDir, name);
166
+ if (!existsSync(stagedFile))
167
+ throw new Error(`npm did not create the ${name} executable link`);
168
+ if (!existsSync(executable))
169
+ throw new Error(`staged executable target is missing for ${name}`);
170
+ symlinkSync(path.relative(targetBinDir, executable), path.join(stagedBins, name));
171
+ }
172
+ }
173
+ if (existsSync(targetPackage)) {
174
+ renameSync(targetPackage, backupPackage);
175
+ packageBackedUp = true;
176
+ }
177
+ renameSync(candidatePackage, targetPackage);
178
+ packageActivated = true;
179
+ for (const name of names) {
180
+ for (const extension of platform === "win32" ? ["", ".cmd", ".ps1"] : [""]) {
181
+ const filename = `${name}${extension}`;
182
+ const target = path.join(targetBinDir, filename);
183
+ const staged = path.join(stagedBins, filename);
184
+ if (existsSync(target) || lstatExists(target)) {
185
+ const backup = path.join(backupBins, filename);
186
+ renameSync(target, backup);
187
+ backedUpBins.push({ target, backup });
188
+ }
189
+ if (existsSync(staged) || lstatExists(staged)) {
190
+ renameSync(staged, target);
191
+ activatedBins.push(target);
192
+ }
193
+ }
194
+ }
195
+ bestEffortRemove(packageBackedUp ? backupPackage : undefined);
196
+ bestEffortRemove(backupBins);
197
+ return { ok: true };
198
+ }
199
+ catch (error) {
200
+ for (const target of activatedBins.reverse())
201
+ bestEffortRemove(target);
202
+ for (const item of backedUpBins.reverse()) {
203
+ if (existsSync(item.backup) || lstatExists(item.backup)) {
204
+ try {
205
+ renameSync(item.backup, item.target);
206
+ }
207
+ catch (restoreError) {
208
+ console.error("[hq-cli-update] could not restore previous executable link", {
209
+ path: item.target,
210
+ error: restoreError instanceof Error ? restoreError.message : String(restoreError),
211
+ });
212
+ }
213
+ }
214
+ }
215
+ if (packageActivated)
216
+ bestEffortRemove(packageRoot(prefix, platform));
217
+ if (packageBackedUp && backupPackage && existsSync(backupPackage)) {
218
+ try {
219
+ renameSync(backupPackage, packageRoot(prefix, platform));
220
+ }
221
+ catch (restoreError) {
222
+ console.error("[hq-cli-update] could not restore previous package", {
223
+ path: packageRoot(prefix, platform),
224
+ error: restoreError instanceof Error ? restoreError.message : String(restoreError),
225
+ });
226
+ }
227
+ }
228
+ return {
229
+ ok: false,
230
+ detail: error instanceof Error ? error.message : String(error),
231
+ code: error?.code,
232
+ };
233
+ }
234
+ finally {
235
+ bestEffortRemove(candidatePackage);
236
+ bestEffortRemove(stagedBins);
237
+ bestEffortRemove(backupBins);
238
+ if (!packageBackedUp)
239
+ bestEffortRemove(backupPackage);
240
+ }
241
+ }
242
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) && process.argv[2] === "--activate") {
243
+ const [prefix, stagePrefix, platform] = process.argv.slice(3);
244
+ if (!prefix || !stagePrefix) {
245
+ console.error("usage: staged-npm-install.js --activate <prefix> <stage-prefix> [platform]");
246
+ process.exit(2);
247
+ }
248
+ else {
249
+ const result = activateStagedNpmPackage(prefix, stagePrefix, platform || process.platform);
250
+ if (!result.ok)
251
+ console.error(result.detail ?? "staged npm activation failed");
252
+ process.exit(result.ok ? 0 : 1);
253
+ }
254
+ }
255
+ function lstatExists(target) {
256
+ try {
257
+ lstatSync(target);
258
+ return true;
259
+ }
260
+ catch {
261
+ return false;
262
+ }
263
+ }
264
+ //# sourceMappingURL=staged-npm-install.js.map
@@ -3,6 +3,18 @@
3
3
  const { spawn: nativeSpawn } = require("node:child_process");
4
4
  const fs = require("node:fs");
5
5
 
6
+ const KNOWN_NPM_ERROR_CODES = new Set([
7
+ "E404",
8
+ "EACCES",
9
+ "EAI_AGAIN",
10
+ "ECONNREFUSED",
11
+ "ECONNRESET",
12
+ "ENOTFOUND",
13
+ "EPERM",
14
+ "ETARGET",
15
+ "ETIMEDOUT",
16
+ ]);
17
+
6
18
  function windowsTaskkillArgs(pid) {
7
19
  return ["/PID", String(pid), "/T", "/F"];
8
20
  }
@@ -24,6 +36,14 @@ function writeMarker(filename, contents, log) {
24
36
  }
25
37
  }
26
38
 
39
+ function npmErrorCodeFromLine(line) {
40
+ const escape = String.fromCharCode(0x1b);
41
+ const plainLine = line.replace(new RegExp(`${escape}\\[[0-9;]*m`, "g"), "").trim();
42
+ const match = /^npm\s+(?:ERR!|error)\s+code\s+([A-Z0-9_]{1,32})$/i.exec(plainLine);
43
+ const code = match && match[1].toUpperCase();
44
+ return code && KNOWN_NPM_ERROR_CODES.has(code) ? code : undefined;
45
+ }
46
+
27
47
  /**
28
48
  * Spawn and supervise one package-manager update. On POSIX the detached child
29
49
  * is its own process-group leader, so signals reach the leader and every
@@ -50,6 +70,31 @@ function runUpdateSupervisor(config, dependencies = {}) {
50
70
  let timedOut = false;
51
71
  let childClosed = false;
52
72
  let treeTerminationConfirmed = false;
73
+ let stderrLineBuffer = "";
74
+ let diagnosticRecorded = false;
75
+
76
+ function inspectNpmStderrLine(line) {
77
+ if (diagnosticRecorded || !config.diagnosticMarker) return;
78
+ const code = npmErrorCodeFromLine(line);
79
+ if (!code) return;
80
+ writeMarker(config.diagnosticMarker, code, log);
81
+ diagnosticRecorded = true;
82
+ }
83
+
84
+ function inspectNpmStderrChunk(chunk) {
85
+ if (diagnosticRecorded || !config.diagnosticMarker) return;
86
+ const lines = `${stderrLineBuffer}${chunk.toString("utf8")}`.split(/\r?\n/);
87
+ stderrLineBuffer = lines.pop() || "";
88
+ for (const line of lines) inspectNpmStderrLine(line);
89
+ if (stderrLineBuffer.length > 128) {
90
+ stderrLineBuffer = stderrLineBuffer.slice(-128);
91
+ }
92
+ }
93
+
94
+ function flushNpmStderrLine() {
95
+ if (stderrLineBuffer) inspectNpmStderrLine(stderrLineBuffer);
96
+ stderrLineBuffer = "";
97
+ }
53
98
 
54
99
  function clear(timer) {
55
100
  if (timer !== undefined) clearTimer(timer);
@@ -141,7 +186,7 @@ function runUpdateSupervisor(config, dependencies = {}) {
141
186
  try {
142
187
  child = spawn(config.cmd, config.args, {
143
188
  detached: true,
144
- stdio: ["ignore", "inherit", "inherit"],
189
+ stdio: ["ignore", "inherit", config.diagnosticMarker ? "pipe" : "inherit"],
145
190
  shell: config.shell,
146
191
  });
147
192
  } catch (error) {
@@ -156,6 +201,7 @@ function runUpdateSupervisor(config, dependencies = {}) {
156
201
  });
157
202
  child.once("close", (code) => {
158
203
  childClosed = true;
204
+ flushNpmStderrLine();
159
205
  if (!timedOut) {
160
206
  finish(code === 0 ? 0 : (code ?? 1));
161
207
  return;
@@ -163,6 +209,12 @@ function runUpdateSupervisor(config, dependencies = {}) {
163
209
  if (treeTerminationConfirmed) timeoutAfterReap();
164
210
  });
165
211
 
212
+ if (config.diagnosticMarker && child.stderr) {
213
+ child.stderr.on("data", inspectNpmStderrChunk);
214
+ child.stderr.on("end", flushNpmStderrLine);
215
+ child.stderr.pipe(process.stderr);
216
+ }
217
+
166
218
  deadlineTimer = setTimer(() => {
167
219
  if (finished) return;
168
220
  timedOut = true;
@@ -256,6 +256,9 @@ export type UpdateResult = {
256
256
  ok: boolean;
257
257
  detail?: string;
258
258
  code?: string;
259
+ /** Sanitized installer error code for diagnostics; never drives retry or exit behavior. */
260
+ diagnosticCode?: string;
261
+ failureKind?: "package_manager_exit";
259
262
  };
260
263
  type UpdateRunner = (cmd: string, args: string[], env?: NodeJS.ProcessEnv) => UpdateResult;
261
264
  export { buildSpawnPlan, quoteForWindowsShell };
@@ -323,6 +326,10 @@ export interface InstallOutput {
323
326
  stdio: ("ignore" | "inherit" | number)[];
324
327
  /** Captured stderr tail, or "" when output was inherited by a terminal. */
325
328
  stderrTail: () => string;
329
+ /** Allowlisted npm error code from captured stderr, when output was captured. */
330
+ diagnosticCode: () => string | undefined;
331
+ /** Supervisor marker used to inspect npm errors while forwarding terminal stderr. */
332
+ diagnosticMarker?: string;
326
333
  /** Private side-channel from the supervisor, kept out of installer output. */
327
334
  timeoutMarker: string;
328
335
  /** Private spawn-error side-channel that preserves the prior result contract. */
@@ -414,6 +421,7 @@ interface EnforceUpdateDeps {
414
421
  * returns nothing still exercises the success path.
415
422
  */
416
423
  checkConvergence?: (targetVersion: string) => boolean | void;
424
+ installNpmPackageStaged?: typeof import("./staged-npm-install.js").installNpmPackageStaged;
417
425
  }
418
426
  /**
419
427
  * Hard enforcement when the server says we're below `minVersion`. Print a