import type { OpenClawConfig } from "../config/config.js"; import { expectedIntegrityForUpdate, readInstalledPackageVersion, } from "../infra/package-update-utils.js"; import { installHooksFromNpmSpec, type HookNpmIntegrityDriftParams, resolveHookInstallDir, } from "./install.js"; import { recordHookInstall } from "./installs.js"; export type HookPackUpdateLogger = { info?: (message: string) => void; warn?: (message: string) => void; }; export type HookPackUpdateStatus = "updated" | "unchanged" | "skipped" | "error"; export type HookPackUpdateOutcome = { hookId: string; status: HookPackUpdateStatus; message: string; currentVersion?: string; nextVersion?: string; }; export type HookPackUpdateSummary = { config: OpenClawConfig; changed: boolean; outcomes: HookPackUpdateOutcome[]; }; export type HookPackUpdateIntegrityDriftParams = HookNpmIntegrityDriftParams & { hookId: string; resolvedSpec?: string; resolvedVersion?: string; dryRun: boolean; }; function createHookPackUpdateIntegrityDriftHandler(params: { hookId: string; dryRun: boolean; logger: HookPackUpdateLogger; onIntegrityDrift?: (params: HookPackUpdateIntegrityDriftParams) => boolean | Promise; }) { return async (drift: HookNpmIntegrityDriftParams) => { const payload: HookPackUpdateIntegrityDriftParams = { hookId: params.hookId, spec: drift.spec, expectedIntegrity: drift.expectedIntegrity, actualIntegrity: drift.actualIntegrity, resolution: drift.resolution, resolvedSpec: drift.resolution.resolvedSpec, resolvedVersion: drift.resolution.version, dryRun: params.dryRun, }; if (params.onIntegrityDrift) { return await params.onIntegrityDrift(payload); } params.logger.warn?.( `Integrity drift for hook pack "${params.hookId}" (${payload.resolvedSpec ?? payload.spec}): expected ${payload.expectedIntegrity}, got ${payload.actualIntegrity}`, ); return true; }; } export async function updateNpmInstalledHookPacks(params: { config: OpenClawConfig; logger?: HookPackUpdateLogger; hookIds?: string[]; dryRun?: boolean; specOverrides?: Record; onIntegrityDrift?: (params: HookPackUpdateIntegrityDriftParams) => boolean | Promise; }): Promise { const logger = params.logger ?? {}; const installs = params.config.hooks?.internal?.installs ?? {}; const targets = params.hookIds?.length ? params.hookIds : Object.keys(installs); const outcomes: HookPackUpdateOutcome[] = []; let next = params.config; let changed = false; for (const hookId of targets) { const record = installs[hookId]; if (!record) { outcomes.push({ hookId, status: "skipped", message: `No install record for hook pack "${hookId}".`, }); continue; } if (record.source !== "npm") { outcomes.push({ hookId, status: "skipped", message: `Skipping hook pack "${hookId}" (source: ${record.source}).`, }); continue; } const effectiveSpec = params.specOverrides?.[hookId] ?? record.spec; const expectedIntegrity = effectiveSpec === record.spec ? expectedIntegrityForUpdate(record.spec, record.integrity) : undefined; if (!effectiveSpec) { outcomes.push({ hookId, status: "skipped", message: `Skipping hook pack "${hookId}" (missing npm spec).`, }); continue; } let installPath: string; try { installPath = record.installPath ?? resolveHookInstallDir(hookId); } catch (err) { outcomes.push({ hookId, status: "error", message: `Invalid install path for hook pack "${hookId}": ${String(err)}`, }); continue; } const currentVersion = await readInstalledPackageVersion(installPath); const result = await installHooksFromNpmSpec({ spec: effectiveSpec, mode: "update", dryRun: params.dryRun, expectedHookPackId: hookId, expectedIntegrity, onIntegrityDrift: createHookPackUpdateIntegrityDriftHandler({ hookId, dryRun: Boolean(params.dryRun), logger, onIntegrityDrift: params.onIntegrityDrift, }), logger, }); if (!result.ok) { outcomes.push({ hookId, status: "error", message: `Failed to ${params.dryRun ? "check" : "update"} hook pack "${hookId}": ${result.error}`, }); continue; } const nextVersion = result.version ?? (await readInstalledPackageVersion(result.targetDir)); const currentLabel = currentVersion ?? "unknown"; const nextLabel = nextVersion ?? "unknown"; const status = currentVersion && nextVersion && currentVersion === nextVersion ? "unchanged" : "updated"; if (params.dryRun) { outcomes.push({ hookId, status, currentVersion: currentVersion ?? undefined, nextVersion: nextVersion ?? undefined, message: status === "unchanged" ? `Hook pack "${hookId}" is up to date (${currentLabel}).` : `Would update hook pack "${hookId}": ${currentLabel} -> ${nextLabel}.`, }); continue; } next = recordHookInstall(next, { hookId, source: "npm", spec: effectiveSpec, installPath: result.targetDir, version: nextVersion, resolvedName: result.npmResolution?.name, resolvedSpec: result.npmResolution?.resolvedSpec, integrity: result.npmResolution?.integrity, hooks: result.hooks, }); changed = true; outcomes.push({ hookId, status, currentVersion: currentVersion ?? undefined, nextVersion: nextVersion ?? undefined, message: status === "unchanged" ? `Hook pack "${hookId}" already at ${currentLabel}.` : `Updated hook pack "${hookId}": ${currentLabel} -> ${nextLabel}.`, }); } return { config: next, changed, outcomes }; }