mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
706 lines
22 KiB
JavaScript
706 lines
22 KiB
JavaScript
#!/usr/bin/env node
|
|
// Runs after install to keep packaged dist safe and compatible.
|
|
// Keep packaged dist safe and compatible. Plugin package dependencies are
|
|
// installed only by explicit plugin install/update flows, never postinstall.
|
|
import {
|
|
existsSync,
|
|
lstatSync,
|
|
opendirSync,
|
|
readdirSync,
|
|
readFileSync,
|
|
readlinkSync,
|
|
realpathSync,
|
|
rmdirSync,
|
|
rmSync,
|
|
unlinkSync,
|
|
} from "node:fs";
|
|
import { homedir } from "node:os";
|
|
import { basename, dirname, isAbsolute, join, relative, resolve as pathResolve } from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { expandPackageDistImportClosure } from "./lib/package-dist-imports.mjs";
|
|
|
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
const DEFAULT_PACKAGE_ROOT = join(scriptDir, "..");
|
|
const DISABLE_POSTINSTALL_ENV = "OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL";
|
|
const DIST_INVENTORY_PATH = "dist/postinstall-inventory.json";
|
|
// One budget covers all three prune walks (legacy-deps prepass, file listing,
|
|
// empty-dir sweep). npm upgrades transiently hold old+new content-hashed dist
|
|
// files, so a real upgrade scan totals ~24k entries today (2026.6.x); keep ~4x
|
|
// headroom so dist growth cannot fail `npm install -g` while still refusing
|
|
// pathological/unbounded trees.
|
|
export const MAX_INSTALLED_DIST_SCAN_ENTRIES = 100_000;
|
|
const LEGACY_PLUGIN_RUNTIME_DEPS_DIR = "plugin-runtime-deps";
|
|
class InstalledDistScanLimitError extends Error {}
|
|
|
|
function normalizeRelativePath(filePath) {
|
|
return filePath.replace(/\\/g, "/");
|
|
}
|
|
|
|
function resolvePostinstallOsHomeDir(env, getHomedir = homedir) {
|
|
return env?.HOME?.trim() || env?.USERPROFILE?.trim() || getHomedir();
|
|
}
|
|
|
|
function resolvePostinstallTildePath(input, homeDir) {
|
|
if (input === "~") {
|
|
return homeDir;
|
|
}
|
|
if (input.startsWith("~/") || input.startsWith("~\\")) {
|
|
return join(homeDir, input.slice(2));
|
|
}
|
|
return input;
|
|
}
|
|
|
|
function resolvePostinstallOpenClawHomeDir(env, getHomedir = homedir) {
|
|
const osHome = resolvePostinstallOsHomeDir(env, getHomedir);
|
|
const override = env?.OPENCLAW_HOME?.trim();
|
|
return override ? pathResolve(resolvePostinstallTildePath(override, osHome)) : osHome;
|
|
}
|
|
|
|
function resolvePostinstallUserPath(input, openClawHome) {
|
|
return pathResolve(resolvePostinstallTildePath(input, openClawHome));
|
|
}
|
|
|
|
function readInstalledDistInventory(params = {}) {
|
|
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
|
const pathExists = params.existsSync ?? existsSync;
|
|
const readFile = params.readFileSync ?? readFileSync;
|
|
const inventoryPath = join(packageRoot, DIST_INVENTORY_PATH);
|
|
if (!pathExists(inventoryPath)) {
|
|
throw new Error(`missing dist inventory: ${DIST_INVENTORY_PATH}`);
|
|
}
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(readFile(inventoryPath, "utf8"));
|
|
} catch {
|
|
throw new Error(`invalid dist inventory: ${DIST_INVENTORY_PATH}`);
|
|
}
|
|
if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) {
|
|
throw new Error(`invalid dist inventory: ${DIST_INVENTORY_PATH}`);
|
|
}
|
|
return new Set(parsed.map(normalizeRelativePath));
|
|
}
|
|
|
|
function isRecoverableInstalledDistInventoryError(error) {
|
|
return error instanceof Error && /^(missing|invalid) dist inventory: /u.test(error.message);
|
|
}
|
|
|
|
function resolveInstalledDistRoot(params = {}) {
|
|
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
|
const pathExists = params.existsSync ?? existsSync;
|
|
const pathLstat = params.lstatSync ?? lstatSync;
|
|
const resolveRealPath = params.realpathSync ?? realpathSync;
|
|
const distDir = join(packageRoot, "dist");
|
|
if (!pathExists(distDir)) {
|
|
return null;
|
|
}
|
|
const distStats = pathLstat(distDir);
|
|
if (!distStats.isDirectory() || distStats.isSymbolicLink()) {
|
|
throw new Error("unsafe dist root: dist must be a real directory");
|
|
}
|
|
const packageRootReal = resolveRealPath(packageRoot);
|
|
const distDirReal = resolveRealPath(distDir);
|
|
const relativeDistPath = relative(packageRootReal, distDirReal);
|
|
if (relativeDistPath !== "dist") {
|
|
throw new Error("unsafe dist root: dist escaped package root");
|
|
}
|
|
return { distDir, distDirReal, packageRootReal };
|
|
}
|
|
|
|
function assertSafeInstalledDistPath(relativePath, params) {
|
|
const resolveRealPath = params.realpathSync ?? realpathSync;
|
|
const candidatePath = join(params.packageRoot, relativePath);
|
|
const candidateRealPath = resolveRealPath(candidatePath);
|
|
const relativeCandidatePath = relative(params.distDirReal, candidateRealPath);
|
|
if (relativeCandidatePath.startsWith("..") || isAbsolute(relativeCandidatePath)) {
|
|
throw new Error(`unsafe dist path: ${relativePath}`);
|
|
}
|
|
return candidatePath;
|
|
}
|
|
|
|
function createInstalledDistScanBudget(params = {}) {
|
|
return {
|
|
entries: 0,
|
|
limit: params.maxDistScanEntries ?? MAX_INSTALLED_DIST_SCAN_ENTRIES,
|
|
};
|
|
}
|
|
|
|
function resolveInstalledDistScanBudget(params = {}) {
|
|
return params.distScanBudget ?? createInstalledDistScanBudget(params);
|
|
}
|
|
|
|
function countInstalledDistScanEntry(budget) {
|
|
budget.entries += 1;
|
|
if (budget.entries > budget.limit) {
|
|
throw new InstalledDistScanLimitError(
|
|
`installed dist scan exceeded ${budget.limit} filesystem entries; refusing to scan unbounded package contents`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function* iterateInstalledDistEntries(currentDir, params = {}) {
|
|
if (params.readdirSync) {
|
|
yield* params.readdirSync(currentDir, { withFileTypes: true });
|
|
return;
|
|
}
|
|
|
|
const dir = opendirSync(currentDir);
|
|
try {
|
|
while (true) {
|
|
const entry = dir.readSync();
|
|
if (!entry) {
|
|
break;
|
|
}
|
|
yield entry;
|
|
}
|
|
} finally {
|
|
dir.closeSync();
|
|
}
|
|
}
|
|
|
|
function* iterateOptionalInstalledDistEntries(currentDir, params = {}) {
|
|
try {
|
|
yield* iterateInstalledDistEntries(currentDir, params);
|
|
} catch (error) {
|
|
if (error instanceof InstalledDistScanLimitError) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
function listInstalledDistFiles(params = {}) {
|
|
const distRoot = resolveInstalledDistRoot(params);
|
|
if (distRoot === null) {
|
|
return [];
|
|
}
|
|
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
|
const pending = [distRoot.distDir];
|
|
const files = [];
|
|
const budget = resolveInstalledDistScanBudget(params);
|
|
while (pending.length > 0) {
|
|
const currentDir = pending.pop();
|
|
if (!currentDir) {
|
|
continue;
|
|
}
|
|
for (const entry of iterateInstalledDistEntries(currentDir, params)) {
|
|
countInstalledDistScanEntry(budget);
|
|
const entryPath = join(currentDir, entry.name);
|
|
if (entry.isSymbolicLink()) {
|
|
throw new Error(
|
|
`unsafe dist entry: ${normalizeRelativePath(relative(packageRoot, entryPath))}`,
|
|
);
|
|
}
|
|
if (entry.isDirectory()) {
|
|
pending.push(entryPath);
|
|
continue;
|
|
}
|
|
if (!entry.isFile()) {
|
|
continue;
|
|
}
|
|
const relativePath = normalizeRelativePath(relative(packageRoot, entryPath));
|
|
if (relativePath === DIST_INVENTORY_PATH) {
|
|
continue;
|
|
}
|
|
files.push(relativePath);
|
|
}
|
|
}
|
|
return files.toSorted((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
function pruneEmptyDistDirectories(params = {}) {
|
|
const removeDirectory = params.rmdirSync ?? rmdirSync;
|
|
const distRoot = resolveInstalledDistRoot(params);
|
|
if (distRoot === null) {
|
|
return;
|
|
}
|
|
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
|
const pathLstat = params.lstatSync ?? lstatSync;
|
|
const budget = resolveInstalledDistScanBudget(params);
|
|
|
|
function isDirectoryEmpty(currentDir) {
|
|
for (const entry of iterateInstalledDistEntries(currentDir, params)) {
|
|
void entry;
|
|
countInstalledDistScanEntry(budget);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function prune(currentDir) {
|
|
const childDirs = [];
|
|
for (const entry of iterateInstalledDistEntries(currentDir, params)) {
|
|
countInstalledDistScanEntry(budget);
|
|
if (entry.isSymbolicLink()) {
|
|
throw new Error(
|
|
`unsafe dist entry: ${normalizeRelativePath(relative(packageRoot, join(currentDir, entry.name)))}`,
|
|
);
|
|
}
|
|
if (!entry.isDirectory()) {
|
|
continue;
|
|
}
|
|
childDirs.push(join(currentDir, entry.name));
|
|
}
|
|
for (const childDir of childDirs) {
|
|
prune(childDir);
|
|
}
|
|
if (currentDir === distRoot.distDir) {
|
|
return;
|
|
}
|
|
const currentStats = pathLstat(currentDir);
|
|
if (!currentStats.isDirectory() || currentStats.isSymbolicLink()) {
|
|
throw new Error(
|
|
`unsafe dist directory: ${normalizeRelativePath(relative(packageRoot, currentDir))}`,
|
|
);
|
|
}
|
|
if (isDirectoryEmpty(currentDir)) {
|
|
removeDirectory(
|
|
assertSafeInstalledDistPath(normalizeRelativePath(relative(packageRoot, currentDir)), {
|
|
packageRoot,
|
|
distDirReal: distRoot.distDirReal,
|
|
realpathSync: params.realpathSync,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
prune(distRoot.distDir);
|
|
}
|
|
|
|
function isLegacyInstalledPluginDependencyDirName(name) {
|
|
return name === "node_modules" || /^\.openclaw-install-stage(?:-[^/]+)?$/iu.test(name);
|
|
}
|
|
|
|
function pruneLegacyInstalledPluginDependencyDirs(params) {
|
|
const removePath = params.rmSync ?? rmSync;
|
|
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
|
const extensionsDir = join(packageRoot, "dist", "extensions");
|
|
const budget = resolveInstalledDistScanBudget(params);
|
|
const removed = [];
|
|
|
|
for (const pluginEntry of iterateOptionalInstalledDistEntries(extensionsDir, params)) {
|
|
countInstalledDistScanEntry(budget);
|
|
if (!pluginEntry.isDirectory() || pluginEntry.isSymbolicLink()) {
|
|
continue;
|
|
}
|
|
const pluginDir = join(extensionsDir, pluginEntry.name);
|
|
const dependencyDirNames = [];
|
|
for (const childEntry of iterateOptionalInstalledDistEntries(pluginDir, params)) {
|
|
countInstalledDistScanEntry(budget);
|
|
if (!isLegacyInstalledPluginDependencyDirName(childEntry.name)) {
|
|
continue;
|
|
}
|
|
dependencyDirNames.push(childEntry.name);
|
|
}
|
|
if (dependencyDirNames.length === 0) {
|
|
continue;
|
|
}
|
|
const safePluginDir = assertSafeInstalledDistPath(
|
|
normalizeRelativePath(relative(packageRoot, pluginDir)),
|
|
{
|
|
packageRoot,
|
|
distDirReal: params.distDirReal,
|
|
realpathSync: params.realpathSync,
|
|
},
|
|
);
|
|
for (const dependencyDirName of dependencyDirNames) {
|
|
const relativePath = normalizeRelativePath(
|
|
relative(packageRoot, join(pluginDir, dependencyDirName)),
|
|
);
|
|
removePath(join(safePluginDir, dependencyDirName), { recursive: true, force: true });
|
|
removed.push(relativePath);
|
|
}
|
|
}
|
|
|
|
return removed;
|
|
}
|
|
|
|
function splitPostinstallPathList(value) {
|
|
return value
|
|
? value
|
|
.split(pathDelimiter)
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean)
|
|
: [];
|
|
}
|
|
|
|
const pathDelimiter = process.platform === "win32" ? ";" : ":";
|
|
|
|
export function collectLegacyPluginRuntimeDepsStateRoots(params = {}) {
|
|
const env = params.env ?? process.env;
|
|
const getHomedir = params.homedir ?? homedir;
|
|
const openClawHome = resolvePostinstallOpenClawHomeDir(env, getHomedir);
|
|
const stateRoots = [];
|
|
const addStateRoot = (root) => {
|
|
if (root) {
|
|
stateRoots.push(join(root, LEGACY_PLUGIN_RUNTIME_DEPS_DIR));
|
|
}
|
|
};
|
|
|
|
const stateOverride = env?.OPENCLAW_STATE_DIR?.trim();
|
|
if (stateOverride) {
|
|
addStateRoot(resolvePostinstallUserPath(stateOverride, openClawHome));
|
|
}
|
|
const configPath = env?.OPENCLAW_CONFIG_PATH?.trim();
|
|
if (configPath) {
|
|
addStateRoot(dirname(resolvePostinstallUserPath(configPath, openClawHome)));
|
|
}
|
|
addStateRoot(join(openClawHome, ".openclaw"));
|
|
addStateRoot(join(openClawHome, ".clawdbot"));
|
|
|
|
for (const entry of splitPostinstallPathList(env?.STATE_DIRECTORY)) {
|
|
addStateRoot(resolvePostinstallUserPath(entry, openClawHome));
|
|
}
|
|
|
|
return [...new Set(stateRoots.map((root) => pathResolve(root)))].toSorted((left, right) =>
|
|
left.localeCompare(right),
|
|
);
|
|
}
|
|
|
|
function isPathInsideRoot(candidate, root) {
|
|
const relativePath = relative(root, candidate);
|
|
return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
|
|
}
|
|
|
|
function collectLegacyPluginRuntimeDepsSymlinkPaths(roots, params = {}) {
|
|
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
|
const readDir = params.readdirSync ?? readdirSync;
|
|
const pathLstat = params.lstatSync ?? lstatSync;
|
|
const readLink = params.readlinkSync ?? readlinkSync;
|
|
const pathExists = params.existsSync ?? existsSync;
|
|
const containingNodeModules = dirname(packageRoot);
|
|
if (basename(containingNodeModules) !== "node_modules") {
|
|
return [];
|
|
}
|
|
|
|
const normalizedRoots = roots.map((root) => pathResolve(root));
|
|
const candidates = [];
|
|
function addCandidate(linkPath) {
|
|
let linkStat;
|
|
try {
|
|
linkStat = pathLstat(linkPath);
|
|
} catch {
|
|
return;
|
|
}
|
|
if (!linkStat.isSymbolicLink()) {
|
|
return;
|
|
}
|
|
let target;
|
|
try {
|
|
target = readLink(linkPath);
|
|
} catch {
|
|
return;
|
|
}
|
|
if (!target.includes(LEGACY_PLUGIN_RUNTIME_DEPS_DIR)) {
|
|
return;
|
|
}
|
|
const resolvedTarget = pathResolve(dirname(linkPath), target);
|
|
const pointsIntoPrunedRoot = normalizedRoots.some((root) =>
|
|
isPathInsideRoot(resolvedTarget, root),
|
|
);
|
|
if (pointsIntoPrunedRoot || !pathExists(resolvedTarget)) {
|
|
candidates.push(linkPath);
|
|
}
|
|
}
|
|
|
|
let entries;
|
|
try {
|
|
entries = readDir(containingNodeModules, { withFileTypes: true });
|
|
} catch {
|
|
return [];
|
|
}
|
|
for (const entry of entries) {
|
|
if (entry.isDirectory() && entry.name.startsWith("@")) {
|
|
const scopeDir = join(containingNodeModules, entry.name);
|
|
let scopeEntries;
|
|
try {
|
|
scopeEntries = readDir(scopeDir, { withFileTypes: true });
|
|
} catch {
|
|
continue;
|
|
}
|
|
for (const scopeEntry of scopeEntries) {
|
|
addCandidate(join(scopeDir, scopeEntry.name));
|
|
}
|
|
continue;
|
|
}
|
|
if (entry.isSymbolicLink()) {
|
|
addCandidate(join(containingNodeModules, entry.name));
|
|
}
|
|
}
|
|
return [...new Set(candidates.map((entry) => pathResolve(entry)))].toSorted((left, right) =>
|
|
left.localeCompare(right),
|
|
);
|
|
}
|
|
|
|
export function pruneLegacyPluginRuntimeDepsState(params = {}) {
|
|
const pathExists = params.existsSync ?? existsSync;
|
|
const removePath = params.rmSync ?? rmSync;
|
|
const unlinkPath = params.unlinkSync ?? unlinkSync;
|
|
const log = params.log ?? console;
|
|
const removed = [];
|
|
const removedSymlinks = [];
|
|
const roots = collectLegacyPluginRuntimeDepsStateRoots(params);
|
|
|
|
for (const linkPath of collectLegacyPluginRuntimeDepsSymlinkPaths(roots, params)) {
|
|
try {
|
|
unlinkPath(linkPath);
|
|
removedSymlinks.push(linkPath);
|
|
} catch (error) {
|
|
log.warn?.(
|
|
`[postinstall] could not prune legacy plugin runtime deps symlink ${linkPath}: ${String(error)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
for (const root of roots) {
|
|
if (!pathExists(root)) {
|
|
continue;
|
|
}
|
|
try {
|
|
removePath(root, { recursive: true, force: true, maxRetries: 2, retryDelay: 100 });
|
|
removed.push(root);
|
|
} catch (error) {
|
|
log.warn?.(
|
|
`[postinstall] could not prune legacy plugin runtime deps ${root}: ${String(error)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (removed.length > 0) {
|
|
log.log?.(`[postinstall] pruned legacy plugin runtime deps: ${removed.join(", ")}`);
|
|
}
|
|
if (removedSymlinks.length > 0) {
|
|
log.log?.(
|
|
`[postinstall] pruned legacy plugin runtime deps symlinks: ${removedSymlinks.join(", ")}`,
|
|
);
|
|
}
|
|
|
|
return removed;
|
|
}
|
|
|
|
export function pruneInstalledPackageDist(params = {}) {
|
|
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
|
const removeFile = params.unlinkSync ?? unlinkSync;
|
|
const log = params.log ?? console;
|
|
const distRoot = resolveInstalledDistRoot(params);
|
|
if (distRoot === null) {
|
|
return [];
|
|
}
|
|
const distScanBudget = createInstalledDistScanBudget(params);
|
|
const distScanParams = { ...params, distScanBudget };
|
|
const removedLegacyDependencyDirs = pruneLegacyInstalledPluginDependencyDirs({
|
|
...distScanParams,
|
|
packageRoot,
|
|
distDirReal: distRoot.distDirReal,
|
|
realpathSync: params.realpathSync,
|
|
rmSync: params.rmSync,
|
|
});
|
|
let expectedFiles = params.expectedFiles ?? null;
|
|
if (expectedFiles === null) {
|
|
try {
|
|
expectedFiles = readInstalledDistInventory(params);
|
|
} catch (error) {
|
|
if (!isRecoverableInstalledDistInventoryError(error)) {
|
|
throw error;
|
|
}
|
|
log.warn?.(`[postinstall] skipping dist prune: ${error.message}`);
|
|
return [];
|
|
}
|
|
}
|
|
const installedFiles = listInstalledDistFiles(distScanParams);
|
|
const readFile = params.readFileSync ?? readFileSync;
|
|
expectedFiles = new Set(
|
|
expandPackageDistImportClosure({
|
|
files: installedFiles,
|
|
seedFiles: [...expectedFiles],
|
|
readText(relativePath) {
|
|
try {
|
|
return readFile(join(packageRoot, relativePath), "utf8");
|
|
} catch (error) {
|
|
if (error?.code === "ENOENT") {
|
|
return "";
|
|
}
|
|
throw error;
|
|
}
|
|
},
|
|
}),
|
|
);
|
|
const removed = [];
|
|
|
|
for (const relativePath of installedFiles) {
|
|
if (expectedFiles.has(relativePath)) {
|
|
continue;
|
|
}
|
|
removeFile(
|
|
assertSafeInstalledDistPath(relativePath, {
|
|
packageRoot,
|
|
distDirReal: distRoot.distDirReal,
|
|
realpathSync: params.realpathSync,
|
|
}),
|
|
);
|
|
removed.push(relativePath);
|
|
}
|
|
|
|
pruneEmptyDistDirectories(distScanParams);
|
|
|
|
if (removed.length > 0) {
|
|
log.log(`[postinstall] pruned stale dist files: ${removed.join(", ")}`);
|
|
}
|
|
if (removedLegacyDependencyDirs.length > 0) {
|
|
log.log(
|
|
`[postinstall] pruned legacy plugin dependency dirs: ${removedLegacyDependencyDirs.join(", ")}`,
|
|
);
|
|
}
|
|
return removed;
|
|
}
|
|
|
|
function resolveDistModuleUrl(packageRoot, distPath) {
|
|
return pathToFileURL(join(packageRoot, distPath)).href;
|
|
}
|
|
|
|
async function importInstalledDistModule(params, distPath) {
|
|
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
|
const pathExists = params.existsSync ?? existsSync;
|
|
const modulePath = join(packageRoot, distPath);
|
|
if (!pathExists(modulePath)) {
|
|
return null;
|
|
}
|
|
const importModule = params.importModule ?? ((specifier) => import(specifier));
|
|
return await importModule(resolveDistModuleUrl(packageRoot, distPath));
|
|
}
|
|
|
|
export async function runPluginRegistryPostinstallMigration(params = {}) {
|
|
const log = params.log ?? console;
|
|
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
|
const env = params.env ?? process.env;
|
|
const pathExists = params.existsSync ?? existsSync;
|
|
|
|
// Registry migration belongs to installed-package upgrades. Source checkouts
|
|
// can contain stale dist from a different build and must not touch operator state.
|
|
if (isSourceCheckoutRoot({ packageRoot, existsSync: pathExists })) {
|
|
return { status: "skipped", reason: "source-checkout" };
|
|
}
|
|
|
|
try {
|
|
const migrationModule = await importInstalledDistModule(
|
|
{ ...params, existsSync: pathExists },
|
|
"dist/commands/doctor/shared/plugin-registry-migration.js",
|
|
);
|
|
if (!migrationModule) {
|
|
return { status: "skipped", reason: "missing-dist-entry" };
|
|
}
|
|
if (typeof migrationModule.migratePluginRegistryForInstall !== "function") {
|
|
return { status: "skipped", reason: "missing-dist-contract" };
|
|
}
|
|
|
|
const result = await migrationModule.migratePluginRegistryForInstall({
|
|
env,
|
|
packageRoot,
|
|
});
|
|
if (result.migrated) {
|
|
log.log(
|
|
`[postinstall] migrated plugin registry: ${result.current.plugins.length} plugin(s) indexed`,
|
|
);
|
|
}
|
|
return result;
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
log.warn(`[postinstall] could not migrate plugin registry: ${message}`);
|
|
return { status: "failed", error: message };
|
|
}
|
|
}
|
|
|
|
export function isSourceCheckoutRoot(params) {
|
|
const pathExists = params.existsSync ?? existsSync;
|
|
const hasPostinstallInventory = pathExists(join(params.packageRoot, DIST_INVENTORY_PATH));
|
|
return (
|
|
(pathExists(join(params.packageRoot, ".git")) ||
|
|
(pathExists(join(params.packageRoot, "pnpm-workspace.yaml")) && !hasPostinstallInventory)) &&
|
|
pathExists(join(params.packageRoot, "src")) &&
|
|
pathExists(join(params.packageRoot, "extensions"))
|
|
);
|
|
}
|
|
|
|
export function pruneBundledPluginSourceNodeModules(params = {}) {
|
|
const extensionsDir = params.extensionsDir ?? join(DEFAULT_PACKAGE_ROOT, "extensions");
|
|
const pathExists = params.existsSync ?? existsSync;
|
|
const readDir = params.readdirSync ?? readdirSync;
|
|
const removePath = params.rmSync ?? rmSync;
|
|
|
|
if (!pathExists(extensionsDir)) {
|
|
return;
|
|
}
|
|
|
|
for (const entry of readDir(extensionsDir, { withFileTypes: true })) {
|
|
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
continue;
|
|
}
|
|
|
|
const pluginDir = join(extensionsDir, entry.name);
|
|
if (!pathExists(join(pluginDir, "package.json"))) {
|
|
continue;
|
|
}
|
|
|
|
removePath(join(pluginDir, "node_modules"), { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
export function runBundledPluginPostinstall(params = {}) {
|
|
const env = params.env ?? process.env;
|
|
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
|
const pathExists = params.existsSync ?? existsSync;
|
|
const log = params.log ?? console;
|
|
if (env?.[DISABLE_POSTINSTALL_ENV]?.trim()) {
|
|
return;
|
|
}
|
|
if (isSourceCheckoutRoot({ packageRoot, existsSync: pathExists })) {
|
|
try {
|
|
pruneBundledPluginSourceNodeModules({
|
|
extensionsDir: join(packageRoot, "extensions"),
|
|
existsSync: pathExists,
|
|
readdirSync: params.readdirSync,
|
|
rmSync: params.rmSync,
|
|
});
|
|
} catch (e) {
|
|
log.warn(`[postinstall] could not prune bundled plugin source node_modules: ${String(e)}`);
|
|
}
|
|
return;
|
|
}
|
|
pruneLegacyPluginRuntimeDepsState({
|
|
env,
|
|
packageRoot,
|
|
existsSync: pathExists,
|
|
lstatSync: params.lstatSync,
|
|
readlinkSync: params.readlinkSync,
|
|
rmSync: params.rmSync,
|
|
unlinkSync: params.unlinkSync,
|
|
log,
|
|
homedir: params.homedir,
|
|
});
|
|
pruneInstalledPackageDist({
|
|
packageRoot,
|
|
existsSync: pathExists,
|
|
readFileSync: params.readFileSync,
|
|
readdirSync: params.readdirSync,
|
|
rmSync: params.rmSync,
|
|
log,
|
|
});
|
|
}
|
|
|
|
export function isDirectPostinstallInvocation(params = {}) {
|
|
const entryPath = params.entryPath ?? process.argv[1];
|
|
if (!entryPath) {
|
|
return false;
|
|
}
|
|
const modulePath = params.modulePath ?? fileURLToPath(import.meta.url);
|
|
const resolveRealPath = params.realpathSync ?? realpathSync;
|
|
try {
|
|
return resolveRealPath(entryPath) === resolveRealPath(modulePath);
|
|
} catch {
|
|
return pathToFileURL(entryPath).href === pathToFileURL(modulePath).href;
|
|
}
|
|
}
|
|
|
|
if (isDirectPostinstallInvocation()) {
|
|
runBundledPluginPostinstall();
|
|
await runPluginRegistryPostinstallMigration();
|
|
}
|