Files
openclaw/src/cli/plugins-update-command.ts
2026-04-25 11:37:10 -07:00

147 lines
5.1 KiB
TypeScript

import { loadConfig, readConfigFileSnapshot, replaceConfigFile } from "../config/config.js";
import { updateNpmInstalledHookPacks } from "../hooks/update.js";
import {
loadPluginInstallRecords,
PLUGIN_INSTALLS_CONFIG_PATH,
withoutPluginInstallRecords,
writePersistedPluginInstallLedger,
withPluginInstallRecords,
} from "../plugins/install-ledger-store.js";
import { updateNpmInstalledPlugins } from "../plugins/update.js";
import { defaultRuntime } from "../runtime.js";
import { theme } from "../terminal/theme.js";
import { refreshPluginRegistryAfterConfigMutation } from "./plugins-registry-refresh.js";
import {
resolveHookPackUpdateSelection,
resolvePluginUpdateSelection,
} from "./plugins-update-selection.js";
import { promptYesNo } from "./prompt.js";
export async function runPluginUpdateCommand(params: {
id?: string;
opts: { all?: boolean; dryRun?: boolean; dangerouslyForceUnsafeInstall?: boolean };
}) {
const sourceSnapshotPromise = readConfigFileSnapshot().catch(() => null);
const cfg = loadConfig();
const pluginInstallRecords = await loadPluginInstallRecords({ config: cfg });
const cfgWithPluginInstallRecords = withPluginInstallRecords(cfg, pluginInstallRecords);
const logger = {
info: (msg: string) => defaultRuntime.log(msg),
warn: (msg: string) => defaultRuntime.log(theme.warn(msg)),
};
const pluginSelection = resolvePluginUpdateSelection({
installs: pluginInstallRecords,
rawId: params.id,
all: params.opts.all,
});
const hookSelection = resolveHookPackUpdateSelection({
installs: cfg.hooks?.internal?.installs ?? {},
rawId: params.id,
all: params.opts.all,
});
if (pluginSelection.pluginIds.length === 0 && hookSelection.hookIds.length === 0) {
if (params.opts.all) {
defaultRuntime.log("No tracked plugins or hook packs to update.");
return;
}
defaultRuntime.error("Provide a plugin or hook-pack id, or use --all.");
return defaultRuntime.exit(1);
}
const pluginResult = await updateNpmInstalledPlugins({
config: cfgWithPluginInstallRecords,
pluginIds: pluginSelection.pluginIds,
specOverrides: pluginSelection.specOverrides,
dryRun: params.opts.dryRun,
dangerouslyForceUnsafeInstall: params.opts.dangerouslyForceUnsafeInstall,
logger,
onIntegrityDrift: async (drift) => {
const specLabel = drift.resolvedSpec ?? drift.spec;
defaultRuntime.log(
theme.warn(
`Integrity drift detected for "${drift.pluginId}" (${specLabel})` +
`\nExpected: ${drift.expectedIntegrity}` +
`\nActual: ${drift.actualIntegrity}`,
),
);
if (drift.dryRun) {
return true;
}
return await promptYesNo(`Continue updating "${drift.pluginId}" with this artifact?`);
},
});
const hookResult = await updateNpmInstalledHookPacks({
config: pluginResult.config,
hookIds: hookSelection.hookIds,
specOverrides: hookSelection.specOverrides,
dryRun: params.opts.dryRun,
logger,
onIntegrityDrift: async (drift) => {
const specLabel = drift.resolvedSpec ?? drift.spec;
defaultRuntime.log(
theme.warn(
`Integrity drift detected for hook pack "${drift.hookId}" (${specLabel})` +
`\nExpected: ${drift.expectedIntegrity}` +
`\nActual: ${drift.actualIntegrity}`,
),
);
if (drift.dryRun) {
return true;
}
return await promptYesNo(`Continue updating hook pack "${drift.hookId}" with this artifact?`);
},
});
for (const outcome of pluginResult.outcomes) {
if (outcome.status === "error") {
defaultRuntime.log(theme.error(outcome.message));
continue;
}
if (outcome.status === "skipped") {
defaultRuntime.log(theme.warn(outcome.message));
continue;
}
defaultRuntime.log(outcome.message);
}
for (const outcome of hookResult.outcomes) {
if (outcome.status === "error") {
defaultRuntime.log(theme.error(outcome.message));
continue;
}
if (outcome.status === "skipped") {
defaultRuntime.log(theme.warn(outcome.message));
continue;
}
defaultRuntime.log(outcome.message);
}
if (!params.opts.dryRun && (pluginResult.changed || hookResult.changed)) {
const nextPluginInstallRecords = pluginResult.config.plugins?.installs ?? {};
const shouldPersistPluginInstallLedger =
pluginResult.changed || Object.keys(pluginInstallRecords).length > 0;
if (shouldPersistPluginInstallLedger) {
await writePersistedPluginInstallLedger(nextPluginInstallRecords);
}
const nextConfig = shouldPersistPluginInstallLedger
? withoutPluginInstallRecords(hookResult.config)
: hookResult.config;
await replaceConfigFile({
nextConfig,
baseHash: (await sourceSnapshotPromise)?.hash,
...(shouldPersistPluginInstallLedger
? { writeOptions: { unsetPaths: [Array.from(PLUGIN_INSTALLS_CONFIG_PATH)] } }
: {}),
});
if (pluginResult.changed) {
await refreshPluginRegistryAfterConfigMutation({
config: nextConfig,
reason: "source-changed",
logger,
});
}
defaultRuntime.log("Restart the gateway to load plugins and hooks.");
}
}