Merge branch 'main' into improve/edge-tts-followup
This commit is contained in:
commit
5e00e7645d
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,8 +6,8 @@ import process from "node:process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { runRuntimePostBuild } from "./runtime-postbuild.mjs";
|
||||
|
||||
const compiler = "tsdown";
|
||||
const compilerArgs = ["exec", compiler, "--no-clean"];
|
||||
const buildScript = "scripts/tsdown-build.mjs";
|
||||
const compilerArgs = [buildScript, "--no-clean"];
|
||||
|
||||
const runNodeSourceRoots = ["src", "extensions"];
|
||||
const runNodeConfigFiles = ["tsconfig.json", "package.json", "tsdown.config.ts"];
|
||||
@ -313,7 +313,6 @@ export async function runNodeMain(params = {}) {
|
||||
cwd: params.cwd ?? process.cwd(),
|
||||
args: params.args ?? process.argv.slice(2),
|
||||
env: params.env ? { ...params.env } : { ...process.env },
|
||||
platform: params.platform ?? process.platform,
|
||||
};
|
||||
|
||||
deps.distRoot = path.join(deps.cwd, "dist");
|
||||
@ -333,9 +332,8 @@ export async function runNodeMain(params = {}) {
|
||||
}
|
||||
|
||||
logRunner("Building TypeScript (dist is stale).", deps);
|
||||
const buildCmd = deps.platform === "win32" ? "cmd.exe" : "pnpm";
|
||||
const buildArgs =
|
||||
deps.platform === "win32" ? ["/d", "/s", "/c", "pnpm", ...compilerArgs] : compilerArgs;
|
||||
const buildCmd = deps.execPath;
|
||||
const buildArgs = compilerArgs;
|
||||
const build = deps.spawn(buildCmd, buildArgs, {
|
||||
cwd: deps.cwd,
|
||||
env: deps.env,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,9 +3,10 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const logLevel = process.env.OPENCLAW_BUILD_VERBOSE ? "info" : "warn";
|
||||
const extraArgs = process.argv.slice(2);
|
||||
const result = spawnSync(
|
||||
"pnpm",
|
||||
["exec", "tsdown", "--config-loader", "unrun", "--logLevel", logLevel],
|
||||
["exec", "tsdown", "--config-loader", "unrun", "--logLevel", logLevel, ...extraArgs],
|
||||
{
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
export { resolveProviderPluginChoice } from "../../../plugins/provider-wizard.js";
|
||||
export { resolvePluginProviders } from "../../../plugins/providers.js";
|
||||
@ -0,0 +1,54 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import { applyNonInteractivePluginProviderChoice } from "./auth-choice.plugin-providers.js";
|
||||
|
||||
const resolvePreferredProviderForAuthChoice = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
vi.mock("../../auth-choice.preferred-provider.js", () => ({
|
||||
resolvePreferredProviderForAuthChoice,
|
||||
}));
|
||||
|
||||
const resolveProviderPluginChoice = vi.hoisted(() => vi.fn());
|
||||
const resolvePluginProviders = vi.hoisted(() => vi.fn(() => []));
|
||||
vi.mock("./auth-choice.plugin-providers.runtime.js", () => ({
|
||||
resolveProviderPluginChoice,
|
||||
resolvePluginProviders,
|
||||
PROVIDER_PLUGIN_CHOICE_PREFIX: "provider-plugin:",
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
function createRuntime() {
|
||||
return {
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
it("loads plugin providers for provider-plugin auth choices", async () => {
|
||||
const runtime = createRuntime();
|
||||
const runNonInteractive = vi.fn(async () => ({ plugins: { allow: ["vllm"] } }));
|
||||
resolvePluginProviders.mockReturnValue([{ id: "vllm", pluginId: "vllm" }] as never);
|
||||
resolveProviderPluginChoice.mockReturnValue({
|
||||
provider: { id: "vllm", pluginId: "vllm", label: "vLLM" },
|
||||
method: { runNonInteractive },
|
||||
});
|
||||
|
||||
const result = await applyNonInteractivePluginProviderChoice({
|
||||
nextConfig: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
authChoice: "provider-plugin:vllm:custom",
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
resolveApiKey: vi.fn(),
|
||||
toApiKeyCredential: vi.fn(),
|
||||
});
|
||||
|
||||
expect(resolvePluginProviders).toHaveBeenCalledOnce();
|
||||
expect(resolveProviderPluginChoice).toHaveBeenCalledOnce();
|
||||
expect(runNonInteractive).toHaveBeenCalledOnce();
|
||||
expect(result).toEqual({ plugins: { allow: ["vllm"] } });
|
||||
});
|
||||
});
|
||||
@ -3,11 +3,6 @@ import type { ApiKeyCredential } from "../../../agents/auth-profiles/types.js";
|
||||
import { resolveDefaultAgentWorkspaceDir } from "../../../agents/workspace.js";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import { enablePluginInConfig } from "../../../plugins/enable.js";
|
||||
import {
|
||||
PROVIDER_PLUGIN_CHOICE_PREFIX,
|
||||
resolveProviderPluginChoice,
|
||||
} from "../../../plugins/provider-wizard.js";
|
||||
import { resolvePluginProviders } from "../../../plugins/providers.js";
|
||||
import type {
|
||||
ProviderNonInteractiveApiKeyCredentialParams,
|
||||
ProviderResolveNonInteractiveApiKeyParams,
|
||||
@ -16,6 +11,12 @@ import type { RuntimeEnv } from "../../../runtime.js";
|
||||
import { resolvePreferredProviderForAuthChoice } from "../../auth-choice.preferred-provider.js";
|
||||
import type { OnboardOptions } from "../../onboard-types.js";
|
||||
|
||||
const PROVIDER_PLUGIN_CHOICE_PREFIX = "provider-plugin:";
|
||||
|
||||
async function loadPluginProviderRuntime() {
|
||||
return import("./auth-choice.plugin-providers.runtime.js");
|
||||
}
|
||||
|
||||
function buildIsolatedProviderResolutionConfig(
|
||||
cfg: OpenClawConfig,
|
||||
providerId: string | undefined,
|
||||
@ -73,6 +74,7 @@ export async function applyNonInteractivePluginProviderChoice(params: {
|
||||
params.nextConfig,
|
||||
preferredProviderId,
|
||||
);
|
||||
const { resolveProviderPluginChoice, resolvePluginProviders } = await loadPluginProviderRuntime();
|
||||
const providerChoice = resolveProviderPluginChoice({
|
||||
providers: resolvePluginProviders({
|
||||
config: resolutionConfig,
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import { applyNonInteractiveAuthChoice } from "./auth-choice.js";
|
||||
|
||||
const applySimpleNonInteractiveApiKeyChoice = vi.hoisted(() =>
|
||||
vi.fn<() => Promise<OpenClawConfig | null | undefined>>(async () => undefined),
|
||||
);
|
||||
vi.mock("./auth-choice.api-key-providers.js", () => ({
|
||||
applySimpleNonInteractiveApiKeyChoice,
|
||||
}));
|
||||
|
||||
const applyNonInteractivePluginProviderChoice = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
vi.mock("./auth-choice.plugin-providers.js", () => ({
|
||||
applyNonInteractivePluginProviderChoice,
|
||||
}));
|
||||
|
||||
const resolveNonInteractiveApiKey = vi.hoisted(() => vi.fn());
|
||||
vi.mock("../api-keys.js", () => ({
|
||||
resolveNonInteractiveApiKey,
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
function createRuntime() {
|
||||
return {
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
log: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe("applyNonInteractiveAuthChoice", () => {
|
||||
it("resolves builtin API key auth before plugin provider resolution", async () => {
|
||||
const runtime = createRuntime();
|
||||
const nextConfig = { agents: { defaults: {} } } as OpenClawConfig;
|
||||
const resolvedConfig = { auth: { profiles: { "openai:default": { mode: "api_key" } } } };
|
||||
applySimpleNonInteractiveApiKeyChoice.mockResolvedValueOnce(resolvedConfig as never);
|
||||
|
||||
const result = await applyNonInteractiveAuthChoice({
|
||||
nextConfig,
|
||||
authChoice: "openai-api-key",
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: nextConfig,
|
||||
});
|
||||
|
||||
expect(result).toBe(resolvedConfig);
|
||||
expect(applySimpleNonInteractiveApiKeyChoice).toHaveBeenCalledOnce();
|
||||
expect(applyNonInteractivePluginProviderChoice).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -161,24 +161,6 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pluginProviderChoice = await applyNonInteractivePluginProviderChoice({
|
||||
nextConfig,
|
||||
authChoice,
|
||||
opts,
|
||||
runtime,
|
||||
baseConfig,
|
||||
resolveApiKey: (input) =>
|
||||
resolveApiKey({
|
||||
...input,
|
||||
cfg: baseConfig,
|
||||
runtime,
|
||||
}),
|
||||
toApiKeyCredential,
|
||||
});
|
||||
if (pluginProviderChoice !== undefined) {
|
||||
return pluginProviderChoice;
|
||||
}
|
||||
|
||||
if (authChoice === "token") {
|
||||
const providerRaw = opts.tokenProvider?.trim();
|
||||
if (!providerRaw) {
|
||||
@ -484,6 +466,24 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const pluginProviderChoice = await applyNonInteractivePluginProviderChoice({
|
||||
nextConfig,
|
||||
authChoice,
|
||||
opts,
|
||||
runtime,
|
||||
baseConfig,
|
||||
resolveApiKey: (input) =>
|
||||
resolveApiKey({
|
||||
...input,
|
||||
cfg: baseConfig,
|
||||
runtime,
|
||||
}),
|
||||
toApiKeyCredential,
|
||||
});
|
||||
if (pluginProviderChoice !== undefined) {
|
||||
return pluginProviderChoice;
|
||||
}
|
||||
|
||||
if (
|
||||
authChoice === "oauth" ||
|
||||
authChoice === "chutes" ||
|
||||
|
||||
@ -33,10 +33,8 @@ async function writeRuntimePostBuildScaffold(tmp: string): Promise<void> {
|
||||
await fs.utimes(pluginSdkAliasPath, baselineTime, baselineTime);
|
||||
}
|
||||
|
||||
function expectedBuildSpawn(platform: NodeJS.Platform = process.platform) {
|
||||
return platform === "win32"
|
||||
? ["cmd.exe", "/d", "/s", "/c", "pnpm", "exec", "tsdown", "--no-clean"]
|
||||
: ["pnpm", "exec", "tsdown", "--no-clean"];
|
||||
function expectedBuildSpawn() {
|
||||
return [process.execPath, "scripts/tsdown-build.mjs", "--no-clean"];
|
||||
}
|
||||
|
||||
describe("run-node script", () => {
|
||||
@ -44,7 +42,7 @@ describe("run-node script", () => {
|
||||
"preserves control-ui assets by building with tsdown --no-clean",
|
||||
async () => {
|
||||
await withTempDir(async (tmp) => {
|
||||
const argsPath = path.join(tmp, ".pnpm-args.txt");
|
||||
const argsPath = path.join(tmp, ".build-args.txt");
|
||||
const indexPath = path.join(tmp, "dist", "control-ui", "index.html");
|
||||
|
||||
await writeRuntimePostBuildScaffold(tmp);
|
||||
@ -53,7 +51,7 @@ describe("run-node script", () => {
|
||||
|
||||
const nodeCalls: string[][] = [];
|
||||
const spawn = (cmd: string, args: string[]) => {
|
||||
if (cmd === "pnpm") {
|
||||
if (cmd === process.execPath && args[0] === "scripts/tsdown-build.mjs") {
|
||||
fsSync.writeFileSync(argsPath, args.join(" "), "utf-8");
|
||||
if (!args.includes("--no-clean")) {
|
||||
fsSync.rmSync(path.join(tmp, "dist", "control-ui"), { recursive: true, force: true });
|
||||
@ -87,9 +85,14 @@ describe("run-node script", () => {
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
await expect(fs.readFile(argsPath, "utf-8")).resolves.toContain("exec tsdown --no-clean");
|
||||
await expect(fs.readFile(argsPath, "utf-8")).resolves.toContain(
|
||||
"scripts/tsdown-build.mjs --no-clean",
|
||||
);
|
||||
await expect(fs.readFile(indexPath, "utf-8")).resolves.toContain("sentinel");
|
||||
expect(nodeCalls).toEqual([[process.execPath, "openclaw.mjs", "--version"]]);
|
||||
expect(nodeCalls).toEqual([
|
||||
[process.execPath, "scripts/tsdown-build.mjs", "--no-clean"],
|
||||
[process.execPath, "openclaw.mjs", "--version"],
|
||||
]);
|
||||
});
|
||||
},
|
||||
);
|
||||
@ -151,8 +154,10 @@ describe("run-node script", () => {
|
||||
fs.readFile(path.join(tmp, "dist", "plugin-sdk", "root-alias.cjs"), "utf-8"),
|
||||
).resolves.toContain("module.exports = {};");
|
||||
await expect(
|
||||
fs.readFile(path.join(tmp, "dist", "extensions", "demo", "openclaw.plugin.json"), "utf-8"),
|
||||
).resolves.toContain('"id":"demo"');
|
||||
fs
|
||||
.readFile(path.join(tmp, "dist", "extensions", "demo", "openclaw.plugin.json"), "utf-8")
|
||||
.then((raw) => JSON.parse(raw)),
|
||||
).resolves.toMatchObject({ id: "demo" });
|
||||
await expect(
|
||||
fs.readFile(path.join(tmp, "dist", "extensions", "demo", "package.json"), "utf-8"),
|
||||
).resolves.toContain(
|
||||
@ -222,7 +227,7 @@ describe("run-node script", () => {
|
||||
it("returns the build exit code when the compiler step fails", async () => {
|
||||
await withTempDir(async (tmp) => {
|
||||
const spawn = (cmd: string, args: string[] = []) => {
|
||||
if (cmd === "pnpm" || (cmd === "cmd.exe" && args.includes("pnpm"))) {
|
||||
if (cmd === process.execPath && args[0] === "scripts/tsdown-build.mjs") {
|
||||
return createExitedProcess(23);
|
||||
}
|
||||
return createExitedProcess(0);
|
||||
@ -501,7 +506,11 @@ describe("run-node script", () => {
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(spawnCalls).toEqual([[process.execPath, "openclaw.mjs", "status"]]);
|
||||
await expect(fs.readFile(distManifestPath, "utf-8")).resolves.toContain('"id":"demo"');
|
||||
await expect(
|
||||
fs.readFile(distManifestPath, "utf-8").then((raw) => JSON.parse(raw)),
|
||||
).resolves.toMatchObject({
|
||||
id: "demo",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -567,7 +576,11 @@ describe("run-node script", () => {
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(spawnCalls).toEqual([[process.execPath, "openclaw.mjs", "status"]]);
|
||||
await expect(fs.readFile(distManifestPath, "utf-8")).resolves.toContain('"id":"demo"');
|
||||
await expect(
|
||||
fs.readFile(distManifestPath, "utf-8").then((raw) => JSON.parse(raw)),
|
||||
).resolves.toMatchObject({
|
||||
id: "demo",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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…
x
Reference in New Issue
Block a user