@indigoai-us/hq-cli 5.300.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 (53) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/assets/scaffold/core/scripts/token-usage-report.sh +9 -0
  3. package/dist/command-catalog.generated.d.ts +28 -2
  4. package/dist/command-catalog.generated.js +34 -2
  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/core-company-index-scope-flag.d.ts +3 -0
  11. package/dist/commands/core-company-index-scope-flag.js +6 -0
  12. package/dist/commands/core-job-native-utils.d.ts +6 -0
  13. package/dist/commands/core-job-native-utils.js +80 -0
  14. package/dist/commands/core-job-native.d.ts +2 -4
  15. package/dist/commands/core-job-native.js +6 -10
  16. package/dist/commands/core-job-notify-native.d.ts +36 -0
  17. package/dist/commands/core-job-notify-native.js +359 -0
  18. package/dist/commands/core-job-remediate-native.d.ts +26 -0
  19. package/dist/commands/core-job-remediate-native.js +374 -0
  20. package/dist/commands/core.js +26 -8
  21. package/dist/commands/files.js +31 -14
  22. package/dist/commands/integrations-api.d.ts +6 -6
  23. package/dist/commands/integrations-api.js +4 -13
  24. package/dist/commands/integrations-manage.js +1 -4
  25. package/dist/commands/lanes-message.js +10 -2
  26. package/dist/commands/lanes-shared.d.ts +2 -0
  27. package/dist/commands/lanes.d.ts +2 -0
  28. package/dist/commands/lanes.js +1 -0
  29. package/dist/commands/meetings.js +35 -16
  30. package/dist/commands/safe-extract.d.ts +4 -4
  31. package/dist/commands/safe-extract.js +6 -7
  32. package/dist/lib/core-utils/token-usage-report.js +17 -3
  33. package/dist/lib/index-render/companies.d.ts +4 -1
  34. package/dist/lib/index-render/companies.js +66 -3
  35. package/dist/lib/index-render/index.d.ts +1 -0
  36. package/dist/lib/index-render/index.js +2 -1
  37. package/dist/lib/index-render/shared.d.ts +1 -1
  38. package/dist/lib/index-render/shared.js +4 -2
  39. package/dist/lib/mesh/live/daemon/mqtt-events.d.ts +7 -0
  40. package/dist/lib/mesh/live/daemon/mqtt-events.js +2 -0
  41. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.d.ts +2 -0
  42. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.js +10 -0
  43. package/dist/lib/mesh/live/daemon/presence.d.ts +2 -2
  44. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +2 -2
  45. package/dist/utils/self-update.js +51 -4
  46. package/dist/utils/staged-npm-install.d.ts +16 -0
  47. package/dist/utils/staged-npm-install.js +264 -0
  48. package/dist/utils/update-command-supervisor.cjs +53 -1
  49. package/dist/utils/version-gate.d.ts +8 -0
  50. package/dist/utils/version-gate.js +82 -15
  51. package/package.json +2 -2
  52. package/dist/lib/install-path-compatibility-flag.d.ts +0 -9
  53. package/dist/lib/install-path-compatibility-flag.js +0 -12
@@ -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
@@ -28,7 +28,6 @@
28
28
  * to silence both check + gate).
29
29
  */
30
30
  import { spawnSync } from "node:child_process";
31
- import { installPathCompatibilityEnabled } from "../lib/install-path-compatibility-flag.js";
32
31
  import { buildSpawnPlan, quoteForWindowsShell } from "./windows-spawn.js";
33
32
  import { accessSync, closeSync, constants as fsConstants, copyFileSync, existsSync, mkdtempSync, openSync, readdirSync, realpathSync, readFileSync, rmSync, } from "node:fs";
34
33
  import os from "node:os";
@@ -106,8 +105,7 @@ function findPackageRootForExecutable(bin) {
106
105
  path.join(storeRoot, "node_modules", ...packageSegments),
107
106
  ];
108
107
  // pnpm v11 adds a hash between its global store version and node_modules.
109
- // Its `vN` layout remains subject to the existing compatibility gate unless
110
- // PNPM_HOME itself authorizes the global root via isPnpmManagedPackageDir.
108
+ // Its documented `vN` global layout is recognized unconditionally below.
111
109
  if (/^v\d+$/.test(storeVersion)) {
112
110
  try {
113
111
  for (const hash of readdirSync(storeRoot)) {
@@ -122,7 +120,7 @@ function findPackageRootForExecutable(bin) {
122
120
  const packageRoot = findCliPackageRoot(candidate);
123
121
  if (packageRoot &&
124
122
  samePath(packageRoot, candidate) &&
125
- isPnpmManagedPackageDir(packageRoot, installPathCompatibilityEnabled())) {
123
+ isPnpmManagedPackageDir(packageRoot, true)) {
126
124
  return packageRoot;
127
125
  }
128
126
  }
@@ -145,7 +143,7 @@ function samePath(left, right) {
145
143
  : a === b;
146
144
  }
147
145
  function globalInstallManagerForPath(bin, packageRoot) {
148
- if (isPnpmManagedPackageDir(packageRoot, installPathCompatibilityEnabled())) {
146
+ if (isPnpmManagedPackageDir(packageRoot, true)) {
149
147
  return "pnpm";
150
148
  }
151
149
  if (isBunManagedPackageDir(packageRoot))
@@ -209,13 +207,13 @@ function pnpmHomeGlobalRoot() {
209
207
  * i.e. the gate would re-fire and re-install globally on every subsequent run.
210
208
  * Those layouts fall through to the ordinary npm-prefix/manual handling.
211
209
  */
212
- export function isPnpmManagedPackageDir(pkgDir, acceptVersionPrefixedGlobalDir = false) {
210
+ export function isPnpmManagedPackageDir(pkgDir, acceptVersionPrefixedGlobalDir = true) {
213
211
  const normalized = pkgDir.replace(/\\/g, "/").replace(/\/+$/, "");
214
212
  const segments = normalized.split("/").filter(Boolean);
215
213
  // pnpm through v10 uses `global/<digits>`; pnpm v11+ uses
216
- // `global/v<major>`. Keep the newer form behind the default-off rollout
217
- // flag, and require a documented version segment so `global/next` or a
218
- // `v11` directory elsewhere cannot be mistaken for a global install.
214
+ // `global/v<major>`. Recognize both documented layouts, and require a
215
+ // version segment so `global/next` or a `v11` directory elsewhere cannot be
216
+ // mistaken for a global install.
219
217
  for (let i = 0; i < segments.length - 1; i += 1) {
220
218
  if (segments[i] !== "global")
221
219
  continue;
@@ -355,7 +353,7 @@ export function resolveRunningInstall() {
355
353
  const packageRoot = findRunningPackageRoot();
356
354
  if (!packageRoot)
357
355
  return { manager: "npm", prefix: null, packageRoot: null };
358
- if (isPnpmManagedPackageDir(packageRoot, installPathCompatibilityEnabled())) {
356
+ if (isPnpmManagedPackageDir(packageRoot, true)) {
359
357
  return { manager: "pnpm", prefix: null, packageRoot };
360
358
  }
361
359
  if (isBunManagedPackageDir(packageRoot)) {
@@ -643,6 +641,47 @@ export function inOwnProcessGroup(options) {
643
641
  }
644
642
  /** Tail of captured installer stderr kept to explain a failure. */
645
643
  const INSTALL_DETAIL_MAX_CHARS = 400;
644
+ const KNOWN_NPM_UPDATE_ERROR_CODES = new Set([
645
+ "E404",
646
+ "EACCES",
647
+ "EAI_AGAIN",
648
+ "ECONNREFUSED",
649
+ "ECONNRESET",
650
+ "ENOTFOUND",
651
+ "EPERM",
652
+ "ETARGET",
653
+ "ETIMEDOUT",
654
+ ]);
655
+ function knownNpmUpdateErrorCode(stderr) {
656
+ const escape = String.fromCharCode(0x1b);
657
+ const plainStderr = stderr.replace(new RegExp(`${escape}\\[[0-9;]*m`, "g"), "");
658
+ const match = /^[ \t]*npm[ \t]+(?:ERR!|error)[ \t]+code[ \t]+([A-Z0-9_]{1,32})[ \t]*$/im.exec(plainStderr);
659
+ const code = match?.[1]?.toUpperCase();
660
+ return code && KNOWN_NPM_UPDATE_ERROR_CODES.has(code) ? code : undefined;
661
+ }
662
+ function readKnownNpmDiagnosticCode(filename) {
663
+ if (!existsSync(filename))
664
+ return undefined;
665
+ try {
666
+ const contents = readFileSync(filename, "utf-8").trim();
667
+ if (KNOWN_NPM_UPDATE_ERROR_CODES.has(contents))
668
+ return contents;
669
+ return knownNpmUpdateErrorCode(contents);
670
+ }
671
+ catch (error) {
672
+ const code = error &&
673
+ typeof error === "object" &&
674
+ "code" in error &&
675
+ typeof error.code === "string" &&
676
+ /^[A-Z0-9_]{1,32}$/.test(error.code)
677
+ ? error.code
678
+ : error instanceof Error
679
+ ? "Error"
680
+ : "UnknownError";
681
+ console.error(chalk.yellow(`Could not read update diagnostic output (${code}).`));
682
+ return undefined;
683
+ }
684
+ }
646
685
  /** Give a package-manager update ample time without letting it wedge every CLI call. */
647
686
  const DEFAULT_UPDATE_TIMEOUT_MS = 30 * 60 * 1_000;
648
687
  /** Node clamps longer timer delays to 1ms, so accepting them would invert the deadline. */
@@ -753,9 +792,12 @@ export function openInstallOutput(verbose, isTty = process.stdout.isTTY === true
753
792
  const dir = mkdtempSync(path.join(os.tmpdir(), "hq-cli-install-"));
754
793
  const timeoutMarker = path.join(dir, "timed-out");
755
794
  const errorMarker = path.join(dir, "spawn-error.json");
795
+ const diagnosticMarker = path.join(dir, "npm-error-code");
756
796
  return {
757
797
  stdio: ["ignore", "inherit", "inherit"],
758
798
  stderrTail: () => "",
799
+ diagnosticCode: () => readKnownNpmDiagnosticCode(diagnosticMarker),
800
+ diagnosticMarker,
759
801
  timeoutMarker,
760
802
  errorMarker,
761
803
  timedOut: () => existsSync(timeoutMarker),
@@ -786,6 +828,7 @@ export function openInstallOutput(verbose, isTty = process.stdout.isTTY === true
786
828
  return "";
787
829
  }
788
830
  },
831
+ diagnosticCode: () => readKnownNpmDiagnosticCode(errPath),
789
832
  timeoutMarker: timeoutPath,
790
833
  errorMarker: errorPath,
791
834
  timedOut: () => existsSync(timeoutPath),
@@ -827,6 +870,7 @@ export function runSupervisedUpdateCommand(cmd, args, output, env) {
827
870
  reapMs: UPDATE_REAP_GRACE_MS,
828
871
  timeoutMarker: output.timeoutMarker,
829
872
  errorMarker: output.errorMarker,
873
+ diagnosticMarker: output.diagnosticMarker,
830
874
  }),
831
875
  ], inOwnProcessGroup({
832
876
  stdio: output.stdio,
@@ -863,6 +907,8 @@ export function runSupervisedUpdateCommand(cmd, args, output, env) {
863
907
  const captured = output.stderrTail();
864
908
  return {
865
909
  ok: false,
910
+ diagnosticCode: output.diagnosticCode(),
911
+ failureKind: "package_manager_exit",
866
912
  detail: captured || `exit ${result.status ?? "signal"}`,
867
913
  };
868
914
  }
@@ -1214,6 +1260,7 @@ function attemptRequiredUpdate(decision, deps, install) {
1214
1260
  const command = decision.updateCommand;
1215
1261
  const isManagedOutsideNpm = install.manager !== "npm";
1216
1262
  const prefix = install.prefix;
1263
+ const stagedNpmPrefix = Boolean(!isManagedOutsideNpm && prefix && /\s/.test(prefix));
1217
1264
  const runner = deps.runner ?? runUpdateCommand;
1218
1265
  // Resolve the concrete install argv once so the sudo fallback below can re-run
1219
1266
  // the EXACT same command under elevation.
@@ -1250,8 +1297,20 @@ function attemptRequiredUpdate(decision, deps, install) {
1250
1297
  }
1251
1298
  else if (prefix) {
1252
1299
  console.error(chalk.dim(` Installing into npm prefix: ${prefix}`));
1253
- console.error(chalk.dim(` Running: npm ${primaryArgs.join(" ")}`));
1254
- result = performUpdateCommand("npm", primaryArgs, runner);
1300
+ if (stagedNpmPrefix) {
1301
+ console.error(chalk.dim(" Building in a space-free temporary prefix before activation."));
1302
+ const installStaged = deps.installNpmPackageStaged;
1303
+ if (!installStaged) {
1304
+ result = { ok: false, detail: "staged npm installer was not loaded" };
1305
+ }
1306
+ else {
1307
+ result = installStaged(prefix, LATEST_PACKAGE_SPEC, runner);
1308
+ }
1309
+ }
1310
+ else {
1311
+ console.error(chalk.dim(` Running: npm ${primaryArgs.join(" ")}`));
1312
+ result = performUpdateCommand("npm", primaryArgs, runner);
1313
+ }
1255
1314
  }
1256
1315
  else {
1257
1316
  console.error(chalk.dim(` Running: ${command}`));
@@ -1267,7 +1326,7 @@ function attemptRequiredUpdate(decision, deps, install) {
1267
1326
  // artifacts and retry the install ONCE. Guarded on `cleaned.length > 0` so a
1268
1327
  // plain EACCES on an otherwise-healthy prefix falls straight through to the
1269
1328
  // sudo retry below without a redundant reinstall attempt.
1270
- if (!result.ok && result.code !== "ETIMEDOUT" && prefix) {
1329
+ if (!result.ok && result.code !== "ETIMEDOUT" && prefix && !stagedNpmPrefix) {
1271
1330
  const cleaner = deps.cleanStale ?? cleanStalePartialInstall;
1272
1331
  const cleaned = cleaner(prefix);
1273
1332
  if (cleaned.length > 0) {
@@ -1292,7 +1351,8 @@ function attemptRequiredUpdate(decision, deps, install) {
1292
1351
  !result.ok &&
1293
1352
  result.code !== "ETIMEDOUT" &&
1294
1353
  primaryCmd &&
1295
- !isManagedOutsideNpm) {
1354
+ !isManagedOutsideNpm &&
1355
+ !stagedNpmPrefix) {
1296
1356
  console.error(chalk.dim(` Update failed unprivileged; retrying with: sudo -n ${primaryCmd} ${primaryArgs.join(" ")}`));
1297
1357
  const sudoResult = performUpdateCommand("sudo", ["-n", primaryCmd, ...primaryArgs], runner);
1298
1358
  if (sudoResult.ok)
@@ -1358,9 +1418,16 @@ export async function enforceVersionGate(onUpdateRecommended, options = {}) {
1358
1418
  // neither downstream path repeats it.
1359
1419
  const install = resolveRunningInstall();
1360
1420
  if (decision.updateRequired) {
1421
+ // Load the staged installer only for the uncommon spaced npm-prefix path.
1422
+ // The version gate is imported during every CLI startup, including native
1423
+ // fast routes, so eagerly importing this filesystem-heavy module adds cost
1424
+ // to commands that never update.
1425
+ const installNpmPackageStaged = install.manager === "npm" && install.prefix && /\s/.test(install.prefix)
1426
+ ? (await import("./staged-npm-install.js")).installNpmPackageStaged
1427
+ : undefined;
1361
1428
  // Exits the process, EXCEPT when another updater holds the shared update
1362
1429
  // lock — then it returns and the CLI continues on the current version.
1363
- enforceUpdateRequired(decision, { resolveInstall: () => install });
1430
+ enforceUpdateRequired(decision, { resolveInstall: () => install, installNpmPackageStaged });
1364
1431
  return "continue";
1365
1432
  }
1366
1433
  if (decision.updateRecommended) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.300.0",
3
+ "version": "5.301.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  "build": "node scripts/generate-dsn.mjs && node scripts/build-flag-registry-worker.mjs && tsc && node scripts/generate-command-catalog.mjs --check && node scripts/copy-schemas.mjs && node scripts/chmod-bins.mjs",
26
26
  "prepublishOnly": "npm run build",
27
27
  "pretypecheck": "node scripts/generate-dsn.mjs && node scripts/build-flag-registry-worker.mjs",
28
- "typecheck": "tsc --noEmit",
28
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.typetest.json",
29
29
  "gen:scan-golden": "node scripts/generate-scan-packages-table.mjs > src/utils/__fixtures__/scan-packages.generated-block.sh",
30
30
  "gen:command-catalog": "tsc && node scripts/generate-command-catalog.mjs",
31
31
  "lint": "eslint .",
@@ -1,9 +0,0 @@
1
- export declare const INSTALL_PATH_COMPATIBILITY_FLAG = "cli.install-path-compatibility";
2
- export declare const INSTALL_PATH_COMPATIBILITY_FALLBACK = false;
3
- /**
4
- * Read the default-off rollout flag from the process snapshot. Missing,
5
- * unreadable, or not-yet-loaded registry state keeps the compatibility fixes
6
- * disabled until the operator enables a canary.
7
- */
8
- export declare function installPathCompatibilityEnabled(): boolean;
9
- //# sourceMappingURL=install-path-compatibility-flag.d.ts.map
@@ -1,12 +0,0 @@
1
- import { resolveProcessFlagGate } from "./flag-registry.js";
2
- export const INSTALL_PATH_COMPATIBILITY_FLAG = "cli.install-path-compatibility";
3
- export const INSTALL_PATH_COMPATIBILITY_FALLBACK = false;
4
- /**
5
- * Read the default-off rollout flag from the process snapshot. Missing,
6
- * unreadable, or not-yet-loaded registry state keeps the compatibility fixes
7
- * disabled until the operator enables a canary.
8
- */
9
- export function installPathCompatibilityEnabled() {
10
- return resolveProcessFlagGate(INSTALL_PATH_COMPATIBILITY_FLAG, {}, () => INSTALL_PATH_COMPATIBILITY_FALLBACK);
11
- }
12
- //# sourceMappingURL=install-path-compatibility-flag.js.map