@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.
- package/CHANGELOG.md +26 -0
- package/assets/scaffold/core/scripts/token-usage-report.sh +9 -0
- package/dist/command-catalog.generated.d.ts +32 -6
- package/dist/command-catalog.generated.js +38 -6
- 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/cloud.js +12 -7
- 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.d.ts +1 -0
- package/dist/commands/files.js +90 -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 +78 -37
- package/dist/commands/lanes-resume.js +247 -210
- package/dist/commands/lanes-shared.d.ts +5 -1
- package/dist/commands/lanes.d.ts +4 -0
- package/dist/commands/lanes.js +7 -4
- 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/lanes/reconcile.js +22 -4
- package/dist/lib/lanes/story-switch.d.ts +21 -0
- package/dist/lib/lanes/story-switch.js +75 -0
- package/dist/lib/lanes/types.d.ts +25 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.js +15 -4
- 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
|
@@ -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
|
|
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([
|
|
805
|
-
|
|
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)
|
|
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
|