mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-20 14:30:57 +00:00
Plugins: surface compatibility notices
This commit is contained in:
@@ -62,6 +62,7 @@ describe("handleCommands /plugins", () => {
|
||||
expect(showResult.reply?.text).toContain('"id": "superpowers"');
|
||||
expect(showResult.reply?.text).toContain('"bundleFormat": "claude"');
|
||||
expect(showResult.reply?.text).toContain('"shape":');
|
||||
expect(showResult.reply?.text).toContain('"compatibilityWarnings": []');
|
||||
|
||||
const inspectAllParams = buildCommandTestParams(
|
||||
"/plugins inspect all",
|
||||
@@ -75,6 +76,7 @@ describe("handleCommands /plugins", () => {
|
||||
const inspectAllResult = await handleCommands(inspectAllParams);
|
||||
expect(inspectAllResult.reply?.text).toContain("```json");
|
||||
expect(inspectAllResult.reply?.text).toContain('"plugin"');
|
||||
expect(inspectAllResult.reply?.text).toContain('"compatibilityWarnings"');
|
||||
expect(inspectAllResult.reply?.text).toContain('"superpowers"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,6 +45,11 @@ function buildPluginInspectJson(params: {
|
||||
}
|
||||
return {
|
||||
inspect,
|
||||
compatibilityWarnings: inspect.compatibility.map((warning) => ({
|
||||
code: warning.code,
|
||||
severity: warning.severity,
|
||||
message: `${warning.pluginId} ${warning.message}`,
|
||||
})),
|
||||
install: params.config.plugins?.installs?.[inspect.plugin.id] ?? null,
|
||||
};
|
||||
}
|
||||
@@ -61,6 +66,11 @@ function buildAllPluginInspectJson(params: {
|
||||
report: params.report,
|
||||
}).map((inspect) => ({
|
||||
inspect,
|
||||
compatibilityWarnings: inspect.compatibility.map((warning) => ({
|
||||
code: warning.code,
|
||||
severity: warning.severity,
|
||||
message: `${warning.pluginId} ${warning.message}`,
|
||||
})),
|
||||
install: params.config.plugins?.installs?.[inspect.plugin.id] ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { applyExclusiveSlotSelection } from "../plugins/slots.js";
|
||||
import { resolvePluginSourceRoots, formatPluginSourceForTable } from "../plugins/source-display.js";
|
||||
import {
|
||||
buildAllPluginInspectReports,
|
||||
buildPluginCompatibilityNotices,
|
||||
buildPluginInspectReport,
|
||||
buildPluginStatusReport,
|
||||
} from "../plugins/status.js";
|
||||
@@ -652,6 +653,12 @@ export function registerPluginsCli(program: Command) {
|
||||
: theme.error("error"),
|
||||
Shape: inspect.shape,
|
||||
Capabilities: formatCapabilityKinds(inspect.capabilities),
|
||||
Compatibility:
|
||||
inspect.compatibility.length > 0
|
||||
? inspect.compatibility
|
||||
.map((entry) => (entry.severity === "warn" ? `warn:${entry.code}` : entry.code))
|
||||
.join(", ")
|
||||
: "none",
|
||||
Hooks: formatHookSummary({
|
||||
usesLegacyBeforeAgentStart: inspect.usesLegacyBeforeAgentStart,
|
||||
typedHookCount: inspect.typedHooks.length,
|
||||
@@ -667,6 +674,7 @@ export function registerPluginsCli(program: Command) {
|
||||
{ key: "Status", header: "Status", minWidth: 10 },
|
||||
{ key: "Shape", header: "Shape", minWidth: 18 },
|
||||
{ key: "Capabilities", header: "Capabilities", minWidth: 28, flex: true },
|
||||
{ key: "Compatibility", header: "Compatibility", minWidth: 24, flex: true },
|
||||
{ key: "Hooks", header: "Hooks", minWidth: 20, flex: true },
|
||||
],
|
||||
rows,
|
||||
@@ -751,6 +759,12 @@ export function registerPluginsCli(program: Command) {
|
||||
),
|
||||
),
|
||||
);
|
||||
lines.push(
|
||||
...formatInspectSection(
|
||||
"Compatibility warnings",
|
||||
inspect.compatibility.map((warning) => `${warning.pluginId} ${warning.message}`),
|
||||
),
|
||||
);
|
||||
lines.push(
|
||||
...formatInspectSection(
|
||||
"Custom hooks",
|
||||
@@ -1058,8 +1072,9 @@ export function registerPluginsCli(program: Command) {
|
||||
const report = buildPluginStatusReport();
|
||||
const errors = report.plugins.filter((p) => p.status === "error");
|
||||
const diags = report.diagnostics.filter((d) => d.level === "error");
|
||||
const compatibility = buildPluginCompatibilityNotices({ report });
|
||||
|
||||
if (errors.length === 0 && diags.length === 0) {
|
||||
if (errors.length === 0 && diags.length === 0 && compatibility.length === 0) {
|
||||
defaultRuntime.log("No plugin issues detected.");
|
||||
return;
|
||||
}
|
||||
@@ -1081,6 +1096,16 @@ export function registerPluginsCli(program: Command) {
|
||||
lines.push(`- ${target}${diag.message}`);
|
||||
}
|
||||
}
|
||||
if (compatibility.length > 0) {
|
||||
if (lines.length > 0) {
|
||||
lines.push("");
|
||||
}
|
||||
lines.push(theme.warn("Compatibility:"));
|
||||
for (const notice of compatibility) {
|
||||
const marker = notice.severity === "warn" ? theme.warn("warn") : theme.muted("info");
|
||||
lines.push(`- ${notice.pluginId} [${marker}]: ${notice.message}`);
|
||||
}
|
||||
}
|
||||
const docs = formatDocsLink("/plugin", "docs.openclaw.ai/plugin");
|
||||
lines.push("");
|
||||
lines.push(`${theme.muted("Docs:")} ${docs}`);
|
||||
|
||||
162
src/commands/doctor-workspace-status.test.ts
Normal file
162
src/commands/doctor-workspace-status.test.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import * as noteModule from "../terminal/note.js";
|
||||
|
||||
const resolveAgentWorkspaceDirMock = vi.fn();
|
||||
const resolveDefaultAgentIdMock = vi.fn();
|
||||
const buildWorkspaceSkillStatusMock = vi.fn();
|
||||
const loadOpenClawPluginsMock = vi.fn();
|
||||
|
||||
vi.mock("../agents/agent-scope.js", () => ({
|
||||
resolveAgentWorkspaceDir: (...args: unknown[]) => resolveAgentWorkspaceDirMock(...args),
|
||||
resolveDefaultAgentId: (...args: unknown[]) => resolveDefaultAgentIdMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../agents/skills-status.js", () => ({
|
||||
buildWorkspaceSkillStatus: (...args: unknown[]) => buildWorkspaceSkillStatusMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
loadOpenClawPlugins: (...args: unknown[]) => loadOpenClawPluginsMock(...args),
|
||||
}));
|
||||
|
||||
describe("noteWorkspaceStatus", () => {
|
||||
it("warns when plugins use legacy compatibility paths", async () => {
|
||||
resolveDefaultAgentIdMock.mockReturnValue("default");
|
||||
resolveAgentWorkspaceDirMock.mockReturnValue("/workspace");
|
||||
buildWorkspaceSkillStatusMock.mockReturnValue({
|
||||
skills: [],
|
||||
});
|
||||
loadOpenClawPluginsMock.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "legacy-plugin",
|
||||
name: "Legacy Plugin",
|
||||
source: "/tmp/legacy-plugin/index.ts",
|
||||
origin: "workspace",
|
||||
enabled: true,
|
||||
status: "loaded",
|
||||
toolNames: [],
|
||||
hookNames: [],
|
||||
channelIds: [],
|
||||
providerIds: [],
|
||||
speechProviderIds: [],
|
||||
mediaUnderstandingProviderIds: [],
|
||||
imageGenerationProviderIds: [],
|
||||
webSearchProviderIds: [],
|
||||
gatewayMethods: [],
|
||||
cliCommands: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
httpRoutes: 0,
|
||||
hookCount: 1,
|
||||
configSchema: false,
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
channels: [],
|
||||
channelSetups: [],
|
||||
providers: [],
|
||||
speechProviders: [],
|
||||
mediaUnderstandingProviders: [],
|
||||
imageGenerationProviders: [],
|
||||
webSearchProviders: [],
|
||||
tools: [],
|
||||
hooks: [],
|
||||
typedHooks: [
|
||||
{
|
||||
pluginId: "legacy-plugin",
|
||||
hookName: "before_agent_start",
|
||||
handler: () => undefined,
|
||||
source: "/tmp/legacy-plugin/index.ts",
|
||||
},
|
||||
],
|
||||
httpRoutes: [],
|
||||
gatewayHandlers: {},
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
conversationBindingResolvedHandlers: [],
|
||||
});
|
||||
|
||||
const noteSpy = vi.spyOn(noteModule, "note").mockImplementation(() => {});
|
||||
try {
|
||||
const { noteWorkspaceStatus } = await import("./doctor-workspace-status.js");
|
||||
noteWorkspaceStatus({});
|
||||
|
||||
const compatibilityCalls = noteSpy.mock.calls.filter(
|
||||
([, title]) => title === "Plugin compatibility",
|
||||
);
|
||||
expect(compatibilityCalls).toHaveLength(1);
|
||||
expect(String(compatibilityCalls[0]?.[0])).toContain(
|
||||
"legacy-plugin still relies on legacy before_agent_start",
|
||||
);
|
||||
expect(String(compatibilityCalls[0]?.[0])).toContain(
|
||||
"legacy-plugin is hook-only; this remains supported for compatibility",
|
||||
);
|
||||
} finally {
|
||||
noteSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("omits plugin compatibility note when no legacy compatibility paths are present", async () => {
|
||||
resolveDefaultAgentIdMock.mockReturnValue("default");
|
||||
resolveAgentWorkspaceDirMock.mockReturnValue("/workspace");
|
||||
buildWorkspaceSkillStatusMock.mockReturnValue({
|
||||
skills: [],
|
||||
});
|
||||
loadOpenClawPluginsMock.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "modern-plugin",
|
||||
name: "Modern Plugin",
|
||||
source: "/tmp/modern-plugin/index.ts",
|
||||
origin: "workspace",
|
||||
enabled: true,
|
||||
status: "loaded",
|
||||
toolNames: [],
|
||||
hookNames: [],
|
||||
channelIds: [],
|
||||
providerIds: ["modern"],
|
||||
speechProviderIds: [],
|
||||
mediaUnderstandingProviderIds: [],
|
||||
imageGenerationProviderIds: [],
|
||||
webSearchProviderIds: [],
|
||||
gatewayMethods: [],
|
||||
cliCommands: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
httpRoutes: 0,
|
||||
hookCount: 0,
|
||||
configSchema: false,
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
channels: [],
|
||||
channelSetups: [],
|
||||
providers: [],
|
||||
speechProviders: [],
|
||||
mediaUnderstandingProviders: [],
|
||||
imageGenerationProviders: [],
|
||||
webSearchProviders: [],
|
||||
tools: [],
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
httpRoutes: [],
|
||||
gatewayHandlers: {},
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
conversationBindingResolvedHandlers: [],
|
||||
});
|
||||
|
||||
const noteSpy = vi.spyOn(noteModule, "note").mockImplementation(() => {});
|
||||
try {
|
||||
const { noteWorkspaceStatus } = await import("./doctor-workspace-status.js");
|
||||
noteWorkspaceStatus({});
|
||||
|
||||
expect(noteSpy.mock.calls.some(([, title]) => title === "Plugin compatibility")).toBe(false);
|
||||
} finally {
|
||||
noteSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent
|
||||
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadOpenClawPlugins } from "../plugins/loader.js";
|
||||
import { buildPluginCompatibilityWarnings } from "../plugins/status.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { detectLegacyWorkspaceDirs, formatLegacyWorkspaceWarning } from "./doctor-workspace.js";
|
||||
|
||||
@@ -54,6 +55,17 @@ export function noteWorkspaceStatus(cfg: OpenClawConfig) {
|
||||
|
||||
note(lines.join("\n"), "Plugins");
|
||||
}
|
||||
const compatibilityWarnings = buildPluginCompatibilityWarnings({
|
||||
config: cfg,
|
||||
workspaceDir,
|
||||
report: {
|
||||
workspaceDir,
|
||||
...pluginRegistry,
|
||||
},
|
||||
});
|
||||
if (compatibilityWarnings.length > 0) {
|
||||
note(compatibilityWarnings.map((line) => `- ${line}`).join("\n"), "Plugin compatibility");
|
||||
}
|
||||
if (pluginRegistry.diagnostics.length > 0) {
|
||||
const lines = pluginRegistry.diagnostics.map((diag) => {
|
||||
const prefix = diag.level.toUpperCase();
|
||||
|
||||
@@ -25,6 +25,7 @@ import { getRemoteSkillEligibility } from "../infra/skills-remote.js";
|
||||
import { readTailscaleStatusJson } from "../infra/tailscale.js";
|
||||
import { normalizeUpdateChannel, resolveUpdateChannelDisplay } from "../infra/update-channels.js";
|
||||
import { checkUpdateStatus, formatGitInstallLabel } from "../infra/update-check.js";
|
||||
import { buildPluginCompatibilityNotices } from "../plugins/status.js";
|
||||
import { runExec } from "../process/exec.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { VERSION } from "../version.js";
|
||||
@@ -238,6 +239,7 @@ export async function statusAllCommand(
|
||||
}
|
||||
})()
|
||||
: null;
|
||||
const pluginCompatibility = buildPluginCompatibilityNotices({ config: cfg });
|
||||
|
||||
const controlUiEnabled = cfg.gateway?.controlUi?.enabled ?? true;
|
||||
const dashboard = controlUiEnabled
|
||||
@@ -360,6 +362,7 @@ export async function statusAllCommand(
|
||||
tailscale,
|
||||
tailscaleHttpsUrl,
|
||||
skillStatus,
|
||||
pluginCompatibility,
|
||||
channelsStatus,
|
||||
channelIssues,
|
||||
gatewayReachable,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type RestartSentinelPayload,
|
||||
summarizeRestartSentinel,
|
||||
} from "../../infra/restart-sentinel.js";
|
||||
import type { PluginCompatibilityNotice } from "../../plugins/status.js";
|
||||
import { formatTimeAgo, redactSecrets } from "./format.js";
|
||||
import { readFileTailLines, summarizeLogTail } from "./gateway.js";
|
||||
|
||||
@@ -59,6 +60,7 @@ export async function appendStatusAllDiagnosis(params: {
|
||||
tailscale: TailscaleStatusLike;
|
||||
tailscaleHttpsUrl: string | null;
|
||||
skillStatus: SkillStatusLike | null;
|
||||
pluginCompatibility: PluginCompatibilityNotice[];
|
||||
channelsStatus: unknown;
|
||||
channelIssues: ChannelIssueLike[];
|
||||
gatewayReachable: boolean;
|
||||
@@ -176,6 +178,18 @@ export async function appendStatusAllDiagnosis(params: {
|
||||
);
|
||||
}
|
||||
|
||||
emitCheck(
|
||||
`Plugin compatibility (${params.pluginCompatibility.length || "none"})`,
|
||||
params.pluginCompatibility.length === 0 ? "ok" : "warn",
|
||||
);
|
||||
for (const notice of params.pluginCompatibility.slice(0, 12)) {
|
||||
const severity = notice.severity === "warn" ? "warn" : "info";
|
||||
lines.push(` - ${notice.pluginId} [${severity}] ${notice.message}`);
|
||||
}
|
||||
if (params.pluginCompatibility.length > 12) {
|
||||
lines.push(` ${muted(`… +${params.pluginCompatibility.length - 12} more`)}`);
|
||||
}
|
||||
|
||||
params.progress.setLabel("Reading logs…");
|
||||
const logPaths = (() => {
|
||||
try {
|
||||
|
||||
@@ -60,6 +60,7 @@ describe("buildStatusAllReportLines", () => {
|
||||
},
|
||||
tailscaleHttpsUrl: null,
|
||||
skillStatus: null,
|
||||
pluginCompatibility: [],
|
||||
channelsStatus: null,
|
||||
channelIssues: [],
|
||||
gatewayReachable: false,
|
||||
|
||||
@@ -137,6 +137,7 @@ export async function statusCommand(
|
||||
secretDiagnostics,
|
||||
memory,
|
||||
memoryPlugin,
|
||||
pluginCompatibility,
|
||||
} = scan;
|
||||
|
||||
const usage = opts.usage
|
||||
@@ -217,6 +218,10 @@ export async function statusCommand(
|
||||
agents: agentStatus,
|
||||
securityAudit,
|
||||
secretDiagnostics,
|
||||
pluginCompatibility: {
|
||||
count: pluginCompatibility.length,
|
||||
warnings: pluginCompatibility,
|
||||
},
|
||||
...(health || usage || lastHeartbeat ? { health, usage, lastHeartbeat } : {}),
|
||||
},
|
||||
null,
|
||||
@@ -416,6 +421,12 @@ export async function statusCommand(
|
||||
const updateLine = formatUpdateOneLiner(update).replace(/^Update:\s*/i, "");
|
||||
const channelLabel = channelInfo.label;
|
||||
const gitLabel = formatGitInstallLabel(update);
|
||||
const pluginCompatibilityValue =
|
||||
pluginCompatibility.length === 0
|
||||
? ok("none")
|
||||
: warn(
|
||||
`${pluginCompatibility.length} notice${pluginCompatibility.length === 1 ? "" : "s"} · ${new Set(pluginCompatibility.map((entry) => entry.pluginId)).size} plugin${new Set(pluginCompatibility.map((entry) => entry.pluginId)).size === 1 ? "" : "s"}`,
|
||||
);
|
||||
|
||||
const overviewRows = [
|
||||
{ Item: "Dashboard", Value: dashboard },
|
||||
@@ -443,6 +454,7 @@ export async function statusCommand(
|
||||
{ Item: "Node service", Value: nodeDaemonValue },
|
||||
{ Item: "Agents", Value: agentsValue },
|
||||
{ Item: "Memory", Value: memoryValue },
|
||||
{ Item: "Plugin compatibility", Value: pluginCompatibilityValue },
|
||||
{ Item: "Probes", Value: probesValue },
|
||||
{ Item: "Events", Value: eventsValue },
|
||||
{ Item: "Heartbeat", Value: heartbeatValue },
|
||||
@@ -467,6 +479,18 @@ export async function statusCommand(
|
||||
}).trimEnd(),
|
||||
);
|
||||
|
||||
if (pluginCompatibility.length > 0) {
|
||||
runtime.log("");
|
||||
runtime.log(theme.heading("Plugin compatibility"));
|
||||
for (const notice of pluginCompatibility.slice(0, 8)) {
|
||||
const label = notice.severity === "warn" ? theme.warn("WARN") : theme.muted("INFO");
|
||||
runtime.log(` ${label} ${notice.pluginId} ${notice.message}`);
|
||||
}
|
||||
if (pluginCompatibility.length > 8) {
|
||||
runtime.log(theme.muted(` … +${pluginCompatibility.length - 8} more`));
|
||||
}
|
||||
}
|
||||
|
||||
if (pairingRecovery) {
|
||||
runtime.log("");
|
||||
runtime.log(theme.warn("Gateway pairing approval required."));
|
||||
|
||||
@@ -14,6 +14,7 @@ const mocks = vi.hoisted(() => ({
|
||||
probeGateway: vi.fn(),
|
||||
resolveGatewayProbeAuthResolution: vi.fn(),
|
||||
ensurePluginRegistryLoaded: vi.fn(),
|
||||
buildPluginCompatibilityNotices: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -91,6 +92,10 @@ vi.mock("../cli/plugin-registry.js", () => ({
|
||||
ensurePluginRegistryLoaded: mocks.ensurePluginRegistryLoaded,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/status.js", () => ({
|
||||
buildPluginCompatibilityNotices: mocks.buildPluginCompatibilityNotices,
|
||||
}));
|
||||
|
||||
import { scanStatus } from "./status.scan.js";
|
||||
|
||||
describe("scanStatus", () => {
|
||||
|
||||
@@ -8,6 +8,10 @@ import { readBestEffortConfig } from "../config/config.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import type { collectChannelStatusIssues as collectChannelStatusIssuesFn } from "../infra/channels-status-issues.js";
|
||||
import { resolveOsSummary } from "../infra/os-summary.js";
|
||||
import {
|
||||
buildPluginCompatibilityNotices,
|
||||
type PluginCompatibilityNotice,
|
||||
} from "../plugins/status.js";
|
||||
import { runExec } from "../process/exec.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { createLazyRuntimeSurface } from "../shared/lazy-runtime.js";
|
||||
@@ -107,6 +111,7 @@ export type StatusScanResult = {
|
||||
summary: Awaited<ReturnType<typeof getStatusSummary>>;
|
||||
memory: MemoryStatusSnapshot | null;
|
||||
memoryPlugin: MemoryPluginStatus;
|
||||
pluginCompatibility: PluginCompatibilityNotice[];
|
||||
};
|
||||
|
||||
async function resolveMemoryStatusSnapshot(params: {
|
||||
@@ -192,6 +197,7 @@ async function scanStatusJsonFast(opts: {
|
||||
const memoryPlugin = resolveMemoryPluginStatus(cfg);
|
||||
const memoryPromise = resolveMemoryStatusSnapshot({ cfg, agentStatus, memoryPlugin });
|
||||
const memory = await memoryPromise;
|
||||
const pluginCompatibility = buildPluginCompatibilityNotices({ config: cfg });
|
||||
|
||||
return {
|
||||
cfg,
|
||||
@@ -216,6 +222,7 @@ async function scanStatusJsonFast(opts: {
|
||||
summary,
|
||||
memory,
|
||||
memoryPlugin,
|
||||
pluginCompatibility,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -233,7 +240,7 @@ export async function scanStatus(
|
||||
return await withProgress(
|
||||
{
|
||||
label: "Scanning status…",
|
||||
total: 10,
|
||||
total: 11,
|
||||
enabled: true,
|
||||
},
|
||||
async (progress) => {
|
||||
@@ -325,6 +332,10 @@ export async function scanStatus(
|
||||
const memory = await resolveMemoryStatusSnapshot({ cfg, agentStatus, memoryPlugin });
|
||||
progress.tick();
|
||||
|
||||
progress.setLabel("Checking plugins…");
|
||||
const pluginCompatibility = buildPluginCompatibilityNotices({ config: cfg });
|
||||
progress.tick();
|
||||
|
||||
progress.setLabel("Reading sessions…");
|
||||
const summary = unwrapDeferredResult(await summaryPromise);
|
||||
progress.tick();
|
||||
@@ -355,6 +366,7 @@ export async function scanStatus(
|
||||
summary,
|
||||
memory,
|
||||
memoryPlugin,
|
||||
pluginCompatibility,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
@@ -205,6 +205,7 @@ const mocks = vi.hoisted(() => ({
|
||||
},
|
||||
],
|
||||
}),
|
||||
buildPluginCompatibilityNotices: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
vi.mock("../memory/manager.js", () => ({
|
||||
@@ -385,6 +386,9 @@ vi.mock("../daemon/node-service.js", () => ({
|
||||
vi.mock("../security/audit.js", () => ({
|
||||
runSecurityAudit: mocks.runSecurityAudit,
|
||||
}));
|
||||
vi.mock("../plugins/status.js", () => ({
|
||||
buildPluginCompatibilityNotices: mocks.buildPluginCompatibilityNotices,
|
||||
}));
|
||||
|
||||
import { statusCommand } from "./status.js";
|
||||
|
||||
@@ -403,6 +407,15 @@ describe("statusCommand", () => {
|
||||
});
|
||||
|
||||
it("prints JSON when requested", async () => {
|
||||
mocks.buildPluginCompatibilityNotices.mockReturnValue([
|
||||
{
|
||||
pluginId: "legacy-plugin",
|
||||
code: "legacy-before-agent-start",
|
||||
severity: "warn",
|
||||
message:
|
||||
"still relies on legacy before_agent_start; keep upgrade coverage on this plugin and prefer before_model_resolve/before_prompt_build for new work.",
|
||||
},
|
||||
]);
|
||||
await statusCommand({ json: true }, runtime as never);
|
||||
const payload = JSON.parse(String(runtimeLogMock.mock.calls[0]?.[0]));
|
||||
expect(payload.linkChannel).toBeUndefined();
|
||||
@@ -424,6 +437,18 @@ describe("statusCommand", () => {
|
||||
expect(payload.securityAudit.summary.warn).toBe(1);
|
||||
expect(payload.gatewayService.label).toBe("LaunchAgent");
|
||||
expect(payload.nodeService.label).toBe("LaunchAgent");
|
||||
expect(payload.pluginCompatibility).toEqual({
|
||||
count: 1,
|
||||
warnings: [
|
||||
{
|
||||
pluginId: "legacy-plugin",
|
||||
code: "legacy-before-agent-start",
|
||||
severity: "warn",
|
||||
message:
|
||||
"still relies on legacy before_agent_start; keep upgrade coverage on this plugin and prefer before_model_resolve/before_prompt_build for new work.",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(mocks.runSecurityAudit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
includeFilesystem: true,
|
||||
@@ -452,6 +477,15 @@ describe("statusCommand", () => {
|
||||
});
|
||||
|
||||
it("prints formatted lines otherwise", async () => {
|
||||
mocks.buildPluginCompatibilityNotices.mockReturnValue([
|
||||
{
|
||||
pluginId: "legacy-plugin",
|
||||
code: "legacy-before-agent-start",
|
||||
severity: "warn",
|
||||
message:
|
||||
"still relies on legacy before_agent_start; keep upgrade coverage on this plugin and prefer before_model_resolve/before_prompt_build for new work.",
|
||||
},
|
||||
]);
|
||||
const logs = await runStatusAndGetLogs();
|
||||
for (const token of [
|
||||
"OpenClaw status",
|
||||
@@ -462,6 +496,7 @@ describe("statusCommand", () => {
|
||||
"Dashboard",
|
||||
"macos 14.0 (arm64)",
|
||||
"Memory",
|
||||
"Plugin compatibility",
|
||||
"Channels",
|
||||
"WhatsApp",
|
||||
"bootstrap files",
|
||||
@@ -476,6 +511,9 @@ describe("statusCommand", () => {
|
||||
]) {
|
||||
expect(logs.some((line) => line.includes(token))).toBe(true);
|
||||
}
|
||||
expect(
|
||||
logs.some((line) => line.includes("legacy-plugin still relies on legacy before_agent_start")),
|
||||
).toBe(true);
|
||||
expect(
|
||||
logs.some(
|
||||
(line) =>
|
||||
|
||||
@@ -5,6 +5,8 @@ const loadOpenClawPluginsMock = vi.fn();
|
||||
let buildPluginStatusReport: typeof import("./status.js").buildPluginStatusReport;
|
||||
let buildPluginInspectReport: typeof import("./status.js").buildPluginInspectReport;
|
||||
let buildAllPluginInspectReports: typeof import("./status.js").buildAllPluginInspectReports;
|
||||
let buildPluginCompatibilityNotices: typeof import("./status.js").buildPluginCompatibilityNotices;
|
||||
let buildPluginCompatibilityWarnings: typeof import("./status.js").buildPluginCompatibilityWarnings;
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
loadConfig: () => loadConfigMock(),
|
||||
@@ -48,8 +50,13 @@ describe("buildPluginStatusReport", () => {
|
||||
services: [],
|
||||
commands: [],
|
||||
});
|
||||
({ buildAllPluginInspectReports, buildPluginInspectReport, buildPluginStatusReport } =
|
||||
await import("./status.js"));
|
||||
({
|
||||
buildAllPluginInspectReports,
|
||||
buildPluginCompatibilityNotices,
|
||||
buildPluginCompatibilityWarnings,
|
||||
buildPluginInspectReport,
|
||||
buildPluginStatusReport,
|
||||
} = await import("./status.js"));
|
||||
});
|
||||
|
||||
it("forwards an explicit env to plugin loading", () => {
|
||||
@@ -148,6 +155,15 @@ describe("buildPluginStatusReport", () => {
|
||||
"web-search",
|
||||
]);
|
||||
expect(inspect?.usesLegacyBeforeAgentStart).toBe(true);
|
||||
expect(inspect?.compatibility).toEqual([
|
||||
{
|
||||
pluginId: "google",
|
||||
code: "legacy-before-agent-start",
|
||||
severity: "warn",
|
||||
message:
|
||||
"still relies on legacy before_agent_start; keep upgrade coverage on this plugin and prefer before_model_resolve/before_prompt_build for new work.",
|
||||
},
|
||||
]);
|
||||
expect(inspect?.policy).toEqual({
|
||||
allowPromptInjection: false,
|
||||
allowModelOverride: true,
|
||||
@@ -257,4 +273,219 @@ describe("buildPluginStatusReport", () => {
|
||||
"web-search",
|
||||
]);
|
||||
});
|
||||
|
||||
it("builds compatibility warnings for legacy compatibility paths", () => {
|
||||
loadOpenClawPluginsMock.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "lca",
|
||||
name: "LCA",
|
||||
description: "Legacy hook plugin",
|
||||
source: "/tmp/lca/index.ts",
|
||||
origin: "workspace",
|
||||
enabled: true,
|
||||
status: "loaded",
|
||||
toolNames: [],
|
||||
hookNames: [],
|
||||
channelIds: [],
|
||||
providerIds: [],
|
||||
speechProviderIds: [],
|
||||
mediaUnderstandingProviderIds: [],
|
||||
imageGenerationProviderIds: [],
|
||||
webSearchProviderIds: [],
|
||||
gatewayMethods: [],
|
||||
cliCommands: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
httpRoutes: 0,
|
||||
hookCount: 1,
|
||||
configSchema: false,
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
channels: [],
|
||||
channelSetups: [],
|
||||
providers: [],
|
||||
speechProviders: [],
|
||||
mediaUnderstandingProviders: [],
|
||||
imageGenerationProviders: [],
|
||||
webSearchProviders: [],
|
||||
tools: [],
|
||||
hooks: [],
|
||||
typedHooks: [
|
||||
{
|
||||
pluginId: "lca",
|
||||
hookName: "before_agent_start",
|
||||
handler: () => undefined,
|
||||
source: "/tmp/lca/index.ts",
|
||||
},
|
||||
],
|
||||
httpRoutes: [],
|
||||
gatewayHandlers: {},
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
});
|
||||
|
||||
expect(buildPluginCompatibilityWarnings()).toEqual([
|
||||
"lca still relies on legacy before_agent_start; keep upgrade coverage on this plugin and prefer before_model_resolve/before_prompt_build for new work.",
|
||||
"lca is hook-only; this remains supported for compatibility, but it has not migrated to explicit capability registration.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("builds structured compatibility notices with deterministic ordering", () => {
|
||||
loadOpenClawPluginsMock.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "hook-only",
|
||||
name: "Hook Only",
|
||||
description: "",
|
||||
source: "/tmp/hook-only/index.ts",
|
||||
origin: "workspace",
|
||||
enabled: true,
|
||||
status: "loaded",
|
||||
toolNames: [],
|
||||
hookNames: [],
|
||||
channelIds: [],
|
||||
providerIds: [],
|
||||
speechProviderIds: [],
|
||||
mediaUnderstandingProviderIds: [],
|
||||
imageGenerationProviderIds: [],
|
||||
webSearchProviderIds: [],
|
||||
gatewayMethods: [],
|
||||
cliCommands: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
httpRoutes: 0,
|
||||
hookCount: 1,
|
||||
configSchema: false,
|
||||
},
|
||||
{
|
||||
id: "legacy-only",
|
||||
name: "Legacy Only",
|
||||
description: "",
|
||||
source: "/tmp/legacy-only/index.ts",
|
||||
origin: "workspace",
|
||||
enabled: true,
|
||||
status: "loaded",
|
||||
toolNames: [],
|
||||
hookNames: [],
|
||||
channelIds: [],
|
||||
providerIds: ["legacy-only"],
|
||||
speechProviderIds: [],
|
||||
mediaUnderstandingProviderIds: [],
|
||||
imageGenerationProviderIds: [],
|
||||
webSearchProviderIds: [],
|
||||
gatewayMethods: [],
|
||||
cliCommands: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
httpRoutes: 0,
|
||||
hookCount: 1,
|
||||
configSchema: false,
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
channels: [],
|
||||
channelSetups: [],
|
||||
providers: [],
|
||||
speechProviders: [],
|
||||
mediaUnderstandingProviders: [],
|
||||
imageGenerationProviders: [],
|
||||
webSearchProviders: [],
|
||||
tools: [],
|
||||
hooks: [
|
||||
{
|
||||
pluginId: "hook-only",
|
||||
events: ["message"],
|
||||
entry: {
|
||||
hook: {
|
||||
name: "legacy",
|
||||
handler: () => undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
typedHooks: [
|
||||
{
|
||||
pluginId: "legacy-only",
|
||||
hookName: "before_agent_start",
|
||||
handler: () => undefined,
|
||||
source: "/tmp/legacy-only/index.ts",
|
||||
},
|
||||
],
|
||||
httpRoutes: [],
|
||||
gatewayHandlers: {},
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
});
|
||||
|
||||
expect(buildPluginCompatibilityNotices()).toEqual([
|
||||
{
|
||||
pluginId: "hook-only",
|
||||
code: "hook-only",
|
||||
severity: "info",
|
||||
message:
|
||||
"is hook-only; this remains supported for compatibility, but it has not migrated to explicit capability registration.",
|
||||
},
|
||||
{
|
||||
pluginId: "legacy-only",
|
||||
code: "legacy-before-agent-start",
|
||||
severity: "warn",
|
||||
message:
|
||||
"still relies on legacy before_agent_start; keep upgrade coverage on this plugin and prefer before_model_resolve/before_prompt_build for new work.",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns no compatibility warnings for modern capability plugins", () => {
|
||||
loadOpenClawPluginsMock.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "modern",
|
||||
name: "Modern",
|
||||
description: "",
|
||||
source: "/tmp/modern/index.ts",
|
||||
origin: "workspace",
|
||||
enabled: true,
|
||||
status: "loaded",
|
||||
toolNames: [],
|
||||
hookNames: [],
|
||||
channelIds: [],
|
||||
providerIds: ["modern"],
|
||||
speechProviderIds: [],
|
||||
mediaUnderstandingProviderIds: [],
|
||||
imageGenerationProviderIds: [],
|
||||
webSearchProviderIds: [],
|
||||
gatewayMethods: [],
|
||||
cliCommands: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
httpRoutes: 0,
|
||||
hookCount: 0,
|
||||
configSchema: false,
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
channels: [],
|
||||
channelSetups: [],
|
||||
providers: [],
|
||||
speechProviders: [],
|
||||
mediaUnderstandingProviders: [],
|
||||
imageGenerationProviders: [],
|
||||
webSearchProviders: [],
|
||||
tools: [],
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
httpRoutes: [],
|
||||
gatewayHandlers: {},
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
});
|
||||
|
||||
expect(buildPluginCompatibilityNotices()).toEqual([]);
|
||||
expect(buildPluginCompatibilityWarnings()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,13 @@ export type PluginInspectShape =
|
||||
| "hybrid-capability"
|
||||
| "non-capability";
|
||||
|
||||
export type PluginCompatibilityNotice = {
|
||||
pluginId: string;
|
||||
code: "legacy-before-agent-start" | "hook-only";
|
||||
severity: "warn" | "info";
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type PluginInspectReport = {
|
||||
workspaceDir?: string;
|
||||
plugin: PluginRegistry["plugins"][number];
|
||||
@@ -61,8 +68,34 @@ export type PluginInspectReport = {
|
||||
hasAllowedModelsConfig: boolean;
|
||||
};
|
||||
usesLegacyBeforeAgentStart: boolean;
|
||||
compatibility: PluginCompatibilityNotice[];
|
||||
};
|
||||
|
||||
function buildCompatibilityNoticesForInspect(
|
||||
inspect: Pick<PluginInspectReport, "plugin" | "shape" | "usesLegacyBeforeAgentStart">,
|
||||
): PluginCompatibilityNotice[] {
|
||||
const warnings: PluginCompatibilityNotice[] = [];
|
||||
if (inspect.usesLegacyBeforeAgentStart) {
|
||||
warnings.push({
|
||||
pluginId: inspect.plugin.id,
|
||||
code: "legacy-before-agent-start",
|
||||
severity: "warn",
|
||||
message:
|
||||
"still relies on legacy before_agent_start; keep upgrade coverage on this plugin and prefer before_model_resolve/before_prompt_build for new work.",
|
||||
});
|
||||
}
|
||||
if (inspect.shape === "hook-only") {
|
||||
warnings.push({
|
||||
pluginId: inspect.plugin.id,
|
||||
code: "hook-only",
|
||||
severity: "info",
|
||||
message:
|
||||
"is hook-only; this remains supported for compatibility, but it has not migrated to explicit capability registration.",
|
||||
});
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
const log = createSubsystemLogger("plugins");
|
||||
|
||||
export function buildPluginStatusReport(params?: {
|
||||
@@ -176,21 +209,30 @@ export function buildPluginInspectReport(params: {
|
||||
const diagnostics = report.diagnostics.filter((entry) => entry.pluginId === plugin.id);
|
||||
const policyEntry = normalizePluginsConfig(config.plugins).entries[plugin.id];
|
||||
const capabilityCount = capabilities.length;
|
||||
const shape = deriveInspectShape({
|
||||
capabilityCount,
|
||||
typedHookCount: typedHooks.length,
|
||||
customHookCount: customHooks.length,
|
||||
toolCount: tools.length,
|
||||
commandCount: plugin.commands.length,
|
||||
cliCount: plugin.cliCommands.length,
|
||||
serviceCount: plugin.services.length,
|
||||
gatewayMethodCount: plugin.gatewayMethods.length,
|
||||
httpRouteCount: plugin.httpRoutes,
|
||||
});
|
||||
|
||||
const usesLegacyBeforeAgentStart = typedHooks.some(
|
||||
(entry) => entry.name === "before_agent_start",
|
||||
);
|
||||
const compatibility = buildCompatibilityNoticesForInspect({
|
||||
plugin,
|
||||
shape,
|
||||
usesLegacyBeforeAgentStart,
|
||||
});
|
||||
return {
|
||||
workspaceDir: report.workspaceDir,
|
||||
plugin,
|
||||
shape: deriveInspectShape({
|
||||
capabilityCount,
|
||||
typedHookCount: typedHooks.length,
|
||||
customHookCount: customHooks.length,
|
||||
toolCount: tools.length,
|
||||
commandCount: plugin.commands.length,
|
||||
cliCount: plugin.cliCommands.length,
|
||||
serviceCount: plugin.services.length,
|
||||
gatewayMethodCount: plugin.gatewayMethods.length,
|
||||
httpRouteCount: plugin.httpRoutes,
|
||||
}),
|
||||
shape,
|
||||
capabilityMode: capabilityCount === 0 ? "none" : capabilityCount === 1 ? "plain" : "hybrid",
|
||||
capabilityCount,
|
||||
capabilities,
|
||||
@@ -209,7 +251,8 @@ export function buildPluginInspectReport(params: {
|
||||
allowedModels: [...(policyEntry?.subagent?.allowedModels ?? [])],
|
||||
hasAllowedModelsConfig: policyEntry?.subagent?.hasAllowedModelsConfig === true,
|
||||
},
|
||||
usesLegacyBeforeAgentStart: typedHooks.some((entry) => entry.name === "before_agent_start"),
|
||||
usesLegacyBeforeAgentStart,
|
||||
compatibility,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -238,3 +281,23 @@ export function buildAllPluginInspectReports(params?: {
|
||||
)
|
||||
.filter((entry): entry is PluginInspectReport => entry !== null);
|
||||
}
|
||||
|
||||
export function buildPluginCompatibilityWarnings(params?: {
|
||||
config?: ReturnType<typeof loadConfig>;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
report?: PluginStatusReport;
|
||||
}): string[] {
|
||||
return buildAllPluginInspectReports(params).flatMap((inspect) =>
|
||||
inspect.compatibility.map((warning) => `${warning.pluginId} ${warning.message}`),
|
||||
);
|
||||
}
|
||||
|
||||
export function buildPluginCompatibilityNotices(params?: {
|
||||
config?: ReturnType<typeof loadConfig>;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
report?: PluginStatusReport;
|
||||
}): PluginCompatibilityNotice[] {
|
||||
return buildAllPluginInspectReports(params).flatMap((inspect) => inspect.compatibility);
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ const ensureControlUiAssetsBuilt = vi.hoisted(() => vi.fn(async () => ({ ok: tru
|
||||
const runTui = vi.hoisted(() => vi.fn(async (_options: unknown) => {}));
|
||||
const setupWizardShellCompletion = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const probeGatewayReachable = vi.hoisted(() => vi.fn(async () => ({ ok: true })));
|
||||
const buildPluginCompatibilityNotices = vi.hoisted(() => vi.fn(() => []));
|
||||
|
||||
vi.mock("../commands/onboard-channels.js", () => ({
|
||||
setupChannels,
|
||||
@@ -172,6 +173,10 @@ vi.mock("../infra/control-ui-assets.js", () => ({
|
||||
ensureControlUiAssetsBuilt,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/status.js", () => ({
|
||||
buildPluginCompatibilityNotices,
|
||||
}));
|
||||
|
||||
vi.mock("../channels/plugins/index.js", () => ({
|
||||
listChannelPlugins,
|
||||
}));
|
||||
@@ -398,6 +403,62 @@ describe("runSetupWizard", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("shows plugin compatibility notices for an existing valid config", async () => {
|
||||
buildPluginCompatibilityNotices.mockReturnValue([
|
||||
{
|
||||
pluginId: "legacy-plugin",
|
||||
code: "legacy-before-agent-start",
|
||||
severity: "warn",
|
||||
message:
|
||||
"still relies on legacy before_agent_start; keep upgrade coverage on this plugin and prefer before_model_resolve/before_prompt_build for new work.",
|
||||
},
|
||||
]);
|
||||
readConfigFileSnapshot.mockResolvedValueOnce({
|
||||
path: "/tmp/.openclaw/openclaw.json",
|
||||
exists: true,
|
||||
raw: "{}",
|
||||
parsed: {},
|
||||
resolved: {},
|
||||
valid: true,
|
||||
config: {
|
||||
gateway: {},
|
||||
},
|
||||
issues: [],
|
||||
warnings: [],
|
||||
legacyIssues: [],
|
||||
});
|
||||
|
||||
const note: WizardPrompter["note"] = vi.fn(async () => {});
|
||||
const select = vi.fn(async (opts: WizardSelectParams<unknown>) => {
|
||||
if (opts.message === "Config handling") {
|
||||
return "keep";
|
||||
}
|
||||
return "quickstart";
|
||||
}) as unknown as WizardPrompter["select"];
|
||||
const prompter = buildWizardPrompter({ note, select });
|
||||
const runtime = createRuntime();
|
||||
|
||||
await runSetupWizard(
|
||||
{
|
||||
acceptRisk: true,
|
||||
flow: "quickstart",
|
||||
authChoice: "skip",
|
||||
installDaemon: false,
|
||||
skipProviders: true,
|
||||
skipSkills: true,
|
||||
skipSearch: true,
|
||||
skipHealth: true,
|
||||
skipUi: true,
|
||||
},
|
||||
runtime,
|
||||
prompter,
|
||||
);
|
||||
|
||||
const calls = (note as unknown as { mock: { calls: unknown[][] } }).mock.calls;
|
||||
expect(calls.some((call) => call?.[1] === "Plugin compatibility")).toBe(true);
|
||||
expect(calls.some((call) => String(call?.[0] ?? "").includes("legacy-plugin"))).toBe(true);
|
||||
});
|
||||
|
||||
it("resolves gateway.auth.password SecretRef for local setup probe", async () => {
|
||||
const previous = process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||
process.env.OPENCLAW_GATEWAY_PASSWORD = "gateway-ref-password"; // pragma: allowlist secret
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
writeConfigFile,
|
||||
} from "../config/config.js";
|
||||
import { normalizeSecretInputString } from "../config/types.secrets.js";
|
||||
import { buildPluginCompatibilityNotices } from "../plugins/status.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
@@ -102,6 +103,27 @@ export async function runSetupWizard(
|
||||
return;
|
||||
}
|
||||
|
||||
const compatibilityNotices = snapshot.valid
|
||||
? buildPluginCompatibilityNotices({ config: baseConfig })
|
||||
: [];
|
||||
if (compatibilityNotices.length > 0) {
|
||||
await prompter.note(
|
||||
[
|
||||
`Detected ${compatibilityNotices.length} plugin compatibility notice${compatibilityNotices.length === 1 ? "" : "s"} in the current config.`,
|
||||
...compatibilityNotices
|
||||
.slice(0, 4)
|
||||
.map((notice) => `- ${notice.pluginId}: ${notice.message}`),
|
||||
...(compatibilityNotices.length > 4
|
||||
? [`- ... +${compatibilityNotices.length - 4} more`]
|
||||
: []),
|
||||
"",
|
||||
`Review: ${formatCliCommand("openclaw doctor")}`,
|
||||
`Inspect: ${formatCliCommand("openclaw plugins inspect --all")}`,
|
||||
].join("\n"),
|
||||
"Plugin compatibility",
|
||||
);
|
||||
}
|
||||
|
||||
const quickstartHint = `Configure details later via ${formatCliCommand("openclaw configure")}.`;
|
||||
const manualHint = "Configure port, network, Tailscale, and auth options.";
|
||||
const explicitFlowRaw = opts.flow?.trim();
|
||||
|
||||
Reference in New Issue
Block a user