feat: add plugin update tracking

This commit is contained in:
Peter Steinberger
2026-01-16 05:54:47 +00:00
parent d0c70178e0
commit 54ec14262b
12 changed files with 370 additions and 7 deletions

View File

@@ -1,10 +1,17 @@
import fs from "node:fs";
import fsp from "node:fs/promises";
import path from "node:path";
import chalk from "chalk";
import type { Command } from "commander";
import { loadConfig, writeConfigFile } from "../config/config.js";
import { installPluginFromArchive, installPluginFromNpmSpec } from "../plugins/install.js";
import type { ClawdbotConfig } from "../config/config.js";
import {
installPluginFromArchive,
installPluginFromNpmSpec,
resolvePluginInstallDir,
} from "../plugins/install.js";
import { recordPluginInstall } from "../plugins/installs.js";
import type { PluginRecord } from "../plugins/registry.js";
import { buildPluginStatusReport } from "../plugins/status.js";
import { defaultRuntime } from "../runtime.js";
@@ -22,6 +29,11 @@ export type PluginInfoOptions = {
json?: boolean;
};
export type PluginUpdateOptions = {
all?: boolean;
dryRun?: boolean;
};
function formatPluginLine(plugin: PluginRecord, verbose = false): string {
const status =
plugin.status === "loaded"
@@ -56,6 +68,16 @@ function formatPluginLine(plugin: PluginRecord, verbose = false): string {
return parts.join("\n");
}
async function readInstalledPackageVersion(dir: string): Promise<string | undefined> {
try {
const raw = await fsp.readFile(path.join(dir, "package.json"), "utf-8");
const parsed = JSON.parse(raw) as { version?: unknown };
return typeof parsed.version === "string" ? parsed.version : undefined;
} catch {
return undefined;
}
}
export function registerPluginsCli(program: Command) {
const plugins = program
.command("plugins")
@@ -118,6 +140,8 @@ export function registerPluginsCli(program: Command) {
defaultRuntime.error(`Plugin not found: ${id}`);
process.exit(1);
}
const cfg = loadConfig();
const install = cfg.plugins?.installs?.[plugin.id];
if (opts.json) {
defaultRuntime.log(JSON.stringify(plugin, null, 2));
@@ -151,6 +175,15 @@ export function registerPluginsCli(program: Command) {
lines.push(`Services: ${plugin.services.join(", ")}`);
}
if (plugin.error) lines.push(chalk.red(`Error: ${plugin.error}`));
if (install) {
lines.push("");
lines.push(`Install: ${install.source}`);
if (install.spec) lines.push(`Spec: ${install.spec}`);
if (install.sourcePath) lines.push(`Source path: ${install.sourcePath}`);
if (install.installPath) lines.push(`Install path: ${install.installPath}`);
if (install.version) lines.push(`Recorded version: ${install.version}`);
if (install.installedAt) lines.push(`Installed at: ${install.installedAt}`);
}
defaultRuntime.log(lines.join("\n"));
});
@@ -223,7 +256,7 @@ export function registerPluginsCli(program: Command) {
process.exit(1);
}
const next = {
let next: ClawdbotConfig = {
...cfg,
plugins: {
...cfg.plugins,
@@ -236,6 +269,13 @@ export function registerPluginsCli(program: Command) {
},
},
};
next = recordPluginInstall(next, {
pluginId: result.pluginId,
source: "archive",
sourcePath: resolved,
installPath: result.targetDir,
version: result.version,
});
await writeConfigFile(next);
defaultRuntime.log(`Installed plugin: ${result.pluginId}`);
defaultRuntime.log(`Restart the gateway to load plugins.`);
@@ -287,7 +327,7 @@ export function registerPluginsCli(program: Command) {
process.exit(1);
}
const next = {
let next: ClawdbotConfig = {
...cfg,
plugins: {
...cfg.plugins,
@@ -300,11 +340,124 @@ export function registerPluginsCli(program: Command) {
},
},
};
next = recordPluginInstall(next, {
pluginId: result.pluginId,
source: "npm",
spec: raw,
installPath: result.targetDir,
version: result.version,
});
await writeConfigFile(next);
defaultRuntime.log(`Installed plugin: ${result.pluginId}`);
defaultRuntime.log(`Restart the gateway to load plugins.`);
});
plugins
.command("update")
.description("Update installed plugins (npm installs only)")
.argument("[id]", "Plugin id (omit with --all)")
.option("--all", "Update all tracked plugins", false)
.option("--dry-run", "Show what would change without writing", false)
.action(async (id: string | undefined, opts: PluginUpdateOptions) => {
const cfg = loadConfig();
const installs = cfg.plugins?.installs ?? {};
const targets = opts.all ? Object.keys(installs) : id ? [id] : [];
if (targets.length === 0) {
defaultRuntime.error("Provide a plugin id or use --all.");
process.exit(1);
}
let nextCfg = cfg;
let updatedCount = 0;
for (const pluginId of targets) {
const record = installs[pluginId];
if (!record) {
defaultRuntime.log(chalk.yellow(`No install record for "${pluginId}".`));
continue;
}
if (record.source !== "npm") {
defaultRuntime.log(
chalk.yellow(`Skipping "${pluginId}" (source: ${record.source}).`),
);
continue;
}
if (!record.spec) {
defaultRuntime.log(chalk.yellow(`Skipping "${pluginId}" (missing npm spec).`));
continue;
}
const installPath = record.installPath ?? resolvePluginInstallDir(pluginId);
const currentVersion = await readInstalledPackageVersion(installPath);
if (opts.dryRun) {
const probe = await installPluginFromNpmSpec({
spec: record.spec,
mode: "update",
dryRun: true,
expectedPluginId: pluginId,
logger: {
info: (msg) => defaultRuntime.log(msg),
warn: (msg) => defaultRuntime.log(chalk.yellow(msg)),
},
});
if (!probe.ok) {
defaultRuntime.log(chalk.red(`Failed to check ${pluginId}: ${probe.error}`));
continue;
}
const nextVersion = probe.version ?? "unknown";
const currentLabel = currentVersion ?? "unknown";
if (currentVersion && probe.version && currentVersion === probe.version) {
defaultRuntime.log(`${pluginId} is up to date (${currentLabel}).`);
} else {
defaultRuntime.log(
`Would update ${pluginId}: ${currentLabel}${nextVersion}.`,
);
}
continue;
}
const result = await installPluginFromNpmSpec({
spec: record.spec,
mode: "update",
expectedPluginId: pluginId,
logger: {
info: (msg) => defaultRuntime.log(msg),
warn: (msg) => defaultRuntime.log(chalk.yellow(msg)),
},
});
if (!result.ok) {
defaultRuntime.log(chalk.red(`Failed to update ${pluginId}: ${result.error}`));
continue;
}
const nextVersion = result.version ?? (await readInstalledPackageVersion(result.targetDir));
nextCfg = recordPluginInstall(nextCfg, {
pluginId,
source: "npm",
spec: record.spec,
installPath: result.targetDir,
version: nextVersion,
});
updatedCount += 1;
const currentLabel = currentVersion ?? "unknown";
const nextLabel = nextVersion ?? "unknown";
if (currentVersion && nextVersion && currentVersion === nextVersion) {
defaultRuntime.log(`${pluginId} already at ${currentLabel}.`);
} else {
defaultRuntime.log(`Updated ${pluginId}: ${currentLabel}${nextLabel}.`);
}
}
if (updatedCount > 0) {
await writeConfigFile(nextCfg);
defaultRuntime.log("Restart the gateway to load plugins.");
}
});
plugins
.command("doctor")
.description("Report plugin load issues")