From 3e63b2a4fade671f2150c8e0acf4c5258a2d8353 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 9 Feb 2026 13:05:41 -0600 Subject: [PATCH] fix(cli): improve plugins list source display --- CHANGELOG.md | 1 + src/cli/plugins-cli.ts | 27 ++++++++++++- src/plugins/source-display.test.ts | 52 ++++++++++++++++++++++++ src/plugins/source-display.ts | 65 ++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 src/plugins/source-display.test.ts create mode 100644 src/plugins/source-display.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 27fc08963..10f6ea8d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/cli/plugins-cli.ts b/src/cli/plugins-cli.ts index a3832d8c9..21bc6a5cc 100644 --- a/src/cli/plugins-cli.ts +++ b/src/cli/plugins-cli.ts @@ -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(); 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, diff --git a/src/plugins/source-display.test.ts b/src/plugins/source-display.test.ts new file mode 100644 index 000000000..c555f627d --- /dev/null +++ b/src/plugins/source-display.test.ts @@ -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"); + }); +}); diff --git a/src/plugins/source-display.ts b/src/plugins/source-display.ts new file mode 100644 index 000000000..7660af22c --- /dev/null +++ b/src/plugins/source-display.ts @@ -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, + 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) }; +}