mirror of https://github.com/openclaw/openclaw.git
Merge branch 'main' into fix/followup-audio-transcription
This commit is contained in:
commit
cacd3f10d5
|
|
@ -1,7 +1,13 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { removeFileIfExists, writeTextFileIfChanged } from "./runtime-postbuild-shared.mjs";
|
||||
import {
|
||||
removeFileIfExists,
|
||||
removePathIfExists,
|
||||
writeTextFileIfChanged,
|
||||
} from "./runtime-postbuild-shared.mjs";
|
||||
|
||||
const GENERATED_BUNDLED_SKILLS_DIR = "bundled-skills";
|
||||
|
||||
export function rewritePackageExtensions(entries) {
|
||||
if (!Array.isArray(entries)) {
|
||||
|
|
@ -30,6 +36,31 @@ function ensurePathInsideRoot(rootDir, rawPath) {
|
|||
throw new Error(`path escapes plugin root: ${rawPath}`);
|
||||
}
|
||||
|
||||
function normalizeManifestRelativePath(rawPath) {
|
||||
return rawPath.replaceAll("\\", "/").replace(/^\.\//u, "");
|
||||
}
|
||||
|
||||
function resolveBundledSkillTarget(rawPath) {
|
||||
const normalized = normalizeManifestRelativePath(rawPath);
|
||||
if (/^node_modules(?:\/|$)/u.test(normalized)) {
|
||||
// Bundled dist/plugin roots must not publish nested node_modules trees. Relocate
|
||||
// dependency-backed skill assets into a dist-owned directory and rewrite the manifest.
|
||||
const trimmed = normalized.replace(/^node_modules\/?/u, "");
|
||||
if (!trimmed) {
|
||||
throw new Error(`node_modules skill path must point to a package: ${rawPath}`);
|
||||
}
|
||||
const bundledRelativePath = `${GENERATED_BUNDLED_SKILLS_DIR}/${trimmed}`;
|
||||
return {
|
||||
manifestPath: `./${bundledRelativePath}`,
|
||||
outputPath: bundledRelativePath,
|
||||
};
|
||||
}
|
||||
return {
|
||||
manifestPath: rawPath,
|
||||
outputPath: normalized,
|
||||
};
|
||||
}
|
||||
|
||||
function copyDeclaredPluginSkillPaths(params) {
|
||||
const skills = Array.isArray(params.manifest.skills) ? params.manifest.skills : [];
|
||||
const copiedSkills = [];
|
||||
|
|
@ -37,8 +68,8 @@ function copyDeclaredPluginSkillPaths(params) {
|
|||
if (typeof raw !== "string" || raw.trim().length === 0) {
|
||||
continue;
|
||||
}
|
||||
const normalized = raw.replace(/^\.\//u, "");
|
||||
const sourcePath = ensurePathInsideRoot(params.pluginDir, raw);
|
||||
const target = resolveBundledSkillTarget(raw);
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
// Some Docker/lightweight builds intentionally omit optional plugin-local
|
||||
// dependencies. Only advertise skill paths that were actually bundled.
|
||||
|
|
@ -47,14 +78,25 @@ function copyDeclaredPluginSkillPaths(params) {
|
|||
);
|
||||
continue;
|
||||
}
|
||||
const targetPath = ensurePathInsideRoot(params.distPluginDir, normalized);
|
||||
const targetPath = ensurePathInsideRoot(params.distPluginDir, target.outputPath);
|
||||
removePathIfExists(targetPath);
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
const shouldExcludeNestedNodeModules = /^node_modules(?:\/|$)/u.test(
|
||||
normalizeManifestRelativePath(raw),
|
||||
);
|
||||
fs.cpSync(sourcePath, targetPath, {
|
||||
dereference: true,
|
||||
force: true,
|
||||
recursive: true,
|
||||
filter: (candidatePath) => {
|
||||
if (!shouldExcludeNestedNodeModules || candidatePath === sourcePath) {
|
||||
return true;
|
||||
}
|
||||
const relativeCandidate = path.relative(sourcePath, candidatePath).replaceAll("\\", "/");
|
||||
return !relativeCandidate.split("/").includes("node_modules");
|
||||
},
|
||||
});
|
||||
copiedSkills.push(raw);
|
||||
copiedSkills.push(target.manifestPath);
|
||||
}
|
||||
return copiedSkills;
|
||||
}
|
||||
|
|
@ -68,6 +110,12 @@ export function copyBundledPluginMetadata(params = {}) {
|
|||
}
|
||||
|
||||
const sourcePluginDirs = new Set();
|
||||
const removeGeneratedPluginArtifacts = (distPluginDir) => {
|
||||
removeFileIfExists(path.join(distPluginDir, "openclaw.plugin.json"));
|
||||
removeFileIfExists(path.join(distPluginDir, "package.json"));
|
||||
removePathIfExists(path.join(distPluginDir, GENERATED_BUNDLED_SKILLS_DIR));
|
||||
removePathIfExists(path.join(distPluginDir, "node_modules"));
|
||||
};
|
||||
|
||||
for (const dirent of fs.readdirSync(extensionsRoot, { withFileTypes: true })) {
|
||||
if (!dirent.isDirectory()) {
|
||||
|
|
@ -81,12 +129,15 @@ export function copyBundledPluginMetadata(params = {}) {
|
|||
const distManifestPath = path.join(distPluginDir, "openclaw.plugin.json");
|
||||
const distPackageJsonPath = path.join(distPluginDir, "package.json");
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
removeFileIfExists(distManifestPath);
|
||||
removeFileIfExists(distPackageJsonPath);
|
||||
removeGeneratedPluginArtifacts(distPluginDir);
|
||||
continue;
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
// Generated skill assets live under a dedicated dist-owned directory. Also
|
||||
// remove the older bad node_modules tree so release packs cannot pick it up.
|
||||
removePathIfExists(path.join(distPluginDir, GENERATED_BUNDLED_SKILLS_DIR));
|
||||
removePathIfExists(path.join(distPluginDir, "node_modules"));
|
||||
const copiedSkills = copyDeclaredPluginSkillPaths({ manifest, pluginDir, distPluginDir });
|
||||
const bundledManifest = Array.isArray(manifest.skills)
|
||||
? { ...manifest, skills: copiedSkills }
|
||||
|
|
@ -119,8 +170,7 @@ export function copyBundledPluginMetadata(params = {}) {
|
|||
continue;
|
||||
}
|
||||
const distPluginDir = path.join(distExtensionsRoot, dirent.name);
|
||||
removeFileIfExists(path.join(distPluginDir, "openclaw.plugin.json"));
|
||||
removeFileIfExists(path.join(distPluginDir, "package.json"));
|
||||
removeGeneratedPluginArtifacts(distPluginDir);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,3 +24,12 @@ export function removeFileIfExists(filePath) {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function removePathIfExists(filePath) {
|
||||
try {
|
||||
fs.rmSync(filePath, { recursive: true, force: true });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,2 @@
|
|||
export {
|
||||
resolveProviderPluginChoice,
|
||||
} from "../../../plugins/provider-wizard.js";
|
||||
export { resolveProviderPluginChoice } from "../../../plugins/provider-wizard.js";
|
||||
export { resolvePluginProviders } from "../../../plugins/providers.js";
|
||||
|
|
|
|||
|
|
@ -66,13 +66,20 @@ describe("copyBundledPluginMetadata", () => {
|
|||
"utf8",
|
||||
),
|
||||
).toContain("ACP Router");
|
||||
const bundledManifest = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(repoRoot, "dist", "extensions", "acpx", "openclaw.plugin.json"),
|
||||
"utf8",
|
||||
),
|
||||
) as { skills?: string[] };
|
||||
expect(bundledManifest.skills).toEqual(["./skills"]);
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(path.join(repoRoot, "dist", "extensions", "acpx", "package.json"), "utf8"),
|
||||
) as { openclaw?: { extensions?: string[] } };
|
||||
expect(packageJson.openclaw?.extensions).toEqual(["./index.js"]);
|
||||
});
|
||||
|
||||
it("dereferences node_modules-backed skill paths into the bundled dist tree", () => {
|
||||
it("relocates node_modules-backed skill paths into bundled-skills and rewrites the manifest", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-bundled-plugin-node-modules-");
|
||||
const pluginDir = path.join(repoRoot, "extensions", "tlon");
|
||||
const storeSkillDir = path.join(
|
||||
|
|
@ -86,6 +93,12 @@ describe("copyBundledPluginMetadata", () => {
|
|||
);
|
||||
fs.mkdirSync(storeSkillDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(storeSkillDir, "SKILL.md"), "# Tlon Skill\n", "utf8");
|
||||
fs.mkdirSync(path.join(storeSkillDir, "node_modules", ".bin"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(storeSkillDir, "node_modules", ".bin", "tlon"),
|
||||
"#!/bin/sh\n",
|
||||
"utf8",
|
||||
);
|
||||
fs.mkdirSync(path.join(pluginDir, "node_modules", "@tloncorp"), { recursive: true });
|
||||
fs.symlinkSync(
|
||||
storeSkillDir,
|
||||
|
|
@ -101,10 +114,7 @@ describe("copyBundledPluginMetadata", () => {
|
|||
name: "@openclaw/tlon",
|
||||
openclaw: { extensions: ["./index.ts"] },
|
||||
});
|
||||
|
||||
copyBundledPluginMetadata({ repoRoot });
|
||||
|
||||
const copiedSkillDir = path.join(
|
||||
const staleNodeModulesSkillDir = path.join(
|
||||
repoRoot,
|
||||
"dist",
|
||||
"extensions",
|
||||
|
|
@ -113,11 +123,36 @@ describe("copyBundledPluginMetadata", () => {
|
|||
"@tloncorp",
|
||||
"tlon-skill",
|
||||
);
|
||||
fs.mkdirSync(staleNodeModulesSkillDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(staleNodeModulesSkillDir, "stale.txt"), "stale\n", "utf8");
|
||||
|
||||
copyBundledPluginMetadata({ repoRoot });
|
||||
|
||||
const copiedSkillDir = path.join(
|
||||
repoRoot,
|
||||
"dist",
|
||||
"extensions",
|
||||
"tlon",
|
||||
"bundled-skills",
|
||||
"@tloncorp",
|
||||
"tlon-skill",
|
||||
);
|
||||
expect(fs.existsSync(path.join(copiedSkillDir, "SKILL.md"))).toBe(true);
|
||||
expect(fs.lstatSync(copiedSkillDir).isSymbolicLink()).toBe(false);
|
||||
expect(fs.existsSync(path.join(copiedSkillDir, "node_modules"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(repoRoot, "dist", "extensions", "tlon", "node_modules"))).toBe(
|
||||
false,
|
||||
);
|
||||
const bundledManifest = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(repoRoot, "dist", "extensions", "tlon", "openclaw.plugin.json"),
|
||||
"utf8",
|
||||
),
|
||||
) as { skills?: string[] };
|
||||
expect(bundledManifest.skills).toEqual(["./bundled-skills/@tloncorp/tlon-skill"]);
|
||||
});
|
||||
|
||||
it("omits missing declared skill paths from the bundled manifest", () => {
|
||||
it("omits missing declared skill paths and removes stale generated outputs", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-bundled-plugin-missing-skill-");
|
||||
const pluginDir = path.join(repoRoot, "extensions", "tlon");
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
|
|
@ -130,6 +165,19 @@ describe("copyBundledPluginMetadata", () => {
|
|||
name: "@openclaw/tlon",
|
||||
openclaw: { extensions: ["./index.ts"] },
|
||||
});
|
||||
const staleBundledSkillDir = path.join(
|
||||
repoRoot,
|
||||
"dist",
|
||||
"extensions",
|
||||
"tlon",
|
||||
"bundled-skills",
|
||||
"@tloncorp",
|
||||
"tlon-skill",
|
||||
);
|
||||
fs.mkdirSync(staleBundledSkillDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(staleBundledSkillDir, "SKILL.md"), "# stale\n", "utf8");
|
||||
const staleNodeModulesDir = path.join(repoRoot, "dist", "extensions", "tlon", "node_modules");
|
||||
fs.mkdirSync(staleNodeModulesDir, { recursive: true });
|
||||
|
||||
copyBundledPluginMetadata({ repoRoot });
|
||||
|
||||
|
|
@ -140,5 +188,56 @@ describe("copyBundledPluginMetadata", () => {
|
|||
),
|
||||
) as { skills?: string[] };
|
||||
expect(bundledManifest.skills).toEqual([]);
|
||||
expect(fs.existsSync(path.join(repoRoot, "dist", "extensions", "tlon", "bundled-skills"))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(fs.existsSync(staleNodeModulesDir)).toBe(false);
|
||||
});
|
||||
|
||||
it("removes generated outputs for plugins no longer present in source", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-bundled-plugin-removed-");
|
||||
const staleBundledSkillDir = path.join(
|
||||
repoRoot,
|
||||
"dist",
|
||||
"extensions",
|
||||
"removed-plugin",
|
||||
"bundled-skills",
|
||||
"@scope",
|
||||
"skill",
|
||||
);
|
||||
fs.mkdirSync(staleBundledSkillDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(staleBundledSkillDir, "SKILL.md"), "# stale\n", "utf8");
|
||||
const staleNodeModulesDir = path.join(
|
||||
repoRoot,
|
||||
"dist",
|
||||
"extensions",
|
||||
"removed-plugin",
|
||||
"node_modules",
|
||||
);
|
||||
fs.mkdirSync(staleNodeModulesDir, { recursive: true });
|
||||
writeJson(path.join(repoRoot, "dist", "extensions", "removed-plugin", "openclaw.plugin.json"), {
|
||||
id: "removed-plugin",
|
||||
configSchema: { type: "object" },
|
||||
skills: ["./bundled-skills/@scope/skill"],
|
||||
});
|
||||
writeJson(path.join(repoRoot, "dist", "extensions", "removed-plugin", "package.json"), {
|
||||
name: "@openclaw/removed-plugin",
|
||||
});
|
||||
fs.mkdirSync(path.join(repoRoot, "extensions"), { recursive: true });
|
||||
|
||||
copyBundledPluginMetadata({ repoRoot });
|
||||
|
||||
expect(
|
||||
fs.existsSync(
|
||||
path.join(repoRoot, "dist", "extensions", "removed-plugin", "openclaw.plugin.json"),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
fs.existsSync(path.join(repoRoot, "dist", "extensions", "removed-plugin", "package.json")),
|
||||
).toBe(false);
|
||||
expect(
|
||||
fs.existsSync(path.join(repoRoot, "dist", "extensions", "removed-plugin", "bundled-skills")),
|
||||
).toBe(false);
|
||||
expect(fs.existsSync(staleNodeModulesDir)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue