

























@@ -1,6 +1,7 @@
1+import { spawnSync } from "node:child_process";
12import fs from "node:fs";
23import path from "node:path";
3-import { describe, expect, it } from "vitest";
4+import { describe, expect, it, vi } from "vitest";
4556type PluginManifestShape = {
67id?: unknown;
@@ -54,10 +55,100 @@ function normalizeText(value: unknown): string | undefined {
5455return trimmed || undefined;
5556}
565758+function listBundledPluginDirs(): string[] {
59+const externalDirs = listExternalBundledPluginDirs();
60+if (externalDirs) {
61+return externalDirs;
62+}
63+return fs.readdirSync(EXTENSIONS_ROOT).toSorted();
64+}
65+66+function listExternalBundledPluginDirs(): string[] | null {
67+const files =
68+listGitPluginMetadataFiles() ??
69+listFindPluginMetadataFiles();
70+if (!files) {
71+return null;
72+}
73+74+const metadataByDir = new Map<string, Set<string>>();
75+for (const file of files) {
76+const match = /^extensions\/([^/]+)\/(openclaw\.plugin\.json|package\.json)$/u.exec(file);
77+if (!match) {
78+continue;
79+}
80+const [, dirName, fileName] = match;
81+const metadataFiles = metadataByDir.get(dirName) ?? new Set<string>();
82+metadataFiles.add(fileName);
83+metadataByDir.set(dirName, metadataFiles);
84+}
85+86+return [...metadataByDir.entries()]
87+.filter(([, metadataFiles]) =>
88+metadataFiles.has("package.json") && metadataFiles.has("openclaw.plugin.json"),
89+)
90+.map(([dirName]) => dirName)
91+.toSorted();
92+}
93+94+function listGitPluginMetadataFiles(): string[] | null {
95+const result = spawnSync(
96+"git",
97+["ls-files", "--", "extensions/*/package.json", "extensions/*/openclaw.plugin.json"],
98+{
99+cwd: process.cwd(),
100+encoding: "utf8",
101+maxBuffer: 1024 * 1024,
102+stdio: ["ignore", "pipe", "ignore"],
103+},
104+);
105+if (result.status !== 0) {
106+return null;
107+}
108+return result.stdout
109+.split("\n")
110+.map((line) => line.trim())
111+.filter((line) => line.length > 0)
112+.toSorted();
113+}
114+115+function listFindPluginMetadataFiles(): string[] | null {
116+const result = spawnSync(
117+"find",
118+[
119+EXTENSIONS_ROOT,
120+"-maxdepth",
121+"2",
122+"-type",
123+"f",
124+"(",
125+"-name",
126+"package.json",
127+"-o",
128+"-name",
129+"openclaw.plugin.json",
130+")",
131+],
132+{
133+cwd: process.cwd(),
134+encoding: "utf8",
135+maxBuffer: 1024 * 1024,
136+stdio: ["ignore", "pipe", "ignore"],
137+},
138+);
139+if (result.status !== 0) {
140+return null;
141+}
142+return result.stdout
143+.split("\n")
144+.map((line) => line.trim())
145+.filter((line) => line.length > 0)
146+.map((file) => path.relative(process.cwd(), file).split(path.sep).join("/"))
147+.toSorted();
148+}
149+57150function readBundledPluginRecords(): BundledPluginRecord[] {
58-return fs
59-.readdirSync(EXTENSIONS_ROOT)
60-.toSorted()
151+return listBundledPluginDirs()
61152.flatMap((dirName) => {
62153const rootDir = path.join(EXTENSIONS_ROOT, dirName);
63154const packagePath = path.join(rootDir, "package.json");
@@ -108,6 +199,19 @@ function expectNoBundledPluginNamingMismatches(params: {
108199}
109200110201describe("bundled plugin naming guardrails", () => {
202+it("lists bundled plugin metadata without scanning extension directories in-process", () => {
203+const readDir = vi.spyOn(fs, "readdirSync");
204+try {
205+const records = readBundledPluginRecords();
206+207+expect(records.length).toBeGreaterThan(0);
208+expect(records.every((record) => record.dirName.length > 0)).toBe(true);
209+expect(readDir).not.toHaveBeenCalled();
210+} finally {
211+readDir.mockRestore();
212+}
213+});
214+111215it.each([
112216{
113217name: "keeps bundled workspace package names anchored to the plugin id",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。