fix(cli): improve plugins list source display
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
52
src/plugins/source-display.test.ts
Normal file
52
src/plugins/source-display.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
65
src/plugins/source-display.ts
Normal file
65
src/plugins/source-display.ts
Normal 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) };
|
||||
}
|
||||
Reference in New Issue
Block a user