UNPKG

@napi-rs/cli

Version:

Cli tools for napi-rs

995 lines 1.89 MB
import { builtinModules, createRequire } from "node:module"; import { Cli, Command, Option } from "clipanion"; import { access, chmod, copyFile, cp, link, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rename, rm, rmdir, stat, symlink, unlink, writeFile } from "node:fs/promises"; import { homedir, tmpdir } from "node:os"; import path, { basename, dirname, extname, isAbsolute, join, parse, relative, resolve, sep } from "node:path"; import * as colors from "colorette"; import { underline, yellow } from "colorette"; import { createDebug } from "obug"; import { exec, execFile, execSync, spawn, spawnSync } from "node:child_process"; import { AsyncLocalStorage } from "node:async_hooks"; import fs, { constants, existsSync, lstatSync, mkdirSync, promises, readFileSync, readdirSync, realpathSync, statSync } from "node:fs"; import { createHash, randomUUID } from "node:crypto"; import { performance } from "node:perf_hooks"; import { setTimeout } from "node:timers"; import { setTimeout as setTimeout$1 } from "node:timers/promises"; import { Comparator, Range, minVersion, subset } from "semver"; import { isNil, merge, omit, omitBy, pick, sortBy } from "es-toolkit"; import { dump, load } from "js-yaml"; import * as typanion from "typanion"; import { isDeepStrictEqual } from "node:util"; import { Octokit } from "@octokit/rest"; import { checkbox, confirm, input, select } from "@inquirer/prompts"; //#region src/def/artifacts.ts var BaseArtifactsCommand = class extends Command { static paths = [["artifacts"]]; static usage = Command.Usage({ description: "Copy artifacts from Github Actions into npm packages and ready to publish" }); cwd = Option.String("--cwd", process.cwd(), { description: "The working directory of where napi command will be executed in, all other paths options are relative to this path" }); configPath = Option.String("--config-path,-c", { description: "Path to `napi` config json file" }); packageJsonPath = Option.String("--package-json-path", "package.json", { description: "Path to `package.json`" }); outputDir = Option.String("--output-dir,-o,-d", "./artifacts", { description: "Path to the folder where all built `.node` files put, same as `--output-dir` of build command" }); npmDir = Option.String("--npm-dir", "npm", { description: "Path to the folder where the npm packages put" }); buildOutputDir = Option.String("--build-output-dir", { description: "Path to the build output dir, only needed when targets contain a WASI target" }); getOptions() { return { cwd: this.cwd, configPath: this.configPath, packageJsonPath: this.packageJsonPath, outputDir: this.outputDir, npmDir: this.npmDir, buildOutputDir: this.buildOutputDir }; } }; function applyDefaultArtifactsOptions(options) { return { cwd: process.cwd(), packageJsonPath: "package.json", outputDir: "./artifacts", npmDir: "npm", ...options }; } //#endregion //#region src/utils/log.ts const debugFactory = (namespace) => { const debug = createDebug(`napi:${namespace}`, { formatters: { i(v) { return colors.green(v); } } }); debug.info = (...args) => console.error(colors.black(colors.bgGreen(" INFO ")), ...args); debug.warn = (...args) => console.error(colors.black(colors.bgYellow(" WARNING ")), ...args); debug.error = (...args) => console.error(colors.white(colors.bgRed(" ERROR ")), ...args.map((arg) => arg instanceof Error ? arg.stack ?? arg.message : arg)); return debug; }; const debug$10 = debugFactory("utils"); //#endregion //#region package.json var version$1 = "3.8.2"; //#endregion //#region src/utils/misc.ts const readFileAsync = readFile; const writeFileAsync = writeFile; const unlinkAsync = unlink; const mkdirAsync = mkdir; const statAsync = stat; const readdirAsync = readdir; const reconciliationTails = /* @__PURE__ */ new Map(); const reconciliationLockName = ".napi-rs-filesystem-reconciliation"; const reconciliationReclaimMarker = ".reclaim."; const reconciliationCandidateMarker = ".candidate."; const reconciliationRetiredMarker = ".retired."; const reconciliationMetadataExtension = ".swp"; const reconciliationLockKind = "napi-rs-filesystem-reconciliation-lock"; const reconciliationReclaimKind = "napi-rs-filesystem-reconciliation-reclaim"; const reconciliationStateVersion = 1; const reconciliationLockAcquisitionTimeout = 12e4; const reconciliationLockCleanupTimeout = 5e3; const reconciliationLockCleanupRetryInterval = 250; const reconciliationMetadataMaximumSize = 64 * 1024; const processIncarnationCommandTimeout = 2e3; const incompleteProcessExecutionIdentityCacheDuration = processIncarnationCommandTimeout; const processIncarnationObservationCacheDuration = 1e3; const fileSystemTransactionJournalName = ".napi-rs-filesystem-transaction.swp"; const fileSystemTransactionCandidateMarker = ".candidate."; const fileSystemTransactionRetiredMarker = ".retired."; const fileSystemTransactionOwnerName = "owner.json"; const fileSystemTransactionStateName = "state.json"; const fileSystemTransactionKind = "napi-rs-filesystem-transaction"; const legacyFileSystemTransactionStateVersion = 1; const previousFileSystemTransactionStateVersion = 2; const fileSystemTransactionStateVersion = 3; const fileSystemTransactionStateMaximumSize = 16 * 1024 * 1024; const fileSystemTransactionMaximumEntries = 1e5; const fileSystemTransactionCleanupTimeout = 5e3; const fileSystemTransactionCleanupInitialRetryDelay = 10; const fileSystemTransactionCleanupMaximumRetryDelay = 250; const processIncarnationObservations = /* @__PURE__ */ new Map(); let currentProcessIncarnation; let currentProcessIncarnationProbe; let linuxBootId; const fileSystemReconciliationCapability = new AsyncLocalStorage(); async function writeFileAtomic(path, data, options) { await mkdir(dirname(path), { recursive: true }); while (true) { const temporaryPath = atomicTemporaryPath(path); const exclusiveOptions = typeof options === "string" ? { encoding: options, flag: "wx" } : { ...options, flag: "wx" }; try { await writeFile(temporaryPath, data, exclusiveOptions); } catch (error) { if (error.code === "EEXIST") continue; throw error; } let committed = false; try { await syncFile(temporaryPath); await rename(temporaryPath, path); await syncDirectory(dirname(path)); committed = true; return; } finally { if (!committed) await unlinkFileIfExists(temporaryPath); } } } async function copyFileAtomic(source, destination, mode) { await mkdir(dirname(destination), { recursive: true }); while (true) { const temporaryPath = atomicTemporaryPath(destination); try { await copyFile(source, temporaryPath, constants.COPYFILE_EXCL); } catch (error) { if (error.code === "EEXIST") continue; throw error; } let committed = false; try { if (mode !== void 0) await chmod(temporaryPath, mode); await syncFile(temporaryPath); await rename(temporaryPath, destination); await syncDirectory(dirname(destination)); committed = true; return; } finally { if (!committed) await unlinkFileIfExists(temporaryPath); } } } async function withFileSystemReconciliation(path, operation) { const localKey = resolve(path); const previous = reconciliationTails.get(localKey) ?? Promise.resolve(); let release; const current = new Promise((resolveCurrent) => { release = resolveCurrent; }); const tail = previous.catch(() => {}).then(() => current); reconciliationTails.set(localKey, tail); const releaseCrossProcessLocks = []; let operationFailed = false; let operationError; let result; try { await previous.catch(() => {}); const identities = await resolveReconciliationLockIdentities(path); const acquisitionDeadline = createReconciliationLockDeadline(reconciliationLockAcquisitionTimeout); for (const identity of identities) releaseCrossProcessLocks.push(await acquireReconciliationLock(identity, acquisitionDeadline)); await recoverFileSystemTransaction(identities[0].anchorPath); const currentCapability = fileSystemReconciliationCapability.getStore(); const roots = new Set(currentCapability === null || currentCapability === void 0 ? void 0 : currentCapability.roots); roots.add(fileSystemReconciliationCapabilityRoot(identities[0].anchorPath)); result = await fileSystemReconciliationCapability.run({ roots }, operation); } catch (error) { operationFailed = true; operationError = error; } const releaseErrors = []; for (let index = releaseCrossProcessLocks.length - 1; index >= 0; index--) try { await releaseCrossProcessLocks[index](); } catch (error) { releaseErrors.push(error); } release(); if (reconciliationTails.get(localKey) === tail) reconciliationTails.delete(localKey); if (operationFailed) { if (releaseErrors.length > 0) throw new AggregateError([operationError, ...releaseErrors], "Filesystem reconciliation operation and lock release both failed", { cause: operationError }); throw operationError; } if (releaseErrors.length === 1) throw releaseErrors[0]; if (releaseErrors.length > 1) throw new AggregateError(releaseErrors, "Multiple filesystem reconciliation locks could not be released", { cause: releaseErrors[0] }); return result; } function fileSystemReconciliationCapabilityRoot(path) { const resolvedPath = resolve(path); return process.platform === "win32" ? resolvedPath.toLowerCase() : resolvedPath; } function getPackageReconciliationRoot(cwd, packageJsonPath = "package.json") { return dirname(resolve(cwd, packageJsonPath)); } function resolvePackageReconciliationPaths(cwd, packageJsonPath = "package.json", managedPaths = []) { const canonicalCwd = canonicalizeManagedPackagePath(cwd); const requestedPackageJsonPath = resolve(cwd, packageJsonPath); const canonicalPackageJsonPath = canonicalizeManagedPackagePath(requestedPackageJsonPath); const canonicalPackageRoot = canonicalizeManagedPackagePath(dirname(requestedPackageJsonPath)); const canonicalManagedPaths = managedPaths.map((path) => canonicalizeManagedPackagePath(resolve(cwd, path))); if (!managedPackagePathIsWithin(canonicalPackageRoot, canonicalPackageJsonPath)) throw new Error(`Package manifest escapes its package root: ${requestedPackageJsonPath}`); if (!(managedPackagePathIsWithin(canonicalPackageRoot, canonicalCwd) || managedPackagePathIsWithin(canonicalCwd, canonicalPackageRoot))) throw managedPackageBoundaryError(canonicalCwd, canonicalPackageRoot, canonicalManagedPaths); const discoveryBoundary = managedPackagePathIsWithin(canonicalCwd, canonicalPackageRoot) ? canonicalCwd : canonicalPackageRoot; let candidate = canonicalPackageRoot; while (true) { if (hasPackageWorkspaceBoundaryMarker(candidate) && canonicalManagedPaths.every((path) => managedPackagePathIsWithin(candidate, path))) return { boundary: candidate, cwd: canonicalCwd, managedPaths: canonicalManagedPaths, packageJsonPath: canonicalPackageJsonPath, packageRoot: canonicalPackageRoot }; if (candidate === discoveryBoundary) break; const parent = dirname(candidate); if (parent === candidate || !managedPackagePathIsWithin(discoveryBoundary, parent)) break; candidate = parent; } if (dirname(canonicalPackageRoot) !== canonicalPackageRoot && canonicalManagedPaths.every((path) => managedPackagePathIsWithin(canonicalPackageRoot, path))) return { boundary: canonicalPackageRoot, cwd: canonicalCwd, managedPaths: canonicalManagedPaths, packageJsonPath: canonicalPackageJsonPath, packageRoot: canonicalPackageRoot }; throw managedPackageBoundaryError(canonicalCwd, canonicalPackageRoot, canonicalManagedPaths); } function getPackageReconciliationRoots({ boundary, cwd, packageRoot }) { const roots = []; const rootIdentities = /* @__PURE__ */ new Set(); for (const root of [ packageRoot, boundary, cwd ]) { const resolvedRoot = resolve(root); const identity = fileSystemReconciliationCapabilityRoot(resolvedRoot); if (rootIdentities.has(identity)) continue; if (roots.some((existingRoot) => !managedPackagePathIsWithin(existingRoot, resolvedRoot) && !managedPackagePathIsWithin(resolvedRoot, existingRoot))) throw new Error(`Package reconciliation roots must be nested: ${roots.join(", ")}, ${resolvedRoot}`); rootIdentities.add(identity); roots.push(resolvedRoot); } return roots; } async function withPackageFileSystemReconciliation(paths, operation) { const roots = getPackageReconciliationRoots(paths); const acquire = (index) => { if (index === roots.length) return operation(); return withFileSystemReconciliation(roots[index], () => acquire(index + 1)); }; return acquire(0); } function managedPackagePathIsWithin(root, path) { const relativePath = relative(resolve(root), resolve(path)); return relativePath === "" || !isAbsolute(relativePath) && relativePath !== ".." && !relativePath.startsWith(`..${sep}`); } function canonicalizeManagedPackagePath(path) { let current = resolve(path); const missingSegments = []; while (true) try { return join(realpathSync.native(current), ...missingSegments); } catch (error) { if (error.code !== "ENOENT") throw error; const parent = dirname(current); if (parent === current) return join(current, ...missingSegments); missingSegments.unshift(basename(current)); current = parent; } } function hasPackageWorkspaceBoundaryMarker(directory) { if (existsSync(join(directory, "pnpm-workspace.yaml"))) return true; const manifestPath = join(directory, "package.json"); if (!existsSync(manifestPath)) return false; try { const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); return Array.isArray(manifest.workspaces) || typeof manifest.workspaces === "object" && manifest.workspaces !== null && Array.isArray(manifest.workspaces.packages); } catch { return false; } } function managedPackageBoundaryError(cwd, packageRoot, managedPaths) { return /* @__PURE__ */ new Error(`Managed package paths must stay within the project or workspace boundary discovered from ${cwd}: ${[packageRoot, ...managedPaths].join(", ")}`); } async function commitFileSystemTransaction(root, writes, removals) { const requestedTransactionRoot = resolve(root); for (const path of [...writes.flatMap(({ destination, removeBeforeWrite }) => removeBeforeWrite ? [destination, removeBeforeWrite] : [destination]), ...removals]) assertFileSystemTransactionPathIsNotReserved(requestedTransactionRoot, resolveTransactionPath(requestedTransactionRoot, path)); const capability = fileSystemReconciliationCapability.getStore(); if (capability) { const transactionRoot = await canonicalizeReconciliationPath(requestedTransactionRoot); if (capability.roots.has(fileSystemReconciliationCapabilityRoot(transactionRoot))) return commitFileSystemTransactionUnlocked(requestedTransactionRoot, transactionRoot, writes, removals); } return withFileSystemReconciliation(requestedTransactionRoot, async () => { const transactionRoot = await canonicalizeReconciliationPath(requestedTransactionRoot); await commitFileSystemTransactionUnlocked(requestedTransactionRoot, transactionRoot, writes, removals); }); } async function commitFileSystemTransactionUnlocked(requestedTransactionRoot, transactionRoot, writes, removals) { const transactionWrites = await Promise.all(writes.map(async (write) => { const destination = await resolveCanonicalTransactionPath(requestedTransactionRoot, transactionRoot, write.destination); await mkdir(dirname(destination), { recursive: true }); const stableDestination = await resolveCanonicalTransactionPath(requestedTransactionRoot, transactionRoot, write.destination); if (!pathsHaveEquivalentPlatformSpelling(destination, stableDestination)) throw new Error(`Filesystem transaction destination parent changed while it was created: ${write.destination}`); return { destination: stableDestination, mode: write.mode, removeBeforeWrite: write.removeBeforeWrite ? await resolveCanonicalTransactionPath(requestedTransactionRoot, transactionRoot, write.removeBeforeWrite) : void 0, source: write.source }; })); const destinationKeys = /* @__PURE__ */ new Set(); for (const { destination } of transactionWrites) { const destinationKey = fileSystemReconciliationCapabilityRoot(destination); if (destinationKeys.has(destinationKey)) throw new Error(`Duplicate canonical filesystem transaction destination: ${destination}`); destinationKeys.add(destinationKey); } const writesByDestination = new Map(transactionWrites.map((write) => [write.destination, write])); const resolvedRemovals = await Promise.all(removals.map((path) => resolveCanonicalTransactionPath(requestedTransactionRoot, transactionRoot, path))); const preWriteRemovals = new Set(transactionWrites.flatMap(({ removeBeforeWrite }) => removeBeforeWrite ? [removeBeforeWrite] : [])); const affected = /* @__PURE__ */ new Set([ ...writesByDestination.keys(), ...resolvedRemovals, ...preWriteRemovals ]); const journalRoot = fileSystemTransactionJournalPath(transactionRoot); for (const path of affected) assertFileSystemTransactionPathIsNotReserved(transactionRoot, path); const affectedStats = /* @__PURE__ */ new Map(); for (const path of affected) { const stats = await lstatIfExists$1(path, { bigint: true }); if (stats && !stats.isFile()) throw new Error(`Filesystem transaction path is not a regular file: ${path}`); affectedStats.set(path, stats); } const replacementAliasParents = /* @__PURE__ */ new Map(); const findReplacementAlias = (path) => { const parent = replacementAliasParents.get(path); if (parent === void 0) { replacementAliasParents.set(path, path); return path; } if (parent === path) return path; const root = findReplacementAlias(parent); replacementAliasParents.set(path, root); return root; }; const joinReplacementAliases = (left, right) => { const leftRoot = findReplacementAlias(left); const rightRoot = findReplacementAlias(right); if (leftRoot !== rightRoot) replacementAliasParents.set(rightRoot, leftRoot); }; for (const { destination, removeBeforeWrite } of transactionWrites) { if (!removeBeforeWrite) continue; const replacementStats = affectedStats.get(removeBeforeWrite); if (!replacementStats) throw new Error(`Filesystem transaction replacement source does not exist: ${removeBeforeWrite}`); const destinationStats = affectedStats.get(destination); if (destinationStats) { if (!await pathsReferToSameDirectoryEntry(removeBeforeWrite, destination, replacementStats, destinationStats)) throw new Error(`Filesystem transaction replacement destination is occupied: ${destination}`); joinReplacementAliases(removeBeforeWrite, destination); } } const replacementAliasBackups = /* @__PURE__ */ new Map(); for (const { removeBeforeWrite } of transactionWrites) { if (!removeBeforeWrite || !replacementAliasParents.has(removeBeforeWrite)) continue; const root = findReplacementAlias(removeBeforeWrite); if (!replacementAliasBackups.has(root)) replacementAliasBackups.set(root, removeBeforeWrite); } const replacementBackupPaths = new Set(replacementAliasBackups.values()); const parentIdentities = /* @__PURE__ */ new Map(); for (const path of affected) { const parent = dirname(path); if (!parentIdentities.has(parent)) parentIdentities.set(parent, await captureTransactionParentIdentity(transactionRoot, parent)); } const { path: candidateRoot, stats: candidateStats, token: transactionToken } = await createFileSystemTransactionCandidate(transactionRoot); const candidateBackupRoot = join(candidateRoot, "backups"); const candidateInputRoot = join(candidateRoot, "inputs"); const publishedBackupRoot = join(journalRoot, "backups"); const backups = /* @__PURE__ */ new Map(); let preserveJournalRoot = false; let journal; let committed = false; let published = false; let transactionError; try { await writeFileAtomic(join(candidateRoot, fileSystemTransactionOwnerName), JSON.stringify({ kind: fileSystemTransactionKind, token: transactionToken, version: fileSystemTransactionStateVersion }), { mode: 420 }); await syncDirectory(transactionRoot); await Promise.all([mkdir(candidateBackupRoot), mkdir(candidateInputRoot)]); const preparedWrites = []; const snapshots = /* @__PURE__ */ new Map(); for (let index = 0; index < transactionWrites.length; index++) { const write = transactionWrites[index]; let snapshot = snapshots.get(write.source); if (!snapshot) { const inputName = String(snapshots.size); const inputPath = join(candidateInputRoot, inputName); snapshot = { final: fileSystemTransactionFileContents(await snapshotFileSystemTransactionInput(write.source, inputPath)), input: join("inputs", inputName) }; snapshots.set(write.source, snapshot); } preparedWrites.push({ destination: write.destination, final: { hash: snapshot.final.hash, mode: write.mode ?? snapshot.final.mode }, input: snapshot.input, removeBeforeWrite: write.removeBeforeWrite }); } for (let index = 0; index < preparedWrites.length; index++) { const write = preparedWrites[index]; const prepared = fileSystemTransactionArtifactPath(write.destination, transactionToken, index, "prepared"); if (await lstatIfExists$1(prepared) !== void 0) throw new Error(`Filesystem transaction artifact path already exists for ${write.destination}`); write.prepared = prepared; } const artifactPaths = /* @__PURE__ */ new Map(); let artifactIndex = 0; for (const path of affected) { const retired = fileSystemTransactionArtifactPath(path, transactionToken, artifactIndex, "retired"); const rollbackRetired = fileSystemTransactionArtifactPath(path, transactionToken, artifactIndex, "rollback"); artifactIndex += 1; if (await lstatIfExists$1(retired) !== void 0 || await lstatIfExists$1(rollbackRetired) !== void 0) throw new Error(`Filesystem transaction artifact path already exists for ${path}`); artifactPaths.set(path, { retired, rollbackRetired }); } const journalParentForPath = (path) => { const parentIdentity = parentIdentities.get(dirname(path)); if (!parentIdentity) throw new Error(`Filesystem transaction parent metadata was not captured for ${path}`); return { canonicalParent: fileSystemTransactionRelativePath(transactionRoot, parentIdentity.canonicalParent, "Transaction canonical parent", true), dev: parentIdentity.dev, identityPath: fileSystemTransactionRelativePath(transactionRoot, parentIdentity.identityPath, "Transaction parent identity", true), ino: parentIdentity.ino }; }; const preparingJournal = { entries: preparedWrites.map((write) => ({ final: write.final, parent: journalParentForPath(write.destination), path: fileSystemTransactionRelativePath(transactionRoot, write.destination, "Transaction path"), prepared: fileSystemTransactionRelativePath(transactionRoot, write.prepared, "Transaction prepared replacement") })), phase: "preparing", token: transactionToken, version: fileSystemTransactionStateVersion }; await writeFileSystemTransactionJournal(candidateRoot, preparingJournal); journal = preparingJournal; let preparedIdentityCount = 0; let preparedIdentityPublicationClosed = false; let resolvePreparedIdentityPublication; let rejectPreparedIdentityPublication; const preparedIdentityPublication = new Promise((resolve, reject) => { resolvePreparedIdentityPublication = resolve; rejectPreparedIdentityPublication = reject; }); preparedIdentityPublication.catch(() => {}); const failPreparedIdentityPublication = (error) => { if (!preparedIdentityPublicationClosed) { preparedIdentityPublicationClosed = true; rejectPreparedIdentityPublication(error); } }; const recordPreparedIdentity = (index, identity) => { if (!preparedIdentityPublicationClosed) { const entry = preparingJournal.entries[index]; if (!(entry === null || entry === void 0 ? void 0 : entry.final)) { var _preparedWrites$index; failPreparedIdentityPublication(/* @__PURE__ */ new Error(`Filesystem transaction preparing metadata was not captured for ${(_preparedWrites$index = preparedWrites[index]) === null || _preparedWrites$index === void 0 ? void 0 : _preparedWrites$index.destination}`)); } else { entry.final = { ...entry.final, ...identity }; preparedIdentityCount += 1; if (preparedIdentityCount === preparedWrites.length) { preparedIdentityPublicationClosed = true; writeFileSystemTransactionJournal(candidateRoot, preparingJournal).then(resolvePreparedIdentityPublication, rejectPreparedIdentityPublication); } } } return preparedIdentityPublication; }; const preparations = preparedWrites.map(async (write, index) => { try { await assertTransactionParentUnchanged(transactionRoot, write.destination, parentIdentities); write.final = await prepareFileSystemTransactionReplacement(join(candidateInputRoot, basename(write.input)), write.prepared, write.final, () => assertTransactionParentUnchanged(transactionRoot, write.destination, parentIdentities), (identity) => recordPreparedIdentity(index, identity)); } catch (error) { failPreparedIdentityPublication(error); throw error; } }); try { await Promise.all(preparations); } catch (error) { await Promise.allSettled(preparations); throw error; } const preparedWritesByDestination = new Map(preparedWrites.map((write) => [write.destination, write])); let backupIndex = 0; for (const path of affected) { if (replacementAliasParents.has(path) && !replacementBackupPaths.has(path)) continue; const stats = affectedStats.get(path); if (!stats) continue; await assertTransactionParentUnchanged(transactionRoot, path, parentIdentities); const backupName = String(backupIndex++); const state = await snapshotFileSystemTransactionInput(path, join(candidateBackupRoot, backupName), Number(stats.mode & 4095n), stats); backups.set(path, { path: join(publishedBackupRoot, backupName), state }); } const backupForPath = (path) => { const direct = backups.get(path); if (direct) return direct; if (!replacementAliasParents.has(path)) return; const aliasBackupPath = replacementAliasBackups.get(findReplacementAlias(path)); return aliasBackupPath ? backups.get(aliasBackupPath) : void 0; }; const finalWriteForPath = (path) => { const direct = preparedWritesByDestination.get(path); if (direct) return direct; if (!replacementAliasParents.has(path)) return; const aliasRoot = findReplacementAlias(path); return preparedWrites.find((write) => replacementAliasParents.has(write.destination) && findReplacementAlias(write.destination) === aliasRoot); }; const preparedJournal = { entries: [...affected].map((path) => { const originalStats = affectedStats.get(path); const backup = backupForPath(path); const finalWrite = finalWriteForPath(path); const artifacts = artifactPaths.get(path); if (!artifacts) throw new Error(`Filesystem transaction metadata was not captured for ${path}`); if (finalWrite && !finalWrite.prepared) throw new Error(`Filesystem transaction replacement was not prepared for ${path}`); return { backup: backup ? fileSystemTransactionRelativePath(transactionRoot, backup.path, "Transaction backup") : void 0, final: finalWrite === null || finalWrite === void 0 ? void 0 : finalWrite.final, original: originalStats && backup ? backup.state : void 0, parent: journalParentForPath(path), path: fileSystemTransactionRelativePath(transactionRoot, path, "Transaction path"), prepared: finalWrite ? fileSystemTransactionRelativePath(transactionRoot, finalWrite.prepared, "Transaction prepared replacement") : void 0, retired: fileSystemTransactionRelativePath(transactionRoot, artifacts.retired, "Transaction retirement path"), rollbackRetired: fileSystemTransactionRelativePath(transactionRoot, artifacts.rollbackRetired, "Transaction rollback retirement path") }; }), phase: "prepared", token: transactionToken, version: fileSystemTransactionStateVersion }; await writeFileSystemTransactionJournal(candidateRoot, preparedJournal); journal = preparedJournal; const journalEntriesByPath = new Map(preparedJournal.entries.map((entry) => [resolveFileSystemTransactionRelativePath(transactionRoot, entry.path, "Transaction path"), entry])); await Promise.all([syncDirectory(candidateBackupRoot), syncDirectory(candidateInputRoot)]); await syncDirectory(candidateRoot); if (await lstatIfExists$1(journalRoot)) throw new Error(`Filesystem transaction recovery state already exists: ${journalRoot}`); await rename(candidateRoot, journalRoot); published = true; await syncDirectory(transactionRoot); if (!fileSystemTransactionStateMatches(candidateStats, await lstatIfExists$1(journalRoot, { bigint: true }))) throw new Error(`Filesystem transaction recovery state changed during publication: ${journalRoot}`); if ((await readFileSystemTransactionOwner(transactionRoot)).token !== transactionToken) throw new Error(`Filesystem transaction recovery state owner changed during publication: ${journalRoot}`); for (const path of preWriteRemovals) { const entry = journalEntriesByPath.get(path); if (!entry) throw new Error(`Filesystem transaction journal omitted path: ${path}`); await removeFileSystemTransactionPath(transactionRoot, path, parentIdentities, entry.original, resolveFileSystemTransactionRelativePath(transactionRoot, entry.retired, "Transaction retirement path"), "before pre-write removal"); } for (const write of preparedWrites) { const { destination } = write; const entry = journalEntriesByPath.get(destination); if (!entry || !entry.final || !entry.prepared || !entry.retired) throw new Error(`Filesystem transaction journal omitted write destination: ${destination}`); await commitFileSystemTransactionReplacement(transactionRoot, destination, parentIdentities, write.removeBeforeWrite && replacementAliasParents.has(write.removeBeforeWrite) && replacementAliasParents.has(destination) && findReplacementAlias(write.removeBeforeWrite) === findReplacementAlias(destination) ? void 0 : entry.original, resolveFileSystemTransactionRelativePath(transactionRoot, entry.prepared, "Transaction prepared replacement"), entry.final, resolveFileSystemTransactionRelativePath(transactionRoot, entry.retired, "Transaction retirement path")); } for (const path of affected) if (!writesByDestination.has(path) && !preWriteRemovals.has(path)) { const entry = journalEntriesByPath.get(path); if (!entry) throw new Error(`Filesystem transaction journal omitted path: ${path}`); await removeFileSystemTransactionPath(transactionRoot, path, parentIdentities, entry.original, resolveFileSystemTransactionRelativePath(transactionRoot, entry.retired, "Transaction retirement path"), "before transaction removal"); } journal = { ...journal, phase: "committed" }; await writeFileSystemTransactionJournal(journalRoot, journal); committed = true; } catch (error) { transactionError = error; if (published && journal && !committed) { const rollbackErrors = []; for (const rollbackError of await rollbackFileSystemTransaction(transactionRoot, journal)) rollbackErrors.push(rollbackError); if (rollbackErrors.length > 0) { preserveJournalRoot = true; transactionError = new AggregateError([error, ...rollbackErrors], `Filesystem transaction failed and rollback was incomplete; recovery state is preserved at ${journalRoot}`, { cause: error }); } } } let cleanupError; if (!preserveJournalRoot) try { if (published) { if (journal) await cleanupFileSystemTransactionArtifacts(transactionRoot, journal); await removeFileSystemTransactionJournal(transactionRoot, transactionToken, candidateStats); } else { if (journal) await cleanupUnpublishedFileSystemTransactionArtifacts(transactionRoot, journal); await removeFileSystemTransactionCandidate(transactionRoot, candidateRoot, candidateStats); } } catch (error) { cleanupError = error; } if (transactionError !== void 0) { if (cleanupError !== void 0) throw new AggregateError([transactionError, cleanupError], `Filesystem transaction failed and its recovery state could not be removed from ${journalRoot}`, { cause: transactionError }); throw transactionError; } if (cleanupError !== void 0) debug$10.warn(`Filesystem transaction committed but recovery-state cleanup failed at ${journalRoot}: ${errorMessage(cleanupError)}`); } async function pathsReferToSameDirectoryEntry(left, right, leftStats, rightStats) { if (resolve(left) === resolve(right)) return true; if (!leftStats.isFile() || !rightStats.isFile() || !statIdentitiesMatch(leftStats, rightStats)) return false; if (process.platform !== "darwin" && process.platform !== "win32") return false; const [leftParent, rightParent] = await Promise.all([realpath(dirname(left)), realpath(dirname(right))]); if (!pathsHaveEquivalentPlatformSpelling(leftParent, rightParent)) return false; const entries = await readdir(leftParent); const leftName = basename(left); const rightName = basename(right); return !(leftName !== rightName && entries.includes(leftName) && entries.includes(rightName)); } async function lstatIfExists$1(path, options) { try { return (options === null || options === void 0 ? void 0 : options.bigint) ? await lstat(path, { bigint: true }) : await lstat(path); } catch (error) { if (error.code === "ENOENT") return; throw error; } } /** * Exact 64-bit dev/ino identity equality between two bigint stat observations. * Every ownership or continuity decision in the reconciliation and transaction * subsystems must compare identity through this helper (or on the decimal * strings derived from a bigint stat), never on the lossy Number * `Stats.dev`/`Stats.ino` fields: Windows NTFS file IDs and volume serials * exceed Number.MAX_SAFE_INTEGER, so two distinct filesystem objects can * collapse onto the same JS double and defeat the check. */ function statIdentitiesMatch(expected, current) { return current !== void 0 && expected.dev === current.dev && expected.ino === current.ino; } async function readReconciliationMetadata(path, label) { let handle; try { handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)); } catch (error) { if (error.code === "ELOOP") throw reconciliationPathCollisionError(path, `is not a regular ${label} file`); throw error; } try { const stats = await handle.stat({ bigint: true }); if (!stats.isFile()) throw reconciliationPathCollisionError(path, `is not a regular ${label} file`); const content = Buffer.allocUnsafe(65537); let offset = 0; while (offset < content.length) { const { bytesRead } = await handle.read(content, offset, content.length - offset, offset); if (bytesRead === 0) break; offset += bytesRead; } if (offset > reconciliationMetadataMaximumSize) throw reconciliationPathCollisionError(path, `exceeds the maximum ${label} size`); return { content: content.subarray(0, offset).toString("utf8"), stats }; } finally { await handle.close(); } } async function canonicalizeReconciliationPath(path) { let current = resolve(path); const missingSegments = []; while (true) try { return join(await realpath(current), ...missingSegments); } catch (error) { if (error.code !== "ENOENT") throw error; const parent = dirname(current); if (parent === current) return join(current, ...missingSegments); missingSegments.unshift(basename(current)); current = parent; } } async function resolveReconciliationLockIdentities(path) { const requestedPath = resolve(path); const anchorPath = await canonicalizeReconciliationPath(path); let anchorStats; try { anchorStats = await lstat(anchorPath, { bigint: true }); } catch (error) { if (error.code === "ENOENT") throw reconciliationAnchorError(anchorPath, "does not exist", "ENOENT"); throw error; } if (!anchorStats.isDirectory()) throw reconciliationAnchorError(anchorPath, "is not a directory", "ENOTDIR"); const guardKeys = /* @__PURE__ */ new Set(); if (await directoryIsWritable(dirname(anchorPath))) guardKeys.add(anchorPath); let currentPath = requestedPath; while (true) { const currentStats = await lstatIfExists$1(currentPath); if (currentStats === null || currentStats === void 0 ? void 0 : currentStats.isSymbolicLink()) { if (await directoryIsWritable(dirname(currentPath))) guardKeys.add(currentPath); } const parent = dirname(currentPath); if (parent === currentPath) break; currentPath = parent; } const pathIdentities = await Promise.all([...guardKeys].map(async (key) => { const lockRootPath = await realpath(dirname(key)); const lockRootStats = await lstat(lockRootPath, { bigint: true }); if (!lockRootStats.isDirectory()) throw reconciliationPathCollisionError(lockRootPath, "is not a lock namespace directory"); return { anchorPath, dev: anchorStats.dev, ino: anchorStats.ino, key, lockRootDev: lockRootStats.dev, lockRootIno: lockRootStats.ino, lockRootPath, reportTopologyChange: key === anchorPath, requestedPath }; })); const anchorParentPath = await realpath(dirname(anchorPath)); const anchorParentStats = await lstat(anchorParentPath, { bigint: true }); const anchorParentIsShared = (anchorParentStats.mode & 512n) === 0n && await directoryIsWritable(anchorParentPath); const objectLockRootPath = anchorParentIsShared ? anchorParentPath : anchorPath; const objectLockRootStats = anchorParentIsShared ? anchorParentStats : anchorStats; if (!anchorParentIsShared && !await directoryIsWritable(anchorPath)) throw reconciliationPathCollisionError(anchorPath, "does not provide a writable lock namespace"); const objectIdentity = { anchorPath, dev: anchorStats.dev, ino: anchorStats.ino, key: `inode:${anchorStats.ino}`, lockRootDev: objectLockRootStats.dev, lockRootIno: objectLockRootStats.ino, lockRootPath: objectLockRootPath, reportTopologyChange: !guardKeys.has(anchorPath), requestedPath }; return [...pathIdentities, objectIdentity].sort((left, right) => { const leftPath = reconciliationLockPath(left); const rightPath = reconciliationLockPath(right); return leftPath < rightPath ? -1 : leftPath > rightPath ? 1 : 0; }); } async function directoryIsWritable(path) { try { await access(path, constants.W_OK); return true; } catch { return false; } } async function assertReconciliationRequestedPathUnchanged(identity) { let currentPath; try { currentPath = await canonicalizeReconciliationPath(identity.requestedPath); } catch { throw reconciliationAnchorError(identity.requestedPath, "changed after lock identity was captured", "ESTALE"); } const currentStats = await lstatIfExists$1(currentPath, { bigint: true }); if (!(currentStats === null || currentStats === void 0 ? void 0 : currentStats.isDirectory()) || !statIdentitiesMatch(identity, currentStats)) throw reconciliationAnchorError(identity.requestedPath, "changed after lock identity was captured", "ESTALE"); } async function assertReconciliationAnchorUnchanged(identity) { let anchorStats; try { anchorStats = await lstat(identity.anchorPath, { bigint: true }); } catch (error) { if (error.code === "ENOENT") throw reconciliationAnchorError(identity.anchorPath, "changed after lock identity was captured", "ESTALE"); throw error; } if (!anchorStats.isDirectory() || !statIdentitiesMatch(identity, anchorStats)) throw reconciliationAnchorError(identity.anchorPath, "changed after lock identity was captured", "ESTALE"); } async function assertReconciliationLockRootUnchanged(identity) { const lockRootStats = await lstatIfExists$1(identity.lockRootPath, { bigint: true }); if (!(lockRootStats === null || lockRootStats === void 0 ? void 0 : lockRootStats.isDirectory()) || !statIdentitiesMatch({ dev: identity.lockRootDev, ino: identity.lockRootIno }, lockRootStats)) throw reconciliationAnchorError(identity.lockRootPath, "changed after lock identity was captured", "ESTALE"); } function reconciliationAnchorError(anchorPath, message, code) { const error = /* @__PURE__ */ new Error(`Filesystem reconciliation root ${anchorPath} ${message}`); error.code = code; error.path = anchorPath; return error; } function reconciliationLockPath(identity) { return join(identity.lockRootPath, `${reconciliationLockName}.${createHash("sha256").update(identity.key).digest("hex")}${reconciliationMetadataExtension}`); } function reconciliationReclaimPath(identity) { return join(identity.lockRootPath, `${reconciliationLockName}${reconciliationReclaimMarker}${createHash("sha256").update(identity.key).digest("hex")}${reconciliationMetadataExtension}`); } function reconciliationCandidateName(path, token) { return `${basename(path, extname(path))}${reconciliationCandidateMarker}${token}${reconciliationMetadataExtension}`; } function reconciliationCandidatePath(path, token) { return join(dirname(path), reconciliationCandidateName(path, token)); } function reconciliationRetiredPath(path) { return join(dirname(path), `${basename(path, extname(path))}${reconciliationRetiredMarker}${randomUUID()}${reconciliationMetadataExtension}`); } async function acquireReconciliationLock(identity, acquisitionDeadline) { const { key } = identity; const [incarnation, executionIdentity] = await Promise.all([getCurrentProcessIncarnation(), getCurrentProcessExecutionIdentity()]); const lockPath = reconciliationLockPath(identity); await assertReconciliationAnchorUnchanged(identity); await assertReconciliationRequestedPathUnchanged(identity); await assertReconciliationLockRootUnchanged(identity); await removeStaleReconciliationCandidates(identity, lockPath, isReconciliationLockOwner); await removeStaleReconciliationCandidates(identity, reconciliationReclaimPath(identity), isReconciliationReclaimOwner); while (true) { assertReconciliationLockAcquisitionTimeRemaining(acquisitionDeadline, key); await assertReconciliationAnchorUnchanged(identity); await assertReconciliationRequestedPathUnchanged(identity); await assertReconciliationLockRootUnchanged(identity); await waitForReconciliationReclaim(identity, acquisitionDeadline); const token = randomUUID(); const owner = { candidate: reconciliationCandidateName(lockPath, token), createdAt: Date.now(), boot: executionIdentity.boot, bootSession: executionIdentity.bootSession, incarnation, key, kind: reconciliationLockKind, machine: executionIdentity.machine, namespace: executionIdentity.namespace, pid: process.pid, token, version: reconciliationStateVersion }; let lockStats; try { lockStats = await createExclusiveReconciliationMetadata(identity, lockPath, owner); } catch (error) { if (isReconciliationCandidateCollisionError(error)) continue; if (!await reconciliationMetadataAlreadyExists(lockPath, error)) throw error; const state = await inspectReconciliationLock(identity, lockPath); if (state === null || state === void 0 ? void 0 : state.unverifiableReason) { await waitForUnverifiableReconciliationLock(identity, lockPath, state, acquisitionDeadline); continue; } if ((state === null || state === void 0 ? void 0 : state.owner) && state.stale && await tryReclaimStaleReconciliationLock(identity, lockPath, state)) continue; await delayReconciliationLockRetry(acquisitionDeadline, key); continue; } try { await waitForReconciliationReclaim(identity, acquisitionDeadline); if (!await reconciliationLockIsOwnedBy(identity, lockPath, lockStats, token)) throw new Error(`Lost filesystem reconciliation lock ownership before initialization: ${key}`); return maintainReconciliationLock(identity, lockPath, lockStats, token); } catch (error) { await cleanupFailedReconciliationLock(identity, lockPath, lockStats, token); throw error; } } } async function createExclusiveReconciliationMetadata(identity, path, owner) { const candidatePath = reconciliationCandidatePath(path, owner.token); if (owner.candidate !== basename(candidatePath)) throw new Error(`Invalid filesystem reconciliation candidate name: ${owner.candidate}`); await assertReconciliationLockRootUnchanged(identity); let handle; try { handle = await open(candidatePath, "wx", 420); } catch (error) { if (error.code === "EEXIST") throw reconciliationCandidateCollisionError(candidatePath); throw error; } const createdStats = await handle.stat({ bigint: true }); let published = false; try { if (process.platform !== "win32") await handle.chmod(420); await handle.writeFile(JSON.stringify(owner), "utf8"); await handle.sync(); await handle.close(); await assertReconciliationLockRootUnchanged(identity); try { await link(candidatePath, path); published = true; } catch (error) { if (error.code === "EEXIST") throw reconciliationMetadataExistsError(path); if (isUnsupportedReconciliationHardLinkError(error)) throw reconciliationHardLinkRequiredError(path, error); throw error; } await assertReconciliationLockRootUnchanged(identity); if (!await reconciliationPathMatches(path, createdStats)) throw new Error(`Lost filesystem reconciliation metadata: ${path}`); } catch (error) { await handle.close().catch(() => {}); try { await assertReconciliationLockRootUnchanged(identity); if (published) await retireReconciliationPathIfMatches(identity, path, createdStats, async (retiredPath) => await readFile(retiredPath, "utf8") === JSON.stringify(owner)); await removeReconciliationCandidateIfMatches(identity, candidatePath, createdStats); } catch (cleanupError) { if (!isReconciliationAnchorChangedError(cleanupError)) debug$10.warn(`Failed to clean up filesystem reconciliation metadata: ${errorMessage(cleanupError)}`); } throw error; } try { await removeReconciliationCandidateIfMatches(identity, candidatePath, createdStats); } catch (cleanupError) { if (!isReconciliationAnchorChangedError(cleanupError)) debug$10.warn(`Failed to remove filesystem reconciliation publication candidate: ${errorMessage(cleanupError)}`); } return createdStats; } async function cleanupFailedReconciliationLock(identity, lockPath, lockStats, token) { try { await waitForReconciliationReclaim(identity, createReconciliationLockDeadline(reconciliationLockCleanupTimeout)); } catch (cleanupError) { debug$10.warn(`Failed to wait for filesystem reconciliation reclaim during acquisition cleanup: ${errorMessage(cleanupError)}`); if (isReconciliationAnchorChangedError(cleanupError) || isReconciliationPathCollisionError(cleanupError)) return; scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token); return; } try { if (await removeOwnedReconciliationLock(identity, lockPath, lockStats, token) === "blocked") scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token); } catch (cleanupError) { debug$10.warn(`Failed to clean up filesystem reconciliation lock after acquisition failure: ${errorMessage(cleanupError)}`); if (!isReconciliationAnchorChangedError(cleanupError) && !isReconciliationPathCollisionError(cleanupError)) scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token); } } function scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token) { setTimeout(() => { retryFailedReconciliationLockCleanup(identity, lockPath, lockStats, token).then((complete) => { if (!complete) scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token); }).catch((cleanupError) => { debug$10.warn(`Failed to retry filesystem reconciliation lock cleanup: ${errorMessage(cleanupError)}`); if (!isReconciliationAnchorChangedError(cleanupError) && !isReconciliationPathCollisionError(cleanupError)) scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token); }); }, reconciliationLockCleanupRetryInterval).unref(); } async function retryFailedReconciliationLockCleanup(identity, lockPath, lockStats, token) { await assertReconciliationLockRootUnchanged(identity); if (await pathExistsAsync(reconciliationReclaimPath(identity))) { await removeStaleReconciliationReclaim(identity); if (await pathExistsAsync(reconciliationReclaimPath(identity))) return false; } return await removeOwnedReconciliationLock(identity, lockPath, lockStats, token) !== "blocked"; } function isReconciliationAnchorChangedError(error) { return error.code === "ESTALE"; } function maintainReconciliationLock(identity, lockPath, lockStats, token) { const { key } = identity; return async () => { let anchorError; if (identity.reportTopologyChange) try { await assertReconciliationAnchorUnchanged(identity); await assertReconciliationRequestedPathUnchanged(identity); } catch (error) { anchorError = error; } await assertReconciliationLockRootUnchanged(identity); try { await waitForReconciliationReclaim(identity, createReconciliationLockDeadline(reconciliationLockCleanupTimeout)); } catch (error) { if (!isReconciliationAnchorChangedError(error) && !isReconciliationPathCollisionError(error)) scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token); throw error; } let cleanupResult; try { cleanupResult = await removeOwnedReconciliationLock(identity, lockPath, lockStats, token); } catch (error) { if (!isReconciliationAnchorChangedError(error) && !isReconciliationPathCollisionError(error)) scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token); throw error; } if (cleanupResult === "blocked") { scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token); const error = /* @__PURE__ */ new Error(`Filesystem reconciliation lock cleanup was blocked by a reclaimer: ${key}`); error.code = "EBUSY"; throw error; } if (anchorError !== void 0) throw anchorError; }; } async function