fix(plugins): prefer usable bundled plugin trees

This commit is contained in:
Vincent Koc
2026-04-06 18:51:33 +01:00
parent e77d72a91d
commit 739ce82015
2 changed files with 73 additions and 3 deletions

View File

@@ -46,6 +46,21 @@ function createOpenClawRoot(params: {
return repoRoot;
}
function seedBundledPluginTree(rootDir: string, relativeDir: string, pluginId = "discord") {
const pluginDir = path.join(rootDir, relativeDir, pluginId);
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(
path.join(pluginDir, "package.json"),
`${JSON.stringify({ name: `@openclaw/${pluginId}` }, null, 2)}\n`,
"utf8",
);
fs.writeFileSync(
path.join(pluginDir, "openclaw.plugin.json"),
`${JSON.stringify({ id: pluginId }, null, 2)}\n`,
"utf8",
);
}
function expectResolvedBundledDir(params: {
cwd: string;
expectedDir: string;
@@ -205,6 +220,12 @@ describe("resolveBundledPluginsDir", () => {
],
] as const)("%s", (_name, layout, expectation) => {
const repoRoot = createOpenClawRoot(layout);
if (expectation.expectedRelativeDir === path.join("dist-runtime", "extensions")) {
seedBundledPluginTree(repoRoot, path.join("dist", "extensions"));
seedBundledPluginTree(repoRoot, path.join("dist-runtime", "extensions"));
} else if (expectation.expectedRelativeDir === path.join("dist", "extensions")) {
seedBundledPluginTree(repoRoot, path.join("dist", "extensions"));
}
expectResolvedBundledDirFromRoot({
repoRoot,
expectedRelativeDir: expectation.expectedRelativeDir,
@@ -212,6 +233,26 @@ describe("resolveBundledPluginsDir", () => {
});
});
it("falls back to source extensions when dist trees exist but do not contain real plugin manifests", () => {
const repoRoot = createOpenClawRoot({
prefix: "openclaw-bundled-dir-incomplete-built-",
hasExtensions: true,
hasSrc: true,
hasDistRuntimeExtensions: true,
hasDistExtensions: true,
hasGitCheckout: true,
});
fs.mkdirSync(path.join(repoRoot, "dist", "extensions", "discord"), { recursive: true });
fs.mkdirSync(path.join(repoRoot, "dist-runtime", "extensions", "discord"), {
recursive: true,
});
expectResolvedBundledDirFromRoot({
repoRoot,
expectedRelativeDir: "extensions",
});
});
it("returns a stable empty bundled plugin directory when bundled plugins are disabled", () => {
const repoRoot = createOpenClawRoot({
prefix: "openclaw-bundled-dir-disabled-",
@@ -239,6 +280,7 @@ describe("resolveBundledPluginsDir", () => {
prefix: "openclaw-bundled-dir-installed-",
hasDistExtensions: true,
});
seedBundledPluginTree(installedRoot, path.join("dist", "extensions"));
const cwdRepoRoot = createOpenClawRoot({
prefix: "openclaw-bundled-dir-cwd-",
hasExtensions: true,
@@ -259,6 +301,7 @@ describe("resolveBundledPluginsDir", () => {
prefix: "openclaw-bundled-dir-override-",
hasDistExtensions: true,
});
seedBundledPluginTree(installedRoot, path.join("dist", "extensions"));
return {
installedRoot,
argv1: path.join(installedRoot, "openclaw.mjs"),

View File

@@ -25,12 +25,33 @@ function isSourceCheckoutRoot(packageRoot: string): boolean {
);
}
function hasUsableBundledPluginTree(pluginsDir: string): boolean {
if (!fs.existsSync(pluginsDir)) {
return false;
}
try {
return fs.readdirSync(pluginsDir, { withFileTypes: true }).some((entry) => {
if (!entry.isDirectory()) {
return false;
}
const pluginDir = path.join(pluginsDir, entry.name);
return (
fs.existsSync(path.join(pluginDir, "package.json")) ||
fs.existsSync(path.join(pluginDir, "openclaw.plugin.json"))
);
});
} catch {
return false;
}
}
function resolveBundledDirFromPackageRoot(
packageRoot: string,
preferSourceCheckout: boolean,
): string | undefined {
const sourceExtensionsDir = path.join(packageRoot, "extensions");
const builtExtensionsDir = path.join(packageRoot, "dist", "extensions");
const sourceCheckout = isSourceCheckoutRoot(packageRoot);
if (preferSourceCheckout && fs.existsSync(sourceExtensionsDir)) {
return sourceExtensionsDir;
}
@@ -38,13 +59,19 @@ function resolveBundledDirFromPackageRoot(
// dist-runtime/. Prefer that over source extensions only when the paired
// dist/ tree exists; otherwise wrappers can drift ahead of the last build.
const runtimeExtensionsDir = path.join(packageRoot, "dist-runtime", "extensions");
if (fs.existsSync(runtimeExtensionsDir) && fs.existsSync(builtExtensionsDir)) {
const hasUsableRuntimeTree = sourceCheckout
? hasUsableBundledPluginTree(runtimeExtensionsDir)
: fs.existsSync(runtimeExtensionsDir);
const hasUsableBuiltTree = sourceCheckout
? hasUsableBundledPluginTree(builtExtensionsDir)
: fs.existsSync(builtExtensionsDir);
if (hasUsableRuntimeTree && hasUsableBuiltTree) {
return runtimeExtensionsDir;
}
if (fs.existsSync(builtExtensionsDir)) {
if (hasUsableBuiltTree) {
return builtExtensionsDir;
}
if (isSourceCheckoutRoot(packageRoot) && fs.existsSync(sourceExtensionsDir)) {
if (sourceCheckout && fs.existsSync(sourceExtensionsDir)) {
return sourceExtensionsDir;
}
return undefined;