mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-24 00:11:31 +00:00
perf: lazy-load hook install runtime helpers
This commit is contained in:
41
src/hooks/install.runtime.ts
Normal file
41
src/hooks/install.runtime.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { fileExists, readJsonFile, resolveArchiveKind } from "../infra/archive.js";
|
||||
import { resolveExistingInstallPath, withExtractedArchiveRoot } from "../infra/install-flow.js";
|
||||
import { installFromValidatedNpmSpecArchive } from "../infra/install-from-npm-spec.js";
|
||||
import {
|
||||
resolveInstallModeOptions,
|
||||
resolveTimedInstallModeOptions,
|
||||
} from "../infra/install-mode-options.js";
|
||||
import {
|
||||
installPackageDir,
|
||||
installPackageDirWithManifestDeps,
|
||||
} 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 { isPathInside, isPathInsideWithRealpath } from "../security/scan-paths.js";
|
||||
|
||||
export type { NpmIntegrityDrift, NpmSpecResolution };
|
||||
|
||||
export {
|
||||
ensureInstallTargetAvailable,
|
||||
fileExists,
|
||||
installFromValidatedNpmSpecArchive,
|
||||
installPackageDir,
|
||||
installPackageDirWithManifestDeps,
|
||||
isPathInside,
|
||||
isPathInsideWithRealpath,
|
||||
readJsonFile,
|
||||
resolveArchiveKind,
|
||||
resolveArchiveSourcePath,
|
||||
resolveCanonicalInstallTarget,
|
||||
resolveExistingInstallPath,
|
||||
resolveInstallModeOptions,
|
||||
resolveTimedInstallModeOptions,
|
||||
withExtractedArchiveRoot,
|
||||
};
|
||||
@@ -1,31 +1,18 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { MANIFEST_KEY } from "../compat/legacy-names.js";
|
||||
import { fileExists, readJsonFile, resolveArchiveKind } from "../infra/archive.js";
|
||||
import { resolveExistingInstallPath, withExtractedArchiveRoot } from "../infra/install-flow.js";
|
||||
import { installFromValidatedNpmSpecArchive } from "../infra/install-from-npm-spec.js";
|
||||
import {
|
||||
resolveInstallModeOptions,
|
||||
resolveTimedInstallModeOptions,
|
||||
} from "../infra/install-mode-options.js";
|
||||
import {
|
||||
installPackageDir,
|
||||
installPackageDirWithManifestDeps,
|
||||
} from "../infra/install-package-dir.js";
|
||||
import { resolveSafeInstallDir, 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 { isPathInside, isPathInsideWithRealpath } from "../security/scan-paths.js";
|
||||
import { type NpmIntegrityDrift, type NpmSpecResolution } from "../infra/install-source-utils.js";
|
||||
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
||||
import { parseFrontmatter } from "./frontmatter.js";
|
||||
|
||||
let hookInstallRuntimePromise: Promise<typeof import("./install.runtime.js")> | undefined;
|
||||
|
||||
async function loadHookInstallRuntime() {
|
||||
hookInstallRuntimePromise ??= import("./install.runtime.js");
|
||||
return hookInstallRuntimePromise;
|
||||
}
|
||||
|
||||
export type HookInstallLogger = {
|
||||
info?: (message: string) => void;
|
||||
warn?: (message: string) => void;
|
||||
@@ -128,8 +115,9 @@ async function resolveInstallTargetDir(
|
||||
id: string,
|
||||
hooksDir?: string,
|
||||
): Promise<{ ok: true; targetDir: string } | { ok: false; error: string }> {
|
||||
const runtime = await loadHookInstallRuntime();
|
||||
const baseHooksDir = hooksDir ? resolveUserPath(hooksDir) : path.join(CONFIG_DIR, "hooks");
|
||||
return await resolveCanonicalInstallTarget({
|
||||
return await runtime.resolveCanonicalInstallTarget({
|
||||
baseDir: baseHooksDir,
|
||||
id,
|
||||
invalidNameMessage: "invalid hook name: path traversal detected",
|
||||
@@ -143,12 +131,13 @@ async function resolveAvailableHookInstallTarget(params: {
|
||||
mode: "install" | "update";
|
||||
alreadyExistsError: (targetDir: string) => string;
|
||||
}): Promise<{ ok: true; targetDir: string } | { ok: false; error: string }> {
|
||||
const runtime = await loadHookInstallRuntime();
|
||||
const targetDirResult = await resolveInstallTargetDir(params.id, params.hooksDir);
|
||||
if (!targetDirResult.ok) {
|
||||
return targetDirResult;
|
||||
}
|
||||
const targetDir = targetDirResult.targetDir;
|
||||
const availability = await ensureInstallTargetAvailable({
|
||||
const availability = await runtime.ensureInstallTargetAvailable({
|
||||
mode: params.mode,
|
||||
targetDir,
|
||||
alreadyExistsError: params.alreadyExistsError(targetDir),
|
||||
@@ -163,8 +152,9 @@ async function installFromResolvedHookDir(
|
||||
resolvedDir: string,
|
||||
params: HookInstallForwardParams,
|
||||
): Promise<InstallHooksResult> {
|
||||
const runtime = await loadHookInstallRuntime();
|
||||
const manifestPath = path.join(resolvedDir, "package.json");
|
||||
if (await fileExists(manifestPath)) {
|
||||
if (await runtime.fileExists(manifestPath)) {
|
||||
return await installHookPackageFromDir({
|
||||
packageDir: resolvedDir,
|
||||
hooksDir: params.hooksDir,
|
||||
@@ -186,8 +176,9 @@ async function installFromResolvedHookDir(
|
||||
}
|
||||
|
||||
async function resolveHookNameFromDir(hookDir: string): Promise<string> {
|
||||
const runtime = await loadHookInstallRuntime();
|
||||
const hookMdPath = path.join(hookDir, "HOOK.md");
|
||||
if (!(await fileExists(hookMdPath))) {
|
||||
if (!(await runtime.fileExists(hookMdPath))) {
|
||||
throw new Error(`HOOK.md missing in ${hookDir}`);
|
||||
}
|
||||
const raw = await fs.readFile(hookMdPath, "utf-8");
|
||||
@@ -196,14 +187,15 @@ async function resolveHookNameFromDir(hookDir: string): Promise<string> {
|
||||
}
|
||||
|
||||
async function validateHookDir(hookDir: string): Promise<void> {
|
||||
const runtime = await loadHookInstallRuntime();
|
||||
const hookMdPath = path.join(hookDir, "HOOK.md");
|
||||
if (!(await fileExists(hookMdPath))) {
|
||||
if (!(await runtime.fileExists(hookMdPath))) {
|
||||
throw new Error(`HOOK.md missing in ${hookDir}`);
|
||||
}
|
||||
|
||||
const handlerCandidates = ["handler.ts", "handler.js", "index.ts", "index.js"];
|
||||
const hasHandler = await Promise.all(
|
||||
handlerCandidates.map(async (candidate) => fileExists(path.join(hookDir, candidate))),
|
||||
handlerCandidates.map(async (candidate) => runtime.fileExists(path.join(hookDir, candidate))),
|
||||
).then((results) => results.some(Boolean));
|
||||
|
||||
if (!hasHandler) {
|
||||
@@ -214,16 +206,20 @@ async function validateHookDir(hookDir: string): Promise<void> {
|
||||
async function installHookPackageFromDir(
|
||||
params: HookPackageInstallParams,
|
||||
): Promise<InstallHooksResult> {
|
||||
const { logger, timeoutMs, mode, dryRun } = resolveTimedInstallModeOptions(params, defaultLogger);
|
||||
const runtime = await loadHookInstallRuntime();
|
||||
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: "package.json missing" };
|
||||
}
|
||||
|
||||
let manifest: HookPackageManifest;
|
||||
try {
|
||||
manifest = await readJsonFile<HookPackageManifest>(manifestPath);
|
||||
manifest = await runtime.readJsonFile<HookPackageManifest>(manifestPath);
|
||||
} catch (err) {
|
||||
return { ok: false, error: `invalid package.json: ${String(err)}` };
|
||||
}
|
||||
@@ -262,7 +258,7 @@ async function installHookPackageFromDir(
|
||||
const resolvedHooks = [] as string[];
|
||||
for (const entry of hookEntries) {
|
||||
const hookDir = path.resolve(params.packageDir, entry);
|
||||
if (!isPathInside(params.packageDir, hookDir)) {
|
||||
if (!runtime.isPathInside(params.packageDir, hookDir)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `openclaw.hooks entry escapes package directory: ${entry}`,
|
||||
@@ -270,7 +266,7 @@ async function installHookPackageFromDir(
|
||||
}
|
||||
await validateHookDir(hookDir);
|
||||
if (
|
||||
!isPathInsideWithRealpath(params.packageDir, hookDir, {
|
||||
!runtime.isPathInsideWithRealpath(params.packageDir, hookDir, {
|
||||
requireRealpath: true,
|
||||
})
|
||||
) {
|
||||
@@ -293,7 +289,7 @@ async function installHookPackageFromDir(
|
||||
};
|
||||
}
|
||||
|
||||
const installRes = await installPackageDirWithManifestDeps({
|
||||
const installRes = await runtime.installPackageDirWithManifestDeps({
|
||||
sourceDir: params.packageDir,
|
||||
targetDir,
|
||||
mode,
|
||||
@@ -324,7 +320,8 @@ async function installHookFromDir(params: {
|
||||
dryRun?: boolean;
|
||||
expectedHookPackId?: string;
|
||||
}): Promise<InstallHooksResult> {
|
||||
const { logger, mode, dryRun } = resolveInstallModeOptions(params, defaultLogger);
|
||||
const runtime = await loadHookInstallRuntime();
|
||||
const { logger, mode, dryRun } = runtime.resolveInstallModeOptions(params, defaultLogger);
|
||||
|
||||
await validateHookDir(params.hookDir);
|
||||
const hookName = await resolveHookNameFromDir(params.hookDir);
|
||||
@@ -355,7 +352,7 @@ async function installHookFromDir(params: {
|
||||
return { ok: true, hookPackId: hookName, hooks: [hookName], targetDir };
|
||||
}
|
||||
|
||||
const installRes = await installPackageDir({
|
||||
const installRes = await runtime.installPackageDir({
|
||||
sourceDir: params.hookDir,
|
||||
targetDir,
|
||||
mode,
|
||||
@@ -375,15 +372,16 @@ async function installHookFromDir(params: {
|
||||
export async function installHooksFromArchive(
|
||||
params: HookArchiveInstallParams,
|
||||
): Promise<InstallHooksResult> {
|
||||
const runtime = await loadHookInstallRuntime();
|
||||
const logger = params.logger ?? defaultLogger;
|
||||
const timeoutMs = params.timeoutMs ?? 120_000;
|
||||
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-hook-",
|
||||
timeoutMs,
|
||||
@@ -414,12 +412,16 @@ export async function installHooksFromNpmSpec(params: {
|
||||
expectedIntegrity?: string;
|
||||
onIntegrityDrift?: (params: HookNpmIntegrityDriftParams) => boolean | Promise<boolean>;
|
||||
}): Promise<InstallHooksResult> {
|
||||
const { logger, timeoutMs, mode, dryRun } = resolveTimedInstallModeOptions(params, defaultLogger);
|
||||
const runtime = await loadHookInstallRuntime();
|
||||
const { logger, timeoutMs, mode, dryRun } = runtime.resolveTimedInstallModeOptions(
|
||||
params,
|
||||
defaultLogger,
|
||||
);
|
||||
const expectedHookPackId = params.expectedHookPackId;
|
||||
const spec = params.spec;
|
||||
|
||||
logger.info?.(`Downloading ${spec.trim()}…`);
|
||||
return await installFromValidatedNpmSpecArchive({
|
||||
return await runtime.installFromValidatedNpmSpecArchive({
|
||||
tempDirPrefix: "openclaw-hook-pack-",
|
||||
spec,
|
||||
timeoutMs,
|
||||
@@ -443,7 +445,8 @@ export async function installHooksFromNpmSpec(params: {
|
||||
export async function installHooksFromPath(
|
||||
params: HookPathInstallParams,
|
||||
): Promise<InstallHooksResult> {
|
||||
const pathResult = await resolveExistingInstallPath(params.path);
|
||||
const runtime = await loadHookInstallRuntime();
|
||||
const pathResult = await runtime.resolveExistingInstallPath(params.path);
|
||||
if (!pathResult.ok) {
|
||||
return pathResult;
|
||||
}
|
||||
@@ -461,7 +464,7 @@ export async function installHooksFromPath(
|
||||
return await installFromResolvedHookDir(resolved, forwardParams);
|
||||
}
|
||||
|
||||
if (!resolveArchiveKind(resolved)) {
|
||||
if (!runtime.resolveArchiveKind(resolved)) {
|
||||
return { ok: false, error: `unsupported hook file: ${resolved}` };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user