mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-23 15:11:42 +00:00
perf: lazy-load plugin install runtime helpers
This commit is contained in:
62
src/plugins/install.runtime.ts
Normal file
62
src/plugins/install.runtime.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { fileExists, readJsonFile, resolveArchiveKind } from "../infra/archive.js";
|
||||
import { writeFileFromPathWithinRoot } from "../infra/fs-safe.js";
|
||||
import { resolveExistingInstallPath, withExtractedArchiveRoot } from "../infra/install-flow.js";
|
||||
import {
|
||||
resolveInstallModeOptions,
|
||||
resolveTimedInstallModeOptions,
|
||||
} from "../infra/install-mode-options.js";
|
||||
import { installPackageDir } from "../infra/install-package-dir.js";
|
||||
import {
|
||||
type NpmIntegrityDrift,
|
||||
type NpmSpecResolution,
|
||||
resolveArchiveSourcePath,
|
||||
} from "../infra/install-source-utils.js";
|
||||
import {
|
||||
ensureInstallTargetAvailable,
|
||||
resolveCanonicalInstallTarget,
|
||||
} from "../infra/install-target.js";
|
||||
import {
|
||||
finalizeNpmSpecArchiveInstall,
|
||||
installFromNpmSpecArchiveWithInstaller,
|
||||
} from "../infra/npm-pack-install.js";
|
||||
import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
||||
import { resolveRuntimeServiceVersion } from "../version.js";
|
||||
import { detectBundleManifestFormat, loadBundleManifest } from "./bundle-manifest.js";
|
||||
import { scanBundleInstallSource, scanPackageInstallSource } from "./install-security-scan.js";
|
||||
import {
|
||||
getPackageManifestMetadata,
|
||||
loadPluginManifest,
|
||||
resolvePackageExtensionEntries,
|
||||
} from "./manifest.js";
|
||||
import { checkMinHostVersion } from "./min-host-version.js";
|
||||
import { isPathInside } from "./path-safety.js";
|
||||
|
||||
export type { NpmIntegrityDrift, NpmSpecResolution };
|
||||
|
||||
export {
|
||||
checkMinHostVersion,
|
||||
detectBundleManifestFormat,
|
||||
ensureInstallTargetAvailable,
|
||||
fileExists,
|
||||
finalizeNpmSpecArchiveInstall,
|
||||
getPackageManifestMetadata,
|
||||
installFromNpmSpecArchiveWithInstaller,
|
||||
installPackageDir,
|
||||
isPathInside,
|
||||
loadBundleManifest,
|
||||
loadPluginManifest,
|
||||
readJsonFile,
|
||||
resolveArchiveKind,
|
||||
resolveArchiveSourcePath,
|
||||
resolveCanonicalInstallTarget,
|
||||
resolveExistingInstallPath,
|
||||
resolveInstallModeOptions,
|
||||
resolvePackageExtensionEntries,
|
||||
resolveRuntimeServiceVersion,
|
||||
resolveTimedInstallModeOptions,
|
||||
scanBundleInstallSource,
|
||||
scanPackageInstallSource,
|
||||
validateRegistryNpmSpec,
|
||||
withExtractedArchiveRoot,
|
||||
writeFileFromPathWithinRoot,
|
||||
};
|
||||
@@ -1,45 +1,24 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileExists, readJsonFile, resolveArchiveKind } from "../infra/archive.js";
|
||||
import { writeFileFromPathWithinRoot } from "../infra/fs-safe.js";
|
||||
import { resolveExistingInstallPath, withExtractedArchiveRoot } from "../infra/install-flow.js";
|
||||
import {
|
||||
resolveInstallModeOptions,
|
||||
resolveTimedInstallModeOptions,
|
||||
} from "../infra/install-mode-options.js";
|
||||
import { installPackageDir } from "../infra/install-package-dir.js";
|
||||
import {
|
||||
resolveSafeInstallDir,
|
||||
safeDirName,
|
||||
safePathSegmentHashed,
|
||||
unscopedPackageName,
|
||||
} from "../infra/install-safe-path.js";
|
||||
import {
|
||||
type NpmIntegrityDrift,
|
||||
type NpmSpecResolution,
|
||||
resolveArchiveSourcePath,
|
||||
} from "../infra/install-source-utils.js";
|
||||
import {
|
||||
ensureInstallTargetAvailable,
|
||||
resolveCanonicalInstallTarget,
|
||||
} from "../infra/install-target.js";
|
||||
import {
|
||||
finalizeNpmSpecArchiveInstall,
|
||||
installFromNpmSpecArchiveWithInstaller,
|
||||
} from "../infra/npm-pack-install.js";
|
||||
import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
||||
import { isPathInside } from "../security/scan-paths.js";
|
||||
import { type NpmIntegrityDrift, type NpmSpecResolution } from "../infra/install-source-utils.js";
|
||||
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
||||
import { resolveRuntimeServiceVersion } from "../version.js";
|
||||
import { detectBundleManifestFormat, loadBundleManifest } from "./bundle-manifest.js";
|
||||
import { scanBundleInstallSource, scanPackageInstallSource } from "./install-security-scan.js";
|
||||
import {
|
||||
getPackageManifestMetadata,
|
||||
loadPluginManifest,
|
||||
resolvePackageExtensionEntries,
|
||||
type PackageManifest as PluginPackageManifest,
|
||||
} from "./manifest.js";
|
||||
import { checkMinHostVersion } from "./min-host-version.js";
|
||||
|
||||
let pluginInstallRuntimePromise: Promise<typeof import("./install.runtime.js")> | undefined;
|
||||
|
||||
async function loadPluginInstallRuntime() {
|
||||
pluginInstallRuntimePromise ??= import("./install.runtime.js");
|
||||
return pluginInstallRuntimePromise;
|
||||
}
|
||||
|
||||
type PluginInstallLogger = {
|
||||
info?: (message: string) => void;
|
||||
@@ -281,10 +260,11 @@ async function installPluginDirectoryIntoExtensions(params: {
|
||||
afterCopy?: (installedDir: string) => Promise<void>;
|
||||
nameEncoder?: (pluginId: string) => string;
|
||||
}): Promise<InstallPluginResult> {
|
||||
const runtime = await loadPluginInstallRuntime();
|
||||
const extensionsDir = params.extensionsDir
|
||||
? resolveUserPath(params.extensionsDir)
|
||||
: path.join(CONFIG_DIR, "extensions");
|
||||
const targetDirResult = await resolveCanonicalInstallTarget({
|
||||
const targetDirResult = await runtime.resolveCanonicalInstallTarget({
|
||||
baseDir: extensionsDir,
|
||||
id: params.pluginId,
|
||||
invalidNameMessage: "invalid plugin name: path traversal detected",
|
||||
@@ -295,7 +275,7 @@ async function installPluginDirectoryIntoExtensions(params: {
|
||||
return { ok: false, error: targetDirResult.error };
|
||||
}
|
||||
const targetDir = targetDirResult.targetDir;
|
||||
const availability = await ensureInstallTargetAvailable({
|
||||
const availability = await runtime.ensureInstallTargetAvailable({
|
||||
mode: params.mode,
|
||||
targetDir,
|
||||
alreadyExistsError: `plugin already exists: ${targetDir} (delete it first)`,
|
||||
@@ -314,7 +294,7 @@ async function installPluginDirectoryIntoExtensions(params: {
|
||||
});
|
||||
}
|
||||
|
||||
const installRes = await installPackageDir({
|
||||
const installRes = await runtime.installPackageDir({
|
||||
sourceDir: params.sourceDir,
|
||||
targetDir,
|
||||
mode: params.mode,
|
||||
@@ -363,13 +343,17 @@ async function installBundleFromSourceDir(
|
||||
sourceDir: string;
|
||||
} & PackageInstallCommonParams,
|
||||
): Promise<InstallPluginResult | null> {
|
||||
const bundleFormat = detectBundleManifestFormat(params.sourceDir);
|
||||
const runtime = await loadPluginInstallRuntime();
|
||||
const bundleFormat = runtime.detectBundleManifestFormat(params.sourceDir);
|
||||
if (!bundleFormat) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { logger, timeoutMs, mode, dryRun } = resolveTimedInstallModeOptions(params, defaultLogger);
|
||||
const manifestRes = loadBundleManifest({
|
||||
const { logger, timeoutMs, mode, dryRun } = runtime.resolveTimedInstallModeOptions(
|
||||
params,
|
||||
defaultLogger,
|
||||
);
|
||||
const manifestRes = runtime.loadBundleManifest({
|
||||
rootDir: params.sourceDir,
|
||||
bundleFormat,
|
||||
rejectHardlinks: true,
|
||||
@@ -392,7 +376,7 @@ async function installBundleFromSourceDir(
|
||||
}
|
||||
|
||||
try {
|
||||
await scanBundleInstallSource({
|
||||
await runtime.scanBundleInstallSource({
|
||||
sourceDir: params.sourceDir,
|
||||
pluginId,
|
||||
logger,
|
||||
@@ -446,13 +430,14 @@ async function installPluginFromSourceDir(
|
||||
}
|
||||
|
||||
async function detectNativePackageInstallSource(packageDir: string): Promise<boolean> {
|
||||
const runtime = await loadPluginInstallRuntime();
|
||||
const manifestPath = path.join(packageDir, "package.json");
|
||||
if (!(await fileExists(manifestPath))) {
|
||||
if (!(await runtime.fileExists(manifestPath))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const manifest = await readJsonFile<PackageManifest>(manifestPath);
|
||||
const manifest = await runtime.readJsonFile<PackageManifest>(manifestPath);
|
||||
return ensureOpenClawExtensions({ manifest }).ok;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -464,16 +449,20 @@ async function installPluginFromPackageDir(
|
||||
packageDir: string;
|
||||
} & PackageInstallCommonParams,
|
||||
): Promise<InstallPluginResult> {
|
||||
const { logger, timeoutMs, mode, dryRun } = resolveTimedInstallModeOptions(params, defaultLogger);
|
||||
const runtime = await loadPluginInstallRuntime();
|
||||
const { logger, timeoutMs, mode, dryRun } = runtime.resolveTimedInstallModeOptions(
|
||||
params,
|
||||
defaultLogger,
|
||||
);
|
||||
|
||||
const manifestPath = path.join(params.packageDir, "package.json");
|
||||
if (!(await fileExists(manifestPath))) {
|
||||
if (!(await runtime.fileExists(manifestPath))) {
|
||||
return { ok: false, error: "extracted package missing package.json" };
|
||||
}
|
||||
|
||||
let manifest: PackageManifest;
|
||||
try {
|
||||
manifest = await readJsonFile<PackageManifest>(manifestPath);
|
||||
manifest = await runtime.readJsonFile<PackageManifest>(manifestPath);
|
||||
} catch (err) {
|
||||
return { ok: false, error: `invalid package.json: ${String(err)}` };
|
||||
}
|
||||
@@ -497,7 +486,7 @@ async function installPluginFromPackageDir(
|
||||
// This avoids a latent key-mismatch bug: if the manifest id (e.g. "memory-cognee")
|
||||
// differs from the npm package name (e.g. "cognee-openclaw"), the plugin registry
|
||||
// uses the manifest id as the authoritative key, so the config entry must match it.
|
||||
const ocManifestResult = loadPluginManifest(params.packageDir);
|
||||
const ocManifestResult = runtime.loadPluginManifest(params.packageDir);
|
||||
const manifestPluginId =
|
||||
ocManifestResult.ok && ocManifestResult.manifest.id
|
||||
? ocManifestResult.manifest.id.trim()
|
||||
@@ -529,9 +518,9 @@ async function installPluginFromPackageDir(
|
||||
);
|
||||
}
|
||||
|
||||
const packageMetadata = getPackageManifestMetadata(manifest);
|
||||
const minHostVersionCheck = checkMinHostVersion({
|
||||
currentVersion: resolveRuntimeServiceVersion(),
|
||||
const packageMetadata = runtime.getPackageManifestMetadata(manifest);
|
||||
const minHostVersionCheck = runtime.checkMinHostVersion({
|
||||
currentVersion: runtime.resolveRuntimeServiceVersion(),
|
||||
minHostVersion: packageMetadata?.install?.minHostVersion,
|
||||
});
|
||||
if (!minHostVersionCheck.ok) {
|
||||
@@ -556,7 +545,7 @@ async function installPluginFromPackageDir(
|
||||
};
|
||||
}
|
||||
try {
|
||||
await scanPackageInstallSource({
|
||||
await runtime.scanPackageInstallSource({
|
||||
packageDir: params.packageDir,
|
||||
pluginId,
|
||||
logger,
|
||||
@@ -587,11 +576,11 @@ async function installPluginFromPackageDir(
|
||||
afterCopy: async (installedDir) => {
|
||||
for (const entry of extensions) {
|
||||
const resolvedEntry = path.resolve(installedDir, entry);
|
||||
if (!isPathInside(installedDir, resolvedEntry)) {
|
||||
if (!runtime.isPathInside(installedDir, resolvedEntry)) {
|
||||
logger.warn?.(`extension entry escapes plugin directory: ${entry}`);
|
||||
continue;
|
||||
}
|
||||
if (!(await fileExists(resolvedEntry))) {
|
||||
if (!(await runtime.fileExists(resolvedEntry))) {
|
||||
logger.warn?.(`extension entry not found: ${entry}`);
|
||||
}
|
||||
}
|
||||
@@ -604,16 +593,17 @@ export async function installPluginFromArchive(
|
||||
archivePath: string;
|
||||
} & PackageInstallCommonParams,
|
||||
): Promise<InstallPluginResult> {
|
||||
const runtime = await loadPluginInstallRuntime();
|
||||
const logger = params.logger ?? defaultLogger;
|
||||
const timeoutMs = params.timeoutMs ?? 120_000;
|
||||
const mode = params.mode ?? "install";
|
||||
const archivePathResult = await resolveArchiveSourcePath(params.archivePath);
|
||||
const archivePathResult = await runtime.resolveArchiveSourcePath(params.archivePath);
|
||||
if (!archivePathResult.ok) {
|
||||
return archivePathResult;
|
||||
}
|
||||
const archivePath = archivePathResult.path;
|
||||
|
||||
return await withExtractedArchiveRoot({
|
||||
return await runtime.withExtractedArchiveRoot({
|
||||
archivePath,
|
||||
tempDirPrefix: "openclaw-plugin-",
|
||||
timeoutMs,
|
||||
@@ -639,8 +629,9 @@ export async function installPluginFromDir(
|
||||
dirPath: string;
|
||||
} & PackageInstallCommonParams,
|
||||
): Promise<InstallPluginResult> {
|
||||
const runtime = await loadPluginInstallRuntime();
|
||||
const dirPath = resolveUserPath(params.dirPath);
|
||||
if (!(await fileExists(dirPath))) {
|
||||
if (!(await runtime.fileExists(dirPath))) {
|
||||
return { ok: false, error: `directory not found: ${dirPath}` };
|
||||
}
|
||||
const stat = await fs.stat(dirPath);
|
||||
@@ -661,10 +652,11 @@ export async function installPluginFromFile(params: {
|
||||
mode?: "install" | "update";
|
||||
dryRun?: boolean;
|
||||
}): Promise<InstallPluginResult> {
|
||||
const { logger, mode, dryRun } = resolveInstallModeOptions(params, defaultLogger);
|
||||
const runtime = await loadPluginInstallRuntime();
|
||||
const { logger, mode, dryRun } = runtime.resolveInstallModeOptions(params, defaultLogger);
|
||||
|
||||
const filePath = resolveUserPath(params.filePath);
|
||||
if (!(await fileExists(filePath))) {
|
||||
if (!(await runtime.fileExists(filePath))) {
|
||||
return { ok: false, error: `file not found: ${filePath}` };
|
||||
}
|
||||
|
||||
@@ -681,7 +673,7 @@ export async function installPluginFromFile(params: {
|
||||
}
|
||||
const targetFile = path.join(extensionsDir, `${safeFileName(pluginId)}${path.extname(filePath)}`);
|
||||
|
||||
const availability = await ensureInstallTargetAvailable({
|
||||
const availability = await runtime.ensureInstallTargetAvailable({
|
||||
mode,
|
||||
targetDir: targetFile,
|
||||
alreadyExistsError: `plugin already exists: ${targetFile} (delete it first)`,
|
||||
@@ -696,7 +688,7 @@ export async function installPluginFromFile(params: {
|
||||
|
||||
logger.info?.(`Installing to ${targetFile}…`);
|
||||
try {
|
||||
await writeFileFromPathWithinRoot({
|
||||
await runtime.writeFileFromPathWithinRoot({
|
||||
rootDir: extensionsDir,
|
||||
relativePath: path.basename(targetFile),
|
||||
sourcePath: filePath,
|
||||
@@ -719,10 +711,14 @@ export async function installPluginFromNpmSpec(params: {
|
||||
expectedIntegrity?: string;
|
||||
onIntegrityDrift?: (params: PluginNpmIntegrityDriftParams) => boolean | Promise<boolean>;
|
||||
}): Promise<InstallPluginResult> {
|
||||
const { logger, timeoutMs, mode, dryRun } = resolveTimedInstallModeOptions(params, defaultLogger);
|
||||
const runtime = await loadPluginInstallRuntime();
|
||||
const { logger, timeoutMs, mode, dryRun } = runtime.resolveTimedInstallModeOptions(
|
||||
params,
|
||||
defaultLogger,
|
||||
);
|
||||
const expectedPluginId = params.expectedPluginId;
|
||||
const spec = params.spec.trim();
|
||||
const specError = validateRegistryNpmSpec(spec);
|
||||
const specError = runtime.validateRegistryNpmSpec(spec);
|
||||
if (specError) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -732,7 +728,7 @@ export async function installPluginFromNpmSpec(params: {
|
||||
}
|
||||
|
||||
logger.info?.(`Downloading ${spec}…`);
|
||||
const flowResult = await installFromNpmSpecArchiveWithInstaller({
|
||||
const flowResult = await runtime.installFromNpmSpecArchiveWithInstaller({
|
||||
tempDirPrefix: "openclaw-npm-pack-",
|
||||
spec,
|
||||
timeoutMs,
|
||||
@@ -751,7 +747,7 @@ export async function installPluginFromNpmSpec(params: {
|
||||
expectedPluginId,
|
||||
},
|
||||
});
|
||||
const finalized = finalizeNpmSpecArchiveInstall(flowResult);
|
||||
const finalized = runtime.finalizeNpmSpecArchiveInstall(flowResult);
|
||||
if (!finalized.ok && isNpmPackageNotFoundMessage(finalized.error)) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -767,7 +763,8 @@ export async function installPluginFromPath(
|
||||
path: string;
|
||||
} & PackageInstallCommonParams,
|
||||
): Promise<InstallPluginResult> {
|
||||
const pathResult = await resolveExistingInstallPath(params.path);
|
||||
const runtime = await loadPluginInstallRuntime();
|
||||
const pathResult = await runtime.resolveExistingInstallPath(params.path);
|
||||
if (!pathResult.ok) {
|
||||
return pathResult;
|
||||
}
|
||||
@@ -781,7 +778,7 @@ export async function installPluginFromPath(
|
||||
});
|
||||
}
|
||||
|
||||
const archiveKind = resolveArchiveKind(resolved);
|
||||
const archiveKind = runtime.resolveArchiveKind(resolved);
|
||||
if (archiveKind) {
|
||||
return await installPluginFromArchive({
|
||||
archivePath: resolved,
|
||||
|
||||
Reference in New Issue
Block a user