@gleanwork/pluginpack 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +482 -0
- package/assets/pluginpack-flow.svg +180 -0
- package/dist/chunk-HR2ZVYJA.js +1522 -0
- package/dist/chunk-HR2ZVYJA.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +414 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +299 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/package.json +108 -0
|
@@ -0,0 +1,1522 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
import { promises as fs3 } from "fs";
|
|
5
|
+
import path3 from "path";
|
|
6
|
+
import { pathToFileURL } from "url";
|
|
7
|
+
import { createJiti } from "jiti";
|
|
8
|
+
|
|
9
|
+
// src/components.ts
|
|
10
|
+
var componentDirs = [
|
|
11
|
+
"skills",
|
|
12
|
+
"agents",
|
|
13
|
+
"commands",
|
|
14
|
+
"rules",
|
|
15
|
+
"hooks",
|
|
16
|
+
"scripts",
|
|
17
|
+
"assets",
|
|
18
|
+
"policies",
|
|
19
|
+
"themes"
|
|
20
|
+
];
|
|
21
|
+
var staticFiles = ["README.md", "CHANGELOG.md", "LICENSE"];
|
|
22
|
+
var targetDefaultComponents = {
|
|
23
|
+
claude: ["skills", "agents", "hooks", "scripts", "assets"],
|
|
24
|
+
copilot: ["skills", "agents", "hooks", "scripts", "assets"],
|
|
25
|
+
cursor: ["skills", "agents", "rules", "hooks", "scripts", "assets"],
|
|
26
|
+
antigravity: ["skills", "agents", "rules", "hooks", "scripts", "assets"]
|
|
27
|
+
};
|
|
28
|
+
function resolveTargetComponents(target, pluginConfig) {
|
|
29
|
+
return new Set(pluginConfig.components ?? targetDefaultComponents[target]);
|
|
30
|
+
}
|
|
31
|
+
function isComponentPath(relativePath) {
|
|
32
|
+
return componentDirs.includes(relativePath.split("/")[0]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/fs.ts
|
|
36
|
+
import { promises as fs } from "fs";
|
|
37
|
+
import path from "path";
|
|
38
|
+
import fastGlob from "fast-glob";
|
|
39
|
+
function toPosix(value) {
|
|
40
|
+
return value.split(path.sep).join("/");
|
|
41
|
+
}
|
|
42
|
+
async function walkFiles(dir) {
|
|
43
|
+
const entries = await fastGlob("**/*", {
|
|
44
|
+
cwd: dir,
|
|
45
|
+
absolute: true,
|
|
46
|
+
onlyFiles: true,
|
|
47
|
+
dot: true
|
|
48
|
+
});
|
|
49
|
+
return entries.sort();
|
|
50
|
+
}
|
|
51
|
+
async function writeArtifact(outDir, files) {
|
|
52
|
+
for (const [relativePath, value] of files) {
|
|
53
|
+
const destination = path.join(outDir, relativePath);
|
|
54
|
+
await fs.mkdir(path.dirname(destination), { recursive: true });
|
|
55
|
+
await fs.writeFile(destination, value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function json(value) {
|
|
59
|
+
return `${JSON.stringify(value, null, 2)}
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
function isSafeRelativePath(value) {
|
|
63
|
+
if (!value) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (value.startsWith("http://") || value.startsWith("https://")) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
if (path.isAbsolute(value)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const normalized = path.posix.normalize(value.replace(/\\/g, "/"));
|
|
73
|
+
return normalized !== ".." && !normalized.startsWith("../");
|
|
74
|
+
}
|
|
75
|
+
async function exists(filePath) {
|
|
76
|
+
try {
|
|
77
|
+
await fs.access(filePath);
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/schema.ts
|
|
85
|
+
import { z } from "zod";
|
|
86
|
+
var authorSchema = z.object({
|
|
87
|
+
name: z.string().min(1),
|
|
88
|
+
email: z.string().optional(),
|
|
89
|
+
url: z.string().optional()
|
|
90
|
+
});
|
|
91
|
+
var metadataSchema = z.object({
|
|
92
|
+
displayName: z.string().optional(),
|
|
93
|
+
description: z.string().optional(),
|
|
94
|
+
author: authorSchema.optional(),
|
|
95
|
+
owner: authorSchema.optional(),
|
|
96
|
+
homepage: z.string().optional(),
|
|
97
|
+
repository: z.string().optional(),
|
|
98
|
+
license: z.string().optional(),
|
|
99
|
+
logo: z.string().optional(),
|
|
100
|
+
keywords: z.array(z.string()).optional(),
|
|
101
|
+
category: z.string().optional(),
|
|
102
|
+
tags: z.array(z.string()).optional()
|
|
103
|
+
});
|
|
104
|
+
var rootPluginSchema = metadataSchema.extend({
|
|
105
|
+
id: z.string().min(1).optional(),
|
|
106
|
+
name: z.string().optional(),
|
|
107
|
+
description: z.string().optional()
|
|
108
|
+
});
|
|
109
|
+
var sourceSchema = z.object({
|
|
110
|
+
plugins: z.string().optional(),
|
|
111
|
+
skills: z.string().optional(),
|
|
112
|
+
rootPlugin: rootPluginSchema.optional()
|
|
113
|
+
});
|
|
114
|
+
var emittedPluginSchema = z.object({
|
|
115
|
+
from: z.array(z.string().min(1)).min(1),
|
|
116
|
+
path: z.string().optional(),
|
|
117
|
+
version: z.string().optional(),
|
|
118
|
+
description: z.string().optional(),
|
|
119
|
+
displayName: z.string().optional(),
|
|
120
|
+
manifest: z.record(z.string(), z.unknown()).optional(),
|
|
121
|
+
components: z.array(z.string()).optional()
|
|
122
|
+
});
|
|
123
|
+
var targetSchema = z.object({
|
|
124
|
+
outDir: z.string().min(1),
|
|
125
|
+
marketplaceDir: z.string().optional(),
|
|
126
|
+
pluginRoot: z.string().optional(),
|
|
127
|
+
version: z.string().optional(),
|
|
128
|
+
plugins: z.record(z.string(), emittedPluginSchema),
|
|
129
|
+
manifest: z.record(z.string(), z.unknown()).optional(),
|
|
130
|
+
ignoredDiffPaths: z.array(z.string()).optional()
|
|
131
|
+
});
|
|
132
|
+
var configSchema = z.object({
|
|
133
|
+
name: z.string().min(1),
|
|
134
|
+
version: z.string().min(1),
|
|
135
|
+
source: sourceSchema.optional(),
|
|
136
|
+
metadata: metadataSchema.optional(),
|
|
137
|
+
targets: z.object({
|
|
138
|
+
claude: targetSchema.optional(),
|
|
139
|
+
copilot: targetSchema.optional(),
|
|
140
|
+
cursor: targetSchema.optional(),
|
|
141
|
+
antigravity: targetSchema.optional()
|
|
142
|
+
})
|
|
143
|
+
});
|
|
144
|
+
var sourcePluginManifestSchema = metadataSchema.extend({
|
|
145
|
+
name: z.string().optional(),
|
|
146
|
+
description: z.string().optional(),
|
|
147
|
+
mcpServers: z.record(z.string(), z.unknown()).optional()
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// src/source.ts
|
|
151
|
+
import { promises as fs2 } from "fs";
|
|
152
|
+
import path2 from "path";
|
|
153
|
+
function createFilesystemSourceProvider(plugins) {
|
|
154
|
+
return {
|
|
155
|
+
listPlugins: () => Promise.resolve(plugins),
|
|
156
|
+
readPluginFiles: (pluginId, target) => readPluginFiles(pluginOrThrow(plugins, pluginId), target),
|
|
157
|
+
readMcpServers: (pluginId) => readMcpServers(pluginOrThrow(plugins, pluginId))
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function pluginOrThrow(plugins, pluginId) {
|
|
161
|
+
const plugin = plugins.get(pluginId);
|
|
162
|
+
if (!plugin) {
|
|
163
|
+
throw new Error(`Unknown source plugin "${pluginId}".`);
|
|
164
|
+
}
|
|
165
|
+
return plugin;
|
|
166
|
+
}
|
|
167
|
+
async function readPluginFiles(plugin, target) {
|
|
168
|
+
const files = /* @__PURE__ */ new Map();
|
|
169
|
+
for (const dirName of componentDirs) {
|
|
170
|
+
const dir = plugin.componentRoots?.[dirName] ?? path2.join(plugin.dir, dirName);
|
|
171
|
+
if (!await exists(dir)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
for (const file of await walkFiles(dir)) {
|
|
175
|
+
if (isTargetOverrideFile(file)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const relativeToPlugin = toPosix(
|
|
179
|
+
plugin.componentRoots?.[dirName] ? path2.join(dirName, path2.relative(dir, file)) : path2.relative(plugin.dir, file)
|
|
180
|
+
);
|
|
181
|
+
const resolved = await resolveTargetOverride(plugin.dir, file, target);
|
|
182
|
+
files.set(relativeToPlugin, await fs2.readFile(resolved));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (plugin.includeStaticFiles !== false) {
|
|
186
|
+
for (const fileName of staticFiles) {
|
|
187
|
+
const file = path2.join(plugin.dir, fileName);
|
|
188
|
+
if (!await exists(file)) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const resolved = await resolveTargetOverride(plugin.dir, file, target);
|
|
192
|
+
files.set(fileName, await fs2.readFile(resolved));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return files;
|
|
196
|
+
}
|
|
197
|
+
function isTargetOverrideFile(filePath) {
|
|
198
|
+
return filePath.split(path2.sep).includes("targets");
|
|
199
|
+
}
|
|
200
|
+
async function resolveTargetOverride(pluginDir, file, target) {
|
|
201
|
+
const basenameOverride = path2.join(
|
|
202
|
+
path2.dirname(file),
|
|
203
|
+
"targets",
|
|
204
|
+
target,
|
|
205
|
+
path2.basename(file)
|
|
206
|
+
);
|
|
207
|
+
if (await exists(basenameOverride)) {
|
|
208
|
+
return basenameOverride;
|
|
209
|
+
}
|
|
210
|
+
const relative = path2.relative(pluginDir, file);
|
|
211
|
+
const rootOverride = path2.join(pluginDir, "targets", target, relative);
|
|
212
|
+
if (await exists(rootOverride)) {
|
|
213
|
+
return rootOverride;
|
|
214
|
+
}
|
|
215
|
+
return file;
|
|
216
|
+
}
|
|
217
|
+
async function readMcpServers(plugin) {
|
|
218
|
+
const filePath = path2.join(plugin.dir, ".mcp.json");
|
|
219
|
+
if (await exists(filePath)) {
|
|
220
|
+
let parsed;
|
|
221
|
+
try {
|
|
222
|
+
parsed = JSON.parse(await fs2.readFile(filePath, "utf8"));
|
|
223
|
+
} catch (error2) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Invalid JSON in ${filePath}: ${error2.message}`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const servers = parsed.mcpServers;
|
|
229
|
+
return isObject(servers) ? servers : void 0;
|
|
230
|
+
}
|
|
231
|
+
return isObject(plugin.manifest.mcpServers) ? plugin.manifest.mcpServers : void 0;
|
|
232
|
+
}
|
|
233
|
+
function isObject(value) {
|
|
234
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/config.ts
|
|
238
|
+
function defineConfig(config) {
|
|
239
|
+
return config;
|
|
240
|
+
}
|
|
241
|
+
async function loadConfig(cwd = process.cwd(), configPath) {
|
|
242
|
+
const projectConfig = await loadProjectConfig(cwd, configPath);
|
|
243
|
+
const { config, rootDir } = projectConfig;
|
|
244
|
+
const sourceRoot = path3.resolve(rootDir, config.source?.plugins ?? "plugins");
|
|
245
|
+
const plugins = await discoverSourcePlugins(sourceRoot);
|
|
246
|
+
await addRootSkillsPlugin(rootDir, config, plugins);
|
|
247
|
+
return {
|
|
248
|
+
...projectConfig,
|
|
249
|
+
sourceRoot,
|
|
250
|
+
plugins,
|
|
251
|
+
source: createFilesystemSourceProvider(plugins)
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
async function loadProjectConfig(cwd = process.cwd(), configPath) {
|
|
255
|
+
const resolvedConfigPath = configPath ? path3.resolve(cwd, configPath) : await findConfig(cwd);
|
|
256
|
+
const jiti = createJiti(pathToFileURL(resolvedConfigPath).href, {
|
|
257
|
+
interopDefault: true
|
|
258
|
+
});
|
|
259
|
+
const loaded = await jiti.import(resolvedConfigPath, { default: true });
|
|
260
|
+
const config = parseWithContext(
|
|
261
|
+
configSchema,
|
|
262
|
+
loaded,
|
|
263
|
+
resolvedConfigPath
|
|
264
|
+
);
|
|
265
|
+
const rootDir = path3.dirname(resolvedConfigPath);
|
|
266
|
+
return {
|
|
267
|
+
rootDir,
|
|
268
|
+
configPath: resolvedConfigPath,
|
|
269
|
+
config
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
async function addRootSkillsPlugin(rootDir, config, plugins) {
|
|
273
|
+
if (!config.source?.skills) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const id = config.source.rootPlugin?.id ?? "core";
|
|
277
|
+
if (plugins.has(id)) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`Root skills source plugin "${id}" conflicts with an existing source plugin.`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
const skillsDir = path3.resolve(rootDir, config.source.skills);
|
|
283
|
+
if (!await exists(skillsDir)) {
|
|
284
|
+
throw new Error(`Root skills source directory is missing: ${skillsDir}`);
|
|
285
|
+
}
|
|
286
|
+
const manifest = { ...config.source.rootPlugin ?? {} };
|
|
287
|
+
delete manifest.id;
|
|
288
|
+
plugins.set(id, {
|
|
289
|
+
id,
|
|
290
|
+
dir: rootDir,
|
|
291
|
+
manifest,
|
|
292
|
+
componentRoots: {
|
|
293
|
+
skills: skillsDir
|
|
294
|
+
},
|
|
295
|
+
includeStaticFiles: false
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
async function findConfig(cwd) {
|
|
299
|
+
const names = [
|
|
300
|
+
"pluginpack.config.ts",
|
|
301
|
+
"pluginpack.config.mts",
|
|
302
|
+
"pluginpack.config.mjs",
|
|
303
|
+
"pluginpack.config.js"
|
|
304
|
+
];
|
|
305
|
+
for (const name of names) {
|
|
306
|
+
const candidate = path3.resolve(cwd, name);
|
|
307
|
+
if (await exists(candidate)) {
|
|
308
|
+
return candidate;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
throw new Error(
|
|
312
|
+
`No pluginpack config found in ${cwd}. Expected ${names.join(", ")}.`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
async function discoverSourcePlugins(sourceRoot) {
|
|
316
|
+
const plugins = /* @__PURE__ */ new Map();
|
|
317
|
+
if (!await exists(sourceRoot)) {
|
|
318
|
+
return plugins;
|
|
319
|
+
}
|
|
320
|
+
const entries = await fs3.readdir(sourceRoot, { withFileTypes: true });
|
|
321
|
+
for (const entry of entries) {
|
|
322
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const dir = path3.join(sourceRoot, entry.name);
|
|
326
|
+
if (!await isSourcePluginDir(dir)) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const manifestPath = path3.join(dir, "plugin.pluginpack.json");
|
|
330
|
+
const manifest = await readSourceManifest(manifestPath);
|
|
331
|
+
plugins.set(entry.name, {
|
|
332
|
+
id: entry.name,
|
|
333
|
+
dir,
|
|
334
|
+
manifest
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return plugins;
|
|
338
|
+
}
|
|
339
|
+
async function isSourcePluginDir(dir) {
|
|
340
|
+
if (await exists(path3.join(dir, "plugin.pluginpack.json"))) {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
for (const component of componentDirs) {
|
|
344
|
+
if (await exists(path3.join(dir, component))) {
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
async function readSourceManifest(filePath) {
|
|
351
|
+
if (!await exists(filePath)) {
|
|
352
|
+
return {};
|
|
353
|
+
}
|
|
354
|
+
const raw = await fs3.readFile(filePath, "utf8");
|
|
355
|
+
try {
|
|
356
|
+
return parseWithContext(
|
|
357
|
+
sourcePluginManifestSchema,
|
|
358
|
+
JSON.parse(raw),
|
|
359
|
+
filePath
|
|
360
|
+
);
|
|
361
|
+
} catch (error2) {
|
|
362
|
+
if (error2 instanceof SyntaxError) {
|
|
363
|
+
throw new Error(`Invalid JSON in ${filePath}: ${error2.message}`);
|
|
364
|
+
}
|
|
365
|
+
throw error2;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function parseWithContext(schema, value, context) {
|
|
369
|
+
const parsed = schema.safeParse(value);
|
|
370
|
+
if (parsed.success) {
|
|
371
|
+
return parsed.data;
|
|
372
|
+
}
|
|
373
|
+
const details = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
374
|
+
throw new Error(`Invalid pluginpack config in ${context}: ${details}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/build.ts
|
|
378
|
+
import path6 from "path";
|
|
379
|
+
|
|
380
|
+
// src/managed.ts
|
|
381
|
+
import { promises as fs4 } from "fs";
|
|
382
|
+
import path4 from "path";
|
|
383
|
+
function managedManifestPath(target) {
|
|
384
|
+
return toPosix(path4.join(".pluginpack", `${target}.json`));
|
|
385
|
+
}
|
|
386
|
+
async function writeManagedManifest(artifact2) {
|
|
387
|
+
const manifest = {
|
|
388
|
+
version: 1,
|
|
389
|
+
target: artifact2.target,
|
|
390
|
+
files: artifact2.managedPaths
|
|
391
|
+
};
|
|
392
|
+
const destination = path4.join(
|
|
393
|
+
artifact2.outDir,
|
|
394
|
+
managedManifestPath(artifact2.target)
|
|
395
|
+
);
|
|
396
|
+
await fs4.mkdir(path4.dirname(destination), { recursive: true });
|
|
397
|
+
await fs4.writeFile(destination, json(manifest));
|
|
398
|
+
}
|
|
399
|
+
async function readManagedManifest(outDir, target) {
|
|
400
|
+
const manifestPath = path4.join(outDir, managedManifestPath(target));
|
|
401
|
+
let raw;
|
|
402
|
+
try {
|
|
403
|
+
raw = await fs4.readFile(manifestPath, "utf8");
|
|
404
|
+
} catch (error2) {
|
|
405
|
+
if (isNotFound(error2)) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
throw error2;
|
|
409
|
+
}
|
|
410
|
+
const parsed = JSON.parse(raw);
|
|
411
|
+
if (parsed.version !== 1 || parsed.target !== target || !Array.isArray(parsed.files) || !parsed.files.every((file) => typeof file === "string")) {
|
|
412
|
+
throw new Error(`Invalid managed manifest: ${manifestPath}`);
|
|
413
|
+
}
|
|
414
|
+
return parsed;
|
|
415
|
+
}
|
|
416
|
+
async function pruneManagedFiles(artifact2, options = {}) {
|
|
417
|
+
const previous = await readManagedManifest(artifact2.outDir, artifact2.target);
|
|
418
|
+
const current = new Set(artifact2.managedPaths.map(normalizeManagedPath));
|
|
419
|
+
const stale = (previous?.files ?? []).map(normalizeManagedPath).filter((file) => !current.has(file));
|
|
420
|
+
if (!options.dryRun) {
|
|
421
|
+
assertNoProtectedDeletions(artifact2.outDir, stale, options.guard, "prune");
|
|
422
|
+
}
|
|
423
|
+
const entries = [];
|
|
424
|
+
for (const normalized of stale) {
|
|
425
|
+
entries.push({
|
|
426
|
+
type: "stale",
|
|
427
|
+
target: artifact2.target,
|
|
428
|
+
path: normalized
|
|
429
|
+
});
|
|
430
|
+
if (!options.dryRun) {
|
|
431
|
+
await removeManagedPath(artifact2.outDir, normalized);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
target: artifact2.target,
|
|
436
|
+
outDir: artifact2.outDir,
|
|
437
|
+
entries
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
async function cleanManagedFiles(outDir, target, options = {}) {
|
|
441
|
+
const previous = await readManagedManifest(outDir, target);
|
|
442
|
+
const entries = [];
|
|
443
|
+
if (!previous) {
|
|
444
|
+
return { target, outDir, entries };
|
|
445
|
+
}
|
|
446
|
+
const files = (previous.files ?? []).map(normalizeManagedPath);
|
|
447
|
+
if (!options.dryRun) {
|
|
448
|
+
assertNoProtectedDeletions(outDir, files, options.guard, "clean");
|
|
449
|
+
}
|
|
450
|
+
for (const normalized of files) {
|
|
451
|
+
entries.push({ type: "deleted", target, path: normalized });
|
|
452
|
+
if (!options.dryRun) {
|
|
453
|
+
await removeManagedPath(outDir, normalized);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const manifestPath = managedManifestPath(target);
|
|
457
|
+
entries.push({ type: "deleted", target, path: manifestPath });
|
|
458
|
+
if (!options.dryRun) {
|
|
459
|
+
await removeManagedPath(outDir, manifestPath);
|
|
460
|
+
}
|
|
461
|
+
return { target, outDir, entries };
|
|
462
|
+
}
|
|
463
|
+
function buildDeleteGuard(rootDir, config, configPath, force) {
|
|
464
|
+
const protectedRoots = [];
|
|
465
|
+
if (config.source?.skills) {
|
|
466
|
+
protectedRoots.push(path4.resolve(rootDir, config.source.skills));
|
|
467
|
+
}
|
|
468
|
+
if (config.source?.plugins) {
|
|
469
|
+
protectedRoots.push(path4.resolve(rootDir, config.source.plugins));
|
|
470
|
+
}
|
|
471
|
+
return { protectedRoots, configPath: path4.resolve(configPath), force };
|
|
472
|
+
}
|
|
473
|
+
function assertNoProtectedDeletions(outDir, paths, guard, command) {
|
|
474
|
+
if (!guard || guard.force) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const blocked = paths.filter(
|
|
478
|
+
(file) => isProtectedDeletion(outDir, file, guard)
|
|
479
|
+
);
|
|
480
|
+
if (blocked.length === 0) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
throw new Error(
|
|
484
|
+
`Refusing to ${command} ${blocked.length} path(s) that resolve inside your source tree or config:
|
|
485
|
+
${blocked.map((file) => ` ${file}`).join("\n")}
|
|
486
|
+
This usually means a target outDir overlaps source.skills/source.plugins. Fix the config, or re-run with --force to delete anyway.`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
function isProtectedDeletion(outDir, relativePath, guard) {
|
|
490
|
+
const absolute = path4.resolve(outDir, normalizeManagedPath(relativePath));
|
|
491
|
+
if (guard.configPath && absolute === guard.configPath) {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
return guard.protectedRoots.some(
|
|
495
|
+
(root) => absolute === root || absolute.startsWith(`${root}${path4.sep}`)
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
function normalizeManagedPath(value) {
|
|
499
|
+
const normalized = path4.posix.normalize(value.replace(/\\/g, "/"));
|
|
500
|
+
if (!value || path4.posix.isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
|
|
501
|
+
throw new Error(`Unsafe managed path: ${value}`);
|
|
502
|
+
}
|
|
503
|
+
return normalized.startsWith("./") ? normalized.slice(2) : normalized;
|
|
504
|
+
}
|
|
505
|
+
async function removeManagedPath(outDir, relativePath) {
|
|
506
|
+
const root = path4.resolve(outDir);
|
|
507
|
+
const normalized = normalizeManagedPath(relativePath);
|
|
508
|
+
const destination = path4.resolve(root, normalized);
|
|
509
|
+
if (destination !== root && !destination.startsWith(`${root}${path4.sep}`)) {
|
|
510
|
+
throw new Error(`Managed path escapes output directory: ${relativePath}`);
|
|
511
|
+
}
|
|
512
|
+
await fs4.rm(destination, { force: true });
|
|
513
|
+
await removeEmptyParents(path4.dirname(destination), root);
|
|
514
|
+
}
|
|
515
|
+
async function removeEmptyParents(dir, root) {
|
|
516
|
+
let current = dir;
|
|
517
|
+
while (current !== root && current.startsWith(`${root}${path4.sep}`)) {
|
|
518
|
+
try {
|
|
519
|
+
await fs4.rmdir(current);
|
|
520
|
+
} catch (error2) {
|
|
521
|
+
if (isNotFound(error2) || isDirectoryNotEmpty(error2)) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
throw error2;
|
|
525
|
+
}
|
|
526
|
+
current = path4.dirname(current);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function isNotFound(error2) {
|
|
530
|
+
return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
|
|
531
|
+
}
|
|
532
|
+
function isDirectoryNotEmpty(error2) {
|
|
533
|
+
return error2 instanceof Error && "code" in error2 && (error2.code === "ENOTEMPTY" || error2.code === "EEXIST");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/targets.ts
|
|
537
|
+
import path5 from "path";
|
|
538
|
+
|
|
539
|
+
// src/render.ts
|
|
540
|
+
async function collectPluginFiles(project, target, sourceIds, components) {
|
|
541
|
+
const files = /* @__PURE__ */ new Map();
|
|
542
|
+
for (const sourceId of sourceIds) {
|
|
543
|
+
if (!project.plugins.has(sourceId)) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`Target "${target}" references unknown source plugin "${sourceId}".`
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
const pluginFiles = await project.source.readPluginFiles(sourceId, target);
|
|
549
|
+
for (const [relativePath, value] of pluginFiles) {
|
|
550
|
+
if (!shouldEmitFile(relativePath, components)) {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
setFile(files, relativePath, value, sourceId);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return files;
|
|
557
|
+
}
|
|
558
|
+
async function resolveMcpServers(project, sourceIds) {
|
|
559
|
+
const merged = {};
|
|
560
|
+
let found = false;
|
|
561
|
+
for (const sourceId of sourceIds) {
|
|
562
|
+
if (!project.plugins.has(sourceId)) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const servers = await project.source.readMcpServers(sourceId);
|
|
566
|
+
if (!servers) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
found = true;
|
|
570
|
+
for (const [name, config] of Object.entries(servers)) {
|
|
571
|
+
if (name in merged) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
`Duplicate MCP server "${name}" while merging source plugin "${sourceId}".`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
merged[name] = config;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return found ? merged : void 0;
|
|
580
|
+
}
|
|
581
|
+
function shouldEmitFile(relativePath, components) {
|
|
582
|
+
if (!components || !isComponentPath(relativePath)) {
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
return components.has(relativePath.split("/")[0]);
|
|
586
|
+
}
|
|
587
|
+
function setFile(files, relativePath, value, sourceId) {
|
|
588
|
+
if (files.has(relativePath)) {
|
|
589
|
+
throw new Error(
|
|
590
|
+
`Duplicate emitted file "${relativePath}" while merging source plugin "${sourceId}". Add a target override or change the target plugin mapping.`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
files.set(relativePath, value);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/targets.ts
|
|
597
|
+
var emitters = {
|
|
598
|
+
claude: emitClaude,
|
|
599
|
+
copilot: emitCopilot,
|
|
600
|
+
cursor: emitCursor,
|
|
601
|
+
antigravity: emitAntigravity
|
|
602
|
+
};
|
|
603
|
+
async function emitTarget(project, target, outDir) {
|
|
604
|
+
const targetConfig = project.config.targets[target];
|
|
605
|
+
if (!targetConfig) {
|
|
606
|
+
throw new Error(`Target "${target}" is not configured.`);
|
|
607
|
+
}
|
|
608
|
+
const emitter = emitters[target];
|
|
609
|
+
const resolvedOutDir = path5.resolve(
|
|
610
|
+
project.rootDir,
|
|
611
|
+
outDir ?? targetConfig.outDir
|
|
612
|
+
);
|
|
613
|
+
return emitter(project, target, targetConfig, resolvedOutDir);
|
|
614
|
+
}
|
|
615
|
+
async function emitPlugins(project, target, targetConfig, files, options) {
|
|
616
|
+
const entries = [];
|
|
617
|
+
for (const [pluginName, pluginConfig] of Object.entries(
|
|
618
|
+
targetConfig.plugins
|
|
619
|
+
)) {
|
|
620
|
+
const pluginPath = options.resolvePluginPath(pluginName, pluginConfig);
|
|
621
|
+
const pluginFiles = await collectPluginFiles(
|
|
622
|
+
project,
|
|
623
|
+
target,
|
|
624
|
+
pluginConfig.from,
|
|
625
|
+
resolveTargetComponents(target, pluginConfig)
|
|
626
|
+
);
|
|
627
|
+
const componentDirs2 = new Set(
|
|
628
|
+
[...pluginFiles.keys()].map((file) => file.split("/")[0])
|
|
629
|
+
);
|
|
630
|
+
for (const [relativePath, value] of pluginFiles) {
|
|
631
|
+
files.set(toPosix(path5.join(pluginPath, relativePath)), value);
|
|
632
|
+
}
|
|
633
|
+
const mcpServers = await resolveMcpServers(project, pluginConfig.from);
|
|
634
|
+
if (mcpServers && options.mcp === "file") {
|
|
635
|
+
files.set(
|
|
636
|
+
toPosix(path5.join(pluginPath, ".mcp.json")),
|
|
637
|
+
json({ mcpServers })
|
|
638
|
+
);
|
|
639
|
+
} else if (mcpServers && options.mcp === "antigravity") {
|
|
640
|
+
files.set(
|
|
641
|
+
toPosix(path5.join(pluginPath, "mcp_config.json")),
|
|
642
|
+
json({ mcpServers })
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
const metadata = emittedPluginMetadata(project, pluginConfig);
|
|
646
|
+
const manifest = options.buildManifest(
|
|
647
|
+
metadata,
|
|
648
|
+
pluginName,
|
|
649
|
+
pluginConfig,
|
|
650
|
+
componentDirs2,
|
|
651
|
+
mcpServers
|
|
652
|
+
);
|
|
653
|
+
files.set(toPosix(options.pluginManifestPath(pluginPath)), json(manifest));
|
|
654
|
+
if (options.entrySource) {
|
|
655
|
+
entries.push({
|
|
656
|
+
name: pluginName,
|
|
657
|
+
source: options.entrySource(pluginPath),
|
|
658
|
+
description: pluginConfig.description ?? manifest.description
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return entries;
|
|
663
|
+
}
|
|
664
|
+
async function emitCursor(project, target, targetConfig, outDir) {
|
|
665
|
+
const marketplaceDir = targetConfig.marketplaceDir ?? ".cursor-plugin";
|
|
666
|
+
const version = targetConfig.version ?? project.config.version;
|
|
667
|
+
const files = /* @__PURE__ */ new Map();
|
|
668
|
+
const plugins = await emitPlugins(project, target, targetConfig, files, {
|
|
669
|
+
resolvePluginPath: (pluginName, pluginConfig) => pluginConfig.path ?? pluginName,
|
|
670
|
+
pluginManifestPath: (pluginPath) => path5.join(pluginPath, marketplaceDir, "plugin.json"),
|
|
671
|
+
buildManifest: (metadata, pluginName, pluginConfig, componentDirs2, mcpServers) => cursorPluginManifest(
|
|
672
|
+
metadata,
|
|
673
|
+
pluginConfig.version ?? version,
|
|
674
|
+
pluginName,
|
|
675
|
+
pluginConfig,
|
|
676
|
+
componentDirs2,
|
|
677
|
+
mcpServers
|
|
678
|
+
),
|
|
679
|
+
entrySource: (pluginPath) => pluginPath,
|
|
680
|
+
mcp: "file"
|
|
681
|
+
});
|
|
682
|
+
const marketplace = stripUndefined(
|
|
683
|
+
deepMerge(
|
|
684
|
+
{
|
|
685
|
+
name: project.config.name,
|
|
686
|
+
owner: project.config.metadata?.owner ?? project.config.metadata?.author,
|
|
687
|
+
metadata: {
|
|
688
|
+
description: project.config.metadata?.description,
|
|
689
|
+
keywords: project.config.metadata?.keywords
|
|
690
|
+
},
|
|
691
|
+
plugins,
|
|
692
|
+
version
|
|
693
|
+
},
|
|
694
|
+
targetConfig.manifest ?? {}
|
|
695
|
+
)
|
|
696
|
+
);
|
|
697
|
+
files.set(
|
|
698
|
+
toPosix(path5.join(marketplaceDir, "marketplace.json")),
|
|
699
|
+
json(marketplace)
|
|
700
|
+
);
|
|
701
|
+
return artifact(target, outDir, files);
|
|
702
|
+
}
|
|
703
|
+
async function emitClaude(project, target, targetConfig, outDir) {
|
|
704
|
+
const marketplaceDir = targetConfig.marketplaceDir ?? ".claude-plugin";
|
|
705
|
+
const pluginRoot = targetConfig.pluginRoot ?? "plugins";
|
|
706
|
+
const version = targetConfig.version ?? project.config.version;
|
|
707
|
+
const files = /* @__PURE__ */ new Map();
|
|
708
|
+
const plugins = await emitPlugins(project, target, targetConfig, files, {
|
|
709
|
+
resolvePluginPath: (pluginName, pluginConfig) => pluginConfig.path ?? toPosix(path5.join(pluginRoot, pluginName)),
|
|
710
|
+
pluginManifestPath: (pluginPath) => path5.join(pluginPath, marketplaceDir, "plugin.json"),
|
|
711
|
+
buildManifest: (metadata, pluginName, pluginConfig) => claudePluginManifest(
|
|
712
|
+
metadata,
|
|
713
|
+
pluginConfig.version ?? version,
|
|
714
|
+
pluginName,
|
|
715
|
+
pluginConfig
|
|
716
|
+
),
|
|
717
|
+
entrySource: (pluginPath) => `./${pluginPath}`,
|
|
718
|
+
mcp: "file"
|
|
719
|
+
});
|
|
720
|
+
const marketplace = stripUndefined(
|
|
721
|
+
deepMerge(
|
|
722
|
+
{
|
|
723
|
+
$schema: "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
724
|
+
name: project.config.name,
|
|
725
|
+
version,
|
|
726
|
+
description: project.config.metadata?.description,
|
|
727
|
+
owner: project.config.metadata?.owner ?? project.config.metadata?.author,
|
|
728
|
+
plugins
|
|
729
|
+
},
|
|
730
|
+
targetConfig.manifest ?? {}
|
|
731
|
+
)
|
|
732
|
+
);
|
|
733
|
+
files.set(
|
|
734
|
+
toPosix(path5.join(marketplaceDir, "marketplace.json")),
|
|
735
|
+
json(marketplace)
|
|
736
|
+
);
|
|
737
|
+
return artifact(target, outDir, files);
|
|
738
|
+
}
|
|
739
|
+
async function emitAntigravity(project, target, targetConfig, outDir) {
|
|
740
|
+
const version = targetConfig.version ?? project.config.version;
|
|
741
|
+
const files = /* @__PURE__ */ new Map();
|
|
742
|
+
await emitPlugins(project, target, targetConfig, files, {
|
|
743
|
+
resolvePluginPath: (pluginName, pluginConfig) => pluginConfig.path ?? pluginName,
|
|
744
|
+
pluginManifestPath: (pluginPath) => path5.join(pluginPath, "plugin.json"),
|
|
745
|
+
buildManifest: (metadata, pluginName, pluginConfig) => antigravityPluginManifest(
|
|
746
|
+
metadata,
|
|
747
|
+
pluginConfig.version ?? version,
|
|
748
|
+
pluginName,
|
|
749
|
+
pluginConfig
|
|
750
|
+
),
|
|
751
|
+
mcp: "antigravity"
|
|
752
|
+
});
|
|
753
|
+
return artifact(target, outDir, files);
|
|
754
|
+
}
|
|
755
|
+
async function emitCopilot(project, target, targetConfig, outDir) {
|
|
756
|
+
const version = targetConfig.version ?? project.config.version;
|
|
757
|
+
const pluginRoot = targetConfig.pluginRoot ?? "plugins";
|
|
758
|
+
const files = /* @__PURE__ */ new Map();
|
|
759
|
+
const plugins = [];
|
|
760
|
+
for (const [pluginName, pluginConfig] of Object.entries(
|
|
761
|
+
targetConfig.plugins
|
|
762
|
+
)) {
|
|
763
|
+
const pluginPath = pluginConfig.path ?? toPosix(path5.join(pluginRoot, pluginName));
|
|
764
|
+
const pluginFiles = await collectPluginFiles(
|
|
765
|
+
project,
|
|
766
|
+
target,
|
|
767
|
+
pluginConfig.from,
|
|
768
|
+
resolveTargetComponents(target, pluginConfig)
|
|
769
|
+
);
|
|
770
|
+
for (const [relativePath, value] of pluginFiles) {
|
|
771
|
+
files.set(toPosix(path5.join(pluginPath, relativePath)), value);
|
|
772
|
+
}
|
|
773
|
+
const mcpServers = await resolveMcpServers(project, pluginConfig.from);
|
|
774
|
+
if (mcpServers) {
|
|
775
|
+
files.set(
|
|
776
|
+
toPosix(path5.join(pluginPath, ".mcp.json")),
|
|
777
|
+
json({ mcpServers })
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
const skills = [
|
|
781
|
+
...new Set(
|
|
782
|
+
[...pluginFiles.keys()].filter((file) => file.startsWith("skills/")).map((file) => `./skills/${file.split("/")[1]}`)
|
|
783
|
+
)
|
|
784
|
+
].sort();
|
|
785
|
+
const metadata = emittedPluginMetadata(project, pluginConfig);
|
|
786
|
+
plugins.push(
|
|
787
|
+
stripUndefined({
|
|
788
|
+
name: pluginName,
|
|
789
|
+
source: `./${pluginPath}`,
|
|
790
|
+
description: pluginConfig.description ?? metadata?.description,
|
|
791
|
+
version: pluginConfig.version ?? version,
|
|
792
|
+
skills,
|
|
793
|
+
mcpServers: mcpServers ? ".mcp.json" : void 0
|
|
794
|
+
})
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
const marketplace = stripUndefined(
|
|
798
|
+
deepMerge(
|
|
799
|
+
{
|
|
800
|
+
name: project.config.name,
|
|
801
|
+
metadata: stripUndefined({
|
|
802
|
+
description: project.config.metadata?.description,
|
|
803
|
+
version,
|
|
804
|
+
keywords: project.config.metadata?.keywords
|
|
805
|
+
}),
|
|
806
|
+
owner: project.config.metadata?.owner ?? project.config.metadata?.author,
|
|
807
|
+
plugins
|
|
808
|
+
},
|
|
809
|
+
targetConfig.manifest ?? {}
|
|
810
|
+
)
|
|
811
|
+
);
|
|
812
|
+
const marketplaceJson = json(marketplace);
|
|
813
|
+
files.set(
|
|
814
|
+
toPosix(path5.join(".claude-plugin", "marketplace.json")),
|
|
815
|
+
marketplaceJson
|
|
816
|
+
);
|
|
817
|
+
files.set(
|
|
818
|
+
toPosix(path5.join(".github", "plugin", "marketplace.json")),
|
|
819
|
+
marketplaceJson
|
|
820
|
+
);
|
|
821
|
+
return artifact(target, outDir, files);
|
|
822
|
+
}
|
|
823
|
+
function emittedPluginMetadata(project, pluginConfig) {
|
|
824
|
+
const sourceMetadata = pluginConfig.from.length === 1 ? project.plugins.get(pluginConfig.from[0])?.manifest : void 0;
|
|
825
|
+
return stripUndefined({
|
|
826
|
+
...project.config.metadata,
|
|
827
|
+
...sourceMetadata
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
function cursorPluginManifest(metadata, version, pluginName, pluginConfig, componentDirs2, mcpServers) {
|
|
831
|
+
const manifest = {
|
|
832
|
+
name: pluginName,
|
|
833
|
+
displayName: pluginConfig.displayName ?? metadata?.displayName ?? titleCase(pluginName),
|
|
834
|
+
version,
|
|
835
|
+
description: pluginConfig.description ?? metadata?.description,
|
|
836
|
+
author: metadata?.author,
|
|
837
|
+
homepage: metadata?.homepage,
|
|
838
|
+
repository: metadata?.repository,
|
|
839
|
+
license: metadata?.license,
|
|
840
|
+
logo: metadata?.logo,
|
|
841
|
+
keywords: metadata?.keywords,
|
|
842
|
+
category: metadata?.category,
|
|
843
|
+
tags: metadata?.tags
|
|
844
|
+
};
|
|
845
|
+
const components = pluginConfig.components ?? [
|
|
846
|
+
"skills",
|
|
847
|
+
"agents",
|
|
848
|
+
"commands",
|
|
849
|
+
"rules",
|
|
850
|
+
"hooks"
|
|
851
|
+
];
|
|
852
|
+
for (const component of components) {
|
|
853
|
+
if (componentDirs2.has(component)) {
|
|
854
|
+
manifest[component] = `./${component}/`;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (mcpServers) {
|
|
858
|
+
manifest.mcpServers = "./.mcp.json";
|
|
859
|
+
}
|
|
860
|
+
return stripUndefined(deepMerge(manifest, pluginConfig.manifest ?? {}));
|
|
861
|
+
}
|
|
862
|
+
function claudePluginManifest(metadata, version, pluginName, pluginConfig) {
|
|
863
|
+
const manifest = {
|
|
864
|
+
name: pluginName,
|
|
865
|
+
version,
|
|
866
|
+
description: pluginConfig.description ?? metadata?.description,
|
|
867
|
+
author: metadata?.author,
|
|
868
|
+
homepage: metadata?.homepage,
|
|
869
|
+
repository: metadata?.repository,
|
|
870
|
+
license: metadata?.license,
|
|
871
|
+
keywords: metadata?.keywords
|
|
872
|
+
};
|
|
873
|
+
return stripUndefined(deepMerge(manifest, pluginConfig.manifest ?? {}));
|
|
874
|
+
}
|
|
875
|
+
function antigravityPluginManifest(metadata, version, pluginName, pluginConfig) {
|
|
876
|
+
const manifest = {
|
|
877
|
+
name: pluginName,
|
|
878
|
+
version,
|
|
879
|
+
description: pluginConfig.description ?? metadata?.description
|
|
880
|
+
};
|
|
881
|
+
return stripUndefined(deepMerge(manifest, pluginConfig.manifest ?? {}));
|
|
882
|
+
}
|
|
883
|
+
function artifact(target, outDir, files) {
|
|
884
|
+
const managedPaths = [...files.keys()].sort();
|
|
885
|
+
return {
|
|
886
|
+
target,
|
|
887
|
+
outDir,
|
|
888
|
+
files: new Map([...files.entries()].sort(([a], [b]) => a.localeCompare(b))),
|
|
889
|
+
managedPaths
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function stripUndefined(value) {
|
|
893
|
+
for (const key of Object.keys(value)) {
|
|
894
|
+
if (value[key] === void 0) {
|
|
895
|
+
delete value[key];
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return value;
|
|
899
|
+
}
|
|
900
|
+
function isPlainObject(value) {
|
|
901
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
902
|
+
}
|
|
903
|
+
function deepMerge(base, override) {
|
|
904
|
+
const result = { ...base };
|
|
905
|
+
for (const [key, value] of Object.entries(override)) {
|
|
906
|
+
const existing = result[key];
|
|
907
|
+
result[key] = isPlainObject(existing) && isPlainObject(value) ? deepMerge(existing, value) : value;
|
|
908
|
+
}
|
|
909
|
+
return result;
|
|
910
|
+
}
|
|
911
|
+
function titleCase(value) {
|
|
912
|
+
return value.split(/[-_.]/).filter(Boolean).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join(" ");
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/build.ts
|
|
916
|
+
var allTargets = ["cursor", "claude", "antigravity", "copilot"];
|
|
917
|
+
async function build(options = {}) {
|
|
918
|
+
const project = await loadConfig(options.cwd, options.configPath);
|
|
919
|
+
const targets = options.target ? [options.target] : allTargets.filter((target) => project.config.targets[target]);
|
|
920
|
+
const guard = buildDeleteGuard(
|
|
921
|
+
project.rootDir,
|
|
922
|
+
project.config,
|
|
923
|
+
project.configPath
|
|
924
|
+
);
|
|
925
|
+
const artifacts = [];
|
|
926
|
+
for (const target of targets) {
|
|
927
|
+
artifacts.push(await emitTarget(project, target, options.outDir));
|
|
928
|
+
}
|
|
929
|
+
assertNoCrossTargetCollisions(artifacts);
|
|
930
|
+
if (!options.dryRun) {
|
|
931
|
+
for (const artifact2 of artifacts) {
|
|
932
|
+
await pruneManagedFiles(artifact2, { guard });
|
|
933
|
+
await writeArtifact(artifact2.outDir, artifact2.files);
|
|
934
|
+
await writeManagedManifest(artifact2);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return artifacts;
|
|
938
|
+
}
|
|
939
|
+
function assertNoCrossTargetCollisions(artifacts) {
|
|
940
|
+
const owner = /* @__PURE__ */ new Map();
|
|
941
|
+
const collisions = [];
|
|
942
|
+
for (const artifact2 of artifacts) {
|
|
943
|
+
for (const managedPath of artifact2.managedPaths) {
|
|
944
|
+
const absolute = path6.resolve(artifact2.outDir, managedPath);
|
|
945
|
+
const previous = owner.get(absolute);
|
|
946
|
+
if (previous && previous !== artifact2.target) {
|
|
947
|
+
collisions.push(` ${previous} and ${artifact2.target}: ${absolute}`);
|
|
948
|
+
} else {
|
|
949
|
+
owner.set(absolute, artifact2.target);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (collisions.length > 0) {
|
|
954
|
+
throw new Error(
|
|
955
|
+
`Targets write overlapping output paths; give them distinct outDirs:
|
|
956
|
+
${collisions.join("\n")}`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// src/cleanup.ts
|
|
962
|
+
import path7 from "path";
|
|
963
|
+
async function prune(options = {}) {
|
|
964
|
+
const project = await loadProjectConfig(options.cwd, options.configPath);
|
|
965
|
+
const guard = buildDeleteGuard(
|
|
966
|
+
project.rootDir,
|
|
967
|
+
project.config,
|
|
968
|
+
project.configPath,
|
|
969
|
+
options.force
|
|
970
|
+
);
|
|
971
|
+
const artifacts = await build({
|
|
972
|
+
cwd: options.cwd,
|
|
973
|
+
configPath: options.configPath,
|
|
974
|
+
target: options.target,
|
|
975
|
+
dryRun: true
|
|
976
|
+
});
|
|
977
|
+
const results = [];
|
|
978
|
+
for (const artifact2 of artifacts) {
|
|
979
|
+
results.push(
|
|
980
|
+
await pruneManagedFiles(artifact2, { dryRun: options.dryRun, guard })
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
return results;
|
|
984
|
+
}
|
|
985
|
+
async function clean(options = {}) {
|
|
986
|
+
const project = await loadProjectConfig(options.cwd, options.configPath);
|
|
987
|
+
const guard = buildDeleteGuard(
|
|
988
|
+
project.rootDir,
|
|
989
|
+
project.config,
|
|
990
|
+
project.configPath,
|
|
991
|
+
options.force
|
|
992
|
+
);
|
|
993
|
+
const targets = options.target ? [options.target] : Object.keys(project.config.targets);
|
|
994
|
+
const results = [];
|
|
995
|
+
for (const target of targets) {
|
|
996
|
+
const targetConfig = project.config.targets[target];
|
|
997
|
+
if (!targetConfig) {
|
|
998
|
+
throw new Error(`Target "${target}" is not configured.`);
|
|
999
|
+
}
|
|
1000
|
+
const outDir = path7.resolve(project.rootDir, targetConfig.outDir);
|
|
1001
|
+
results.push(
|
|
1002
|
+
await cleanManagedFiles(outDir, target, {
|
|
1003
|
+
dryRun: options.dryRun,
|
|
1004
|
+
guard
|
|
1005
|
+
})
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
return results;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/diff.ts
|
|
1012
|
+
import { promises as fs5 } from "fs";
|
|
1013
|
+
import { tmpdir } from "os";
|
|
1014
|
+
import path8 from "path";
|
|
1015
|
+
async function diffTarget(options) {
|
|
1016
|
+
const tempDir = await fs5.mkdtemp(path8.join(tmpdir(), "pluginpack-diff-"));
|
|
1017
|
+
try {
|
|
1018
|
+
const project = await loadConfig(options.cwd, options.configPath);
|
|
1019
|
+
const ignoredDiffPaths = project.config.targets[options.target]?.ignoredDiffPaths ?? [];
|
|
1020
|
+
const [artifact2] = await build({
|
|
1021
|
+
cwd: options.cwd,
|
|
1022
|
+
configPath: options.configPath,
|
|
1023
|
+
target: options.target,
|
|
1024
|
+
outDir: tempDir
|
|
1025
|
+
});
|
|
1026
|
+
const againstRoot = path8.resolve(
|
|
1027
|
+
options.cwd ?? process.cwd(),
|
|
1028
|
+
options.against
|
|
1029
|
+
);
|
|
1030
|
+
const entries = [];
|
|
1031
|
+
const currentManagedPaths = new Set(
|
|
1032
|
+
artifact2.managedPaths.map(normalizeManagedPath)
|
|
1033
|
+
);
|
|
1034
|
+
for (const relativePath of artifact2.managedPaths) {
|
|
1035
|
+
if (isIgnoredDiffPath(relativePath, ignoredDiffPaths)) {
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
const generatedPath = path8.join(tempDir, relativePath);
|
|
1039
|
+
const againstPath = path8.join(againstRoot, relativePath);
|
|
1040
|
+
if (!await exists(againstPath)) {
|
|
1041
|
+
entries.push({ type: "added", path: relativePath });
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
const [generated, existing] = await Promise.all([
|
|
1045
|
+
fs5.readFile(generatedPath),
|
|
1046
|
+
fs5.readFile(againstPath)
|
|
1047
|
+
]);
|
|
1048
|
+
if (!generated.equals(existing)) {
|
|
1049
|
+
entries.push({ type: "changed", path: relativePath });
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const previous = await readManagedManifest(againstRoot, options.target);
|
|
1053
|
+
for (const previousPath of previous?.files ?? []) {
|
|
1054
|
+
const normalized = normalizeManagedPath(previousPath);
|
|
1055
|
+
if (currentManagedPaths.has(normalized) || isIgnoredDiffPath(normalized, ignoredDiffPaths)) {
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
if (await exists(path8.join(againstRoot, normalized))) {
|
|
1059
|
+
entries.push({ type: "removed", path: normalized });
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return {
|
|
1063
|
+
ok: entries.length === 0,
|
|
1064
|
+
entries
|
|
1065
|
+
};
|
|
1066
|
+
} finally {
|
|
1067
|
+
await fs5.rm(tempDir, { force: true, recursive: true });
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
function isIgnoredDiffPath(relativePath, ignoredPaths) {
|
|
1071
|
+
const normalized = normalizeDiffPath(relativePath);
|
|
1072
|
+
return ignoredPaths.some((ignoredPath) => {
|
|
1073
|
+
const ignored = normalizeDiffPath(ignoredPath);
|
|
1074
|
+
return normalized === ignored || normalized.startsWith(`${ignored}/`);
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
function normalizeDiffPath(value) {
|
|
1078
|
+
const normalized = path8.posix.normalize(value.replace(/\\/g, "/"));
|
|
1079
|
+
return normalized.startsWith("./") ? normalized.slice(2) : normalized;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// src/validate.ts
|
|
1083
|
+
import { promises as fs6, statSync } from "fs";
|
|
1084
|
+
import path9 from "path";
|
|
1085
|
+
import matter from "gray-matter";
|
|
1086
|
+
var pluginNamePattern = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/;
|
|
1087
|
+
var marketplaceNamePattern = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
1088
|
+
async function validateOutput(target, dir) {
|
|
1089
|
+
const root = path9.resolve(dir);
|
|
1090
|
+
const issues = [];
|
|
1091
|
+
if (target === "cursor") {
|
|
1092
|
+
await validateCursor(root, issues);
|
|
1093
|
+
} else if (target === "claude") {
|
|
1094
|
+
await validateClaude(root, issues);
|
|
1095
|
+
} else if (target === "antigravity") {
|
|
1096
|
+
await validateAntigravity(root, issues);
|
|
1097
|
+
} else {
|
|
1098
|
+
await validateCopilot(root, issues);
|
|
1099
|
+
}
|
|
1100
|
+
return {
|
|
1101
|
+
ok: issues.every((issue) => issue.level !== "error"),
|
|
1102
|
+
issues
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
async function validateAntigravity(root, issues) {
|
|
1106
|
+
const entries = await fs6.readdir(root, { withFileTypes: true });
|
|
1107
|
+
const pluginDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => path9.join(root, entry.name));
|
|
1108
|
+
if (pluginDirs.length === 0) {
|
|
1109
|
+
error(
|
|
1110
|
+
issues,
|
|
1111
|
+
"Antigravity output must contain at least one plugin directory."
|
|
1112
|
+
);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
for (const pluginDir of pluginDirs) {
|
|
1116
|
+
const manifest = await readJson(
|
|
1117
|
+
path9.join(pluginDir, "plugin.json"),
|
|
1118
|
+
"Antigravity plugin manifest",
|
|
1119
|
+
issues
|
|
1120
|
+
);
|
|
1121
|
+
if (!manifest) {
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
const pluginName = path9.basename(pluginDir);
|
|
1125
|
+
if (typeof manifest.name !== "string" || !pluginNamePattern.test(manifest.name)) {
|
|
1126
|
+
error(
|
|
1127
|
+
issues,
|
|
1128
|
+
`${pluginName}: plugin.json must have a lowercase kebab-case "name".`
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
if (manifest.name && manifest.name !== pluginName) {
|
|
1132
|
+
error(
|
|
1133
|
+
issues,
|
|
1134
|
+
`${pluginName}: manifest name must match plugin directory name.`
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
for (const field of ["version", "description"]) {
|
|
1138
|
+
if (typeof manifest[field] !== "string" || !manifest[field]) {
|
|
1139
|
+
error(
|
|
1140
|
+
issues,
|
|
1141
|
+
`${pluginName}: plugin.json is missing required field "${field}".`
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
const mcpConfigPath = path9.join(pluginDir, "mcp_config.json");
|
|
1146
|
+
if (await exists(mcpConfigPath)) {
|
|
1147
|
+
await readJson(mcpConfigPath, `${pluginName} MCP config`, issues);
|
|
1148
|
+
}
|
|
1149
|
+
await validateFrontmatter(pluginDir, pluginName, "antigravity", issues);
|
|
1150
|
+
await validateHooks(pluginDir, pluginName, issues);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
async function validateCopilot(root, issues) {
|
|
1154
|
+
const marketplacePath = path9.join(root, ".claude-plugin", "marketplace.json");
|
|
1155
|
+
const marketplace = await readJson(
|
|
1156
|
+
marketplacePath,
|
|
1157
|
+
"Marketplace manifest",
|
|
1158
|
+
issues
|
|
1159
|
+
);
|
|
1160
|
+
if (!marketplace) {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
validateMarketplaceBasics(marketplace, issues);
|
|
1164
|
+
if (!await exists(path9.join(root, ".github", "plugin", "marketplace.json"))) {
|
|
1165
|
+
error(
|
|
1166
|
+
issues,
|
|
1167
|
+
"Copilot output must mirror the marketplace at .github/plugin/marketplace.json."
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
const plugins = Array.isArray(marketplace.plugins) ? marketplace.plugins : [];
|
|
1171
|
+
if (plugins.length === 0) {
|
|
1172
|
+
error(issues, 'Marketplace "plugins" must be a non-empty array.');
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
for (const [index, entry] of plugins.entries()) {
|
|
1176
|
+
const pluginName = validatePluginEntry(entry, index, root, issues);
|
|
1177
|
+
if (!pluginName) {
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
await validateFrontmatter(
|
|
1181
|
+
path9.join(root, entry.source),
|
|
1182
|
+
pluginName,
|
|
1183
|
+
"copilot",
|
|
1184
|
+
issues
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
async function validateCursor(root, issues) {
|
|
1189
|
+
const marketplacePath = path9.join(root, ".cursor-plugin", "marketplace.json");
|
|
1190
|
+
const marketplace = await readJson(
|
|
1191
|
+
marketplacePath,
|
|
1192
|
+
"Marketplace manifest",
|
|
1193
|
+
issues
|
|
1194
|
+
);
|
|
1195
|
+
if (!marketplace) {
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
validateMarketplaceBasics(marketplace, issues);
|
|
1199
|
+
const plugins = Array.isArray(marketplace.plugins) ? marketplace.plugins : [];
|
|
1200
|
+
if (plugins.length === 0) {
|
|
1201
|
+
error(issues, 'Marketplace "plugins" must be a non-empty array.');
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
for (const [index, entry] of plugins.entries()) {
|
|
1205
|
+
const pluginName = validatePluginEntry(entry, index, root, issues);
|
|
1206
|
+
if (!pluginName) {
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
const pluginDir = path9.join(root, entry.source);
|
|
1210
|
+
const manifest = await readJson(
|
|
1211
|
+
path9.join(pluginDir, ".cursor-plugin", "plugin.json"),
|
|
1212
|
+
`${pluginName} plugin manifest`,
|
|
1213
|
+
issues
|
|
1214
|
+
);
|
|
1215
|
+
if (!manifest) {
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
if (manifest.name !== pluginName) {
|
|
1219
|
+
error(
|
|
1220
|
+
issues,
|
|
1221
|
+
`${pluginName}: marketplace entry name does not match plugin.json name ("${manifest.name}").`
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
await validateReferencedManifestPaths(
|
|
1225
|
+
pluginDir,
|
|
1226
|
+
pluginName,
|
|
1227
|
+
manifest,
|
|
1228
|
+
["logo", "commands", "agents", "skills", "rules", "hooks", "mcpServers"],
|
|
1229
|
+
issues
|
|
1230
|
+
);
|
|
1231
|
+
await validateFrontmatter(pluginDir, pluginName, "cursor", issues);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
async function validateClaude(root, issues) {
|
|
1235
|
+
const marketplacePath = path9.join(root, ".claude-plugin", "marketplace.json");
|
|
1236
|
+
const marketplace = await readJson(
|
|
1237
|
+
marketplacePath,
|
|
1238
|
+
"Marketplace manifest",
|
|
1239
|
+
issues
|
|
1240
|
+
);
|
|
1241
|
+
if (!marketplace) {
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
validateMarketplaceBasics(marketplace, issues);
|
|
1245
|
+
const plugins = Array.isArray(marketplace.plugins) ? marketplace.plugins : [];
|
|
1246
|
+
if (plugins.length === 0) {
|
|
1247
|
+
error(issues, 'Marketplace "plugins" must be a non-empty array.');
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
for (const [index, entry] of plugins.entries()) {
|
|
1251
|
+
const pluginName = validatePluginEntry(entry, index, root, issues);
|
|
1252
|
+
if (!pluginName) {
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
const pluginDir = path9.join(root, entry.source);
|
|
1256
|
+
const manifest = await readJson(
|
|
1257
|
+
path9.join(pluginDir, ".claude-plugin", "plugin.json"),
|
|
1258
|
+
`${pluginName} plugin manifest`,
|
|
1259
|
+
issues
|
|
1260
|
+
);
|
|
1261
|
+
if (!manifest) {
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
if (manifest.name !== pluginName) {
|
|
1265
|
+
error(
|
|
1266
|
+
issues,
|
|
1267
|
+
`${pluginName}: marketplace entry name does not match plugin.json name ("${manifest.name}").`
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
for (const field of ["name", "version", "description"]) {
|
|
1271
|
+
if (typeof manifest[field] !== "string" || !manifest[field]) {
|
|
1272
|
+
error(
|
|
1273
|
+
issues,
|
|
1274
|
+
`${pluginName}: plugin.json is missing required field "${field}".`
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (!manifest.author || typeof manifest.author.name !== "string" || !manifest.author.name) {
|
|
1279
|
+
error(issues, `${pluginName}: plugin.json is missing "author.name".`);
|
|
1280
|
+
}
|
|
1281
|
+
await validateFrontmatter(pluginDir, pluginName, "claude", issues);
|
|
1282
|
+
await validateHooks(pluginDir, pluginName, issues);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
function validateMarketplaceBasics(marketplace, issues) {
|
|
1286
|
+
if (typeof marketplace.name !== "string" || !marketplaceNamePattern.test(marketplace.name)) {
|
|
1287
|
+
error(
|
|
1288
|
+
issues,
|
|
1289
|
+
'Marketplace "name" must be lowercase kebab-case and start/end with an alphanumeric character.'
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
const owner = marketplace.owner;
|
|
1293
|
+
if (owner && (typeof owner.name !== "string" || !owner.name)) {
|
|
1294
|
+
error(
|
|
1295
|
+
issues,
|
|
1296
|
+
'Marketplace "owner.name" must be a non-empty string when owner is present.'
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function validatePluginEntry(entry, index, root, issues) {
|
|
1301
|
+
if (!entry || typeof entry !== "object") {
|
|
1302
|
+
error(issues, `plugins[${index}] must be an object.`);
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
if (typeof entry.name !== "string" || !pluginNamePattern.test(entry.name)) {
|
|
1306
|
+
error(
|
|
1307
|
+
issues,
|
|
1308
|
+
`plugins[${index}].name must be lowercase and use only alphanumerics, hyphens, and periods.`
|
|
1309
|
+
);
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
if (typeof entry.source !== "string" || !isSafeRelativePath(entry.source)) {
|
|
1313
|
+
error(issues, `plugins[${index}].source must be a safe relative path.`);
|
|
1314
|
+
return null;
|
|
1315
|
+
}
|
|
1316
|
+
const pluginDir = path9.join(root, entry.source);
|
|
1317
|
+
if (!entry.source.startsWith("http") && !pathExistsSync(pluginDir)) {
|
|
1318
|
+
error(
|
|
1319
|
+
issues,
|
|
1320
|
+
`plugins[${index}].source directory is missing: ${entry.source}`
|
|
1321
|
+
);
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
return entry.name;
|
|
1325
|
+
}
|
|
1326
|
+
async function validateReferencedManifestPaths(pluginDir, pluginName, manifest, fields, issues) {
|
|
1327
|
+
for (const field of fields) {
|
|
1328
|
+
for (const value of extractPathValues(manifest[field])) {
|
|
1329
|
+
if (value.startsWith("http://") || value.startsWith("https://")) {
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
if (!isSafeRelativePath(value)) {
|
|
1333
|
+
error(
|
|
1334
|
+
issues,
|
|
1335
|
+
`${pluginName}: field "${field}" has unsafe path "${value}".`
|
|
1336
|
+
);
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
if (!await exists(path9.join(pluginDir, value))) {
|
|
1340
|
+
error(
|
|
1341
|
+
issues,
|
|
1342
|
+
`${pluginName}: field "${field}" references missing path "${value}".`
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
function extractPathValues(value) {
|
|
1349
|
+
if (typeof value === "string") {
|
|
1350
|
+
return [value];
|
|
1351
|
+
}
|
|
1352
|
+
if (Array.isArray(value)) {
|
|
1353
|
+
return value.flatMap(extractPathValues);
|
|
1354
|
+
}
|
|
1355
|
+
if (value && typeof value === "object") {
|
|
1356
|
+
const object = value;
|
|
1357
|
+
return [object.path, object.file].filter(
|
|
1358
|
+
(entry) => typeof entry === "string"
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
return [];
|
|
1362
|
+
}
|
|
1363
|
+
async function validateFrontmatter(pluginDir, pluginName, target, issues) {
|
|
1364
|
+
const files = await walkFiles(pluginDir);
|
|
1365
|
+
for (const file of files) {
|
|
1366
|
+
const kind = detectFrontmatterKind(file);
|
|
1367
|
+
if (!kind) {
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
const relative = toPosix(path9.relative(pluginDir, file));
|
|
1371
|
+
const parsed = parseFrontmatter(await fs6.readFile(file, "utf8"));
|
|
1372
|
+
if (!parsed.ok) {
|
|
1373
|
+
error(
|
|
1374
|
+
issues,
|
|
1375
|
+
`${pluginName}: ${kind} frontmatter error in ${relative}: ${parsed.error}`
|
|
1376
|
+
);
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
if (kind === "agent") {
|
|
1380
|
+
requireFrontmatter(
|
|
1381
|
+
pluginName,
|
|
1382
|
+
kind,
|
|
1383
|
+
relative,
|
|
1384
|
+
parsed.value,
|
|
1385
|
+
["name", "description"],
|
|
1386
|
+
issues
|
|
1387
|
+
);
|
|
1388
|
+
} else if (kind === "command") {
|
|
1389
|
+
requireFrontmatter(
|
|
1390
|
+
pluginName,
|
|
1391
|
+
kind,
|
|
1392
|
+
relative,
|
|
1393
|
+
parsed.value,
|
|
1394
|
+
["description"],
|
|
1395
|
+
issues
|
|
1396
|
+
);
|
|
1397
|
+
if (target === "cursor" || target === "antigravity" || target === "copilot") {
|
|
1398
|
+
requireFrontmatter(
|
|
1399
|
+
pluginName,
|
|
1400
|
+
kind,
|
|
1401
|
+
relative,
|
|
1402
|
+
parsed.value,
|
|
1403
|
+
["name"],
|
|
1404
|
+
issues
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
} else if (kind === "skill") {
|
|
1408
|
+
if (target === "cursor") {
|
|
1409
|
+
requireFrontmatter(
|
|
1410
|
+
pluginName,
|
|
1411
|
+
kind,
|
|
1412
|
+
relative,
|
|
1413
|
+
parsed.value,
|
|
1414
|
+
["name", "description"],
|
|
1415
|
+
issues
|
|
1416
|
+
);
|
|
1417
|
+
} else if (!parsed.value.description && !parsed.value.when_to_use) {
|
|
1418
|
+
error(
|
|
1419
|
+
issues,
|
|
1420
|
+
`${pluginName}: ${kind} frontmatter error in ${relative}: Missing required "description" field.`
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
} else if (kind === "rule") {
|
|
1424
|
+
requireFrontmatter(
|
|
1425
|
+
pluginName,
|
|
1426
|
+
kind,
|
|
1427
|
+
relative,
|
|
1428
|
+
parsed.value,
|
|
1429
|
+
["description"],
|
|
1430
|
+
issues
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
function detectFrontmatterKind(filePath) {
|
|
1436
|
+
const normalized = toPosix(filePath);
|
|
1437
|
+
const inSkillContent = /\/skills\/[^/]+\//.test(normalized) && !normalized.endsWith("/SKILL.md");
|
|
1438
|
+
if (normalized.includes("/agents/") && /\.(md|mdc|markdown)$/.test(normalized) && !inSkillContent) {
|
|
1439
|
+
return "agent";
|
|
1440
|
+
}
|
|
1441
|
+
if (normalized.includes("/skills/") && normalized.endsWith("/SKILL.md")) {
|
|
1442
|
+
return "skill";
|
|
1443
|
+
}
|
|
1444
|
+
if (normalized.includes("/commands/") && /\.(md|mdc|markdown|txt)$/.test(normalized) && !inSkillContent) {
|
|
1445
|
+
return "command";
|
|
1446
|
+
}
|
|
1447
|
+
if (normalized.includes("/rules/") && /\.(md|mdc|markdown)$/.test(normalized) && !inSkillContent) {
|
|
1448
|
+
return "rule";
|
|
1449
|
+
}
|
|
1450
|
+
return null;
|
|
1451
|
+
}
|
|
1452
|
+
function parseFrontmatter(content) {
|
|
1453
|
+
try {
|
|
1454
|
+
const parsed = matter(content);
|
|
1455
|
+
if (Object.keys(parsed.data).length === 0) {
|
|
1456
|
+
return { ok: false, error: "No frontmatter found" };
|
|
1457
|
+
}
|
|
1458
|
+
return { ok: true, value: parsed.data };
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
return { ok: false, error: `YAML parse failed: ${err.message}` };
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
function requireFrontmatter(pluginName, kind, relative, frontmatter, fields, issues) {
|
|
1464
|
+
for (const field of fields) {
|
|
1465
|
+
if (typeof frontmatter[field] !== "string" || !frontmatter[field]) {
|
|
1466
|
+
error(
|
|
1467
|
+
issues,
|
|
1468
|
+
`${pluginName}: ${kind} frontmatter error in ${relative}: Missing required "${field}" field.`
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
async function validateHooks(pluginDir, pluginName, issues) {
|
|
1474
|
+
const hooksPath = path9.join(pluginDir, "hooks", "hooks.json");
|
|
1475
|
+
if (!await exists(hooksPath)) {
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
const hooks = await readJson(
|
|
1479
|
+
hooksPath,
|
|
1480
|
+
`${pluginName} hooks/hooks.json`,
|
|
1481
|
+
issues
|
|
1482
|
+
);
|
|
1483
|
+
if (hooks && (!hooks.hooks || typeof hooks.hooks !== "object")) {
|
|
1484
|
+
error(
|
|
1485
|
+
issues,
|
|
1486
|
+
`${pluginName}: hooks/hooks.json must have a "hooks" object.`
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
async function readJson(filePath, context, issues) {
|
|
1491
|
+
try {
|
|
1492
|
+
return JSON.parse(await fs6.readFile(filePath, "utf8"));
|
|
1493
|
+
} catch (err) {
|
|
1494
|
+
error(
|
|
1495
|
+
issues,
|
|
1496
|
+
`${context} is missing or invalid (${filePath}): ${err.message}`
|
|
1497
|
+
);
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
function pathExistsSync(filePath) {
|
|
1502
|
+
try {
|
|
1503
|
+
statSync(filePath);
|
|
1504
|
+
return true;
|
|
1505
|
+
} catch {
|
|
1506
|
+
return false;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
function error(issues, message) {
|
|
1510
|
+
issues.push({ level: "error", message });
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
export {
|
|
1514
|
+
defineConfig,
|
|
1515
|
+
loadConfig,
|
|
1516
|
+
build,
|
|
1517
|
+
prune,
|
|
1518
|
+
clean,
|
|
1519
|
+
diffTarget,
|
|
1520
|
+
validateOutput
|
|
1521
|
+
};
|
|
1522
|
+
//# sourceMappingURL=chunk-HR2ZVYJA.js.map
|