@iseoane/agent-depot 0.4.0 → 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.
Files changed (52) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.es.md +969 -0
  3. package/README.md +254 -25
  4. package/dist/src/app-flow.js +52 -51
  5. package/dist/src/app-recipe-removal.js +63 -0
  6. package/dist/src/cli.js +122 -11
  7. package/dist/src/executable-resolution.js +54 -0
  8. package/dist/src/git-source.js +129 -6
  9. package/dist/src/package-bundles.js +614 -0
  10. package/dist/src/package-cli.js +412 -0
  11. package/dist/src/package-content.js +158 -0
  12. package/dist/src/package-flow.js +636 -0
  13. package/dist/src/package-host-plans.js +240 -0
  14. package/dist/src/package-host-state.js +349 -0
  15. package/dist/src/package-model.js +33 -0
  16. package/dist/src/package-owned-skills.js +122 -0
  17. package/dist/src/package-state.js +404 -0
  18. package/dist/src/profile-cli.js +15 -9
  19. package/dist/src/profile-export.js +44 -9
  20. package/dist/src/profile-import.js +80 -17
  21. package/dist/src/profile.js +78 -3
  22. package/dist/src/skill-discovery.js +2 -2
  23. package/dist/src/skill-install.js +16 -0
  24. package/dist/src/sources.js +11 -0
  25. package/dist/src/tui/app-actions.js +1 -1
  26. package/dist/src/tui/app.js +5 -6
  27. package/dist/src/tui/catalog-browse.js +10 -3
  28. package/dist/src/tui/catalog-panel.js +7 -3
  29. package/dist/src/tui/catalog-state.js +57 -6
  30. package/dist/src/tui/catalog-tree.js +52 -10
  31. package/dist/src/tui/catalog-view.js +27 -4
  32. package/dist/src/tui/environment.js +8 -1
  33. package/dist/src/tui/installations-browse.js +20 -2
  34. package/dist/src/tui/installations-rows.js +2 -1
  35. package/dist/src/tui/installations-tree.js +14 -1
  36. package/dist/src/tui/installations-view.js +8 -2
  37. package/dist/src/tui/installations.js +12 -2
  38. package/dist/src/tui/manage-actions.js +2 -0
  39. package/dist/src/tui/manage-panel.js +5 -0
  40. package/dist/src/tui/package-actions.js +165 -0
  41. package/dist/src/tui/package-rows.js +87 -0
  42. package/dist/src/tui/profile-view.js +36 -14
  43. package/dist/src/tui/sources-mode.js +2 -0
  44. package/dist/src/tui/sources-recipes.js +116 -53
  45. package/dist/src/tui/sources-view.js +57 -19
  46. package/dist/src/tui/tree.js +3 -4
  47. package/dist/src/tui/updates-flow.js +4 -1
  48. package/dist/src/tui/updates-panel.js +2 -2
  49. package/dist/src/tui/updates.js +72 -4
  50. package/dist/src/user-global-skill-inventory.js +7 -2
  51. package/package.json +2 -1
  52. package/skills/agent-depot-apprecipe/SKILL.md +29 -5
@@ -0,0 +1,614 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import { EXCLUDED_LIFECYCLE_SEGMENTS, } from "./skill-discovery.js";
4
+ /** A malformed manifest crossing the boundary fails closed instead of guessing. */
5
+ export class PackageManifestError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "PackageManifestError";
9
+ }
10
+ }
11
+ /**
12
+ * Reads the `pi` manifest key, or the conventional `extensions/`, `skills/`,
13
+ * `prompts/`, and `themes/` directories when no `pi` key is present. Component
14
+ * paths are relative to the package root; the conventional fallback declares
15
+ * all four slots and lets discovery drop the ones with no files.
16
+ */
17
+ export function parsePiPackageManifest(raw, root) {
18
+ const packageRoot = normalizeRoot(root);
19
+ const manifest = parseJsonObject(raw);
20
+ const components = resolveComponentPaths(packageRoot, buildComponentInventory("pi", manifest));
21
+ const version = manifestVersionEvidence(manifest.version, "bundle");
22
+ return Object.freeze({
23
+ host: "pi",
24
+ coordinates: Object.freeze({ host: "pi", root: packageRoot }),
25
+ name: manifestName(manifest, packageRoot, "package"),
26
+ ...(version === undefined ? {} : { version }),
27
+ components,
28
+ warnings: Object.freeze([]),
29
+ installable: true,
30
+ manifestDigest: manifestDigest([{ path: manifestPathFor(packageRoot, "package.json"), content: raw }]),
31
+ });
32
+ }
33
+ /**
34
+ * Reads one Claude Code plugin manifest. `name` is required. Component paths
35
+ * are relative to the plugin root. The marketplace binding is added by
36
+ * `parseClaudeMarketplace` or `discoverBundlesInSnapshot`, which own the
37
+ * `marketplaceRoot` coordinate.
38
+ */
39
+ export function parseClaudePluginManifest(raw, root) {
40
+ const pluginRoot = normalizeRoot(root);
41
+ const manifest = parseJsonObject(raw);
42
+ const name = requiredName(manifest, "plugin.json");
43
+ const components = resolveComponentPaths(pluginRoot, buildComponentInventory("claude", manifest));
44
+ const version = manifestVersionEvidence(manifest.version, "bundle");
45
+ return Object.freeze({
46
+ host: "claude",
47
+ coordinates: Object.freeze({ host: "claude", marketplaceRoot: pluginRoot, pluginName: name }),
48
+ name,
49
+ ...(version === undefined ? {} : { version }),
50
+ components,
51
+ warnings: Object.freeze([]),
52
+ installable: true,
53
+ manifestDigest: manifestDigest([{ path: manifestPathFor(pluginRoot, ".claude-plugin/plugin.json"), content: raw }]),
54
+ });
55
+ }
56
+ /**
57
+ * Reads one marketplace and returns a descriptor per `plugins` entry. The
58
+ * `readPluginManifest` resolver supplies the raw plugin.json for an in-repo
59
+ * entry, because the entry version overrides the plugin manifest version and
60
+ * the plugin manifest owns the component inventory (agents, hooks, and so on).
61
+ * A `command` source and any source outside the repository yield a descriptor
62
+ * with a warning rather than a throw.
63
+ */
64
+ export function parseClaudeMarketplace(raw, root, readPluginManifest) {
65
+ const marketplaceRoot = normalizeRoot(root);
66
+ const marketplace = parseJsonObject(raw);
67
+ const entries = marketplace.plugins;
68
+ if (!Array.isArray(entries)) {
69
+ throw new PackageManifestError("marketplace.json must define a plugins array");
70
+ }
71
+ const metadataPluginRoot = isRecord(marketplace.metadata) && typeof marketplace.metadata.pluginRoot === "string"
72
+ ? normalizeComponentPath(marketplace.metadata.pluginRoot)
73
+ : "";
74
+ const descriptors = [];
75
+ for (const [index, entry] of entries.entries()) {
76
+ if (!isRecord(entry)) {
77
+ throw new PackageManifestError(`marketplace.json plugins[${index}] must be an object`);
78
+ }
79
+ descriptors.push(buildMarketplaceDescriptor(entry, `plugins[${index}]`, marketplaceRoot, metadataPluginRoot, raw, readPluginManifest));
80
+ }
81
+ return Object.freeze(descriptors);
82
+ }
83
+ /**
84
+ * The `name` a Claude marketplace manifest declares. `planHostSteps` needs it to
85
+ * build `<plugin>@<marketplace>` and to name a marketplace refresh, because the
86
+ * name lives only in the Source's `.claude-plugin/marketplace.json`.
87
+ */
88
+ export function claudeMarketplaceName(raw) {
89
+ const manifest = parseJsonObject(raw);
90
+ return typeof manifest.name === "string" && manifest.name.trim() !== "" ? manifest.name.trim() : undefined;
91
+ }
92
+ /**
93
+ * The typed component inventory for one host manifest. Pi reads the `pi` key or
94
+ * the conventional directories; Claude reads the plugin component keys plus the
95
+ * default `skills/` directory. Kinds are normalized once; ownership is derived
96
+ * from the kind, and the deferred kinds are referenced only from
97
+ * `DEFERRED_COMPONENT_KINDS`, never re-listed here.
98
+ */
99
+ export function buildComponentInventory(host, manifest) {
100
+ if (!isRecord(manifest)) {
101
+ return Object.freeze([]);
102
+ }
103
+ return host === "pi" ? buildPiInventory(manifest) : buildClaudeInventory(manifest);
104
+ }
105
+ /**
106
+ * One pass over the snapshot. Indexes `package.json` at the scope root and
107
+ * `.claude-plugin/*.json` anywhere under it. A dual-format repository yields
108
+ * one Pi descriptor and one Claude descriptor over the same skills directory.
109
+ * Refusals are warnings, not throws: a scoped Source URL refuses a Pi package,
110
+ * a Pi manifest below the scope root is not a bundle, and a Claude plugin with
111
+ * no in-repo marketplace binding is reported but not installable. A
112
+ * conventional Pi bundle must show Pi intent, because `skills/` and `themes/`
113
+ * are also other Hosts' default locations; see `conventionalPiBundle`.
114
+ */
115
+ export function discoverBundlesInSnapshot(files, scope = {}) {
116
+ const scopeRoot = normalizeRoot(scope.directory ?? "");
117
+ const descriptors = [];
118
+ const rootPackage = files.find((file) => file.path === manifestPathFor(scopeRoot, "package.json"));
119
+ if (rootPackage !== undefined) {
120
+ const bundle = hasPiManifestKey(rootPackage.content)
121
+ ? parsePiPackageManifest(rootPackage.content, scopeRoot)
122
+ : conventionalPiBundle(rootPackage.content, scopeRoot, files);
123
+ if (bundle !== undefined) {
124
+ descriptors.push(scope.directory === undefined
125
+ ? bundle
126
+ : { ...withWarnings(bundle, ["Pi packages are not installable from a scoped Source; register the repository root"]), installable: false });
127
+ }
128
+ }
129
+ const pluginManifests = new Map();
130
+ for (const file of files) {
131
+ const pluginRoot = pluginRootForPath(file.path);
132
+ if (pluginRoot !== undefined) {
133
+ pluginManifests.set(pluginRoot, file.content);
134
+ }
135
+ }
136
+ const boundPluginRoots = new Set();
137
+ const readPluginManifest = (pluginRoot) => {
138
+ boundPluginRoots.add(pluginRoot);
139
+ return pluginManifests.get(pluginRoot);
140
+ };
141
+ for (const file of files) {
142
+ const marketplaceRoot = marketplaceRootForPath(file.path);
143
+ if (marketplaceRoot === undefined) {
144
+ continue;
145
+ }
146
+ descriptors.push(...parseClaudeMarketplace(file.content, marketplaceRoot, readPluginManifest));
147
+ }
148
+ for (const [pluginRoot, raw] of pluginManifests) {
149
+ if (boundPluginRoots.has(pluginRoot)) {
150
+ continue;
151
+ }
152
+ const descriptor = parseClaudePluginManifest(raw, pluginRoot);
153
+ descriptors.push({ ...withWarnings(descriptor, [
154
+ `Claude plugin ${JSON.stringify(pluginRoot)} has no in-repo marketplace binding; it is not installable`,
155
+ ]), installable: false });
156
+ }
157
+ return Object.freeze(descriptors.map((descriptor) => populateSkillNames(descriptor, files)));
158
+ }
159
+ /**
160
+ * The owned-skill matcher shared by the guard module. A skill path is owned by
161
+ * a bundle when it sits under a `skill-category` component path, and is not
162
+ * claimed when it crosses an excluded lifecycle segment, matching the filter
163
+ * skill discovery applies.
164
+ */
165
+ export function bundleOwnedSkillMatcher(descriptors) {
166
+ const owners = skillCategoryOwners(descriptors);
167
+ return (skillPath) => {
168
+ const normalized = normalizeSkillDirectory(skillPath);
169
+ if (normalized === undefined || hasLifecycleSegment(normalized)) {
170
+ return undefined;
171
+ }
172
+ for (const owner of owners) {
173
+ if (owner.roots.some((root) => matchesBundlePath(root, normalized))) {
174
+ return owner.descriptor;
175
+ }
176
+ }
177
+ return undefined;
178
+ };
179
+ }
180
+ /**
181
+ * The owned-skill matcher for one bare Skill directory name. Names come from
182
+ * `populateSkillNames`, so they are read from the same snapshot pass that fed
183
+ * discovery rather than from a hand-synced list, and the lifecycle filter is
184
+ * already applied where they are collected.
185
+ */
186
+ export function bundleOwnedSkillNameMatcher(descriptors) {
187
+ const owners = skillCategoryOwners(descriptors);
188
+ return (skillName) => {
189
+ if (skillName === "" || skillName.includes("/") || skillName.includes("\\")) {
190
+ return undefined;
191
+ }
192
+ for (const owner of owners) {
193
+ if (owner.names.has(skillName)) {
194
+ return owner.descriptor;
195
+ }
196
+ }
197
+ return undefined;
198
+ };
199
+ }
200
+ function skillCategoryOwners(descriptors) {
201
+ return descriptors.map((descriptor) => {
202
+ const components = descriptor.components.filter((component) => component.kind === "skills" && component.ownership === "skill-category");
203
+ return {
204
+ descriptor,
205
+ roots: components.flatMap((component) => component.paths),
206
+ names: new Set(components.flatMap((component) => component.skillNames ?? [])),
207
+ };
208
+ });
209
+ }
210
+ const COMPONENT_KIND_ORDER = [
211
+ "skills", "extensions", "prompts", "themes", "agents", "commands", "hooks",
212
+ "mcp-servers", "lsp-servers", "output-styles", "workflows", "monitors",
213
+ ];
214
+ const COMPONENT_EFFECTS = Object.freeze({
215
+ skills: "instruction",
216
+ extensions: "executable",
217
+ prompts: "data",
218
+ themes: "data",
219
+ agents: "instruction",
220
+ commands: "instruction",
221
+ hooks: "executable",
222
+ "mcp-servers": "executable",
223
+ "lsp-servers": "executable",
224
+ "output-styles": "data",
225
+ workflows: "instruction",
226
+ monitors: "executable",
227
+ });
228
+ /**
229
+ * Conventional directories that show Pi intent on their own. `skills/` and
230
+ * `themes/` are also Claude Code default locations, so neither is evidence of
231
+ * Pi packaging by itself. `extensions/` and `prompts/` have no other Host's
232
+ * conventional meaning at a package root.
233
+ */
234
+ const PI_INTENT_COMPONENT_KINDS = new Set(["extensions", "prompts"]);
235
+ const PI_COMPONENT_KEYS = Object.freeze([
236
+ ["skills", "skills"],
237
+ ["extensions", "extensions"],
238
+ ["prompts", "prompts"],
239
+ ["themes", "themes"],
240
+ ]);
241
+ const CLAUDE_COMPONENT_KEYS = Object.freeze([
242
+ ["agents", "agents"],
243
+ ["commands", "commands"],
244
+ ["hooks", "hooks"],
245
+ ["mcpServers", "mcp-servers"],
246
+ ["lspServers", "lsp-servers"],
247
+ ["outputStyles", "output-styles"],
248
+ ["workflows", "workflows"],
249
+ ]);
250
+ function componentOwnership(kind) {
251
+ return kind === "skills" ? "skill-category" : "host-only";
252
+ }
253
+ function makeComponent(kind, paths) {
254
+ return Object.freeze({
255
+ kind,
256
+ ownership: componentOwnership(kind),
257
+ effect: COMPONENT_EFFECTS[kind],
258
+ paths: Object.freeze(dedupeStrings(paths)),
259
+ });
260
+ }
261
+ function buildPiInventory(manifest) {
262
+ const pi = isRecord(manifest.pi) ? manifest.pi : undefined;
263
+ if (pi !== undefined) {
264
+ const components = [];
265
+ for (const [key, kind] of PI_COMPONENT_KEYS) {
266
+ const paths = stringPaths(pi[key]);
267
+ if (paths.length > 0) {
268
+ components.push(makeComponent(kind, paths));
269
+ }
270
+ }
271
+ return Object.freeze(sortComponents(components));
272
+ }
273
+ // Conventional fallback: the four slots, pruned against the snapshot by discovery.
274
+ return Object.freeze(sortComponents(PI_COMPONENT_KEYS.map(([key, kind]) => makeComponent(kind, [key]))));
275
+ }
276
+ function buildClaudeInventory(manifest) {
277
+ const components = [];
278
+ // Skills add to the default skills/ directory rather than replacing it.
279
+ components.push(makeComponent("skills", ["skills", ...stringPaths(manifest.skills)]));
280
+ for (const [key, kind] of CLAUDE_COMPONENT_KEYS) {
281
+ const paths = stringPaths(manifest[key]);
282
+ if (paths.length > 0) {
283
+ components.push(makeComponent(kind, paths));
284
+ }
285
+ }
286
+ if (isRecord(manifest.experimental)) {
287
+ const themes = stringPaths(manifest.experimental.themes);
288
+ if (themes.length > 0) {
289
+ components.push(makeComponent("themes", themes));
290
+ }
291
+ const monitors = stringPaths(manifest.experimental.monitors);
292
+ if (monitors.length > 0) {
293
+ components.push(makeComponent("monitors", monitors));
294
+ }
295
+ }
296
+ return Object.freeze(sortComponents(components));
297
+ }
298
+ function buildMarketplaceDescriptor(entry, label, marketplaceRoot, metadataPluginRoot, marketplaceRaw, readPluginManifest) {
299
+ const entryName = requiredName(entry, label);
300
+ if (entry.source === undefined) {
301
+ throw new PackageManifestError(`marketplace.json ${label}.source is required`);
302
+ }
303
+ const resolution = resolveMarketplaceSource(entry.source, marketplaceRoot, metadataPluginRoot);
304
+ if (resolution.kind === "command") {
305
+ return marketplaceOnlyDescriptor({
306
+ entry, entryName, marketplaceRoot, marketplaceRaw,
307
+ warnings: [`marketplace entry ${JSON.stringify(entryName)} uses a command source, which Agent Depot will not run; it is not installable`],
308
+ installable: false,
309
+ });
310
+ }
311
+ if (resolution.kind === "external") {
312
+ return marketplaceOnlyDescriptor({
313
+ entry, entryName, marketplaceRoot, marketplaceRaw,
314
+ warnings: [`marketplace entry ${JSON.stringify(entryName)} points outside the registered repository; it is not installable`],
315
+ installable: false,
316
+ });
317
+ }
318
+ const pluginRoot = resolution.pluginRoot;
319
+ const pluginRaw = readPluginManifest(pluginRoot);
320
+ if (pluginRaw === undefined) {
321
+ return marketplaceOnlyDescriptor({
322
+ entry, entryName, marketplaceRoot, marketplaceRaw,
323
+ warnings: [`marketplace entry ${JSON.stringify(entryName)} has no plugin manifest at ${manifestPathFor(pluginRoot, ".claude-plugin/plugin.json")}`],
324
+ installable: true,
325
+ });
326
+ }
327
+ const plugin = parseClaudePluginManifest(pluginRaw, pluginRoot);
328
+ const warnings = [];
329
+ if (plugin.name !== entryName) {
330
+ warnings.push(`marketplace entry name ${JSON.stringify(entryName)} differs from plugin manifest name ${JSON.stringify(plugin.name)}`);
331
+ }
332
+ const entryVersion = manifestVersionEvidence(entry.version, "marketplace-entry");
333
+ const version = entryVersion ?? plugin.version;
334
+ return Object.freeze({
335
+ host: "claude",
336
+ coordinates: Object.freeze({ host: "claude", marketplaceRoot, pluginName: entryName }),
337
+ name: entryName,
338
+ ...(version === undefined ? {} : { version }),
339
+ components: plugin.components,
340
+ warnings: Object.freeze(warnings),
341
+ installable: true,
342
+ manifestDigest: manifestDigest([
343
+ { path: manifestPathFor(marketplaceRoot, ".claude-plugin/marketplace.json"), content: marketplaceRaw },
344
+ { path: manifestPathFor(pluginRoot, ".claude-plugin/plugin.json"), content: pluginRaw },
345
+ ]),
346
+ });
347
+ }
348
+ function marketplaceOnlyDescriptor(parts) {
349
+ const version = manifestVersionEvidence(parts.entry.version, "marketplace-entry");
350
+ return Object.freeze({
351
+ host: "claude",
352
+ coordinates: Object.freeze({ host: "claude", marketplaceRoot: parts.marketplaceRoot, pluginName: parts.entryName }),
353
+ name: parts.entryName,
354
+ ...(version === undefined ? {} : { version }),
355
+ components: resolveComponentPaths(parts.marketplaceRoot, buildComponentInventory("claude", parts.entry)),
356
+ warnings: Object.freeze(parts.warnings),
357
+ installable: parts.installable,
358
+ manifestDigest: manifestDigest([{ path: manifestPathFor(parts.marketplaceRoot, ".claude-plugin/marketplace.json"), content: parts.marketplaceRaw }]),
359
+ });
360
+ }
361
+ function resolveMarketplaceSource(source, marketplaceRoot, metadataPluginRoot) {
362
+ if (typeof source === "string") {
363
+ if (source.startsWith("./") || source === ".") {
364
+ // `"."` and `"./"` name the marketplace root, so the plugin is the repository itself.
365
+ const relative = normalizeComponentPath(source);
366
+ return relative === ""
367
+ ? { kind: "in-repo", pluginRoot: marketplaceRoot }
368
+ : { kind: "in-repo", pluginRoot: resolveBundlePath(marketplaceRoot, relative) };
369
+ }
370
+ const bare = normalizeComponentPath(source);
371
+ if (bare !== "" && metadataPluginRoot !== "") {
372
+ return { kind: "in-repo", pluginRoot: resolveBundlePath(marketplaceRoot, `${metadataPluginRoot}/${bare}`) };
373
+ }
374
+ return { kind: "external" };
375
+ }
376
+ if (isRecord(source)) {
377
+ return Object.hasOwn(source, "command") ? { kind: "command" } : { kind: "external" };
378
+ }
379
+ return { kind: "external" };
380
+ }
381
+ function resolveComponentPaths(root, components) {
382
+ const base = normalizeRoot(root);
383
+ if (base === "") {
384
+ return components;
385
+ }
386
+ return Object.freeze(components.map((component) => Object.freeze({
387
+ ...component,
388
+ paths: Object.freeze(component.paths.map((entry) => `${base}/${entry}`)),
389
+ })));
390
+ }
391
+ /**
392
+ * A Pi bundle with no `pi` key is recognized only when the repository shows Pi
393
+ * intent: the `pi-package` keyword, or a Pi-native `extensions/` or `prompts/`
394
+ * directory that holds files in the snapshot. Returns undefined when the only
395
+ * conventional directory is shared with another Host, so an ordinary skill
396
+ * repository with a `package.json` is not reported as a Pi package.
397
+ */
398
+ function conventionalPiBundle(raw, root, files) {
399
+ const pruned = pruneConventionalComponents(parsePiPackageManifest(raw, root), files);
400
+ if (pruned.components.length === 0) {
401
+ return undefined;
402
+ }
403
+ const intended = hasPiPackageKeyword(raw)
404
+ || pruned.components.some((component) => PI_INTENT_COMPONENT_KINDS.has(component.kind));
405
+ return intended ? pruned : undefined;
406
+ }
407
+ function hasPiPackageKeyword(raw) {
408
+ const keywords = parseJsonObject(raw).keywords;
409
+ return Array.isArray(keywords) && keywords.some((keyword) => keyword === "pi-package");
410
+ }
411
+ function pruneConventionalComponents(descriptor, files) {
412
+ const conventionalKinds = new Set(["skills", "extensions", "prompts", "themes"]);
413
+ const components = descriptor.components.filter((component) => {
414
+ if (!conventionalKinds.has(component.kind)) {
415
+ return true;
416
+ }
417
+ return component.paths.some((directory) => files.some((file) => file.path === directory || file.path.startsWith(`${directory}/`)));
418
+ });
419
+ return Object.freeze({ ...descriptor, components: Object.freeze(components) });
420
+ }
421
+ function populateSkillNames(descriptor, files) {
422
+ const components = descriptor.components.map((component) => {
423
+ if (component.kind !== "skills") {
424
+ return component;
425
+ }
426
+ const names = new Set();
427
+ for (const file of files) {
428
+ if (path.posix.basename(file.path) !== "SKILL.md") {
429
+ continue;
430
+ }
431
+ const directory = path.posix.dirname(file.path);
432
+ if (directory === "." || hasLifecycleSegment(directory)) {
433
+ continue;
434
+ }
435
+ if (component.paths.some((spec) => matchesBundlePath(spec, directory))) {
436
+ names.add(path.posix.basename(directory));
437
+ }
438
+ }
439
+ if (names.size === 0) {
440
+ return component;
441
+ }
442
+ return Object.freeze({ ...component, skillNames: Object.freeze([...names].sort()) });
443
+ });
444
+ return Object.freeze({ ...descriptor, components: Object.freeze(components) });
445
+ }
446
+ function matchesBundlePath(spec, skillDirectory) {
447
+ if (skillDirectory === spec) {
448
+ return true;
449
+ }
450
+ if (spec.includes("*")) {
451
+ return globToRegExp(spec).test(skillDirectory) || globToRegExp(`${spec}/*`).test(skillDirectory);
452
+ }
453
+ return skillDirectory.startsWith(`${spec}/`);
454
+ }
455
+ function globToRegExp(pattern) {
456
+ let out = "^";
457
+ for (let index = 0; index < pattern.length; index += 1) {
458
+ const character = pattern[index];
459
+ if (character === "*") {
460
+ out += pattern[index + 1] === "*" ? ".*" : "[^/]*";
461
+ if (pattern[index + 1] === "*") {
462
+ index += 1;
463
+ }
464
+ }
465
+ else if ("\\^$.|+()[]{}-".includes(character)) {
466
+ out += `\\${character}`;
467
+ }
468
+ else {
469
+ out += character;
470
+ }
471
+ }
472
+ return new RegExp(`${out}$`, "u");
473
+ }
474
+ function manifestDigest(files) {
475
+ const sorted = [...files].sort((left, right) => left.path.localeCompare(right.path));
476
+ const hash = createHash("sha256");
477
+ hash.update("agent-depot-package-manifest-v1\0", "utf8");
478
+ for (const file of sorted) {
479
+ const pathBytes = Buffer.from(file.path, "utf8");
480
+ const contentBytes = Buffer.from(file.content, "utf8");
481
+ hash.update(`${pathBytes.byteLength}:`, "utf8");
482
+ hash.update(pathBytes);
483
+ hash.update(`${contentBytes.byteLength}:`, "utf8");
484
+ hash.update(contentBytes);
485
+ hash.update("\0", "utf8");
486
+ }
487
+ return hash.digest("hex");
488
+ }
489
+ function manifestVersionEvidence(value, declaredBy) {
490
+ if (typeof value !== "string" || value.length === 0) {
491
+ return undefined;
492
+ }
493
+ return Object.freeze({ kind: "manifest-version", version: value, declaredBy });
494
+ }
495
+ function manifestName(manifest, root, fallback) {
496
+ if (typeof manifest.name === "string" && manifest.name.trim() !== "") {
497
+ return manifest.name.trim();
498
+ }
499
+ const base = path.posix.basename(root);
500
+ return base === "" || base === "." ? fallback : base;
501
+ }
502
+ function requiredName(manifest, label) {
503
+ if (typeof manifest.name !== "string" || manifest.name.trim() === "") {
504
+ throw new PackageManifestError(`${label} must define a non-empty name`);
505
+ }
506
+ return manifest.name.trim();
507
+ }
508
+ function stringPaths(value) {
509
+ if (typeof value === "string") {
510
+ const normalized = normalizeComponentPath(value);
511
+ return normalized === "" ? Object.freeze([]) : Object.freeze([normalized]);
512
+ }
513
+ if (!Array.isArray(value)) {
514
+ return Object.freeze([]);
515
+ }
516
+ const paths = [];
517
+ for (const entry of value) {
518
+ if (typeof entry !== "string" || entry.trim() === "" || entry.startsWith("!")) {
519
+ continue;
520
+ }
521
+ const normalized = normalizeComponentPath(entry);
522
+ if (normalized !== "") {
523
+ paths.push(normalized);
524
+ }
525
+ }
526
+ return Object.freeze(paths);
527
+ }
528
+ function normalizeComponentPath(entry) {
529
+ const trimmed = entry.trim();
530
+ if (trimmed === "" || trimmed.includes("\\")) {
531
+ return "";
532
+ }
533
+ const normalized = path.posix.normalize(trimmed.replace(/^\.\//u, ""));
534
+ if (normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/")) {
535
+ return "";
536
+ }
537
+ return normalized;
538
+ }
539
+ function normalizeRoot(root) {
540
+ const trimmed = root.trim();
541
+ if (trimmed === "" || trimmed === ".") {
542
+ return "";
543
+ }
544
+ const normalized = path.posix.normalize(trimmed.replace(/^\.\//u, ""));
545
+ return normalized === "." ? "" : normalized.replace(/\/+$/u, "");
546
+ }
547
+ function manifestPathFor(root, filename) {
548
+ const base = normalizeRoot(root);
549
+ return base === "" ? filename : `${base}/${filename}`;
550
+ }
551
+ function resolveBundlePath(root, componentPath) {
552
+ const base = normalizeRoot(root);
553
+ return base === "" ? componentPath : `${base}/${componentPath}`;
554
+ }
555
+ function marketplaceRootForPath(filePath) {
556
+ const marker = ".claude-plugin/marketplace.json";
557
+ if (filePath === marker) {
558
+ return "";
559
+ }
560
+ const suffix = `/${marker}`;
561
+ return filePath.endsWith(suffix) ? normalizeRoot(filePath.slice(0, -suffix.length)) : undefined;
562
+ }
563
+ function pluginRootForPath(filePath) {
564
+ const marker = ".claude-plugin/plugin.json";
565
+ if (filePath === marker) {
566
+ return "";
567
+ }
568
+ const suffix = `/${marker}`;
569
+ return filePath.endsWith(suffix) ? normalizeRoot(filePath.slice(0, -suffix.length)) : undefined;
570
+ }
571
+ function normalizeSkillDirectory(candidate) {
572
+ if (typeof candidate !== "string" || candidate.length === 0 || candidate.includes("\\")) {
573
+ return undefined;
574
+ }
575
+ const normalized = path.posix.normalize(candidate);
576
+ if (normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/")) {
577
+ return undefined;
578
+ }
579
+ return normalized;
580
+ }
581
+ function hasLifecycleSegment(skillDirectory) {
582
+ return skillDirectory.split("/").some((segment) => EXCLUDED_LIFECYCLE_SEGMENTS.includes(segment));
583
+ }
584
+ function hasPiManifestKey(raw) {
585
+ const manifest = parseJsonObject(raw);
586
+ return isRecord(manifest.pi);
587
+ }
588
+ function withWarnings(descriptor, warnings) {
589
+ return Object.freeze({ ...descriptor, warnings: Object.freeze([...descriptor.warnings, ...warnings]) });
590
+ }
591
+ function dedupeStrings(values) {
592
+ return Object.freeze([...new Set(values)]);
593
+ }
594
+ function sortComponents(components) {
595
+ const order = new Map(COMPONENT_KIND_ORDER.map((kind, index) => [kind, index]));
596
+ return Object.freeze([...components].sort((left, right) => (order.get(left.kind) ?? 0) - (order.get(right.kind) ?? 0)));
597
+ }
598
+ function isRecord(value) {
599
+ return typeof value === "object" && value !== null && !Array.isArray(value);
600
+ }
601
+ function parseJsonObject(raw) {
602
+ let value;
603
+ try {
604
+ value = JSON.parse(raw);
605
+ }
606
+ catch (error) {
607
+ const reason = error instanceof Error ? error.message : "the JSON could not be parsed";
608
+ throw new PackageManifestError(`manifest is not valid JSON (${reason})`);
609
+ }
610
+ if (!isRecord(value)) {
611
+ throw new PackageManifestError("manifest must be a JSON object");
612
+ }
613
+ return value;
614
+ }