-
-
Notifications
You must be signed in to change notification settings - Fork 80.7k
Expand file tree
/
Copy pathplugins-cli.ts
More file actions
398 lines (369 loc) · 14.4 KB
/
Copy pathplugins-cli.ts
File metadata and controls
398 lines (369 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
import type { Command } from "commander";
import { getRuntimeConfig, readConfigFileSnapshot, replaceConfigFile } from "../config/config.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { tracePluginLifecyclePhaseAsync } from "../plugins/plugin-lifecycle-trace.js";
import { defaultRuntime } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js";
import { theme } from "../terminal/theme.js";
import type { PluginInspectOptions } from "./plugins-inspect-command.js";
import type { PluginsListOptions } from "./plugins-list-command.js";
import { applyParentDefaultHelpAction } from "./program/parent-default-help.js";
export type PluginUpdateOptions = {
all?: boolean;
dryRun?: boolean;
dangerouslyForceUnsafeInstall?: boolean;
};
export type PluginMarketplaceListOptions = {
json?: boolean;
};
export type PluginSearchOptions = {
json?: boolean;
limit?: number;
};
export type PluginUninstallOptions = {
keepFiles?: boolean;
/** @deprecated Use keepFiles. */
keepConfig?: boolean;
force?: boolean;
dryRun?: boolean;
};
export type PluginRegistryOptions = {
json?: boolean;
refresh?: boolean;
};
function countEnabledPlugins(plugins: readonly { enabled: boolean }[]): number {
return plugins.filter((plugin) => plugin.enabled).length;
}
function formatRegistryState(state: "missing" | "fresh" | "stale"): string {
if (state === "fresh") {
return theme.success(state);
}
if (state === "stale") {
return theme.warn(state);
}
return theme.warn(state);
}
export function registerPluginsCli(program: Command) {
const plugins = program
.command("plugins")
.description("Manage OpenClaw plugins and extensions")
.addHelpText(
"after",
() =>
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/plugins", "docs.openclaw.ai/cli/plugins")}\n`,
);
plugins
.command("list")
.description("List discovered plugins")
.option("--json", "Print JSON")
.option("--enabled", "Only show enabled plugins", false)
.option("--verbose", "Show detailed entries", false)
.action(async (opts: PluginsListOptions) => {
const { runPluginsListCommand } = await import("./plugins-list-command.js");
await runPluginsListCommand(opts);
});
plugins
.command("search")
.description("Search ClawHub plugin packages")
.argument("[query...]", "Search query")
.option("--limit <n>", "Max results", (value) => Number.parseInt(value, 10))
.option("--json", "Print JSON", false)
.action(async (queryParts: string[], opts: PluginSearchOptions) => {
const { runPluginsSearchCommand } = await import("./plugins-search-command.js");
await runPluginsSearchCommand(queryParts, opts);
});
plugins
.command("inspect")
.alias("info")
.description("Inspect plugin details")
.argument("[id]", "Plugin id")
.option("--all", "Inspect all plugins")
.option("--runtime", "Load plugin runtime for hooks/tools/diagnostics")
.option("--json", "Print JSON")
.action(async (id: string | undefined, opts: PluginInspectOptions) => {
const { runPluginsInspectCommand } = await import("./plugins-inspect-command.js");
await runPluginsInspectCommand(id, opts);
});
plugins
.command("enable")
.description("Enable a plugin in config")
.argument("<id>", "Plugin id")
.action(async (id: string) => {
const { enablePluginInConfig } = await import("../plugins/enable.js");
const { applySlotSelectionForPlugin, logSlotWarnings } =
await import("./plugins-command-helpers.js");
const { refreshPluginRegistryAfterConfigMutation } =
await import("./plugins-registry-refresh.js");
const snapshot = await readConfigFileSnapshot();
const cfg = (snapshot.sourceConfig ?? snapshot.config) as OpenClawConfig;
const enableResult = enablePluginInConfig(cfg, id);
let next: OpenClawConfig = enableResult.config;
const slotResult = applySlotSelectionForPlugin(next, id);
next = slotResult.config;
await replaceConfigFile({
nextConfig: next,
...(snapshot.hash !== undefined ? { baseHash: snapshot.hash } : {}),
});
await refreshPluginRegistryAfterConfigMutation({
config: next,
reason: "policy-changed",
policyPluginIds: [enableResult.pluginId],
logger: {
warn: (message) => defaultRuntime.log(theme.warn(message)),
},
});
logSlotWarnings(slotResult.warnings);
if (enableResult.enabled) {
defaultRuntime.log(`Enabled plugin "${id}". Restart the gateway to apply.`);
return;
}
defaultRuntime.log(
theme.warn(
`Plugin "${id}" could not be enabled (${enableResult.reason ?? "unknown reason"}).`,
),
);
});
plugins
.command("disable")
.description("Disable a plugin in config")
.argument("<id>", "Plugin id")
.action(async (id: string) => {
const { setPluginEnabledInConfig } = await import("./plugins-config.js");
const { refreshPluginRegistryAfterConfigMutation } =
await import("./plugins-registry-refresh.js");
const snapshot = await readConfigFileSnapshot();
const cfg = (snapshot.sourceConfig ?? snapshot.config) as OpenClawConfig;
const next = setPluginEnabledInConfig(cfg, id, false);
await replaceConfigFile({
nextConfig: next,
...(snapshot.hash !== undefined ? { baseHash: snapshot.hash } : {}),
});
await refreshPluginRegistryAfterConfigMutation({
config: next,
reason: "policy-changed",
policyPluginIds: [id],
logger: {
warn: (message) => defaultRuntime.log(theme.warn(message)),
},
});
defaultRuntime.log(`Disabled plugin "${id}". Restart the gateway to apply.`);
});
plugins
.command("uninstall")
.description("Uninstall a plugin")
.argument("<id>", "Plugin id")
.option("--keep-files", "Keep installed files on disk", false)
.option("--keep-config", "Deprecated alias for --keep-files", false)
.option("--force", "Skip confirmation prompt", false)
.option("--dry-run", "Show what would be removed without making changes", false)
.action(async (id: string, opts: PluginUninstallOptions) => {
const { runPluginUninstallCommand } = await import("./plugins-uninstall-command.js");
await runPluginUninstallCommand(id, opts);
});
plugins
.command("install")
.description(
"Install a plugin or hook pack (path, archive, npm spec, git repo, clawhub:package, or marketplace entry)",
)
.argument(
"<path-or-spec-or-plugin>",
"Path (.ts/.js/.zip/.tgz/.tar.gz), npm package spec, or marketplace plugin name",
)
.option("-l, --link", "Link a local path instead of copying", false)
.option("--force", "Overwrite an existing installed plugin or hook pack", false)
.option("--pin", "Record npm installs as exact resolved <name>@<version>", false)
.option(
"--dangerously-force-unsafe-install",
"Bypass built-in dangerous-code install blocking (plugin hooks may still block)",
false,
)
.option(
"--marketplace <source>",
"Install a Claude marketplace plugin from a local repo/path or git/GitHub source",
)
.action(
async (
raw: string,
opts: {
dangerouslyForceUnsafeInstall?: boolean;
force?: boolean;
link?: boolean;
pin?: boolean;
marketplace?: string;
},
) => {
await tracePluginLifecyclePhaseAsync(
"install command",
async () => {
const { runPluginInstallCommand } = await import("./plugins-install-command.js");
await runPluginInstallCommand({ raw, opts });
},
{ command: "install" },
);
},
);
plugins
.command("update")
.description("Update installed plugins and tracked hook packs")
.argument("[id]", "Plugin or hook-pack id (omit with --all)")
.option("--all", "Update all tracked plugins and hook packs", false)
.option("--dry-run", "Show what would change without writing", false)
.option(
"--dangerously-force-unsafe-install",
"Bypass built-in dangerous-code update blocking for plugins (plugin hooks may still block)",
false,
)
.action(async (id: string | undefined, opts: PluginUpdateOptions) => {
const { runPluginUpdateCommand } = await import("./plugins-update-command.js");
await runPluginUpdateCommand({ id, opts });
});
plugins
.command("registry")
.description("Inspect or rebuild the persisted plugin registry")
.option("--json", "Print JSON")
.option("--refresh", "Rebuild the persisted registry from current plugin manifests", false)
.action(async (opts: PluginRegistryOptions) => {
const { inspectPluginRegistry, refreshPluginRegistry } =
await import("../plugins/plugin-registry.js");
const cfg = getRuntimeConfig();
if (opts.refresh) {
const index = await refreshPluginRegistry({
config: cfg,
reason: "manual",
});
if (opts.json) {
defaultRuntime.writeJson({
refreshed: true,
registry: index,
});
return;
}
const total = index.plugins.length;
const enabled = countEnabledPlugins(index.plugins);
defaultRuntime.log(
`Plugin registry refreshed: ${enabled}/${total} enabled plugins indexed.`,
);
return;
}
const inspection = await inspectPluginRegistry({ config: cfg });
if (opts.json) {
defaultRuntime.writeJson({
state: inspection.state,
refreshReasons: inspection.refreshReasons,
persisted: inspection.persisted,
current: inspection.current,
});
return;
}
const currentTotal = inspection.current.plugins.length;
const currentEnabled = countEnabledPlugins(inspection.current.plugins);
const persistedTotal = inspection.persisted?.plugins.length ?? 0;
const persistedEnabled = inspection.persisted
? countEnabledPlugins(inspection.persisted.plugins)
: 0;
const lines = [
`${theme.muted("State:")} ${formatRegistryState(inspection.state)}`,
`${theme.muted("Current:")} ${currentEnabled}/${currentTotal} enabled plugins`,
`${theme.muted("Persisted:")} ${persistedEnabled}/${persistedTotal} enabled plugins`,
];
if (inspection.refreshReasons.length > 0) {
lines.push(`${theme.muted("Refresh reasons:")} ${inspection.refreshReasons.join(", ")}`);
lines.push(
`${theme.muted("Repair:")} ${theme.command("openclaw plugins registry --refresh")}`,
);
}
defaultRuntime.log(lines.join("\n"));
});
plugins
.command("doctor")
.description("Report plugin load issues")
.action(async () => {
const {
buildPluginCompatibilityNotices,
buildPluginDiagnosticsReport,
formatPluginCompatibilityNotice,
} = await import("../plugins/status.js");
const report = buildPluginDiagnosticsReport({ effectiveOnly: true });
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 && compatibility.length === 0) {
defaultRuntime.log("No plugin issues detected.");
return;
}
const lines: string[] = [];
if (errors.length > 0) {
lines.push(theme.error("Plugin errors:"));
for (const entry of errors) {
const phase = entry.failurePhase ? ` [${entry.failurePhase}]` : "";
lines.push(`- ${entry.id}${phase}: ${entry.error ?? "failed to load"} (${entry.source})`);
}
}
if (diags.length > 0) {
if (lines.length > 0) {
lines.push("");
}
lines.push(theme.warn("Diagnostics:"));
for (const diag of diags) {
const target = diag.pluginId ? `${diag.pluginId}: ` : "";
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(`- ${formatPluginCompatibilityNotice(notice)} [${marker}]`);
}
}
const docs = formatDocsLink("/plugin", "docs.openclaw.ai/plugin");
lines.push("");
lines.push(`${theme.muted("Docs:")} ${docs}`);
defaultRuntime.log(lines.join("\n"));
});
const marketplace = plugins
.command("marketplace")
.description("Inspect Claude-compatible plugin marketplaces");
marketplace
.command("list")
.description("List plugins published by a marketplace source")
.argument("<source>", "Local marketplace path/repo or git/GitHub source")
.option("--json", "Print JSON")
.action(async (source: string, opts: PluginMarketplaceListOptions) => {
const { listMarketplacePlugins } = await import("../plugins/marketplace.js");
const { createPluginInstallLogger } = await import("./plugins-command-helpers.js");
const result = await listMarketplacePlugins({
marketplace: source,
logger: createPluginInstallLogger(),
});
if (!result.ok) {
defaultRuntime.error(result.error);
return defaultRuntime.exit(1);
}
if (opts.json) {
defaultRuntime.writeJson({
source: result.sourceLabel,
name: result.manifest.name,
version: result.manifest.version,
plugins: result.manifest.plugins,
});
return;
}
if (result.manifest.plugins.length === 0) {
defaultRuntime.log(`No plugins found in marketplace ${result.sourceLabel}.`);
return;
}
defaultRuntime.log(
`${theme.heading("Marketplace")} ${theme.muted(result.manifest.name ?? result.sourceLabel)}`,
);
for (const plugin of result.manifest.plugins) {
const suffix = plugin.version ? theme.muted(` v${plugin.version}`) : "";
const desc = plugin.description ? ` - ${theme.muted(plugin.description)}` : "";
defaultRuntime.log(`${theme.command(plugin.name)}${suffix}${desc}`);
}
});
applyParentDefaultHelpAction(plugins);
}