fix(cli): improve plugins list source display

This commit is contained in:
Peter Steinberger
2026-02-09 13:05:41 -06:00
parent 33c75cb6bf
commit 3e63b2a4fa
4 changed files with 144 additions and 1 deletions

View File

@@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai
- Cron: share isolated announce flow and harden scheduling/delivery reliability. (#11641) Thanks @tyler6204.
- Cron tool: recover flat params when LLM omits the `job` wrapper for add requests. (#12124) Thanks @tyler6204.
- Gateway/CLI: when `gateway.bind=lan`, use a LAN IP for probe URLs and Control UI links. (#11448) Thanks @AnonO6.
- CLI: make `openclaw plugins list` output scannable by hoisting source roots and shortening bundled/global/workspace plugin paths.
- Hooks: fix bundled hooks broken since 2026.2.2 (tsdown migration). (#9295) Thanks @patrickshao.
- Routing: refresh bindings per message by loading config at route resolution so binding changes apply without restart. (#11372) Thanks @juanpablodlc.
- Exec approvals: render forwarded commands in monospace for safer approval scanning. (#11937) Thanks @sebslight.

View File

@@ -8,6 +8,7 @@ import { resolveArchiveKind } from "../infra/archive.js";
import { installPluginFromNpmSpec, installPluginFromPath } from "../plugins/install.js";
import { recordPluginInstall } from "../plugins/installs.js";
import { applyExclusiveSlotSelection } from "../plugins/slots.js";
import { resolvePluginSourceRoots, formatPluginSourceForTable } from "../plugins/source-display.js";
import { buildPluginStatusReport } from "../plugins/status.js";
import { updateNpmInstalledPlugins } from "../plugins/update.js";
import { defaultRuntime } from "../runtime.js";
@@ -140,9 +141,17 @@ export function registerPluginsCli(program: Command) {
if (!opts.verbose) {
const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
const sourceRoots = resolvePluginSourceRoots({
workspaceDir: report.workspaceDir,
});
const usedRoots = new Set<keyof typeof sourceRoots>();
const rows = list.map((plugin) => {
const desc = plugin.description ? theme.muted(plugin.description) : "";
const sourceLine = desc ? `${plugin.source}\n${desc}` : plugin.source;
const formattedSource = formatPluginSourceForTable(plugin, sourceRoots);
if (formattedSource.rootKey) {
usedRoots.add(formattedSource.rootKey);
}
const sourceLine = desc ? `${formattedSource.value}\n${desc}` : formattedSource.value;
return {
Name: plugin.name || plugin.id,
ID: plugin.name && plugin.name !== plugin.id ? plugin.id : "",
@@ -156,6 +165,22 @@ export function registerPluginsCli(program: Command) {
Version: plugin.version ?? "",
};
});
if (usedRoots.size > 0) {
defaultRuntime.log(theme.muted("Source roots:"));
for (const key of ["stock", "workspace", "global"] as const) {
if (!usedRoots.has(key)) {
continue;
}
const dir = sourceRoots[key];
if (!dir) {
continue;
}
defaultRuntime.log(` ${theme.command(`${key}:`)} ${theme.muted(dir)}`);
}
defaultRuntime.log("");
}
defaultRuntime.log(
renderTable({
width: tableWidth,

View File

@@ -0,0 +1,52 @@
import { describe, expect, it } from "vitest";
import { formatPluginSourceForTable } from "./source-display.js";
describe("formatPluginSourceForTable", () => {
it("shortens bundled plugin sources under the stock root", () => {
const out = formatPluginSourceForTable(
{
origin: "bundled",
source: "/opt/homebrew/lib/node_modules/openclaw/extensions/bluebubbles/index.ts",
},
{
stock: "/opt/homebrew/lib/node_modules/openclaw/extensions",
global: "/Users/x/.openclaw/extensions",
workspace: "/Users/x/ws/.openclaw/extensions",
},
);
expect(out.value).toBe("stock:bluebubbles/index.ts");
expect(out.rootKey).toBe("stock");
});
it("shortens workspace plugin sources under the workspace root", () => {
const out = formatPluginSourceForTable(
{
origin: "workspace",
source: "/Users/x/ws/.openclaw/extensions/matrix/index.ts",
},
{
stock: "/opt/homebrew/lib/node_modules/openclaw/extensions",
global: "/Users/x/.openclaw/extensions",
workspace: "/Users/x/ws/.openclaw/extensions",
},
);
expect(out.value).toBe("workspace:matrix/index.ts");
expect(out.rootKey).toBe("workspace");
});
it("shortens global plugin sources under the global root", () => {
const out = formatPluginSourceForTable(
{
origin: "global",
source: "/Users/x/.openclaw/extensions/zalo/index.js",
},
{
stock: "/opt/homebrew/lib/node_modules/openclaw/extensions",
global: "/Users/x/.openclaw/extensions",
workspace: "/Users/x/ws/.openclaw/extensions",
},
);
expect(out.value).toBe("global:zalo/index.js");
expect(out.rootKey).toBe("global");
});
});

View File

@@ -0,0 +1,65 @@
import path from "node:path";
import type { PluginRecord } from "./registry.js";
import { resolveConfigDir, shortenHomeInString } from "../utils.js";
import { resolveBundledPluginsDir } from "./bundled-dir.js";
export type PluginSourceRoots = {
stock?: string;
global?: string;
workspace?: string;
};
function tryRelative(root: string, filePath: string): string | null {
const rel = path.relative(root, filePath);
if (!rel || rel === ".") {
return null;
}
if (rel === "..") {
return null;
}
if (rel.startsWith(`..${path.sep}`) || rel.startsWith("../") || rel.startsWith("..\\")) {
return null;
}
if (path.isAbsolute(rel)) {
return null;
}
return rel;
}
export function resolvePluginSourceRoots(params: { workspaceDir?: string }): PluginSourceRoots {
const stock = resolveBundledPluginsDir();
const global = path.join(resolveConfigDir(), "extensions");
const workspace = params.workspaceDir
? path.join(params.workspaceDir, ".openclaw", "extensions")
: undefined;
return { stock, global, workspace };
}
export function formatPluginSourceForTable(
plugin: Pick<PluginRecord, "source" | "origin">,
roots: PluginSourceRoots,
): { value: string; rootKey?: keyof PluginSourceRoots } {
const raw = plugin.source;
if (plugin.origin === "bundled" && roots.stock) {
const rel = tryRelative(roots.stock, raw);
if (rel) {
return { value: `stock:${rel}`, rootKey: "stock" };
}
}
if (plugin.origin === "workspace" && roots.workspace) {
const rel = tryRelative(roots.workspace, raw);
if (rel) {
return { value: `workspace:${rel}`, rootKey: "workspace" };
}
}
if (plugin.origin === "global" && roots.global) {
const rel = tryRelative(roots.global, raw);
if (rel) {
return { value: `global:${rel}`, rootKey: "global" };
}
}
// Keep this stable/pasteable; only ~-shorten.
return { value: shortenHomeInString(raw) };
}