import { loadConfig, writeConfigFile } from "../config/config.js"; import type { HookInstallRecord } from "../config/types.hooks.js"; import type { PluginInstallRecord } from "../config/types.plugins.js"; import { updateNpmInstalledHookPacks } from "../hooks/update.js"; import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js"; import { updateNpmInstalledPlugins } from "../plugins/update.js"; import { defaultRuntime } from "../runtime.js"; import { theme } from "../terminal/theme.js"; import { extractInstalledNpmHookPackageName, extractInstalledNpmPackageName, } from "./plugins-command-helpers.js"; import { promptYesNo } from "./prompt.js"; function resolvePluginUpdateSelection(params: { installs: Record; rawId?: string; all?: boolean; }): { pluginIds: string[]; specOverrides?: Record } { if (params.all) { return { pluginIds: Object.keys(params.installs) }; } if (!params.rawId) { return { pluginIds: [] }; } const parsedSpec = parseRegistryNpmSpec(params.rawId); if (!parsedSpec || parsedSpec.selectorKind === "none") { return { pluginIds: [params.rawId] }; } const matches = Object.entries(params.installs).filter(([, install]) => { return extractInstalledNpmPackageName(install) === parsedSpec.name; }); if (matches.length !== 1) { return { pluginIds: [params.rawId] }; } const [pluginId] = matches[0]; if (!pluginId) { return { pluginIds: [params.rawId] }; } return { pluginIds: [pluginId], specOverrides: { [pluginId]: parsedSpec.raw, }, }; } function resolveHookPackUpdateSelection(params: { installs: Record; rawId?: string; all?: boolean; }): { hookIds: string[]; specOverrides?: Record } { if (params.all) { return { hookIds: Object.keys(params.installs) }; } if (!params.rawId) { return { hookIds: [] }; } if (params.rawId in params.installs) { return { hookIds: [params.rawId] }; } const parsedSpec = parseRegistryNpmSpec(params.rawId); if (!parsedSpec || parsedSpec.selectorKind === "none") { return { hookIds: [] }; } const matches = Object.entries(params.installs).filter(([, install]) => { return extractInstalledNpmHookPackageName(install) === parsedSpec.name; }); if (matches.length !== 1) { return { hookIds: [] }; } const [hookId] = matches[0]; if (!hookId) { return { hookIds: [] }; } return { hookIds: [hookId], specOverrides: { [hookId]: parsedSpec.raw, }, }; } export async function runPluginUpdateCommand(params: { id?: string; opts: { all?: boolean; dryRun?: boolean }; }) { const cfg = loadConfig(); const logger = { info: (msg: string) => defaultRuntime.log(msg), warn: (msg: string) => defaultRuntime.log(theme.warn(msg)), }; const pluginSelection = resolvePluginUpdateSelection({ installs: cfg.plugins?.installs ?? {}, 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: cfg, pluginIds: pluginSelection.pluginIds, specOverrides: pluginSelection.specOverrides, dryRun: params.opts.dryRun, 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)) { await writeConfigFile(hookResult.config); defaultRuntime.log("Restart the gateway to load plugins and hooks."); } }