mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-21 06:02:13 +00:00
feat: add slash plugin installs
This commit is contained in:
201
src/auto-reply/reply/commands-plugins.install.test.ts
Normal file
201
src/auto-reply/reply/commands-plugins.install.test.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { withTempHome } from "../../config/home-env.test-harness.js";
|
||||
import { handleCommands } from "./commands-core.js";
|
||||
import { createCommandWorkspaceHarness } from "./commands-filesystem.test-support.js";
|
||||
import { buildCommandTestParams } from "./commands.test-harness.js";
|
||||
|
||||
const installPluginFromPathMock = vi.fn();
|
||||
const installPluginFromClawHubMock = vi.fn();
|
||||
const persistPluginInstallMock = vi.fn();
|
||||
|
||||
vi.mock("../../plugins/install.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../plugins/install.js")>(
|
||||
"../../plugins/install.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
installPluginFromPath: installPluginFromPathMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../plugins/clawhub.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../plugins/clawhub.js")>(
|
||||
"../../plugins/clawhub.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
installPluginFromClawHub: installPluginFromClawHubMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../cli/plugins-install-persist.js", () => ({
|
||||
persistPluginInstall: persistPluginInstallMock,
|
||||
}));
|
||||
|
||||
const workspaceHarness = createCommandWorkspaceHarness("openclaw-command-plugins-install-");
|
||||
|
||||
describe("handleCommands /plugins install", () => {
|
||||
afterEach(async () => {
|
||||
installPluginFromPathMock.mockReset();
|
||||
installPluginFromClawHubMock.mockReset();
|
||||
persistPluginInstallMock.mockReset();
|
||||
await workspaceHarness.cleanupWorkspaces();
|
||||
});
|
||||
|
||||
it("installs a plugin from a local path", async () => {
|
||||
installPluginFromPathMock.mockResolvedValue({
|
||||
ok: true,
|
||||
pluginId: "path-install-plugin",
|
||||
targetDir: "/tmp/path-install-plugin",
|
||||
version: "0.0.1",
|
||||
extensions: ["index.js"],
|
||||
});
|
||||
persistPluginInstallMock.mockResolvedValue({});
|
||||
|
||||
await withTempHome("openclaw-command-plugins-home-", async () => {
|
||||
const workspaceDir = await workspaceHarness.createWorkspace();
|
||||
const pluginDir = path.join(workspaceDir, "fixtures", "path-install-plugin");
|
||||
await fs.mkdir(pluginDir, { recursive: true });
|
||||
|
||||
const params = buildCommandTestParams(
|
||||
`/plugins install ${pluginDir}`,
|
||||
{
|
||||
commands: {
|
||||
text: true,
|
||||
plugins: true,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{ workspaceDir },
|
||||
);
|
||||
params.command.senderIsOwner = true;
|
||||
|
||||
const result = await handleCommands(params);
|
||||
expect(result.reply?.text).toContain('Installed plugin "path-install-plugin"');
|
||||
expect(installPluginFromPathMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
path: pluginDir,
|
||||
}),
|
||||
);
|
||||
expect(persistPluginInstallMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pluginId: "path-install-plugin",
|
||||
install: expect.objectContaining({
|
||||
source: "path",
|
||||
sourcePath: pluginDir,
|
||||
installPath: "/tmp/path-install-plugin",
|
||||
version: "0.0.1",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("installs from an explicit clawhub: spec", async () => {
|
||||
installPluginFromClawHubMock.mockResolvedValue({
|
||||
ok: true,
|
||||
pluginId: "clawhub-demo",
|
||||
targetDir: "/tmp/clawhub-demo",
|
||||
version: "1.2.3",
|
||||
extensions: ["index.js"],
|
||||
packageName: "@openclaw/clawhub-demo",
|
||||
clawhub: {
|
||||
source: "clawhub",
|
||||
clawhubUrl: "https://clawhub.ai",
|
||||
clawhubPackage: "@openclaw/clawhub-demo",
|
||||
clawhubFamily: "code-plugin",
|
||||
clawhubChannel: "official",
|
||||
version: "1.2.3",
|
||||
integrity: "sha512-demo",
|
||||
resolvedAt: "2026-03-22T12:00:00.000Z",
|
||||
},
|
||||
});
|
||||
persistPluginInstallMock.mockResolvedValue({});
|
||||
|
||||
await withTempHome("openclaw-command-plugins-home-", async () => {
|
||||
const workspaceDir = await workspaceHarness.createWorkspace();
|
||||
const params = buildCommandTestParams(
|
||||
"/plugins install clawhub:@openclaw/clawhub-demo@1.2.3",
|
||||
{
|
||||
commands: {
|
||||
text: true,
|
||||
plugins: true,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{ workspaceDir },
|
||||
);
|
||||
params.command.senderIsOwner = true;
|
||||
|
||||
const result = await handleCommands(params);
|
||||
expect(result.reply?.text).toContain('Installed plugin "clawhub-demo"');
|
||||
expect(installPluginFromClawHubMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "clawhub:@openclaw/clawhub-demo@1.2.3",
|
||||
}),
|
||||
);
|
||||
expect(persistPluginInstallMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pluginId: "clawhub-demo",
|
||||
install: expect.objectContaining({
|
||||
source: "clawhub",
|
||||
spec: "clawhub:@openclaw/clawhub-demo@1.2.3",
|
||||
installPath: "/tmp/clawhub-demo",
|
||||
version: "1.2.3",
|
||||
integrity: "sha512-demo",
|
||||
clawhubPackage: "@openclaw/clawhub-demo",
|
||||
clawhubChannel: "official",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("treats /plugin add as an install alias", async () => {
|
||||
installPluginFromClawHubMock.mockResolvedValue({
|
||||
ok: true,
|
||||
pluginId: "alias-demo",
|
||||
targetDir: "/tmp/alias-demo",
|
||||
version: "1.0.0",
|
||||
extensions: ["index.js"],
|
||||
packageName: "@openclaw/alias-demo",
|
||||
clawhub: {
|
||||
source: "clawhub",
|
||||
clawhubUrl: "https://clawhub.ai",
|
||||
clawhubPackage: "@openclaw/alias-demo",
|
||||
clawhubFamily: "code-plugin",
|
||||
clawhubChannel: "official",
|
||||
version: "1.0.0",
|
||||
integrity: "sha512-alias",
|
||||
resolvedAt: "2026-03-23T12:00:00.000Z",
|
||||
},
|
||||
});
|
||||
persistPluginInstallMock.mockResolvedValue({});
|
||||
|
||||
await withTempHome("openclaw-command-plugins-home-", async () => {
|
||||
const workspaceDir = await workspaceHarness.createWorkspace();
|
||||
const params = buildCommandTestParams(
|
||||
"/plugin add clawhub:@openclaw/alias-demo@1.0.0",
|
||||
{
|
||||
commands: {
|
||||
text: true,
|
||||
plugins: true,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{ workspaceDir },
|
||||
);
|
||||
params.command.senderIsOwner = true;
|
||||
|
||||
const result = await handleCommands(params);
|
||||
expect(result.reply?.text).toContain('Installed plugin "alias-demo"');
|
||||
expect(installPluginFromClawHubMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "clawhub:@openclaw/alias-demo@1.0.0",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,9 @@ async function createClaudeBundlePlugin(params: { workspaceDir: string; pluginId
|
||||
|
||||
function buildCfg(): OpenClawConfig {
|
||||
return {
|
||||
plugins: {
|
||||
enabled: true,
|
||||
},
|
||||
commands: {
|
||||
text: true,
|
||||
plugins: true,
|
||||
@@ -81,50 +84,6 @@ describe("handleCommands /plugins", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("enables and disables a discovered plugin", async () => {
|
||||
await withTempHome("openclaw-command-plugins-home-", async () => {
|
||||
const workspaceDir = await workspaceHarness.createWorkspace();
|
||||
await createClaudeBundlePlugin({ workspaceDir, pluginId: "superpowers" });
|
||||
|
||||
const enableParams = buildCommandTestParams(
|
||||
"/plugins enable superpowers",
|
||||
buildCfg(),
|
||||
undefined,
|
||||
{
|
||||
workspaceDir,
|
||||
},
|
||||
);
|
||||
enableParams.command.senderIsOwner = true;
|
||||
const enableResult = await handleCommands(enableParams);
|
||||
expect(enableResult.reply?.text).toContain('Plugin "superpowers" enabled');
|
||||
|
||||
const showEnabledParams = buildCommandTestParams(
|
||||
"/plugins show superpowers",
|
||||
buildCfg(),
|
||||
undefined,
|
||||
{
|
||||
workspaceDir,
|
||||
},
|
||||
);
|
||||
showEnabledParams.command.senderIsOwner = true;
|
||||
const showEnabledResult = await handleCommands(showEnabledParams);
|
||||
expect(showEnabledResult.reply?.text).toContain('"status": "loaded"');
|
||||
expect(showEnabledResult.reply?.text).toContain('"enabled": true');
|
||||
|
||||
const disableParams = buildCommandTestParams(
|
||||
"/plugins disable superpowers",
|
||||
buildCfg(),
|
||||
undefined,
|
||||
{
|
||||
workspaceDir,
|
||||
},
|
||||
);
|
||||
disableParams.command.senderIsOwner = true;
|
||||
const disableResult = await handleCommands(disableParams);
|
||||
expect(disableResult.reply?.text).toContain('Plugin "superpowers" disabled');
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects internal writes without operator.admin", async () => {
|
||||
await withTempHome("openclaw-command-plugins-home-", async () => {
|
||||
const workspaceDir = await workspaceHarness.createWorkspace();
|
||||
|
||||
169
src/auto-reply/reply/commands-plugins.toggle.test.ts
Normal file
169
src/auto-reply/reply/commands-plugins.toggle.test.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const {
|
||||
readConfigFileSnapshotMock,
|
||||
validateConfigObjectWithPluginsMock,
|
||||
writeConfigFileMock,
|
||||
buildPluginStatusReportMock,
|
||||
} = vi.hoisted(() => ({
|
||||
readConfigFileSnapshotMock: vi.fn(),
|
||||
validateConfigObjectWithPluginsMock: vi.fn(),
|
||||
writeConfigFileMock: vi.fn(),
|
||||
buildPluginStatusReportMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../config/config.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("../../config/config.js")>("../../config/config.js");
|
||||
return {
|
||||
...actual,
|
||||
readConfigFileSnapshot: readConfigFileSnapshotMock,
|
||||
validateConfigObjectWithPlugins: validateConfigObjectWithPluginsMock,
|
||||
writeConfigFile: writeConfigFileMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../plugins/status.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("../../plugins/status.js")>("../../plugins/status.js");
|
||||
return {
|
||||
...actual,
|
||||
buildPluginStatusReport: buildPluginStatusReportMock,
|
||||
};
|
||||
});
|
||||
|
||||
import { handleCommands } from "./commands-core.js";
|
||||
import { buildCommandTestParams } from "./commands.test-harness.js";
|
||||
|
||||
function buildCfg() {
|
||||
return {
|
||||
plugins: {
|
||||
enabled: true,
|
||||
},
|
||||
commands: {
|
||||
text: true,
|
||||
plugins: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("handleCommands /plugins toggle", () => {
|
||||
afterEach(() => {
|
||||
readConfigFileSnapshotMock.mockReset();
|
||||
validateConfigObjectWithPluginsMock.mockReset();
|
||||
writeConfigFileMock.mockReset();
|
||||
buildPluginStatusReportMock.mockReset();
|
||||
});
|
||||
|
||||
it("enables a discovered plugin", async () => {
|
||||
const config = buildCfg();
|
||||
readConfigFileSnapshotMock.mockResolvedValue({
|
||||
valid: true,
|
||||
path: "/tmp/openclaw.json",
|
||||
resolved: config,
|
||||
});
|
||||
buildPluginStatusReportMock.mockReturnValue({
|
||||
workspaceDir: "/tmp/workspace",
|
||||
plugins: [
|
||||
{
|
||||
id: "superpowers",
|
||||
name: "superpowers",
|
||||
format: "bundle",
|
||||
source: "/tmp/workspace/.openclaw/extensions/superpowers",
|
||||
origin: "workspace",
|
||||
enabled: false,
|
||||
status: "disabled",
|
||||
toolNames: [],
|
||||
hookNames: [],
|
||||
channelIds: [],
|
||||
providerIds: [],
|
||||
speechProviderIds: [],
|
||||
mediaUnderstandingProviderIds: [],
|
||||
imageGenerationProviderIds: [],
|
||||
webSearchProviderIds: [],
|
||||
gatewayMethods: [],
|
||||
cliCommands: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
httpRoutes: 0,
|
||||
hookCount: 0,
|
||||
configSchema: false,
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
validateConfigObjectWithPluginsMock.mockImplementation((next) => ({ ok: true, config: next }));
|
||||
writeConfigFileMock.mockResolvedValue(undefined);
|
||||
|
||||
const params = buildCommandTestParams("/plugins enable superpowers", buildCfg());
|
||||
params.command.senderIsOwner = true;
|
||||
|
||||
const result = await handleCommands(params);
|
||||
expect(result.reply?.text).toContain('Plugin "superpowers" enabled');
|
||||
expect(writeConfigFileMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
plugins: expect.objectContaining({
|
||||
entries: expect.objectContaining({
|
||||
superpowers: expect.objectContaining({ enabled: true }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("disables a discovered plugin", async () => {
|
||||
const config = buildCfg();
|
||||
readConfigFileSnapshotMock.mockResolvedValue({
|
||||
valid: true,
|
||||
path: "/tmp/openclaw.json",
|
||||
resolved: config,
|
||||
});
|
||||
buildPluginStatusReportMock.mockReturnValue({
|
||||
workspaceDir: "/tmp/workspace",
|
||||
plugins: [
|
||||
{
|
||||
id: "superpowers",
|
||||
name: "superpowers",
|
||||
format: "bundle",
|
||||
source: "/tmp/workspace/.openclaw/extensions/superpowers",
|
||||
origin: "workspace",
|
||||
enabled: true,
|
||||
status: "loaded",
|
||||
toolNames: [],
|
||||
hookNames: [],
|
||||
channelIds: [],
|
||||
providerIds: [],
|
||||
speechProviderIds: [],
|
||||
mediaUnderstandingProviderIds: [],
|
||||
imageGenerationProviderIds: [],
|
||||
webSearchProviderIds: [],
|
||||
gatewayMethods: [],
|
||||
cliCommands: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
httpRoutes: 0,
|
||||
hookCount: 0,
|
||||
configSchema: false,
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
validateConfigObjectWithPluginsMock.mockImplementation((next) => ({ ok: true, config: next }));
|
||||
writeConfigFileMock.mockResolvedValue(undefined);
|
||||
|
||||
const params = buildCommandTestParams("/plugins disable superpowers", buildCfg());
|
||||
params.command.senderIsOwner = true;
|
||||
|
||||
const result = await handleCommands(params);
|
||||
expect(result.reply?.text).toContain('Plugin "superpowers" disabled');
|
||||
expect(writeConfigFileMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
plugins: expect.objectContaining({
|
||||
entries: expect.objectContaining({
|
||||
superpowers: expect.objectContaining({ enabled: false }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,12 @@
|
||||
import fs from "node:fs";
|
||||
import { buildNpmInstallRecordFields } from "../../cli/npm-resolution.js";
|
||||
import {
|
||||
buildPreferredClawHubSpec,
|
||||
createPluginInstallLogger,
|
||||
decidePreferredClawHubFallback,
|
||||
resolveFileNpmSpecToLocalPath,
|
||||
} from "../../cli/plugins-command-helpers.js";
|
||||
import { persistPluginInstall } from "../../cli/plugins-install-persist.js";
|
||||
import {
|
||||
readConfigFileSnapshot,
|
||||
validateConfigObjectWithPlugins,
|
||||
@@ -5,6 +14,11 @@ import {
|
||||
} from "../../config/config.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { PluginInstallRecord } from "../../config/types.plugins.js";
|
||||
import { resolveArchiveKind } from "../../infra/archive.js";
|
||||
import { parseClawHubPluginSpec } from "../../infra/clawhub.js";
|
||||
import { installPluginFromClawHub } from "../../plugins/clawhub.js";
|
||||
import { installPluginFromNpmSpec, installPluginFromPath } from "../../plugins/install.js";
|
||||
import { clearPluginManifestRegistryCache } from "../../plugins/manifest-registry.js";
|
||||
import type { PluginRecord } from "../../plugins/registry.js";
|
||||
import {
|
||||
buildAllPluginInspectReports,
|
||||
@@ -14,6 +28,7 @@ import {
|
||||
type PluginStatusReport,
|
||||
} from "../../plugins/status.js";
|
||||
import { setPluginEnabledInConfig } from "../../plugins/toggle-config.js";
|
||||
import { resolveUserPath } from "../../utils.js";
|
||||
import { isInternalMessageChannel } from "../../utils/message-channel.js";
|
||||
import {
|
||||
rejectNonOwnerCommand,
|
||||
@@ -121,6 +136,142 @@ function findPlugin(report: PluginStatusReport, rawName: string): PluginRecord |
|
||||
);
|
||||
}
|
||||
|
||||
function looksLikeLocalPluginInstallSpec(raw: string): boolean {
|
||||
return (
|
||||
raw.startsWith(".") ||
|
||||
raw.startsWith("~") ||
|
||||
raw.startsWith("/") ||
|
||||
raw.endsWith(".ts") ||
|
||||
raw.endsWith(".js") ||
|
||||
raw.endsWith(".mjs") ||
|
||||
raw.endsWith(".cjs") ||
|
||||
raw.endsWith(".tgz") ||
|
||||
raw.endsWith(".tar.gz") ||
|
||||
raw.endsWith(".tar") ||
|
||||
raw.endsWith(".zip")
|
||||
);
|
||||
}
|
||||
|
||||
async function installPluginFromPluginsCommand(params: {
|
||||
raw: string;
|
||||
config: OpenClawConfig;
|
||||
}): Promise<{ ok: true; pluginId: string } | { ok: false; error: string }> {
|
||||
const fileSpec = resolveFileNpmSpecToLocalPath(params.raw);
|
||||
if (fileSpec && !fileSpec.ok) {
|
||||
return { ok: false, error: fileSpec.error };
|
||||
}
|
||||
const normalized = fileSpec && fileSpec.ok ? fileSpec.path : params.raw;
|
||||
const resolved = resolveUserPath(normalized);
|
||||
|
||||
if (fs.existsSync(resolved)) {
|
||||
const result = await installPluginFromPath({
|
||||
path: resolved,
|
||||
logger: createPluginInstallLogger(),
|
||||
});
|
||||
if (!result.ok) {
|
||||
return { ok: false, error: result.error };
|
||||
}
|
||||
clearPluginManifestRegistryCache();
|
||||
const source: "archive" | "path" = resolveArchiveKind(resolved) ? "archive" : "path";
|
||||
await persistPluginInstall({
|
||||
config: params.config,
|
||||
pluginId: result.pluginId,
|
||||
install: {
|
||||
source,
|
||||
sourcePath: resolved,
|
||||
installPath: result.targetDir,
|
||||
version: result.version,
|
||||
},
|
||||
});
|
||||
return { ok: true, pluginId: result.pluginId };
|
||||
}
|
||||
|
||||
if (looksLikeLocalPluginInstallSpec(params.raw)) {
|
||||
return { ok: false, error: `Path not found: ${resolved}` };
|
||||
}
|
||||
|
||||
const clawhubSpec = parseClawHubPluginSpec(params.raw);
|
||||
if (clawhubSpec) {
|
||||
const result = await installPluginFromClawHub({
|
||||
spec: params.raw,
|
||||
logger: createPluginInstallLogger(),
|
||||
});
|
||||
if (!result.ok) {
|
||||
return { ok: false, error: result.error };
|
||||
}
|
||||
clearPluginManifestRegistryCache();
|
||||
await persistPluginInstall({
|
||||
config: params.config,
|
||||
pluginId: result.pluginId,
|
||||
install: {
|
||||
source: "clawhub",
|
||||
spec: params.raw,
|
||||
installPath: result.targetDir,
|
||||
version: result.version,
|
||||
integrity: result.clawhub.integrity,
|
||||
resolvedAt: result.clawhub.resolvedAt,
|
||||
clawhubUrl: result.clawhub.clawhubUrl,
|
||||
clawhubPackage: result.clawhub.clawhubPackage,
|
||||
clawhubFamily: result.clawhub.clawhubFamily,
|
||||
clawhubChannel: result.clawhub.clawhubChannel,
|
||||
},
|
||||
});
|
||||
return { ok: true, pluginId: result.pluginId };
|
||||
}
|
||||
|
||||
const preferredClawHubSpec = buildPreferredClawHubSpec(params.raw);
|
||||
if (preferredClawHubSpec) {
|
||||
const clawhubResult = await installPluginFromClawHub({
|
||||
spec: preferredClawHubSpec,
|
||||
logger: createPluginInstallLogger(),
|
||||
});
|
||||
if (clawhubResult.ok) {
|
||||
clearPluginManifestRegistryCache();
|
||||
await persistPluginInstall({
|
||||
config: params.config,
|
||||
pluginId: clawhubResult.pluginId,
|
||||
install: {
|
||||
source: "clawhub",
|
||||
spec: preferredClawHubSpec,
|
||||
installPath: clawhubResult.targetDir,
|
||||
version: clawhubResult.version,
|
||||
integrity: clawhubResult.clawhub.integrity,
|
||||
resolvedAt: clawhubResult.clawhub.resolvedAt,
|
||||
clawhubUrl: clawhubResult.clawhub.clawhubUrl,
|
||||
clawhubPackage: clawhubResult.clawhub.clawhubPackage,
|
||||
clawhubFamily: clawhubResult.clawhub.clawhubFamily,
|
||||
clawhubChannel: clawhubResult.clawhub.clawhubChannel,
|
||||
},
|
||||
});
|
||||
return { ok: true, pluginId: clawhubResult.pluginId };
|
||||
}
|
||||
if (decidePreferredClawHubFallback(clawhubResult) !== "fallback_to_npm") {
|
||||
return { ok: false, error: clawhubResult.error };
|
||||
}
|
||||
}
|
||||
|
||||
const result = await installPluginFromNpmSpec({
|
||||
spec: params.raw,
|
||||
logger: createPluginInstallLogger(),
|
||||
});
|
||||
if (!result.ok) {
|
||||
return { ok: false, error: result.error };
|
||||
}
|
||||
clearPluginManifestRegistryCache();
|
||||
const installRecord = buildNpmInstallRecordFields({
|
||||
spec: params.raw,
|
||||
installPath: result.targetDir,
|
||||
version: result.version,
|
||||
resolution: result.npmResolution,
|
||||
});
|
||||
await persistPluginInstall({
|
||||
config: params.config,
|
||||
pluginId: result.pluginId,
|
||||
install: installRecord,
|
||||
});
|
||||
return { ok: true, pluginId: result.pluginId };
|
||||
}
|
||||
|
||||
async function loadPluginCommandState(workspaceDir: string): Promise<
|
||||
| {
|
||||
ok: true;
|
||||
@@ -226,6 +377,7 @@ export const handlePluginsCommand: CommandHandler = async (params, allowTextComm
|
||||
reply: {
|
||||
text: renderJsonBlock(`🔌 Plugin "${payload.inspect.plugin.id}"`, {
|
||||
...payload.inspect,
|
||||
compatibilityWarnings: payload.compatibilityWarnings,
|
||||
install: payload.install,
|
||||
}),
|
||||
},
|
||||
@@ -235,12 +387,31 @@ export const handlePluginsCommand: CommandHandler = async (params, allowTextComm
|
||||
const missingAdminScope = requireGatewayClientScopeForInternalChannel(params, {
|
||||
label: "/plugins write",
|
||||
allowedScopes: ["operator.admin"],
|
||||
missingText: "❌ /plugins enable|disable requires operator.admin for gateway clients.",
|
||||
missingText: "❌ /plugins install|enable|disable requires operator.admin for gateway clients.",
|
||||
});
|
||||
if (missingAdminScope) {
|
||||
return missingAdminScope;
|
||||
}
|
||||
|
||||
if (pluginsCommand.action === "install") {
|
||||
const installed = await installPluginFromPluginsCommand({
|
||||
raw: pluginsCommand.spec,
|
||||
config: structuredClone(loaded.config),
|
||||
});
|
||||
if (!installed.ok) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: `⚠️ ${installed.error}` },
|
||||
};
|
||||
}
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text: `🔌 Installed plugin "${installed.pluginId}". Restart the gateway to load plugins.`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const plugin = findPlugin(loaded.report, pluginsCommand.name);
|
||||
if (!plugin) {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export type PluginsCommand =
|
||||
| { action: "list" }
|
||||
| { action: "inspect"; name?: string }
|
||||
| { action: "install"; spec: string }
|
||||
| { action: "enable"; name: string }
|
||||
| { action: "disable"; name: string }
|
||||
| { action: "error"; message: string };
|
||||
@@ -33,6 +34,16 @@ export function parsePluginsCommand(raw: string): PluginsCommand | null {
|
||||
return { action: "inspect", name: name || undefined };
|
||||
}
|
||||
|
||||
if (action === "install" || action === "add") {
|
||||
if (!name) {
|
||||
return {
|
||||
action: "error",
|
||||
message: "Usage: /plugins install <path|archive|npm-spec|clawhub:pkg>",
|
||||
};
|
||||
}
|
||||
return { action: "install", spec: name };
|
||||
}
|
||||
|
||||
if (action === "enable" || action === "disable") {
|
||||
if (!name) {
|
||||
return {
|
||||
@@ -45,6 +56,6 @@ export function parsePluginsCommand(raw: string): PluginsCommand | null {
|
||||
|
||||
return {
|
||||
action: "error",
|
||||
message: "Usage: /plugins list|inspect|show|get|enable|disable [plugin]",
|
||||
message: "Usage: /plugins list|inspect|show|get|install|enable|disable [plugin]",
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user