UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

331 lines (330 loc) 11.8 kB
import { o as isRecord } from "./record-coerce-DHZ4bFlT.js"; import { w as pathExists } from "./fs-safe-aqmM_n6V.js"; import { r as assertCanonicalPathWithinBase } from "./install-safe-path-C0w7ALW6.js"; import { p as resolveUserPath } from "./utils-CCC-BEJH.js"; import { t as movePathWithCopyFallback } from "./replace-file-BrS02dAb.js"; import { m as writeJson, o as tryReadJson } from "./json-files-2umMHm0W.js"; import { r as runCommandWithTimeout } from "./exec-DubsSJS2.js"; import { c as resolvePackedRootDir, t as extractArchive } from "./archive-Dcpo6Wva.js"; import "./archive-CBe_wA_B.js"; import { s as withTempDir } from "./install-source-utils-Zb798u54.js"; import { n as createSafeNpmInstallEnv, t as createSafeNpmInstallArgs } from "./safe-package-install-BpZqyGc8.js"; import path from "node:path"; import fs from "node:fs/promises"; //#region src/infra/install-flow.ts /** Resolve and stat a user-provided install path. */ async function resolveExistingInstallPath(inputPath) { const resolvedPath = resolveUserPath(inputPath); if (!await pathExists(resolvedPath)) return { ok: false, error: `path not found: ${resolvedPath}` }; return { ok: true, resolvedPath, stat: await fs.stat(resolvedPath) }; } /** Extract an archive to a temp dir and run work against the detected package root. */ async function withExtractedArchiveRoot(params) { return await withTempDir(params.tempDirPrefix, async (tmpDir) => { const extractDir = path.join(tmpDir, "extract"); await fs.mkdir(extractDir, { recursive: true }); params.logger?.info?.(`Extracting ${params.archivePath}…`); try { await extractArchive({ archivePath: params.archivePath, destDir: extractDir, timeoutMs: params.timeoutMs, logger: params.logger }); } catch (err) { return { ok: false, error: `failed to extract archive: ${String(err)}` }; } let rootDir; try { rootDir = await resolvePackedRootDir(extractDir, { rootMarkers: params.rootMarkers ? [...params.rootMarkers] : void 0 }); } catch (err) { return { ok: false, error: String(err) }; } return await params.onExtracted(rootDir); }); } //#endregion //#region src/infra/install-package-dir.ts const DEFAULT_INSTALL_SOURCE_HARDLINKS = "reject"; const INSTALL_BASE_CHANGED_ERROR_MESSAGE = "install base directory changed during install"; const INSTALL_BASE_CHANGED_ABORT_WARNING = "Install base directory changed during install; aborting staged publish."; const INSTALL_BASE_CHANGED_BACKUP_WARNING = "Install base directory changed before backup cleanup; leaving backup in place."; const STAGED_NPM_PROJECT_CONFIG_NAME = ".npmrc"; const STAGED_NPM_PROJECT_CONFIG_PREFIX = ".openclaw-install-hidden-npmrc-"; async function sanitizeManifestForNpmInstall(targetDir) { const manifestPath = path.join(targetDir, "package.json"); const parsed = await tryReadJson(manifestPath); if (!isRecord(parsed)) return; const manifest = parsed; const devDependencies = manifest.devDependencies; if (!isRecord(devDependencies)) return; const filteredEntries = Object.entries(devDependencies).filter(([, rawSpec]) => { return !(typeof rawSpec === "string" ? rawSpec.trim() : "").startsWith("workspace:"); }); if (filteredEntries.length === Object.keys(devDependencies).length) return; if (filteredEntries.length === 0) delete manifest.devDependencies; else manifest.devDependencies = Object.fromEntries(filteredEntries); await writeJson(manifestPath, manifest, { trailingNewline: true }); } async function hideProjectNpmConfigForInstall(targetDir) { const originalPath = path.join(targetDir, STAGED_NPM_PROJECT_CONFIG_NAME); let hiddenDir = ""; try { hiddenDir = await fs.mkdtemp(path.join(targetDir, STAGED_NPM_PROJECT_CONFIG_PREFIX)); const hiddenPath = path.join(hiddenDir, STAGED_NPM_PROJECT_CONFIG_NAME); await fs.rename(originalPath, hiddenPath); return { hiddenDir, originalPath, hiddenPath }; } catch (error) { if (hiddenDir) await fs.rm(hiddenDir, { recursive: true, force: true }).catch(() => void 0); if (error.code === "ENOENT") return null; throw error; } } async function restoreProjectNpmConfigAfterInstall(hiddenConfig) { if (!hiddenConfig) return; await fs.rename(hiddenConfig.hiddenPath, hiddenConfig.originalPath); await fs.rm(hiddenConfig.hiddenDir, { recursive: true, force: true }); } async function assertInstallBoundaryPaths(params) { for (const candidatePath of params.candidatePaths) await assertCanonicalPathWithinBase({ baseDir: params.installBaseDir, candidatePath, boundaryLabel: "install directory" }); } function isRelativePathInsideBase(relativePath) { return Boolean(relativePath) && relativePath !== ".." && !relativePath.startsWith(`..${path.sep}`); } function isInstallBaseChangedError(error) { return error instanceof Error && error.message === INSTALL_BASE_CHANGED_ERROR_MESSAGE; } function resolveMoveSourceHardlinks(policy) { return policy === "package-manager" ? "allow" : "reject"; } async function assertInstallBaseStable(params) { if (!(await fs.stat(params.installBaseDir)).isDirectory()) throw new Error(INSTALL_BASE_CHANGED_ERROR_MESSAGE); if (await fs.realpath(params.installBaseDir) !== params.expectedRealPath) throw new Error(INSTALL_BASE_CHANGED_ERROR_MESSAGE); } async function cleanupInstallTempDir(dirPath) { if (!dirPath) return; await fs.rm(dirPath, { recursive: true, force: true }).catch(() => void 0); } async function resolveInstallPublishTarget(params) { const installBaseResolved = path.resolve(params.installBaseDir); const targetResolved = path.resolve(params.targetDir); const targetRelativePath = path.relative(installBaseResolved, targetResolved); if (!isRelativePathInsideBase(targetRelativePath)) throw new Error("invalid install target path"); const installBaseRealPath = await fs.realpath(params.installBaseDir); return { installBaseRealPath, canonicalTargetDir: path.join(installBaseRealPath, targetRelativePath) }; } /** * Publishes a package directory into an install target via a staged copy. * Update mode backs up the existing target, runs optional validation hooks, * and rolls back when copy, dependency install, or validation fails. */ async function installPackageDir(params) { params.logger?.info?.(`Installing to ${params.targetDir}…`); const installBaseDir = path.dirname(params.targetDir); let initialInstallBaseRealPath; try { await fs.mkdir(installBaseDir, { recursive: true }); initialInstallBaseRealPath = await fs.realpath(installBaseDir); await assertInstallBoundaryPaths({ installBaseDir, candidatePaths: [params.targetDir] }); } catch (err) { return { ok: false, error: `${params.copyErrorPrefix}: ${String(err)}` }; } let installBaseRealPath; let canonicalTargetDir; try { ({installBaseRealPath, canonicalTargetDir} = await resolveInstallPublishTarget({ installBaseDir, targetDir: params.targetDir })); if (installBaseRealPath !== initialInstallBaseRealPath) throw new Error(INSTALL_BASE_CHANGED_ERROR_MESSAGE); } catch (err) { if (isInstallBaseChangedError(err)) params.logger?.warn?.(INSTALL_BASE_CHANGED_ABORT_WARNING); return { ok: false, error: `${params.copyErrorPrefix}: ${String(err)}` }; } let stageDir = null; let backupDir = null; const sourceHardlinks = resolveMoveSourceHardlinks(params.sourceHardlinks ?? DEFAULT_INSTALL_SOURCE_HARDLINKS); const fail = async (error, cause) => { if (isInstallBaseChangedError(cause)) params.logger?.warn?.(INSTALL_BASE_CHANGED_ABORT_WARNING); else { await restoreBackup(); if (stageDir) { await cleanupInstallTempDir(stageDir); stageDir = null; } } return { ok: false, error }; }; const failWithCode = async (paramsLocal, cause) => { const failed = await fail(paramsLocal.error, cause); return paramsLocal.code ? { ...failed, code: paramsLocal.code } : failed; }; const restoreBackup = async () => { if (!backupDir) return; await movePathWithCopyFallback({ from: backupDir, sourceHardlinks, to: canonicalTargetDir }).catch(() => void 0); backupDir = null; }; try { await assertInstallBoundaryPaths({ installBaseDir: installBaseRealPath, candidatePaths: [canonicalTargetDir] }); stageDir = await fs.mkdtemp(path.join(installBaseRealPath, ".openclaw-install-stage-")); await fs.cp(params.sourceDir, stageDir, { recursive: true, verbatimSymlinks: true }); } catch (err) { return await fail(`${params.copyErrorPrefix}: ${String(err)}`, err); } try { await params.afterCopy?.(stageDir); } catch (err) { return await fail(`post-copy validation failed: ${String(err)}`, err); } if (params.hasDeps) try { await sanitizeManifestForNpmInstall(stageDir); const hiddenProjectNpmConfig = await hideProjectNpmConfigForInstall(stageDir); params.logger?.info?.(params.depsLogMessage); const npmRes = await (async () => { try { return await runCommandWithTimeout(["npm", ...createSafeNpmInstallArgs({ omitDev: true, loglevel: "error" })], { timeoutMs: Math.max(params.timeoutMs, 3e5), cwd: stageDir, env: createSafeNpmInstallEnv(process.env, { npmConfigCwd: stageDir }) }); } finally { await restoreProjectNpmConfigAfterInstall(hiddenProjectNpmConfig); } })(); if (npmRes.code !== 0) return await fail(`npm install failed: ${npmRes.stderr.trim() || npmRes.stdout.trim()}`); } catch (error) { return await fail(`npm install failed: ${String(error)}`, error); } if (params.afterInstall) try { const postInstallResult = await params.afterInstall(stageDir); if (!postInstallResult.ok) return await failWithCode(postInstallResult); } catch (err) { return await fail(`post-install validation failed: ${String(err)}`, err); } if (params.mode === "update" && await pathExists(canonicalTargetDir)) { const backupRoot = path.join(installBaseRealPath, ".openclaw-install-backups"); backupDir = path.join(backupRoot, `${path.basename(canonicalTargetDir)}-${Date.now()}`); try { await fs.mkdir(backupRoot, { recursive: true }); await assertInstallBoundaryPaths({ installBaseDir: installBaseRealPath, candidatePaths: [backupDir] }); await assertInstallBaseStable({ installBaseDir, expectedRealPath: installBaseRealPath }); await movePathWithCopyFallback({ from: canonicalTargetDir, sourceHardlinks, to: backupDir }); } catch (err) { return await fail(`${params.copyErrorPrefix}: ${String(err)}`, err); } } try { await assertInstallBaseStable({ installBaseDir, expectedRealPath: installBaseRealPath }); await movePathWithCopyFallback({ from: stageDir, sourceHardlinks, to: canonicalTargetDir }); stageDir = null; } catch (err) { return await fail(`${params.copyErrorPrefix}: ${String(err)}`, err); } if (backupDir) try { await assertInstallBaseStable({ installBaseDir, expectedRealPath: installBaseRealPath }); } catch (err) { if (isInstallBaseChangedError(err)) params.logger?.warn?.(INSTALL_BASE_CHANGED_BACKUP_WARNING); backupDir = null; } if (backupDir) await fs.rm(backupDir, { recursive: true, force: true }).catch(() => void 0); if (stageDir) await cleanupInstallTempDir(stageDir); return { ok: true }; } /** * Installs a manifest-backed package directory while deriving whether npm * dependencies must be installed and which hardlink policy is safe to use. */ async function installPackageDirWithManifestDeps(params) { const hasDeps = Object.keys(params.manifestDependencies ?? {}).length > 0; return installPackageDir({ ...params, hasDeps, sourceHardlinks: hasDeps ? "package-manager" : "reject" }); } //#endregion export { withExtractedArchiveRoot as i, installPackageDirWithManifestDeps as n, resolveExistingInstallPath as r, installPackageDir as t };