mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-23 22:55:24 +00:00
ACP: fully rename acpx plugin (#52404)
* ACP: rename acpx plugin package * ACP: fully rename acpx plugin * ACP: remove old acpx paths * Docs: add bundled plugin naming guardrails * Docs: keep plugin naming guardrails internal * ACP: keep acpx plugin id stable * ACP: drop old acpx-plugin tree
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
- Tests: colocated `*.test.ts`.
|
||||
- Docs: `docs/` (images, queue, Pi config). Built output lives in `dist/`.
|
||||
- Nomenclature: use "plugin" / "plugins" in docs, UI, changelogs, and contributor guidance. `extensions/*` remains the internal directory/package path to avoid repo-wide churn from a rename.
|
||||
- Bundled plugin naming: for repo-owned workspace plugins, keep the canonical plugin id aligned across `openclaw.plugin.json:id`, `extensions/<id>` by default, and package names anchored to the same id (`@openclaw/<id>` or approved suffix forms like `-provider`, `-plugin`, `-speech`, `-sandbox`). Keep `openclaw.install.npmSpec` equal to the package name and `openclaw.channel.id` equal to the plugin id when present. Exceptions must be explicit and covered by the repo invariant test.
|
||||
- Plugins: live under `extensions/*` (workspace packages). Keep plugin-only deps in the extension `package.json`; do not add them to the root `package.json` unless core uses them.
|
||||
- Plugins: install runs `npm install --omit=dev` in plugin dir; runtime deps must live in `dependencies`. Avoid `workspace:*` in `dependencies` (npm install breaks); put `openclaw` in `devDependencies` or `peerDependencies` instead (runtime resolves `openclaw/plugin-sdk` via jiti alias).
|
||||
- Import boundaries: extension production code should treat `openclaw/plugin-sdk/*` plus local `api.ts` / `runtime-api.ts` barrels as the public surface. Do not import core `src/**`, `src/plugin-sdk-internal/**`, or another extension's `src/**` directly.
|
||||
|
||||
@@ -510,7 +510,7 @@ See [Configuration Reference](/gateway/configuration-reference).
|
||||
Install and enable plugin:
|
||||
|
||||
```bash
|
||||
openclaw plugins install acpx
|
||||
openclaw plugins install @openclaw/acpx-plugin
|
||||
openclaw config set plugins.entries.acpx.enabled true
|
||||
```
|
||||
|
||||
@@ -528,7 +528,7 @@ Then verify backend health:
|
||||
|
||||
### acpx command and version configuration
|
||||
|
||||
By default, the acpx plugin (published as `@openclaw/acpx`) uses the plugin-local pinned binary:
|
||||
By default, the acpx backend plugin package (`@openclaw/acpx-plugin`) uses the plugin-local pinned binary:
|
||||
|
||||
1. Command defaults to `extensions/acpx/node_modules/.bin/acpx`.
|
||||
2. Expected version defaults to the extension pin.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@openclaw/acpx",
|
||||
"name": "@openclaw/acpx-plugin",
|
||||
"version": "2026.3.14",
|
||||
"description": "OpenClaw ACP runtime backend via acpx",
|
||||
"type": "module",
|
||||
|
||||
@@ -109,7 +109,7 @@ Do not default to subagent runtime for these requests.
|
||||
|
||||
## ACPX install and version policy (direct acpx path)
|
||||
|
||||
For this repo, direct `acpx` calls must follow the same pinned policy as the `@openclaw/acpx` extension.
|
||||
For this repo, direct `acpx` calls must follow the same pinned policy as the `@openclaw/acpx-plugin` extension package.
|
||||
|
||||
1. Prefer plugin-local binary, not global PATH:
|
||||
- `./extensions/acpx/node_modules/.bin/acpx`
|
||||
|
||||
@@ -22,9 +22,11 @@ afterEach(() => {
|
||||
describe("ACP install hints", () => {
|
||||
it("prefers explicit runtime install command", () => {
|
||||
const cfg = withAcpConfig({
|
||||
runtime: { installCommand: "pnpm openclaw plugins install acpx" },
|
||||
runtime: { installCommand: "pnpm openclaw plugins install @openclaw/acpx-plugin" },
|
||||
});
|
||||
expect(resolveAcpInstallCommandHint(cfg)).toBe("pnpm openclaw plugins install acpx");
|
||||
expect(resolveAcpInstallCommandHint(cfg)).toBe(
|
||||
"pnpm openclaw plugins install @openclaw/acpx-plugin",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses local acpx extension path when present", () => {
|
||||
@@ -39,13 +41,15 @@ describe("ACP install hints", () => {
|
||||
expect(hint).toContain(path.join("extensions", "acpx"));
|
||||
});
|
||||
|
||||
it("falls back to npm install hint for acpx when local extension is absent", () => {
|
||||
it("falls back to scoped install hint for acpx when local extension is absent", () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "acp-install-hint-"));
|
||||
tempDirs.push(tempRoot);
|
||||
process.chdir(tempRoot);
|
||||
|
||||
const cfg = withAcpConfig({ backend: "acpx" });
|
||||
expect(resolveAcpInstallCommandHint(cfg)).toBe("openclaw plugins install acpx");
|
||||
expect(resolveAcpInstallCommandHint(cfg)).toBe(
|
||||
"openclaw plugins install @openclaw/acpx-plugin",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns generic plugin hint for non-acpx backend", () => {
|
||||
|
||||
@@ -17,7 +17,7 @@ export function resolveAcpInstallCommandHint(cfg: OpenClawConfig): string {
|
||||
if (existsSync(localPath)) {
|
||||
return `openclaw plugins install ${localPath}`;
|
||||
}
|
||||
return "openclaw plugins install acpx";
|
||||
return "openclaw plugins install @openclaw/acpx-plugin";
|
||||
}
|
||||
return `Install and enable the plugin that provides ACP backend "${backendId}".`;
|
||||
}
|
||||
|
||||
143
src/plugins/bundled-plugin-naming.test.ts
Normal file
143
src/plugins/bundled-plugin-naming.test.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
type PluginManifestShape = {
|
||||
id?: unknown;
|
||||
};
|
||||
|
||||
type OpenClawPackageShape = {
|
||||
name?: unknown;
|
||||
openclaw?: {
|
||||
install?: {
|
||||
npmSpec?: unknown;
|
||||
};
|
||||
channel?: {
|
||||
id?: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type BundledPluginRecord = {
|
||||
dirName: string;
|
||||
packageName: string;
|
||||
manifestId: string;
|
||||
installNpmSpec?: string;
|
||||
channelId?: string;
|
||||
};
|
||||
|
||||
const EXTENSIONS_ROOT = path.resolve(process.cwd(), "extensions");
|
||||
const DIR_ID_EXCEPTIONS = new Map<string, string>([
|
||||
// Historical directory name kept until a wider repo cleanup is worth the churn.
|
||||
["kimi-coding", "kimi"],
|
||||
]);
|
||||
const ALLOWED_PACKAGE_SUFFIXES = ["", "-provider", "-plugin", "-speech", "-sandbox"] as const;
|
||||
|
||||
function readJsonFile<T>(filePath: string): T {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8")) as T;
|
||||
}
|
||||
|
||||
function normalizeText(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function readBundledPluginRecords(): BundledPluginRecord[] {
|
||||
const records: BundledPluginRecord[] = [];
|
||||
for (const dirName of fs.readdirSync(EXTENSIONS_ROOT).toSorted()) {
|
||||
const rootDir = path.join(EXTENSIONS_ROOT, dirName);
|
||||
const packagePath = path.join(rootDir, "package.json");
|
||||
const manifestPath = path.join(rootDir, "openclaw.plugin.json");
|
||||
if (!fs.existsSync(packagePath) || !fs.existsSync(manifestPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const manifest = readJsonFile<PluginManifestShape>(manifestPath);
|
||||
const pkg = readJsonFile<OpenClawPackageShape>(packagePath);
|
||||
const manifestId = normalizeText(manifest.id);
|
||||
const packageName = normalizeText(pkg.name);
|
||||
if (!manifestId || !packageName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
records.push({
|
||||
dirName,
|
||||
packageName,
|
||||
manifestId,
|
||||
installNpmSpec: normalizeText(pkg.openclaw?.install?.npmSpec),
|
||||
channelId: normalizeText(pkg.openclaw?.channel?.id),
|
||||
});
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
function resolveAllowedPackageNamesForId(pluginId: string): string[] {
|
||||
return ALLOWED_PACKAGE_SUFFIXES.map((suffix) => `@openclaw/${pluginId}${suffix}`);
|
||||
}
|
||||
|
||||
describe("bundled plugin naming guardrails", () => {
|
||||
it("keeps bundled workspace package names anchored to the plugin id", () => {
|
||||
const mismatches = readBundledPluginRecords()
|
||||
.filter(
|
||||
({ packageName, manifestId }) =>
|
||||
!resolveAllowedPackageNamesForId(manifestId).includes(packageName),
|
||||
)
|
||||
.map(
|
||||
({ dirName, packageName, manifestId }) => `${dirName}: ${packageName} (id=${manifestId})`,
|
||||
);
|
||||
|
||||
expect(
|
||||
mismatches,
|
||||
`Bundled extension package names must stay anchored to the manifest id via @openclaw/<id> or an approved suffix (${ALLOWED_PACKAGE_SUFFIXES.join(", ")}). Update the plugin naming docs and this invariant before adding a new naming form.\nFound: ${mismatches.join(", ") || "<none>"}`,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps bundled workspace directories aligned with the plugin id unless explicitly allowlisted", () => {
|
||||
const mismatches = readBundledPluginRecords()
|
||||
.filter(
|
||||
({ dirName, manifestId }) => (DIR_ID_EXCEPTIONS.get(dirName) ?? dirName) !== manifestId,
|
||||
)
|
||||
.map(({ dirName, manifestId }) => `${dirName} -> ${manifestId}`);
|
||||
|
||||
expect(
|
||||
mismatches,
|
||||
`Bundled extension directory names should match openclaw.plugin.json:id. If a legacy exception is unavoidable, add it to DIR_ID_EXCEPTIONS with a comment.\nFound: ${mismatches.join(", ") || "<none>"}`,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps bundled openclaw.install.npmSpec aligned with the package name", () => {
|
||||
const mismatches = readBundledPluginRecords()
|
||||
.filter(
|
||||
({ installNpmSpec, packageName }) =>
|
||||
typeof installNpmSpec === "string" && installNpmSpec !== packageName,
|
||||
)
|
||||
.map(
|
||||
({ dirName, packageName, installNpmSpec }) =>
|
||||
`${dirName}: package=${packageName}, npmSpec=${installNpmSpec}`,
|
||||
);
|
||||
|
||||
expect(
|
||||
mismatches,
|
||||
`Bundled openclaw.install.npmSpec values must match the package name so install/update paths stay deterministic.\nFound: ${mismatches.join(", ") || "<none>"}`,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps bundled channel ids aligned with the canonical plugin id", () => {
|
||||
const mismatches = readBundledPluginRecords()
|
||||
.filter(
|
||||
({ channelId, manifestId }) => typeof channelId === "string" && channelId !== manifestId,
|
||||
)
|
||||
.map(
|
||||
({ dirName, manifestId, channelId }) =>
|
||||
`${dirName}: channel=${channelId}, id=${manifestId}`,
|
||||
);
|
||||
|
||||
expect(
|
||||
mismatches,
|
||||
`Bundled openclaw.channel.id values must match openclaw.plugin.json:id for the owning plugin.\nFound: ${mismatches.join(", ") || "<none>"}`,
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -51,7 +51,7 @@ describe("copyBundledPluginMetadata", () => {
|
||||
skills: ["./skills"],
|
||||
});
|
||||
writeJson(path.join(pluginDir, "package.json"), {
|
||||
name: "@openclaw/acpx",
|
||||
name: "@openclaw/acpx-plugin",
|
||||
openclaw: { extensions: ["./index.ts"] },
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user