mirror of https://github.com/openclaw/openclaw.git
199 lines
5.9 KiB
TypeScript
199 lines
5.9 KiB
TypeScript
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<boolean>;
|
|
}) {
|
|
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<string, string>;
|
|
onIntegrityDrift?: (params: HookPackUpdateIntegrityDriftParams) => boolean | Promise<boolean>;
|
|
}): Promise<HookPackUpdateSummary> {
|
|
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 };
|
|
}
|