perf: lazy-load hook install runtime helpers

This commit is contained in:
Peter Steinberger 2026-03-22 21:46:38 +00:00
parent 5d379f92a3
commit f075e2eebd
2 changed files with 85 additions and 41 deletions

View 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,
};

View File

@ -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}` };
}