@timurproko/a1 0.1.8-dev.536 → 0.1.8-dev.544
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/bin/update-recovery.js +25 -3
- package/dist/foundation/release/index.d.ts +2 -2
- package/dist/foundation/release/index.js +1 -1
- package/dist/foundation/release/update-recovery.d.ts +7 -1
- package/dist/foundation/release/update-recovery.js +39 -9
- package/dist/foundation/release/update.d.ts +7 -0
- package/dist/foundation/release/update.js +115 -24
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/package.json +1 -1
package/bin/update-recovery.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { chmod, lstat, mkdir, open, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
8
|
-
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
8
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
|
|
11
11
|
const SCHEMA = "a1-update-recovery-v1";
|
|
@@ -200,9 +200,15 @@ async function readCapsule(path) {
|
|
|
200
200
|
const packageRoot = await realpath(value.packageRoot).catch(() => resolve(value.packageRoot));
|
|
201
201
|
const expectedPackage = resolve(globalRoot, ...value.packageName.split("/"));
|
|
202
202
|
if (!samePath(packageRoot, expectedPackage) || !containedBy(globalRoot, packageRoot)) throw new Error("A1 update recovery package root escapes npm global root");
|
|
203
|
-
const
|
|
204
|
-
|
|
203
|
+
const prefix = npmPrefixForGlobalRoot(globalRoot);
|
|
204
|
+
const expectedNpmArguments = ["install", "--global", "--prefix", prefix, "--loglevel=error", "--no-fund", "--no-audit", `${value.packageName}@${value.targetVersion}`];
|
|
205
|
+
const legacyNpmArguments = ["install", "--global", "--loglevel=error", "--no-fund", "--no-audit", `${value.packageName}@${value.targetVersion}`];
|
|
206
|
+
if (![expectedNpmArguments, legacyNpmArguments].some(arguments_ => sameArguments(value.npmArguments, arguments_))) {
|
|
207
|
+
throw new Error("A1 update recovery npm arguments are invalid");
|
|
208
|
+
}
|
|
205
209
|
const launcherRoot = resolve(value.launcherRoot);
|
|
210
|
+
const expectedLauncherRoot = process.platform === "win32" ? prefix : resolve(prefix, "bin");
|
|
211
|
+
if (!samePath(launcherRoot, expectedLauncherRoot)) throw new Error("A1 update recovery launcher root is invalid");
|
|
206
212
|
const expectedLaunchers = process.platform === "win32"
|
|
207
213
|
? [resolve(launcherRoot, "a1"), resolve(launcherRoot, "a1.cmd"), resolve(launcherRoot, "a1.ps1")]
|
|
208
214
|
: [resolve(launcherRoot, "a1")];
|
|
@@ -306,6 +312,22 @@ async function writeJson(path, value) {
|
|
|
306
312
|
await rename(temporary, path);
|
|
307
313
|
}
|
|
308
314
|
|
|
315
|
+
/** @param {string} globalRoot */
|
|
316
|
+
function npmPrefixForGlobalRoot(globalRoot) {
|
|
317
|
+
const root = resolve(globalRoot);
|
|
318
|
+
const nodeModules = process.platform === "win32" ? basename(root).toLowerCase() === "node_modules" : basename(root) === "node_modules";
|
|
319
|
+
if (!nodeModules) throw new Error("A1 update recovery npm global root layout is invalid");
|
|
320
|
+
if (process.platform === "win32") return dirname(root);
|
|
321
|
+
const libraryRoot = dirname(root);
|
|
322
|
+
if (basename(libraryRoot) !== "lib") throw new Error("A1 update recovery npm global root layout is invalid");
|
|
323
|
+
return dirname(libraryRoot);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** @param {string[]} left @param {string[]} right */
|
|
327
|
+
function sameArguments(left, right) {
|
|
328
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
329
|
+
}
|
|
330
|
+
|
|
309
331
|
/**
|
|
310
332
|
* @param {string} left
|
|
311
333
|
* @param {string} right
|
|
@@ -21,11 +21,11 @@ export type { RestartSeal, RestartValidationEvent } from "./restart-certificatio
|
|
|
21
21
|
export { STABLE_RELEASE_SCHEMA, createStableReleaseEvidence, verifyStableRegistry } from "./stable-release.js";
|
|
22
22
|
export type { StableRegistryState, StableRegistryVerificationOptions, StableReleaseEvidence, StableReleaseEvidenceInput } from "./stable-release.js";
|
|
23
23
|
export { PRODUCT_PACKAGE, assertUpdatePerformanceBudget, createNpmProcessRunner, createUpdateLifecycleCoordinator, planUpdateOwnership, renderUpdateProgressBar, runSelfUpdate, } from "./update.js";
|
|
24
|
-
export type { ProcessRequest, ProcessResult, SelfUpdateOptions, UpdateActivationPhase, UpdateChannel, UpdateFileSystem, UpdateLifecycleCoordinator, UpdateMaterializationProgress, UpdateMeasuredPhase, UpdateOutput, UpdateOwnershipAction, UpdatePackageReplacementInput, UpdatePerformanceEvidence, UpdatePhaseTimingEvent, UpdateProcessRunner, UpdateTransactionJournal, } from "./update.js";
|
|
24
|
+
export type { ProcessRequest, ProcessResult, SelfUpdateOptions, UpdateActivationPhase, UpdateChannel, UpdateFileMetadata, UpdateFileSystem, UpdateLifecycleCoordinator, UpdateMaterializationProgress, UpdateMeasuredPhase, UpdateOutput, UpdateOwnershipAction, UpdatePackageReplacementInput, UpdatePerformanceEvidence, UpdatePhaseTimingEvent, UpdateProcessRunner, UpdateTransactionJournal, } from "./update.js";
|
|
25
25
|
export { UPDATE_ACTIVATION_CONTRACT, UPDATE_ACTIVATION_ENTRY, UPDATE_ACTIVATION_MANIFEST_FIELD, activateInstalledRelease, delegateActivation, readActivationContracts, runActivationEntry, } from "./update-activation.js";
|
|
26
26
|
export type { UpdateActivationCallbacks, UpdateActivationEvent, UpdateActivationRequest } from "./update-activation.js";
|
|
27
27
|
export { selectSupervisorLaunchReleaseId, selectUpdateLaunchRelease } from "./update-launch.js";
|
|
28
|
-
export { UPDATE_RECOVERY_SCHEMA, cleanupUpdateRecoveryCapsules, inspectUpdateLauncherSet, prepareUpdateRecoveryCapsule, readUpdateRecoveryCapsule, removeUpdateRecoveryCapsule, runProtectedPackageReplacement, updateLauncherPaths, } from "./update-recovery.js";
|
|
28
|
+
export { UPDATE_RECOVERY_SCHEMA, cleanupUpdateRecoveryCapsules, inspectUpdateLauncherSet, npmPrefixForGlobalRoot, prepareUpdateRecoveryCapsule, readUpdateRecoveryCapsule, removeUpdateRecoveryCapsule, runProtectedPackageReplacement, updateLauncherPaths, updateNpmInstallArguments, } from "./update-recovery.js";
|
|
29
29
|
export type { ProtectedPackageReplacementOptions, ProtectedPackageReplacementResult, UpdateRecoveryCapsule, UpdateRecoveryResult } from "./update-recovery.js";
|
|
30
30
|
export { UPDATE_JOURNAL_SCHEMA, UpdateTransactionStore } from "./update-transaction.js";
|
|
31
31
|
export type { UpdateRecoveryState, UpdateTransaction, UpdateTransactionPhase } from "./update-transaction.js";
|
|
@@ -12,6 +12,6 @@ export { STABLE_RELEASE_SCHEMA, createStableReleaseEvidence, verifyStableRegistr
|
|
|
12
12
|
export { PRODUCT_PACKAGE, assertUpdatePerformanceBudget, createNpmProcessRunner, createUpdateLifecycleCoordinator, planUpdateOwnership, renderUpdateProgressBar, runSelfUpdate, } from "./update.js";
|
|
13
13
|
export { UPDATE_ACTIVATION_CONTRACT, UPDATE_ACTIVATION_ENTRY, UPDATE_ACTIVATION_MANIFEST_FIELD, activateInstalledRelease, delegateActivation, readActivationContracts, runActivationEntry, } from "./update-activation.js";
|
|
14
14
|
export { selectSupervisorLaunchReleaseId, selectUpdateLaunchRelease } from "./update-launch.js";
|
|
15
|
-
export { UPDATE_RECOVERY_SCHEMA, cleanupUpdateRecoveryCapsules, inspectUpdateLauncherSet, prepareUpdateRecoveryCapsule, readUpdateRecoveryCapsule, removeUpdateRecoveryCapsule, runProtectedPackageReplacement, updateLauncherPaths, } from "./update-recovery.js";
|
|
15
|
+
export { UPDATE_RECOVERY_SCHEMA, cleanupUpdateRecoveryCapsules, inspectUpdateLauncherSet, npmPrefixForGlobalRoot, prepareUpdateRecoveryCapsule, readUpdateRecoveryCapsule, removeUpdateRecoveryCapsule, runProtectedPackageReplacement, updateLauncherPaths, updateNpmInstallArguments, } from "./update-recovery.js";
|
|
16
16
|
export { UPDATE_JOURNAL_SCHEMA, UpdateTransactionStore } from "./update-transaction.js";
|
|
17
17
|
export { warmMaterializedRelease } from "./warmup.js";
|
|
@@ -39,6 +39,8 @@ export interface UpdateRecoveryResult {
|
|
|
39
39
|
export interface ProtectedPackageReplacementOptions {
|
|
40
40
|
readonly dataDir: string;
|
|
41
41
|
readonly globalRoot: string;
|
|
42
|
+
/** Root used only to locate the active npm implementation; installation remains pinned to globalRoot. */
|
|
43
|
+
readonly npmCliRoot?: string;
|
|
42
44
|
readonly packageRoot: string;
|
|
43
45
|
readonly transaction: UpdateTransaction;
|
|
44
46
|
readonly priorRelease: {
|
|
@@ -58,7 +60,11 @@ export interface ProtectedPackageReplacementOptions {
|
|
|
58
60
|
export interface ProtectedPackageReplacementResult extends UpdateRecoveryResult {
|
|
59
61
|
readonly recovery: UpdateRecoveryState;
|
|
60
62
|
}
|
|
61
|
-
/**
|
|
63
|
+
/** Derive the only npm prefix that can own a platform's canonical global package root. */
|
|
64
|
+
export declare function npmPrefixForGlobalRoot(globalRoot: string, platform?: NodeJS.Platform): string;
|
|
65
|
+
/** Fixed replacement arguments pin npm to the installation that owns the invoked launcher. */
|
|
66
|
+
export declare function updateNpmInstallArguments(globalRoot: string, targetVersion: string, platform?: NodeJS.Platform): readonly string[];
|
|
67
|
+
/** Resolve the complete public launcher set npm owns for the selected platform and prefix. */
|
|
62
68
|
export declare function updateLauncherPaths(globalRoot: string, platform?: NodeJS.Platform): readonly string[];
|
|
63
69
|
/** Validate transaction-scoped recovery authority without trusting npm temporary names. */
|
|
64
70
|
export declare function readUpdateRecoveryCapsule(manifestPath: string, platform?: NodeJS.Platform): Promise<UpdateRecoveryCapsule>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
-
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
7
7
|
import { processIsAlive } from "./process-cleanup.js";
|
|
@@ -9,9 +9,29 @@ import { PRIVATE_LAUNCH_CONTRACT, assertCurrentLaunchContract } from "../launch-
|
|
|
9
9
|
export const UPDATE_RECOVERY_SCHEMA = "a1-update-recovery-v1";
|
|
10
10
|
const WORKER_TIMEOUT_MS = 15 * 60 * 1_000;
|
|
11
11
|
const POLL_MS = 50;
|
|
12
|
-
/**
|
|
12
|
+
/** Derive the only npm prefix that can own a platform's canonical global package root. */
|
|
13
|
+
export function npmPrefixForGlobalRoot(globalRoot, platform = process.platform) {
|
|
14
|
+
const root = resolve(globalRoot);
|
|
15
|
+
if (!sameSegment(basename(root), "node_modules", platform))
|
|
16
|
+
throw new Error(`npm global root has an unsupported layout: ${globalRoot}`);
|
|
17
|
+
if (platform === "win32")
|
|
18
|
+
return dirname(root);
|
|
19
|
+
const libraryRoot = dirname(root);
|
|
20
|
+
if (!sameSegment(basename(libraryRoot), "lib", platform))
|
|
21
|
+
throw new Error(`npm global root has an unsupported layout: ${globalRoot}`);
|
|
22
|
+
return dirname(libraryRoot);
|
|
23
|
+
}
|
|
24
|
+
/** Fixed replacement arguments pin npm to the installation that owns the invoked launcher. */
|
|
25
|
+
export function updateNpmInstallArguments(globalRoot, targetVersion, platform = process.platform) {
|
|
26
|
+
return [
|
|
27
|
+
"install", "--global", "--prefix", npmPrefixForGlobalRoot(globalRoot, platform),
|
|
28
|
+
"--loglevel=error", "--no-fund", "--no-audit", `${PRODUCT_TEXT.packageName}@${targetVersion}`,
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
/** Resolve the complete public launcher set npm owns for the selected platform and prefix. */
|
|
13
32
|
export function updateLauncherPaths(globalRoot, platform = process.platform) {
|
|
14
|
-
const
|
|
33
|
+
const prefix = npmPrefixForGlobalRoot(globalRoot, platform);
|
|
34
|
+
const launcherRoot = platform === "win32" ? prefix : resolve(prefix, "bin");
|
|
15
35
|
return platform === "win32"
|
|
16
36
|
? [resolve(launcherRoot, "a1"), resolve(launcherRoot, "a1.cmd"), resolve(launcherRoot, "a1.ps1")]
|
|
17
37
|
: [resolve(launcherRoot, "a1")];
|
|
@@ -33,9 +53,11 @@ export async function readUpdateRecoveryCapsule(manifestPath, platform = process
|
|
|
33
53
|
const expectedPackage = resolve(canonicalGlobal, ...capsule.packageName.split("/"));
|
|
34
54
|
if (!samePath(canonicalPackage, expectedPackage) || !containedBy(canonicalGlobal, canonicalPackage))
|
|
35
55
|
throw new Error("recovery package root is outside npm global root");
|
|
36
|
-
const expectedNpmArguments =
|
|
37
|
-
|
|
38
|
-
|
|
56
|
+
const expectedNpmArguments = updateNpmInstallArguments(canonicalGlobal, capsule.targetVersion, platform);
|
|
57
|
+
const legacyNpmArguments = ["install", "--global", "--loglevel=error", "--no-fund", "--no-audit", `${capsule.packageName}@${capsule.targetVersion}`];
|
|
58
|
+
if (![expectedNpmArguments, legacyNpmArguments].some(arguments_ => sameArguments(capsule.npmArguments, arguments_))) {
|
|
59
|
+
throw new Error("recovery npm arguments differ from the selected target and prefix");
|
|
60
|
+
}
|
|
39
61
|
const expectedLaunchers = updateLauncherPaths(canonicalGlobal, platform);
|
|
40
62
|
if (JSON.stringify(capsule.launchers.map(path => resolve(path))) !== JSON.stringify(expectedLaunchers.map(path => resolve(path)))) {
|
|
41
63
|
throw new Error("recovery launcher set differs from the canonical npm launcher set");
|
|
@@ -94,8 +116,10 @@ export async function prepareUpdateRecoveryCapsule(options) {
|
|
|
94
116
|
await chmod(candidateEntry, 0o500);
|
|
95
117
|
const canonicalGlobal = await realpath(options.globalRoot);
|
|
96
118
|
const platform = options.platform ?? process.platform;
|
|
97
|
-
const
|
|
98
|
-
const
|
|
119
|
+
const prefix = npmPrefixForGlobalRoot(canonicalGlobal, platform);
|
|
120
|
+
const launcherRoot = platform === "win32" ? prefix : resolve(prefix, "bin");
|
|
121
|
+
const npmCliRoot = await realpath(options.npmCliRoot ?? canonicalGlobal);
|
|
122
|
+
const npmCli = await resolveNpmCli(npmCliRoot, options.environment ?? process.env);
|
|
99
123
|
const capsule = {
|
|
100
124
|
schema: UPDATE_RECOVERY_SCHEMA,
|
|
101
125
|
launchContract: PRIVATE_LAUNCH_CONTRACT,
|
|
@@ -114,7 +138,7 @@ export async function prepareUpdateRecoveryCapsule(options) {
|
|
|
114
138
|
recoveryEntryDigest: createHash("sha256").update(entryBytes).digest("hex"),
|
|
115
139
|
nodeExecutable: process.execPath,
|
|
116
140
|
npmCli,
|
|
117
|
-
npmArguments:
|
|
141
|
+
npmArguments: updateNpmInstallArguments(canonicalGlobal, options.transaction.targetVersion, platform),
|
|
118
142
|
cancellationPath: resolve(finalRoot, "cancel.json"),
|
|
119
143
|
resultPath: resolve(finalRoot, "result.json"),
|
|
120
144
|
ownerPath: resolve(finalRoot, "owner.json"),
|
|
@@ -341,6 +365,12 @@ async function readJson(path) { return JSON.parse(await readFile(path, "utf8"));
|
|
|
341
365
|
function samePath(left, right) {
|
|
342
366
|
return process.platform === "win32" ? resolve(left).toLowerCase() === resolve(right).toLowerCase() : resolve(left) === resolve(right);
|
|
343
367
|
}
|
|
368
|
+
function sameSegment(left, right, platform) {
|
|
369
|
+
return platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
370
|
+
}
|
|
371
|
+
function sameArguments(left, right) {
|
|
372
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
373
|
+
}
|
|
344
374
|
function containedBy(parent, child) {
|
|
345
375
|
const fromParent = relative(parent, child);
|
|
346
376
|
return fromParent.length > 0 && fromParent !== ".." && !fromParent.startsWith(`..${sep}`) && !isAbsolute(fromParent);
|
|
@@ -13,9 +13,15 @@ export interface ProcessResult {
|
|
|
13
13
|
stderr?: string;
|
|
14
14
|
}
|
|
15
15
|
export type UpdateProcessRunner = (command: string, arguments_: readonly string[], request: ProcessRequest) => Promise<ProcessResult>;
|
|
16
|
+
export interface UpdateFileMetadata {
|
|
17
|
+
readonly mode: number;
|
|
18
|
+
isFile(): boolean;
|
|
19
|
+
isSymbolicLink(): boolean;
|
|
20
|
+
}
|
|
16
21
|
export interface UpdateFileSystem {
|
|
17
22
|
readFile(path: string): Promise<string>;
|
|
18
23
|
realpath(path: string): Promise<string>;
|
|
24
|
+
lstat?(path: string): Promise<UpdateFileMetadata>;
|
|
19
25
|
access?(path: string): Promise<void>;
|
|
20
26
|
}
|
|
21
27
|
export interface UpdateOutput {
|
|
@@ -85,6 +91,7 @@ export interface UpdateTransactionJournal {
|
|
|
85
91
|
export interface UpdatePackageReplacementInput {
|
|
86
92
|
readonly dataDir: string;
|
|
87
93
|
readonly globalRoot: string;
|
|
94
|
+
readonly npmCliRoot: string;
|
|
88
95
|
readonly packageRoot: string;
|
|
89
96
|
readonly transaction: UpdateTransaction;
|
|
90
97
|
readonly priorRelease: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { access, readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import { access, lstat, readFile, realpath } from "node:fs/promises";
|
|
3
3
|
import { connect } from "node:net";
|
|
4
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
5
|
import crossSpawn from "cross-spawn";
|
|
6
6
|
import { valid as validSemver } from "semver";
|
|
7
7
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
@@ -15,12 +15,13 @@ import { materializeRelease, readMaterializedRelease } from "./release-store.js"
|
|
|
15
15
|
import { scheduleReleaseCleanup } from "./release-gc.js";
|
|
16
16
|
import { UPDATE_ACTIVATION_CONTRACT, activateInstalledRelease, delegateActivation, readActivationContracts, } from "./update-activation.js";
|
|
17
17
|
import { UpdateTransactionStore } from "./update-transaction.js";
|
|
18
|
-
import { removeUpdateRecoveryCapsule, runProtectedPackageReplacement } from "./update-recovery.js";
|
|
18
|
+
import { npmPrefixForGlobalRoot, removeUpdateRecoveryCapsule, runProtectedPackageReplacement, updateLauncherPaths, updateNpmInstallArguments, } from "./update-recovery.js";
|
|
19
19
|
export const PRODUCT_PACKAGE = PRODUCT_TEXT.packageName;
|
|
20
20
|
const UPDATE_DIST_TAGS = { stable: "latest", next: "next" };
|
|
21
21
|
const defaultFileSystem = {
|
|
22
22
|
async readFile(path) { return await readFile(path, "utf8"); },
|
|
23
23
|
realpath,
|
|
24
|
+
lstat,
|
|
24
25
|
access,
|
|
25
26
|
};
|
|
26
27
|
const defaultOutput = {
|
|
@@ -308,6 +309,106 @@ async function resolveRequestedPreview(runner, requested, output) {
|
|
|
308
309
|
output.stderr(`${PRODUCT_TEXT.diagnostic(`published no preview for ${requested}.`)}\n`);
|
|
309
310
|
return { version: null, exitCode: 1 };
|
|
310
311
|
}
|
|
312
|
+
async function resolveManagedNpmInstallation(requestedPackageRoot, fileSystem, runner, output) {
|
|
313
|
+
const rootLookup = await runNpm(runner, ["root", "--global"], true, output, "resolve npm's global package root");
|
|
314
|
+
if (rootLookup.result === null)
|
|
315
|
+
return { installation: null, exitCode: rootLookup.exitCode };
|
|
316
|
+
const activeRootText = rootLookup.result.stdout.trim();
|
|
317
|
+
if (activeRootText.length === 0) {
|
|
318
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic("could not verify its installation because npm returned an empty global package root.")}\n`);
|
|
319
|
+
return { installation: null, exitCode: 1 };
|
|
320
|
+
}
|
|
321
|
+
let packageRoot;
|
|
322
|
+
let activeGlobalRoot;
|
|
323
|
+
try {
|
|
324
|
+
[packageRoot, activeGlobalRoot] = await Promise.all([fileSystem.realpath(requestedPackageRoot), fileSystem.realpath(activeRootText)]);
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`could not canonicalize the running and global npm paths: ${errorMessage(error)}`)}\n`);
|
|
328
|
+
return { installation: null, exitCode: 1 };
|
|
329
|
+
}
|
|
330
|
+
const expectedActivePackage = resolve(activeGlobalRoot, ...PRODUCT_PACKAGE.split("/"));
|
|
331
|
+
if (samePath(packageRoot, expectedActivePackage)) {
|
|
332
|
+
try {
|
|
333
|
+
npmPrefixForGlobalRoot(activeGlobalRoot);
|
|
334
|
+
return {
|
|
335
|
+
installation: {
|
|
336
|
+
packageRoot,
|
|
337
|
+
globalRoot: activeGlobalRoot,
|
|
338
|
+
npmCliRoot: activeGlobalRoot,
|
|
339
|
+
},
|
|
340
|
+
exitCode: 0,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`could not verify npm's active global package layout: ${errorMessage(error)}`)}\n`);
|
|
345
|
+
return { installation: null, exitCode: 1 };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
let candidateGlobalRoot = packageRoot;
|
|
349
|
+
for (const _segment of PRODUCT_PACKAGE.split("/"))
|
|
350
|
+
candidateGlobalRoot = dirname(candidateGlobalRoot);
|
|
351
|
+
let candidatePrefix;
|
|
352
|
+
try {
|
|
353
|
+
candidatePrefix = npmPrefixForGlobalRoot(candidateGlobalRoot);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
reportUnmanagedInstallation(output, packageRoot, activeGlobalRoot);
|
|
357
|
+
return { installation: null, exitCode: 1 };
|
|
358
|
+
}
|
|
359
|
+
if (!samePath(packageRoot, resolve(candidateGlobalRoot, ...PRODUCT_PACKAGE.split("/")))) {
|
|
360
|
+
reportUnmanagedInstallation(output, packageRoot, activeGlobalRoot);
|
|
361
|
+
return { installation: null, exitCode: 1 };
|
|
362
|
+
}
|
|
363
|
+
const confirmation = await runNpm(runner, ["root", "--global", "--prefix", candidatePrefix], true, output, "confirm npm ownership of the running installation");
|
|
364
|
+
if (confirmation.result === null)
|
|
365
|
+
return { installation: null, exitCode: confirmation.exitCode };
|
|
366
|
+
const confirmedRootText = confirmation.result.stdout.trim();
|
|
367
|
+
if (confirmedRootText.length === 0) {
|
|
368
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic("could not verify its installation because npm returned an empty global package root for the inferred prefix.")}\n`);
|
|
369
|
+
return { installation: null, exitCode: 1 };
|
|
370
|
+
}
|
|
371
|
+
let confirmedRoot;
|
|
372
|
+
try {
|
|
373
|
+
confirmedRoot = await fileSystem.realpath(confirmedRootText);
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`could not canonicalize npm's confirmed global package root: ${errorMessage(error)}`)}\n`);
|
|
377
|
+
return { installation: null, exitCode: 1 };
|
|
378
|
+
}
|
|
379
|
+
if (!samePath(confirmedRoot, candidateGlobalRoot)
|
|
380
|
+
|| !await launchersOwnPackage(candidateGlobalRoot, fileSystem)) {
|
|
381
|
+
reportUnmanagedInstallation(output, packageRoot, activeGlobalRoot);
|
|
382
|
+
return { installation: null, exitCode: 1 };
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
installation: {
|
|
386
|
+
packageRoot,
|
|
387
|
+
globalRoot: candidateGlobalRoot,
|
|
388
|
+
npmCliRoot: activeGlobalRoot,
|
|
389
|
+
},
|
|
390
|
+
exitCode: 0,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
async function launchersOwnPackage(globalRoot, fileSystem) {
|
|
394
|
+
const token = `node_modules/${PRODUCT_PACKAGE}/bin/cli.js`;
|
|
395
|
+
try {
|
|
396
|
+
for (const launcher of updateLauncherPaths(globalRoot)) {
|
|
397
|
+
const metadata = await (fileSystem.lstat ?? lstat)(launcher);
|
|
398
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || (process.platform !== "win32" && (metadata.mode & 0o111) === 0))
|
|
399
|
+
return false;
|
|
400
|
+
if (!(await fileSystem.readFile(launcher)).replaceAll("\\", "/").includes(token))
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function reportUnmanagedInstallation(output, packageRoot, activeGlobalRoot) {
|
|
410
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`refused to update automatically because ${packageRoot} could not be verified as an npm-managed ${PRODUCT_PACKAGE} installation; npm's active global package root is ${activeGlobalRoot}. Run npm install --global ${PRODUCT_PACKAGE} to install it manually.`)}\n`);
|
|
411
|
+
}
|
|
311
412
|
export async function runSelfUpdate(options) {
|
|
312
413
|
const fileSystem = options.fileSystem ?? defaultFileSystem;
|
|
313
414
|
const output = options.output ?? defaultOutput;
|
|
@@ -328,6 +429,8 @@ export async function runSelfUpdate(options) {
|
|
|
328
429
|
try {
|
|
329
430
|
const packageJson = JSON.parse(await measure("package-version", async () => await fileSystem.readFile(resolve(options.packageRoot, "package.json"))));
|
|
330
431
|
const parsedVersion = typeof packageJson.version === "string" ? validSemver(packageJson.version) : null;
|
|
432
|
+
if (packageJson.name !== PRODUCT_PACKAGE)
|
|
433
|
+
throw new Error(`package.json does not identify ${PRODUCT_PACKAGE}`);
|
|
331
434
|
if (parsedVersion === null)
|
|
332
435
|
throw new Error("package.json does not contain a valid semantic version");
|
|
333
436
|
runningVersion = parsedVersion;
|
|
@@ -347,26 +450,10 @@ export async function runSelfUpdate(options) {
|
|
|
347
450
|
// and a trailing one reads as part of the version rather than as punctuation.
|
|
348
451
|
output.stdout(`${PRODUCT_TEXT.commandName} update: ${runningVersion} → ${targetVersion}\n`);
|
|
349
452
|
const progress = createUpdateProgress(output, options.progress ?? (options.output === undefined && process.stdout.isTTY === true));
|
|
350
|
-
const
|
|
351
|
-
if (
|
|
352
|
-
return
|
|
353
|
-
|
|
354
|
-
output.stderr(`${PRODUCT_TEXT.diagnostic("could not verify its installation because npm returned an empty global package root.")}\n`);
|
|
355
|
-
return 1;
|
|
356
|
-
}
|
|
357
|
-
let packageRoot;
|
|
358
|
-
let globalRoot;
|
|
359
|
-
try {
|
|
360
|
-
[packageRoot, globalRoot] = await Promise.all([fileSystem.realpath(options.packageRoot), fileSystem.realpath(rootLookup.result.stdout.trim())]);
|
|
361
|
-
}
|
|
362
|
-
catch (error) {
|
|
363
|
-
output.stderr(`${PRODUCT_TEXT.diagnostic(`could not canonicalize the running and global npm paths: ${errorMessage(error)}`)}\n`);
|
|
364
|
-
return 1;
|
|
365
|
-
}
|
|
366
|
-
if (!isContainedBy(globalRoot, packageRoot)) {
|
|
367
|
-
output.stderr(`${PRODUCT_TEXT.diagnostic(`refused to update automatically because ${packageRoot} is not managed beneath npm's global package root ${globalRoot}.`)}\n`);
|
|
368
|
-
return 1;
|
|
369
|
-
}
|
|
453
|
+
const managed = await measure("global-root", async () => await resolveManagedNpmInstallation(options.packageRoot, fileSystem, runner, output));
|
|
454
|
+
if (managed.installation === null)
|
|
455
|
+
return managed.exitCode;
|
|
456
|
+
const { packageRoot, globalRoot, npmCliRoot } = managed.installation;
|
|
370
457
|
const environment = options.environment ?? process.env;
|
|
371
458
|
const paths = resolveProductPaths(environment);
|
|
372
459
|
const lifecycle = options.lifecycle ?? createUpdateLifecycleCoordinator(environment, fileSystem, output);
|
|
@@ -427,6 +514,7 @@ export async function runSelfUpdate(options) {
|
|
|
427
514
|
const replacement = await measure("npm-install", async () => await (options.packageReplacement ?? runProtectedPackageReplacement)({
|
|
428
515
|
dataDir: paths.dataDir,
|
|
429
516
|
globalRoot,
|
|
517
|
+
npmCliRoot,
|
|
430
518
|
packageRoot,
|
|
431
519
|
transaction: replacementTransaction,
|
|
432
520
|
priorRelease,
|
|
@@ -453,7 +541,7 @@ export async function runSelfUpdate(options) {
|
|
|
453
541
|
}
|
|
454
542
|
}
|
|
455
543
|
else {
|
|
456
|
-
const installation = await measure("npm-install", async () => await runNpm(runner,
|
|
544
|
+
const installation = await measure("npm-install", async () => await runNpm(runner, updateNpmInstallArguments(globalRoot, targetVersion), true, output, "start the global npm installation", false));
|
|
457
545
|
if (installation.result === null)
|
|
458
546
|
throw new UpdateFailure(installation.exitCode, "npm process failed");
|
|
459
547
|
if (installation.result.code !== 0) {
|
|
@@ -624,6 +712,9 @@ async function canonicalImmutableRoot(dataDir, releaseRoot) {
|
|
|
624
712
|
return false;
|
|
625
713
|
}
|
|
626
714
|
}
|
|
715
|
+
function samePath(left, right) {
|
|
716
|
+
return process.platform === "win32" ? resolve(left).toLowerCase() === resolve(right).toLowerCase() : resolve(left) === resolve(right);
|
|
717
|
+
}
|
|
627
718
|
function isContainedBy(parent, child) {
|
|
628
719
|
const pathFromParent = relative(parent, child);
|
|
629
720
|
return pathFromParent.length > 0 && pathFromParent !== ".." && !pathFromParent.startsWith(`..${sep}`) && !isAbsolute(pathFromParent);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "darwin",
|
|
6
6
|
"architecture": "arm64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-22T19:10:19.352Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "9db1726bbe3fc2e8292f2ead1e7e9d0fd4d7d0dc9217b372582bf354b0f565dc",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "linux",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-22T19:10:09.829Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "d8cda6b0c7cb36c0cc41e90802aceebf6c02eaebbc08a83d7d963d2e918dbd7a",
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "win32",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-22T19:10:51.843Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "cdef023b39a9328a308f1bcb293d2345240fb063af044e7948154113a3746c73",
|
|
12
12
|
"size": 177664
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
package/package.json
CHANGED