@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.
- package/CHANGELOG.md +20 -0
- package/assets/scaffold/core/scripts/token-usage-report.sh +9 -0
- package/dist/command-catalog.generated.d.ts +28 -2
- package/dist/command-catalog.generated.js +34 -2
- package/dist/commands/__fixtures__/access-vault.d.ts +2 -0
- package/dist/commands/__fixtures__/access-vault.js +1 -1
- package/dist/commands/access.d.ts +2 -1
- package/dist/commands/access.js +58 -49
- package/dist/commands/cloud-provision.js +51 -2
- package/dist/commands/core-company-index-scope-flag.d.ts +3 -0
- package/dist/commands/core-company-index-scope-flag.js +6 -0
- package/dist/commands/core-job-native-utils.d.ts +6 -0
- package/dist/commands/core-job-native-utils.js +80 -0
- package/dist/commands/core-job-native.d.ts +2 -4
- package/dist/commands/core-job-native.js +6 -10
- package/dist/commands/core-job-notify-native.d.ts +36 -0
- package/dist/commands/core-job-notify-native.js +359 -0
- package/dist/commands/core-job-remediate-native.d.ts +26 -0
- package/dist/commands/core-job-remediate-native.js +374 -0
- package/dist/commands/core.js +26 -8
- package/dist/commands/files.js +31 -14
- package/dist/commands/integrations-api.d.ts +6 -6
- package/dist/commands/integrations-api.js +4 -13
- package/dist/commands/integrations-manage.js +1 -4
- package/dist/commands/lanes-message.js +10 -2
- package/dist/commands/lanes-shared.d.ts +2 -0
- package/dist/commands/lanes.d.ts +2 -0
- package/dist/commands/lanes.js +1 -0
- package/dist/commands/meetings.js +35 -16
- package/dist/commands/safe-extract.d.ts +4 -4
- package/dist/commands/safe-extract.js +6 -7
- package/dist/lib/core-utils/token-usage-report.js +17 -3
- package/dist/lib/index-render/companies.d.ts +4 -1
- package/dist/lib/index-render/companies.js +66 -3
- package/dist/lib/index-render/index.d.ts +1 -0
- package/dist/lib/index-render/index.js +2 -1
- package/dist/lib/index-render/shared.d.ts +1 -1
- package/dist/lib/index-render/shared.js +4 -2
- package/dist/lib/mesh/live/daemon/mqtt-events.d.ts +7 -0
- package/dist/lib/mesh/live/daemon/mqtt-events.js +2 -0
- package/dist/lib/mesh/live/daemon/mqtt-events.test-d.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/mqtt-events.test-d.js +10 -0
- package/dist/lib/mesh/live/daemon/presence.d.ts +2 -2
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +2 -2
- package/dist/utils/self-update.js +51 -4
- package/dist/utils/staged-npm-install.d.ts +16 -0
- package/dist/utils/staged-npm-install.js +264 -0
- package/dist/utils/update-command-supervisor.cjs +53 -1
- package/dist/utils/version-gate.d.ts +8 -0
- package/dist/utils/version-gate.js +82 -15
- package/package.json +2 -2
- package/dist/lib/install-path-compatibility-flag.d.ts +0 -9
- 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
|
|
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,
|
|
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,
|
|
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 =
|
|
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>`.
|
|
217
|
-
//
|
|
218
|
-
//
|
|
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,
|
|
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
|
-
|
|
1254
|
-
|
|
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.
|
|
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
|