feat(cli): add plugins uninstall command
Add 'clawdbot plugins uninstall' command to properly remove installed plugins. Features: - Removes plugin install directory (npm/archive installs) - Preserves linked paths (doesn't delete source for --link installs) - Removes from plugins.installs config record - Removes from plugins.load.paths (for linked plugins) - Removes from plugins.entries (disables plugin) - --keep-config flag to preserve plugin settings while removing install This addresses the gap where users had to manually rm extension directories and edit config files to uninstall plugins. Related: Previously there was no programmatic way to uninstall plugins
This commit is contained in:
parent
20f6a5546f
commit
b771a24398
@ -445,6 +445,94 @@ export function registerPluginsCli(program: Command) {
|
||||
defaultRuntime.log(`Restart the gateway to load plugins.`);
|
||||
});
|
||||
|
||||
plugins
|
||||
.command("uninstall")
|
||||
.description("Uninstall a plugin")
|
||||
.argument("<id>", "Plugin id")
|
||||
.option("--keep-config", "Keep plugin config in entries (only remove install)", false)
|
||||
.action(async (id: string, opts: { keepConfig?: boolean }) => {
|
||||
const cfg = loadConfig();
|
||||
const report = buildPluginStatusReport({ config: cfg });
|
||||
const plugin = report.plugins.find((p) => p.id === id || p.name === id);
|
||||
|
||||
if (!plugin) {
|
||||
defaultRuntime.error(`Plugin not found: ${id}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const install = cfg.plugins?.installs?.[plugin.id];
|
||||
if (!install) {
|
||||
defaultRuntime.error(
|
||||
`No install record for plugin "${plugin.id}". It may have been manually installed.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Remove the install directory (but not if it's a linked path)
|
||||
if (install.installPath && install.source !== "path") {
|
||||
try {
|
||||
if (fs.existsSync(install.installPath)) {
|
||||
await fs.promises.rm(install.installPath, { recursive: true, force: true });
|
||||
defaultRuntime.log(`Removed: ${shortenHomePath(install.installPath)}`);
|
||||
}
|
||||
} catch (err) {
|
||||
defaultRuntime.log(theme.warn(`Failed to remove directory: ${String(err)}`));
|
||||
}
|
||||
}
|
||||
|
||||
// Build new config
|
||||
let next = { ...cfg };
|
||||
|
||||
// Remove from installs record
|
||||
const { [plugin.id]: _removed, ...remainingInstalls } = cfg.plugins?.installs ?? {};
|
||||
next = {
|
||||
...next,
|
||||
plugins: {
|
||||
...next.plugins,
|
||||
installs: remainingInstalls,
|
||||
},
|
||||
};
|
||||
|
||||
// Remove from load paths if it was linked
|
||||
if (install.source === "path" && install.installPath) {
|
||||
const paths = cfg.plugins?.load?.paths ?? [];
|
||||
const installPath = install.installPath;
|
||||
const filtered = paths.filter((p) => path.resolve(p) !== path.resolve(installPath));
|
||||
if (filtered.length !== paths.length) {
|
||||
next = {
|
||||
...next,
|
||||
plugins: {
|
||||
...next.plugins,
|
||||
load: {
|
||||
...next.plugins?.load,
|
||||
paths: filtered,
|
||||
},
|
||||
},
|
||||
};
|
||||
defaultRuntime.log(`Removed from load paths: ${shortenHomePath(installPath)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable plugin in entries unless --keep-config
|
||||
if (!opts.keepConfig) {
|
||||
const { [plugin.id]: _removedEntry, ...remainingEntries } = next.plugins?.entries ?? {};
|
||||
next = {
|
||||
...next,
|
||||
plugins: {
|
||||
...next.plugins,
|
||||
entries: remainingEntries,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await writeConfigFile(next);
|
||||
defaultRuntime.log(`Uninstalled plugin: ${plugin.id}`);
|
||||
if (opts.keepConfig) {
|
||||
defaultRuntime.log("Plugin config preserved. Use 'plugins disable' to disable it.");
|
||||
}
|
||||
defaultRuntime.log("Restart the gateway to apply changes.");
|
||||
});
|
||||
|
||||
plugins
|
||||
.command("update")
|
||||
.description("Update installed plugins (npm installs only)")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user