fix: discover source-only plugins in checkouts

This commit is contained in:
Peter Steinberger
2026-05-02 17:30:59 +01:00
parent cc8a8f1df1
commit 5551d9fad4
26 changed files with 227 additions and 268 deletions

View File

@@ -9,13 +9,15 @@ import {
import { resolveUserPath } from "../utils.js";
import { detectBundleManifestFormat, loadBundleManifest } from "./bundle-manifest.js";
import { resolveSourceCheckoutDependencyDiagnostic } from "./bundled-dir.js";
import { resolvePackagedBundledLoadPathAlias } from "./bundled-load-path-aliases.js";
import {
buildLegacyBundledRootPath,
resolvePackagedBundledLoadPathAlias,
} from "./bundled-load-path-aliases.js";
import { listBundledSourceOverlayDirs } from "./bundled-source-overlays.js";
import type { PluginBundleFormat, PluginDiagnostic, PluginFormat } from "./manifest-types.js";
import {
DEFAULT_PLUGIN_ENTRY_CANDIDATES,
getPackageManifestMetadata,
isPackageIncludedInCoreBundle,
loadPluginManifest,
type PluginManifest,
resolvePackageExtensionEntries,
@@ -627,10 +629,6 @@ function discoverInDirectory(params: {
const rejectHardlinks = params.origin !== "bundled";
const fullPathRealPath = safeRealpathSync(fullPath, params.realpathCache) ?? undefined;
const manifest = readPackageManifest(fullPath, rejectHardlinks, fullPathRealPath);
const packageManifest = getPackageManifestMetadata(manifest ?? undefined);
if (params.origin === "bundled" && !isPackageIncludedInCoreBundle(packageManifest)) {
continue;
}
const extensionResolution = resolvePackageExtensionEntries(manifest ?? undefined);
const extensions = extensionResolution.status === "ok" ? extensionResolution.entries : [];
const manifestId = resolveIdHintManifestId(fullPath, rejectHardlinks, fullPathRealPath);
@@ -725,6 +723,61 @@ function discoverInDirectory(params: {
}
}
function hasDiscoverablePluginTree(pluginsDir: string): boolean {
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 isSourceCheckoutExtensionsDir(extensionsDir: string): boolean {
const packageRoot = path.dirname(extensionsDir);
return (
fs.existsSync(path.join(packageRoot, ".git")) &&
fs.existsSync(path.join(packageRoot, "pnpm-workspace.yaml")) &&
fs.existsSync(path.join(packageRoot, "src")) &&
fs.existsSync(extensionsDir) &&
hasDiscoverablePluginTree(extensionsDir)
);
}
function resolveBundledSourceCheckoutExtensionsDir(bundledRoot?: string): string | undefined {
if (!bundledRoot) {
return undefined;
}
const legacyRoot = buildLegacyBundledRootPath(bundledRoot);
if (!legacyRoot || !isSourceCheckoutExtensionsDir(legacyRoot)) {
return undefined;
}
return legacyRoot;
}
function readChildDirectoryNames(dir: string | undefined): Set<string> {
if (!dir || !fs.existsSync(dir)) {
return new Set();
}
try {
return new Set(
fs
.readdirSync(dir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name),
);
} catch {
return new Set();
}
}
function discoverFromPath(params: {
rawPath: string;
origin: PluginOrigin;
@@ -996,6 +1049,24 @@ export function discoverOpenClawPlugins(params: {
realpathCache,
});
}
const sourceCheckoutExtensionsDir = resolveBundledSourceCheckoutExtensionsDir(roots.stock);
const sourceCheckoutMatchesBundledRoot = resolvesToSameDirectory(
sourceCheckoutExtensionsDir,
roots.stock,
realpathCache,
);
if (sourceCheckoutExtensionsDir && !sourceCheckoutMatchesBundledRoot) {
discoverInDirectory({
dir: sourceCheckoutExtensionsDir,
origin: "bundled",
ownershipUid: params.ownershipUid,
candidates: result.candidates,
diagnostics: result.diagnostics,
seen,
realpathCache,
skipDirectories: readChildDirectoryNames(roots.stock),
});
}
for (const installedPath of collectInstalledPluginRecordPaths(params.installRecords, env)) {
discoverFromPath({
rawPath: installedPath,