From f075e2eebdf3bf9bf60167830cb5b065c4d38b8e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Mar 2026 21:46:38 +0000 Subject: [PATCH] perf: lazy-load hook install runtime helpers --- src/hooks/install.runtime.ts | 41 +++++++++++++++++ src/hooks/install.ts | 85 +++++++++++++++++++----------------- 2 files changed, 85 insertions(+), 41 deletions(-) create mode 100644 src/hooks/install.runtime.ts diff --git a/src/hooks/install.runtime.ts b/src/hooks/install.runtime.ts new file mode 100644 index 00000000000..406f859fed5 --- /dev/null +++ b/src/hooks/install.runtime.ts @@ -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, +}; diff --git a/src/hooks/install.ts b/src/hooks/install.ts index 87aed5b0c23..586d26ed168 100644 --- a/src/hooks/install.ts +++ b/src/hooks/install.ts @@ -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 | 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 { + 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 { + 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 { } async function validateHookDir(hookDir: string): Promise { + 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 { async function installHookPackageFromDir( params: HookPackageInstallParams, ): Promise { - 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(manifestPath); + manifest = await runtime.readJsonFile(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 { - 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 { + 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; }): Promise { - 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 { - 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}` }; }