@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.
@@ -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