@yep-ai/cli 0.0.0-stage → 1.0.1

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/bin/market.js ADDED
@@ -0,0 +1,1621 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import {
4
+ chmodSync,
5
+ existsSync,
6
+ lstatSync,
7
+ mkdtempSync,
8
+ mkdirSync,
9
+ readFileSync,
10
+ readdirSync,
11
+ renameSync,
12
+ rmSync,
13
+ writeFileSync
14
+ } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
17
+ import { createInterface } from "node:readline/promises";
18
+ import { fileURLToPath } from "node:url";
19
+ import { installClientDefinitions, normalizeClients, promptClients, resolveClients } from "./clients.js";
20
+
21
+ const __dirname = dirname(fileURLToPath(import.meta.url));
22
+ const rootDir = resolve(__dirname, "../../..");
23
+ const registryPath = join(rootDir, "registry", "skills.json");
24
+ const defaultInstallDir = join(process.cwd(), ".yep", "skills");
25
+ const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
26
+ const defaultRemoteRegistry = "https://yep.nihao.cool";
27
+ let isMarketplace = true;
28
+ const cliName = "yep";
29
+ const configHome = process.env.YEP_CONFIG_DIR ? resolve(process.env.YEP_CONFIG_DIR) : resolve(process.env.YEP_HOME || homedir(), ".config", "yep");
30
+ const configPath = join(configHome, "config.json");
31
+ const maxPackageFiles = 250;
32
+ const maxPackageFileBytes = 8 * 1024 * 1024;
33
+ const maxExpandedPackageBytes = 50 * 1024 * 1024;
34
+ const maxPackagePathBytes = 512;
35
+ const maxPackageSegmentBytes = 255;
36
+ const skillVersionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
37
+
38
+ const commands = {
39
+ list,
40
+ search,
41
+ install,
42
+ uninstall,
43
+ client,
44
+ publish,
45
+ submit,
46
+ withdraw,
47
+ history,
48
+ diff,
49
+ info,
50
+ login,
51
+ logout,
52
+ whoami,
53
+ sync,
54
+ help
55
+ };
56
+
57
+ export async function runMarketplace(argv = process.argv.slice(2)) {
58
+ const { command, args, options } = parseArgs(argv);
59
+ isMarketplace = !options.local;
60
+ options.registry ??=
61
+ process.env.YEP_REGISTRY_URL ??
62
+ (isMarketplace ? readConfig().defaultRegistry : undefined);
63
+ options.token ??= process.env.YEP_TOKEN;
64
+ if (!options.to && options.kind === "skill") options.client ??= process.env.YEP_CLIENT?.trim() || undefined;
65
+ if (typeof options.token === "string") options.token = options.token.trim() || undefined;
66
+
67
+ if (isMarketplace && !options.registry) options.registry = defaultRemoteRegistry;
68
+ if (options.registry) options.registry = normalizeRegistry(options.registry);
69
+ if (options.registry && !options.token) options.token = storedAuth(options.registry)?.token;
70
+
71
+ if (command === "--version" || command === "-v") {
72
+ console.log(version);
73
+ return;
74
+ }
75
+
76
+ const runner = options.kind === "skill" && ["submit", "withdraw"].includes(command) ? undefined : commands[command ?? "help"];
77
+ if (!runner) {
78
+ fail(`Unknown command "${command}". Run "${cliName} help".`);
79
+ }
80
+
81
+ await runner(args, options);
82
+ }
83
+
84
+ async function list(args, options) {
85
+ if (options.kind !== "skill") return listResources(args, options);
86
+ if (options.mine) {
87
+ const registry = options.registry ?? defaultRemoteRegistry;
88
+ const token = requireRegistryToken(options.token, registry, "list your private Skills");
89
+ const data = await apiFetch(registry, "/api/me/skills", undefined, token);
90
+ const query = args.join(" ").toLowerCase();
91
+ printSkills((data?.skills ?? []).filter((skill) => [skill.name, skill.displayName, skill.summary].join(" ").toLowerCase().includes(query)), options);
92
+ return;
93
+ }
94
+ if (!options.remote && (isMarketplace || options.installed || options.client || options.to)) {
95
+ listInstalled(args, options);
96
+ return;
97
+ }
98
+ await listMarketplace(args, options.remote ? { ...options, registry: options.registry ?? defaultRemoteRegistry } : options);
99
+ }
100
+
101
+ async function listMarketplace(args, options) {
102
+ if (options.kind !== "skill") return listResources(args, { ...options, remote: true });
103
+ const query = args.join(" ");
104
+ const skills = options.registry ? (await apiList(options.registry, query, options.token, options.limit)).skills : localList(query);
105
+ printSkills(skills, options);
106
+ }
107
+
108
+ function printSkills(skills, options) {
109
+ const limited = Number.isInteger(options.limit) ? skills.slice(0, options.limit) : skills;
110
+
111
+ if (options.json) {
112
+ console.log(JSON.stringify(limited, null, 2));
113
+ return;
114
+ }
115
+
116
+ if (limited.length === 0) {
117
+ console.log("No skills found.");
118
+ return;
119
+ }
120
+
121
+ for (const skill of limited) {
122
+ const verified = skill.verified ? " verified" : "";
123
+ console.log(`${skill.name}@${skill.version}${verified}`);
124
+ console.log(` ${skill.summary}`);
125
+ if (options.mine) console.log(` ${skill.visibility} / ${skill.status}`);
126
+ }
127
+ }
128
+
129
+ async function search(args, options) {
130
+ if (args.length === 0) fail(`Usage: ${cliName} search <query> [--limit <n>]`);
131
+ await listMarketplace(args, { ...options, registry: options.registry ?? defaultRemoteRegistry });
132
+ }
133
+
134
+ async function info(args, options) {
135
+ if (options.kind !== "skill") return resourceInfo(args, options);
136
+ const { name, version: requestedVersion } = parseSkillSpec(requiredArg(args[0], `Usage: ${cliName} info <skill-name>`));
137
+ const skill = options.registry ? await apiInfoWithOwnerFallback(options.registry, name, options.token, requestedVersion) : findSkill(name);
138
+ assertRequestedVersion(requestedVersion, skill);
139
+ console.log(JSON.stringify(skill, null, 2));
140
+ }
141
+
142
+ async function install(args, options) {
143
+ if (options.kind !== "skill") return installResources(args, options);
144
+ if (args.length === 0) fail(`Usage: ${cliName} install <skill-name> [more-skills...] [--client <client>]`);
145
+ const specs = args.map(parseSkillSpec);
146
+ await selectOperationClients(options);
147
+ // Validate every destination before downloading or changing any package.
148
+ const destinations = new Map();
149
+ for (const { name } of specs) {
150
+ for (const target of resolveInstallTargets(name, options)) {
151
+ if (destinations.has(target.path) && destinations.get(target.path) !== name) throw new Error(`Package names share the same destination: ${destinations.get(target.path)} and ${name}`);
152
+ destinations.set(target.path, name);
153
+ assertInstallDestination(target, name);
154
+ }
155
+ }
156
+ for (const { name, version: requestedVersion } of specs) {
157
+ await installOne(name, requestedVersion, options);
158
+ }
159
+ }
160
+
161
+ async function installOne(name, requestedVersion, options) {
162
+ if (options.registry) {
163
+ const pkg = await apiPackageWithOwnerFallback(options.registry, name, options.token, requestedVersion);
164
+ assertRequestedVersion(requestedVersion, pkg?.skill);
165
+ const installTargets = resolveInstallTargets(name, options);
166
+ const validated = validateRemotePackage(pkg, dirname(installTargets[0].path), name, installTargets[0].path);
167
+ for (const target of installTargets) {
168
+ installRemotePackage(target.path, validated.files, validated.skill, options.registry, target.client);
169
+ }
170
+ const { skill } = validated;
171
+ console.log(`Fetched ${skill.displayName} v${skill.version}`);
172
+ for (const target of installTargets) console.log(`Installed to ${target.path} (${target.label})`);
173
+ console.log(skill.verified ? "Verified registry manifest" : "Installed community package");
174
+ if (shouldRecordDownload(skill)) await recordDownload(options.registry, name, options.token, "installed successfully");
175
+ return;
176
+ }
177
+
178
+ const skill = findSkill(name);
179
+ assertRequestedVersion(requestedVersion, skill);
180
+ const source = resolve(rootDir, skill.source);
181
+ const targets = resolveInstallTargets(name, options);
182
+
183
+ if (!existsSync(source)) {
184
+ fail(`Package source missing for ${skill.name}: ${source}`);
185
+ }
186
+
187
+ for (const target of targets) installLocalPackage(target.path, source, skill, target.client);
188
+
189
+ console.log(`Fetched ${skill.displayName} v${skill.version}`);
190
+ for (const target of targets) console.log(`Installed to ${target.path} (${target.label})`);
191
+ console.log(skill.verified ? "Verified registry manifest" : "Installed community package");
192
+ }
193
+
194
+ async function publish(args, options) {
195
+ if (options.kind !== "skill") return publishResource(args, options);
196
+ const packageDir = resolve(args[0] || ".");
197
+ const manifest = readLocalManifest(packageDir);
198
+ const registry = normalizeRegistry(options.registry ?? defaultRemoteRegistry);
199
+ if (!options.token && process.stdin.isTTY && process.stdout.isTTY) {
200
+ console.log(`Sign in to ${registry} to publish this Skill.`);
201
+ await login([], { ...options, registry });
202
+ options.token = storedAuth(registry)?.token;
203
+ }
204
+ const token = requireRegistryToken(options.token ?? storedAuth(registry)?.token, registry, "publish Skills");
205
+ const packageFiles = collectPackageFiles(packageDir);
206
+ let ownedSkill = await apiFindOwnedSkillIfSupported(registry, manifest.name, token);
207
+ if (ownedSkill?.version === manifest.version) {
208
+ throw new Error(`Version ${manifest.name}@${manifest.version} already exists. Bump the version before publishing another package.`);
209
+ }
210
+
211
+ let response;
212
+ try {
213
+ response = await apiUploadFiles(registry, manifest, packageFiles, token, ownedSkill?.id);
214
+ } catch (error) {
215
+ if (!(error instanceof RegistryError) || error.status !== 409 || ownedSkill) throw error;
216
+ ownedSkill = await apiFindOwnedSkill(registry, manifest.name, token);
217
+ if (!ownedSkill) throw error;
218
+ if (ownedSkill.version === manifest.version) {
219
+ throw new Error(`Version ${manifest.name}@${manifest.version} already exists. Bump the version before publishing another package.`);
220
+ }
221
+ response = await apiUploadFiles(registry, manifest, packageFiles, token, ownedSkill.id);
222
+ }
223
+
224
+ console.log(`Published ${response.name}@${response.version}`);
225
+ console.log("Status: private, no review submitted");
226
+ console.log(`Registry: ${trimSlash(registry)}`);
227
+ console.log(`Next: ${cliName} list --mine; ${cliName} install ${response.name}`);
228
+ }
229
+
230
+ function resourceRegistry(options) {
231
+ return normalizeRegistry(options.registry ?? defaultRemoteRegistry);
232
+ }
233
+
234
+ function resourceRoot(options) {
235
+ return { client: "project", label: `${options.kind} directory`, root: resolve(options.to ?? `${options.kind}s`), legacy: false, kind: options.kind };
236
+ }
237
+
238
+ async function apiResourceList(registry, kind, token, owned, name) {
239
+ const params = new URLSearchParams({ kind });
240
+ if (name) params.set("name", name);
241
+ const data = await apiFetch(registry, `${owned ? "/api/me/resources" : "/api/resources"}?${params}`, undefined, token);
242
+ if (!Array.isArray(data?.resources)) throw new Error("Registry returned an invalid resource list");
243
+ return data.resources.filter((resource) => resource?.kind === kind && (!name || resource.name === name));
244
+ }
245
+
246
+ async function findResource(registry, options, name, owned) {
247
+ const token = owned ? requireRegistryToken(options.token, registry, "read your private resources") : options.token;
248
+ const resources = await apiResourceList(registry, options.kind, token, owned, name);
249
+ if (resources.length > 1) throw new Error(`Registry returned duplicate ${options.kind} resources named ${name}`);
250
+ const resource = resources[0];
251
+ if (resource && (typeof resource.id !== "string" || !resource.id)) throw new Error("Registry returned a resource without its identity");
252
+ return resource;
253
+ }
254
+
255
+ async function listResources(args, options) {
256
+ if (!options.mine && !options.remote) return listInstalledResources(args, options);
257
+ const registry = resourceRegistry(options);
258
+ const token = options.mine ? requireRegistryToken(options.token, registry, "list your private resources") : options.token;
259
+ const query = args.join(" ").toLowerCase();
260
+ const resources = (await apiResourceList(registry, options.kind, token, options.mine)).filter((item) => [item.name, item.displayName, item.summary].join(" ").toLowerCase().includes(query));
261
+ if (!resources.length && !options.json) console.log(`No ${options.kind}s found.`);
262
+ else printSkills(resources, options);
263
+ }
264
+
265
+ function listInstalledResources(args, options) {
266
+ const root = resourceRoot(options);
267
+ const query = args.join(" ").toLowerCase();
268
+ const installed = [];
269
+ if (existsSync(root.root)) {
270
+ for (const entry of readdirSync(root.root, { withFileTypes: true })) {
271
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
272
+ try {
273
+ const path = join(root.root, entry.name);
274
+ const metadata = installedMetadata({ ...root, path });
275
+ if (metadata?.kind === options.kind && metadata.name.toLowerCase().includes(query)) installed.push({ ...metadata, path });
276
+ } catch (error) { console.warn(`Warning: ${error.message}`); }
277
+ }
278
+ }
279
+ installed.sort((a, b) => a.name.localeCompare(b.name));
280
+ const result = options.limit ? installed.slice(0, options.limit) : installed;
281
+ if (options.json) console.log(JSON.stringify(result, null, 2));
282
+ else if (!result.length) console.log(`No installed ${options.kind}s found.`);
283
+ else for (const item of result) console.log(`${item.name}@${item.version}\n ${item.path}`);
284
+ }
285
+
286
+ async function resourceInfo(args, options) {
287
+ const spec = parseSkillSpec(requiredArg(args[0], `Usage: ${cliName} info <name> --kind ${options.kind}`));
288
+ const registry = resourceRegistry(options);
289
+ let resource = await findResource(registry, options, spec.name, false);
290
+ if ((!resource || (spec.version && resource.version !== spec.version)) && options.token) resource = await findResource(registry, options, spec.name, true) ?? resource;
291
+ if (!resource) throw new Error(`${options.kind} not found: ${spec.name}`);
292
+ assertRequestedVersion(spec.version, resource);
293
+ console.log(JSON.stringify(resource, null, 2));
294
+ }
295
+
296
+ function readResourceManifest(packageDir, kind) {
297
+ const manifestPath = join(packageDir, "cpm.json");
298
+ if (!lstatSync(manifestPath).isFile()) throw new Error(`Manifest must be a regular file: ${manifestPath}`);
299
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
300
+ for (const field of ["name", "displayName", "version"]) if (typeof manifest[field] !== "string" || !manifest[field].trim()) throw new Error(`Manifest missing field: ${field}`);
301
+ if (manifest.kind && manifest.kind !== kind) throw new Error(`Manifest kind ${manifest.kind} does not match --kind ${kind}`);
302
+ safeInstallRelativePath(manifest.name);
303
+ if (!skillVersionPattern.test(manifest.version)) throw new Error(`Invalid resource version: ${manifest.version}`);
304
+ const entry = manifest.entry ?? (kind === "agent" ? "AGENT.md" : "plugin.json");
305
+ safePackageOutput(packageDir, entry);
306
+ resolveLocalSkillEntry(packageDir, entry);
307
+ return { kind, name: manifest.name, displayName: manifest.displayName, version: manifest.version, summary: manifest.summary ?? "", entry };
308
+ }
309
+
310
+ async function publishResource(args, options) {
311
+ const packageDir = resolve(args[0] ?? ".");
312
+ const manifest = readResourceManifest(packageDir, options.kind);
313
+ const registry = resourceRegistry(options);
314
+ if (!options.token && process.stdin.isTTY && process.stdout.isTTY) {
315
+ await login([], { ...options, registry });
316
+ options.token = storedAuth(registry)?.token;
317
+ }
318
+ const token = requireRegistryToken(options.token, registry, "publish resources");
319
+ const files = Object.entries(collectPackageFiles(packageDir)).map(([path, content]) => ({ path, contentBase64: Buffer.from(content).toString("base64") }));
320
+ if (!files.some((file) => file.path === manifest.entry)) throw new Error(`Package entry is excluded from publication: ${manifest.entry}`);
321
+ const existing = await findResource(registry, { ...options, token }, manifest.name, true);
322
+ if (existing?.status === "pending") throw new Error(`This resource is awaiting review; run "${cliName} withdraw ${manifest.name} --kind ${options.kind}" before editing it`);
323
+ const body = JSON.stringify({ ...manifest, files, ...(existing ? { expectedRevisionId: existing.currentRevisionId } : {}) });
324
+ const data = await apiFetch(registry, existing ? `/api/me/resources/${encodeURIComponent(existing.id)}` : "/api/me/resources", { method: existing ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, body, signal: AbortSignal.timeout(120_000) }, token);
325
+ const resource = data?.resource;
326
+ if (!resource?.id || resource.name !== manifest.name || resource.kind !== options.kind || !resource.currentRevisionId) throw new Error("Registry returned a saved resource with an unexpected identity");
327
+ let submitted;
328
+ try {
329
+ submitted = (await apiFetch(registry, `/api/me/resources/${encodeURIComponent(resource.id)}/submit`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ expectedRevisionId: resource.currentRevisionId }) }, token))?.resource;
330
+ } catch (error) {
331
+ throw new Error(`Saved ${resource.name}@${resource.version} (revision ${resource.currentRevisionId}), but review submission failed: ${error.message}`);
332
+ }
333
+ if (options.json) console.log(JSON.stringify(submitted ?? resource, null, 2));
334
+ else {
335
+ console.log(`Saved ${resource.name}@${resource.version}`);
336
+ console.log("Submitted for review; it becomes public after administrator approval.");
337
+ console.log(`Next: ${cliName} history ${resource.name} --kind ${options.kind}; ${cliName} install ${resource.name} --kind ${options.kind}`);
338
+ }
339
+ }
340
+
341
+ async function transitionResource(args, options, action) {
342
+ if (options.kind === "skill") throw new Error(`${action} requires --kind agent or --kind plugin; Skill review remains available in the marketplace`);
343
+ const spec = parseSkillSpec(requiredArg(args[0], `Usage: ${cliName} ${action} <name> --kind <agent|plugin>`));
344
+ const registry = resourceRegistry(options);
345
+ const token = requireRegistryToken(options.token, registry, "change resource publication");
346
+ const resource = await findResource(registry, options, spec.name, true);
347
+ if (!resource) throw new Error(`Owned ${options.kind} not found: ${spec.name}`);
348
+ assertRequestedVersion(spec.version, resource);
349
+ const withdrawing = action === "withdraw";
350
+ const response = await apiFetch(registry, `/api/me/resources/${encodeURIComponent(resource.id)}/${withdrawing ? "visibility" : "submit"}`, {
351
+ method: withdrawing ? "PUT" : "POST", headers: { "Content-Type": "application/json" },
352
+ body: JSON.stringify(withdrawing ? { visibility: "private" } : { expectedRevisionId: resource.currentRevisionId })
353
+ }, token);
354
+ if (options.json) console.log(JSON.stringify(response?.resource, null, 2));
355
+ else console.log(withdrawing ? `Withdrew ${spec.name}; it is private and can be edited.` : `Submitted ${spec.name} for review.`);
356
+ }
357
+
358
+ async function submit(args, options) { return transitionResource(args, options, "submit"); }
359
+ async function withdraw(args, options) { return transitionResource(args, options, "withdraw"); }
360
+
361
+ async function resourcePackage(registry, spec, options) {
362
+ let resource;
363
+ let owned = false;
364
+ if (options.revision) {
365
+ resource = await findResource(registry, options, spec.name, true);
366
+ owned = true;
367
+ } else {
368
+ resource = await findResource(registry, options, spec.name, false);
369
+ if ((!resource || (spec.version && resource.version !== spec.version)) && options.token) {
370
+ const privateResource = await findResource(registry, options, spec.name, true);
371
+ if (privateResource) { resource = privateResource; owned = true; }
372
+ }
373
+ }
374
+ if (!resource) throw new Error(`${options.kind} not found: ${spec.name}`);
375
+ const path = options.revision
376
+ ? `/api/me/resources/${encodeURIComponent(resource.id)}/revisions/${encodeURIComponent(options.revision)}/package`
377
+ : `${owned ? "/api/me/resources" : "/api/resources"}/${encodeURIComponent(resource.id)}/package`;
378
+ const pkg = await apiFetch(registry, path, { signal: AbortSignal.timeout(120_000) }, options.token);
379
+ if (pkg?.resource?.id !== resource.id || pkg.resource.name !== spec.name || pkg.resource.kind !== options.kind) throw new Error("Registry returned a package with an unexpected resource identity");
380
+ if (options.revision) {
381
+ const revision = pkg.revision;
382
+ if (!revision || revision.id !== options.revision || revision.resourceId !== resource.id) throw new Error("Registry returned an unexpected revision");
383
+ pkg.resource = { ...pkg.resource, version: revision.version, displayName: revision.displayName, summary: revision.summary, entry: revision.entry, currentRevisionId: revision.id, content: revision.content };
384
+ }
385
+ assertRequestedVersion(spec.version, pkg.resource);
386
+ return pkg;
387
+ }
388
+
389
+ async function installResources(args, options) {
390
+ if (!args.length) throw new Error(`Usage: ${cliName} install <name> --kind ${options.kind} [--target <dir>]`);
391
+ const specs = args.map(parseSkillSpec);
392
+ const root = resourceRoot(options);
393
+ const destinations = new Map();
394
+ for (const spec of specs) {
395
+ const target = installationTarget(root, spec.name);
396
+ if (destinations.has(target.path) && destinations.get(target.path) !== spec.name) throw new Error("Resource names share the same destination");
397
+ destinations.set(target.path, spec.name);
398
+ assertInstallDestination(target, spec.name);
399
+ }
400
+ const registry = resourceRegistry(options);
401
+ for (const spec of specs) {
402
+ const target = installationTarget(root, spec.name);
403
+ const pkg = await resourcePackage(registry, spec, options);
404
+ const validated = validateRemotePackage({ skill: pkg.resource, files: pkg.files }, root.root, spec.name, target.path, false);
405
+ installRemotePackage(target.path, validated.files, validated.skill, registry, "project");
406
+ console.log(`Installed ${spec.name}@${pkg.resource.version} to ${target.path}`);
407
+ }
408
+ }
409
+
410
+ async function uninstallResources(args, options) {
411
+ if (!args.length) throw new Error(`Usage: ${cliName} uninstall <name> --kind ${options.kind} [--target <dir>]`);
412
+ const root = resourceRoot(options);
413
+ const plan = new Map();
414
+ for (const spec of args.map(parseSkillSpec)) {
415
+ const target = installationTarget(root, spec.name);
416
+ const metadata = installedMetadata(target);
417
+ if (!metadata) {
418
+ if (pathStats(target.path)) throw new Error(`Refusing to remove a directory without installation metadata: ${target.path}`);
419
+ plan.set(target.path, { name: spec.name, path: target.path, status: "not-installed" });
420
+ } else {
421
+ if (metadata.name !== spec.name || metadata.kind !== options.kind) throw new Error(`Destination belongs to a different resource: ${target.path}`);
422
+ assertRequestedVersion(spec.version, metadata);
423
+ plan.set(target.path, { name: spec.name, version: metadata.version, path: target.path, status: "removed" });
424
+ }
425
+ }
426
+ for (const item of plan.values()) {
427
+ if (item.status === "removed") rmSync(item.path, { recursive: true });
428
+ if (!options.json) console.log(`${item.status === "removed" ? "Uninstalled" : "Not installed"}: ${item.name}`);
429
+ }
430
+ if (options.json) console.log(JSON.stringify([...plan.values()], null, 2));
431
+ }
432
+
433
+ async function ownedRevisionPath(args, options) {
434
+ const spec = parseSkillSpec(requiredArg(args[0], `Usage: ${cliName} history|diff <name> [--kind <kind>]`));
435
+ const registry = resourceRegistry(options);
436
+ const token = requireRegistryToken(options.token, registry, "read your revision history");
437
+ const resource = options.kind === "skill" ? await apiFindOwnedSkill(registry, spec.name, token) : await findResource(registry, options, spec.name, true);
438
+ if (!resource) throw new Error(`Owned ${options.kind} not found: ${spec.name}`);
439
+ assertRequestedVersion(spec.version, resource);
440
+ return { registry, token, path: `/api/me/${options.kind === "skill" ? "skills" : "resources"}/${encodeURIComponent(resource.id)}` };
441
+ }
442
+
443
+ async function history(args, options) {
444
+ const { registry, token, path } = await ownedRevisionPath(args, options);
445
+ const data = await apiFetch(registry, `${path}/revisions`, undefined, token);
446
+ if (!Array.isArray(data?.revisions)) throw new Error("Registry returned an invalid revision history");
447
+ const revisions = options.limit ? data.revisions.slice(0, options.limit) : data.revisions;
448
+ if (options.json) console.log(JSON.stringify(revisions, null, 2));
449
+ else if (!revisions.length) console.log("No saved Git revisions yet.");
450
+ else for (const revision of revisions) console.log(`${revision.id} ${revision.version} ${revision.createdAt}`);
451
+ }
452
+
453
+ async function diff(args, options) {
454
+ const { registry, token, path } = await ownedRevisionPath(args, options);
455
+ const params = new URLSearchParams({ base: options.base ?? "published", target: options.revision ?? "current" });
456
+ const data = await apiFetch(registry, `${path}/diff?${params}`, { signal: AbortSignal.timeout(120_000) }, token);
457
+ const result = options.kind === "skill" ? data?.diff : data;
458
+ if (!Array.isArray(result?.changes)) throw new Error("Registry returned an invalid diff");
459
+ if (options.json) console.log(JSON.stringify(result, null, 2));
460
+ else if (!result.changes.length) console.log("No content changes.");
461
+ else for (const change of result.changes) {
462
+ console.log(`${change.status} ${change.path}${change.binary ? " (binary)" : ""}`);
463
+ if (change.patch) console.log(change.patch);
464
+ if (change.truncated) console.log("Diff output truncated by the server.");
465
+ }
466
+ }
467
+
468
+ async function sync(_args, options) {
469
+ if (options.registry) {
470
+ const data = await apiList(options.registry, "", options.token);
471
+ console.log(`Synced ${data.skills.length} skills from ${trimSlash(options.registry)}`);
472
+ return;
473
+ }
474
+ const registry = readRegistry();
475
+ registry.generatedAt = new Date().toISOString();
476
+ writeRegistry(registry);
477
+ console.log(`Synced ${registry.skills.length} skills`);
478
+ }
479
+
480
+ async function login(_args, options) {
481
+ const registry = trimSlash(options.registry ?? defaultRemoteRegistry);
482
+ const configuredUsername = options.username ?? process.env.YEP_USERNAME;
483
+ const configuredPassword = options.password ?? process.env.YEP_PASSWORD;
484
+ if (configuredUsername !== undefined || configuredPassword !== undefined) {
485
+ await passwordLogin(registry, configuredUsername, configuredPassword);
486
+ return;
487
+ }
488
+
489
+ const request = await apiFetch(registry, "/api/auth/cli-login", {
490
+ method: "POST",
491
+ headers: { "Content-Type": "application/json" },
492
+ body: JSON.stringify({ client: cliName })
493
+ });
494
+ if (
495
+ typeof request?.requestId !== "string" ||
496
+ typeof request?.pollToken !== "string" ||
497
+ typeof request?.verificationUri !== "string" ||
498
+ !Number.isFinite(request?.expiresIn) ||
499
+ !Number.isFinite(request?.interval)
500
+ ) {
501
+ throw new Error("Registry returned an invalid browser login response");
502
+ }
503
+ const registryURL = new URL(`${registry}/`);
504
+ const verificationURL = new URL(request.verificationUri, registryURL);
505
+ if (verificationURL.origin !== registryURL.origin) {
506
+ throw new Error("Registry returned a browser login URL on a different origin");
507
+ }
508
+
509
+ console.log("Authenticate your account at:");
510
+ console.log(verificationURL.toString());
511
+ if (process.stdin.isTTY && process.stdout.isTTY) {
512
+ await waitForEnter("Press ENTER to open in the browser...");
513
+ try {
514
+ await openBrowser(verificationURL.toString());
515
+ } catch (error) {
516
+ console.warn(`Could not open the browser automatically: ${error.message}`);
517
+ }
518
+ } else {
519
+ console.log("Open this URL in your browser to continue.");
520
+ }
521
+ console.log("Waiting for browser authorization...");
522
+
523
+ const expiresAt = Date.now() + Math.max(1, Math.min(Number(request.expiresIn), 15 * 60)) * 1000;
524
+ const interval = Math.max(250, Math.min(Number(request.interval) * 1000, 10_000));
525
+ while (Date.now() < expiresAt) {
526
+ const auth = await apiFetch(registry, `/api/auth/cli-login/${encodeURIComponent(request.requestId)}/token`, {
527
+ method: "POST",
528
+ headers: { "Content-Type": "application/json" },
529
+ body: JSON.stringify({ pollToken: request.pollToken })
530
+ });
531
+ if (auth?.status === "pending") {
532
+ await delay(interval);
533
+ continue;
534
+ }
535
+ if (!auth?.token || !auth?.user) throw new Error("Registry returned an invalid CLI session");
536
+ saveStoredAuth(registry, {
537
+ token: auth.token,
538
+ user: auth.user,
539
+ savedAt: new Date().toISOString()
540
+ });
541
+ console.log(`Signed in to ${registry} as ${auth.user.username ?? auth.user.email ?? "user"}.`);
542
+ return;
543
+ }
544
+ throw new Error("Browser login expired before it was confirmed");
545
+ }
546
+
547
+ async function passwordLogin(registry, configuredUsername, configuredPassword) {
548
+ const username = configuredUsername ?? (await promptText("Username or email: "));
549
+ const password = configuredPassword ?? (await promptSecret("Password: "));
550
+ if (!String(username).trim() || !password) fail("Username and password are required");
551
+ const auth = await apiFetch(registry, "/api/auth/login", {
552
+ method: "POST",
553
+ headers: { "Content-Type": "application/json" },
554
+ body: JSON.stringify({ identity: String(username).trim(), password: String(password) })
555
+ });
556
+ if (!auth?.token || !auth?.user) throw new Error("Registry returned an invalid login response");
557
+ saveStoredAuth(registry, {
558
+ token: auth.token,
559
+ user: auth.user,
560
+ savedAt: new Date().toISOString()
561
+ });
562
+ console.log(`Signed in to ${registry} as ${auth.user.username ?? auth.user.email ?? "user"}.`);
563
+ }
564
+
565
+ async function logout(_args, options) {
566
+ const registry = trimSlash(options.registry ?? defaultRemoteRegistry);
567
+ const auth = storedAuth(registry);
568
+ if (auth?.token) {
569
+ try {
570
+ await apiFetch(registry, "/api/auth/logout", { method: "POST" }, auth.token);
571
+ } catch (error) {
572
+ if (!(error instanceof RegistryError) || error.status !== 401) throw error;
573
+ }
574
+ }
575
+ deleteStoredAuth(registry);
576
+ console.log(`Signed out from ${registry}.`);
577
+ }
578
+
579
+ async function whoami(_args, options) {
580
+ const registry = trimSlash(options.registry ?? defaultRemoteRegistry);
581
+ const token = options.token ?? storedAuth(registry)?.token;
582
+ if (!token) fail(`Not signed in. Run "${cliName} login --registry ${registry}".`);
583
+ const response = await apiFetch(registry, "/api/auth/me", undefined, token);
584
+ console.log(options.json ? JSON.stringify(response.user, null, 2) : `${response.user.username} (${response.user.email})`);
585
+ }
586
+
587
+ function help() {
588
+ console.log(`${cliName} ${version}
589
+
590
+ Usage:
591
+ ${cliName} login Sign in and save a registry token
592
+ ${cliName} logout Revoke the current saved login
593
+ ${cliName} whoami Show the signed-in account
594
+ ${cliName} search <query> Search marketplace skills
595
+ ${cliName} info <skill-name> Print skill metadata
596
+ ${cliName} client [list|set <clients>|reset] Show, pin or clear default clients
597
+ ${cliName} install <skill> [more-skills...] Fetch and install skills
598
+ ${cliName} list [query] List installed skills (offline)
599
+ ${cliName} list --remote | --mine List marketplace or your workspace
600
+ ${cliName} uninstall <skill> [more-skills...] Remove skills from selected clients
601
+ ${cliName} publish [package-dir] Save Skill privately; submit Agent/Plugin for review
602
+ ${cliName} submit <name> --kind agent|plugin Submit a saved resource for review
603
+ ${cliName} withdraw <name> --kind agent|plugin Make a resource private and cancel pending review
604
+ ${cliName} history <name> List your saved Git revisions
605
+ ${cliName} diff <name> Compare your draft with the published revision
606
+
607
+ Options:
608
+ --kind <skill|agent|plugin> Resource type (default: skill)
609
+ --base <published|empty|revision-id> Diff base (default: published)
610
+ --revision <revision-id> Diff target or Agent/Plugin install revision
611
+ --registry <url> Yep Marketplace API URL
612
+ --token <token> Use a token without saving it
613
+ --client <clients> codex, claude, qoder, kiro, vscode, universal, project, auto, all
614
+ --to, --target <dir> Custom install destination
615
+ --limit <n> Limit search results
616
+ --json Print machine-readable output
617
+ --installed | --remote | --mine Select the list source
618
+ --local Use the checkout local registry
619
+
620
+ Device node:
621
+ yep device start [directories...] [--server <url>] Connect this device
622
+ yep device login | status | logout Manage device authorization
623
+ yep device --help Device command details
624
+
625
+ Client selection:
626
+ --client > YEP_CLIENT > pinned clients > interactive selection.
627
+ In scripts, specify --client or --target; --client auto opts into detection.
628
+ list scans all clients unless --client/--target is provided.
629
+
630
+ Environment:
631
+ YEP_REGISTRY_URL Default registry URL
632
+ YEP_TOKEN Authentication token override
633
+ YEP_CLIENT Default install client(s), or auto
634
+ YEP_USERNAME / YEP_PASSWORD Non-interactive login credentials
635
+
636
+ YEP_CONFIG_DIR Config directory (default: ~/.config/yep)
637
+ YEP_HOME Home override for isolated environments
638
+ `);
639
+ }
640
+
641
+ function parseArgs(argv) {
642
+ const args = [];
643
+ const options = {};
644
+ let command;
645
+ const valueOptions = new Set([
646
+ "registry",
647
+ "token",
648
+ "to",
649
+ "target",
650
+ "client",
651
+ "limit",
652
+ "username",
653
+ "password",
654
+ "kind",
655
+ "base",
656
+ "revision"
657
+ ]);
658
+ const booleanOptions = new Set(["json", "installed", "remote", "mine", "local"]);
659
+ const removedClientOptions = new Set(["--tool", "--agent", "-a"]);
660
+ for (let index = 0; index < argv.length; index++) {
661
+ const item = argv[index];
662
+ if (removedClientOptions.has(item) || [...removedClientOptions].some((option) => item.startsWith(`${option}=`))) {
663
+ fail(`Option ${item.split("=")[0]} has been removed; use --client.`);
664
+ } else if (item === "-h") {
665
+ command = "help";
666
+ } else if (item === "-v") {
667
+ command = "--version";
668
+ } else if (item.startsWith("--")) {
669
+ const equalsIndex = item.indexOf("=");
670
+ const name = item.slice(2, equalsIndex >= 0 ? equalsIndex : undefined);
671
+ if (!valueOptions.has(name) && !booleanOptions.has(name) && name !== "help" && name !== "version") {
672
+ fail(`Unknown option: --${name}`);
673
+ }
674
+ if (booleanOptions.has(name)) {
675
+ if (equalsIndex >= 0) fail(`Option --${name} does not take a value`);
676
+ options[name] = true;
677
+ continue;
678
+ }
679
+ if (name === "help") {
680
+ command = "help";
681
+ continue;
682
+ }
683
+ if (name === "version") {
684
+ command = "--version";
685
+ continue;
686
+ }
687
+ const value = equalsIndex >= 0 ? item.slice(equalsIndex + 1) : argv[++index];
688
+ const aliases = { target: "to" };
689
+ options[aliases[name] ?? name] = requiredArg(value, `Missing value for --${name}`);
690
+ } else if (item.startsWith("-")) {
691
+ fail(`Unknown option: ${item}`);
692
+ } else if (!command) {
693
+ command = item;
694
+ } else {
695
+ args.push(item);
696
+ }
697
+ }
698
+ options.kind ??= "skill";
699
+ if (!["skill", "agent", "plugin"].includes(options.kind)) fail("--kind must be skill, agent or plugin");
700
+ if (options.kind !== "skill" && ["sync", "client"].includes(command)) fail("This command applies to Skill client directories; omit --kind");
701
+ if (options.kind !== "skill" && options.client) fail("Agent/plugin packages use --target or the local agents/plugins directory");
702
+ if (options.base && command !== "diff") fail("--base is only supported by diff");
703
+ if (options.revision && !["install", "diff"].includes(command)) fail("--revision is only supported by install and diff");
704
+ if (options.revision && options.kind === "skill" && command === "install") fail("Skill installation accepts an exact version; --revision is available for agent/plugin installs");
705
+ if (options.limit !== undefined) {
706
+ options.limit = Number(options.limit);
707
+ if (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 100) {
708
+ fail("--limit must be an integer from 1 to 100");
709
+ }
710
+ }
711
+ const sources = ["installed", "remote", "mine"].filter((key) => options[key]);
712
+ if (sources.length > 1) fail("Choose only one of --installed, --remote or --mine");
713
+ if (sources.length && command !== "list") fail("--installed, --remote and --mine are only supported by list");
714
+ if (options.to && options.client) fail("--to/--target cannot be combined with --client");
715
+ if ((options.remote || options.mine) && (options.to || options.client)) fail("Remote lists cannot be combined with --client or --target");
716
+ return { command, args, options };
717
+ }
718
+
719
+ function readConfig() {
720
+ if (!existsSync(configPath)) return { registries: {} };
721
+ try {
722
+ const parsed = JSON.parse(readFileSync(configPath, "utf8"));
723
+ return parsed && typeof parsed === "object" ? parsed : { registries: {} };
724
+ } catch (error) {
725
+ throw new Error(`Cannot read ${configPath}: ${error.message}`);
726
+ }
727
+ }
728
+
729
+ function writeConfig(config) {
730
+ mkdirSync(configHome, { recursive: true, mode: 0o700 });
731
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
732
+ chmodSync(configPath, 0o600);
733
+ }
734
+
735
+ function registryConfigKey(registry) {
736
+ return trimSlash(registry).toLowerCase();
737
+ }
738
+
739
+ function storedAuth(registry) {
740
+ return readConfig().registries?.[registryConfigKey(registry)];
741
+ }
742
+
743
+ function saveStoredAuth(registry, auth) {
744
+ const config = readConfig();
745
+ config.registries = config.registries && typeof config.registries === "object" ? config.registries : {};
746
+ config.registries[registryConfigKey(registry)] = auth;
747
+ config.defaultRegistry = trimSlash(registry);
748
+ writeConfig(config);
749
+ }
750
+
751
+ function deleteStoredAuth(registry) {
752
+ const config = readConfig();
753
+ if (config.registries) delete config.registries[registryConfigKey(registry)];
754
+ writeConfig(config);
755
+ }
756
+
757
+ async function promptText(label) {
758
+ if (!process.stdin.isTTY) fail(`${label.trim()} is required in a non-interactive terminal`);
759
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
760
+ try {
761
+ return await prompt.question(label);
762
+ } finally {
763
+ prompt.close();
764
+ }
765
+ }
766
+
767
+ async function promptSecret(label) {
768
+ if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== "function") {
769
+ fail("Set YEP_PASSWORD or pass --password in a non-interactive terminal");
770
+ }
771
+ process.stdout.write(label);
772
+ process.stdin.setRawMode(true);
773
+ process.stdin.resume();
774
+ return new Promise((resolvePromise, rejectPromise) => {
775
+ let value = "";
776
+ const finish = (error) => {
777
+ process.stdin.off("data", onData);
778
+ process.stdin.setRawMode(false);
779
+ process.stdin.pause();
780
+ process.stdout.write("\n");
781
+ if (error) rejectPromise(error);
782
+ else resolvePromise(value);
783
+ };
784
+ const onData = (chunk) => {
785
+ for (const character of chunk.toString("utf8")) {
786
+ if (character === "\r" || character === "\n") {
787
+ finish();
788
+ return;
789
+ }
790
+ if (character === "\u0003") {
791
+ finish(new Error("Login cancelled"));
792
+ return;
793
+ }
794
+ if (character === "\u007f" || character === "\b") {
795
+ if (value.length > 0) {
796
+ value = value.slice(0, -1);
797
+ process.stdout.write("\b \b");
798
+ }
799
+ continue;
800
+ }
801
+ if (character >= " ") {
802
+ value += character;
803
+ process.stdout.write("*");
804
+ }
805
+ }
806
+ };
807
+ process.stdin.on("data", onData);
808
+ });
809
+ }
810
+
811
+ async function waitForEnter(label) {
812
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
813
+ try {
814
+ await prompt.question(`${label}\n`);
815
+ } finally {
816
+ prompt.close();
817
+ }
818
+ }
819
+
820
+ function openBrowser(target) {
821
+ const command = process.platform === "win32" ? "cmd.exe" : process.platform === "darwin" ? "open" : "xdg-open";
822
+ const args = process.platform === "win32" ? ["/d", "/s", "/c", "start", "", target] : [target];
823
+ return new Promise((resolvePromise, rejectPromise) => {
824
+ const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
825
+ child.once("error", rejectPromise);
826
+ child.once("spawn", () => {
827
+ child.unref();
828
+ resolvePromise();
829
+ });
830
+ });
831
+ }
832
+
833
+ function delay(milliseconds) {
834
+ return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
835
+ }
836
+
837
+ function parseSkillSpec(spec) {
838
+ const value = String(spec).trim();
839
+ const separator = value.lastIndexOf("@");
840
+ const hasVersion = separator > value.indexOf("/");
841
+ const name = hasVersion ? value.slice(0, separator) : value;
842
+ const requestedVersion = hasVersion ? value.slice(separator + 1) : undefined;
843
+ safeInstallRelativePath(name);
844
+ if (hasVersion && !skillVersionPattern.test(requestedVersion)) {
845
+ throw new Error(`Invalid skill version: ${requestedVersion}`);
846
+ }
847
+ return { name, version: requestedVersion };
848
+ }
849
+
850
+ function assertRequestedVersion(requestedVersion, skill) {
851
+ if (!requestedVersion) return;
852
+ if (!skill || skill.version !== requestedVersion) {
853
+ throw new Error(`Version ${requestedVersion} is not available; registry returned ${skill?.version ?? "no version"}`);
854
+ }
855
+ }
856
+
857
+ function scopedSkillDirectory(name) {
858
+ const match = /^@([^/]+)\/(.+)$/.exec(name);
859
+ if (!match) throw new Error(`Unsafe package name: ${name}`);
860
+ return `${match[1]}--${match[2]}`;
861
+ }
862
+
863
+ async function selectOperationClients(options) {
864
+ if (options.to) return;
865
+ options.client ??= readConfig().defaultClients?.join(",");
866
+ if (!options.client && !isMarketplace && !options.registry) return;
867
+ if (!options.client) {
868
+ if (!process.stdin.isTTY || !process.stdout.isTTY || options.json) {
869
+ throw new Error(`No client selected. Pass --client <clients> or --target <dir>, or run "${cliName} client set <clients>". Use --client auto for automatic detection.`);
870
+ }
871
+ options.client = (await promptClients()).join(",");
872
+ console.log(`To pin this selection: ${cliName} client set ${options.client}`);
873
+ }
874
+ options.client = resolveClients(options.client).join(",");
875
+ }
876
+
877
+ async function client(args, options) {
878
+ const action = args[0] ?? "list";
879
+ if (action === "set") {
880
+ if (args.length > 2) throw new Error(`Usage: ${cliName} client set codex,claude`);
881
+ if (!args[1] && (!process.stdin.isTTY || !process.stdout.isTTY || options.json)) throw new Error(`Usage: ${cliName} client set codex,claude`);
882
+ const selected = args[1] ? normalizeClients(args[1]) : await promptClients();
883
+ const config = readConfig();
884
+ config.defaultClients = selected;
885
+ writeConfig(config);
886
+ console.log(`Default clients: ${selected.join(", ")}`);
887
+ return;
888
+ }
889
+ if (action === "reset") {
890
+ if (args.length !== 1) throw new Error(`Usage: ${cliName} client reset`);
891
+ const config = readConfig();
892
+ delete config.defaultClients;
893
+ writeConfig(config);
894
+ console.log("Default clients cleared. The next interactive install or uninstall will ask you to choose.");
895
+ return;
896
+ }
897
+ if (action !== "list" || args.length > 1) throw new Error(`Usage: ${cliName} client [list|set <clients>|reset]`);
898
+ const config = readConfig();
899
+ const pinned = config.defaultClients ?? [];
900
+ const effective = options.client || pinned.join(",");
901
+ const selected = effective ? resolveClients(effective) : [];
902
+ const clients = Object.entries(installClientDefinitions()).map(([id, definition]) => ({
903
+ id, label: definition.label, path: definition.root,
904
+ detected: definition.detect.some((path) => existsSync(path)),
905
+ selected: selected.includes(id)
906
+ }));
907
+ if (options.json) {
908
+ console.log(JSON.stringify({ defaultClients: pinned, environmentClients: process.env.YEP_CLIENT || null, clients }, null, 2));
909
+ return;
910
+ }
911
+ console.log(`Pinned clients: ${pinned.join(", ") || "none"}`);
912
+ for (const entry of clients) console.log(`${entry.selected ? "*" : " "} ${entry.id} — ${entry.label}${entry.detected ? " [detected]" : ""}\n ${entry.path}`);
913
+ console.log(`Pin: ${cliName} client set codex,claude | Clear: ${cliName} client reset`);
914
+ }
915
+
916
+ function installRoots(options, all = false) {
917
+ if (options.to || (!options.client && !isMarketplace && !all)) {
918
+ return [{ client: isMarketplace ? "project" : "local", label: isMarketplace ? "Custom target" : "Local registry", root: resolve(options.to ?? defaultInstallDir), legacy: !isMarketplace }];
919
+ }
920
+ const definitions = installClientDefinitions();
921
+ const clients = resolveClients(options.client ?? "all", definitions);
922
+ const roots = clients.map((id) => ({ client: id, label: definitions[id].label, root: definitions[id].root, legacy: false }));
923
+ return roots.filter((root, index) => roots.findIndex((candidate) => candidate.root === root.root) === index);
924
+ }
925
+
926
+ function resolveInstallTargets(name, options) {
927
+ if (options.to && options.client) throw new Error("--to/--target cannot be combined with --client");
928
+ return installRoots(options).map((root) => installationTarget(root, name));
929
+ }
930
+
931
+ function installationTarget(root, name) {
932
+ safeInstallRelativePath(name);
933
+ return { ...root, path: join(root.root, root.legacy ? safeInstallRelativePath(name) : scopedSkillDirectory(name)) };
934
+ }
935
+
936
+ function pathStats(path) {
937
+ try {
938
+ return lstatSync(path);
939
+ } catch (error) {
940
+ if (error.code === "ENOENT") return undefined;
941
+ throw error;
942
+ }
943
+ }
944
+
945
+ function assertManagedPath(target) {
946
+ const segments = relative(target.root, target.path).split(process.platform === "win32" ? "\\" : "/");
947
+ if (segments.some((part) => !part || part === "..") || isAbsolute(relative(target.root, target.path))) {
948
+ throw new Error(`Unsafe install path: ${target.path}`);
949
+ }
950
+ let current = target.root;
951
+ for (const segment of segments) {
952
+ current = join(current, segment);
953
+ const stats = pathStats(current);
954
+ if (stats && !stats.isDirectory()) throw new Error(`Install path must be a regular directory, not a symbolic link: ${current}`);
955
+ }
956
+ }
957
+
958
+ function installedMetadata(target) {
959
+ assertManagedPath(target);
960
+ const names = target.legacy ? ["yep-installed.json", "cpm-installed.json"] : [".yep.json", ".skills-center.json"];
961
+ const path = names.map(name => join(target.path, name)).find(path => pathStats(path)) ?? join(target.path, names[0]);
962
+ const stats = pathStats(path);
963
+ if (!stats) return undefined;
964
+ if (!stats.isFile()) throw new Error(`Installation metadata must be a regular file: ${path}`);
965
+ let metadata;
966
+ try {
967
+ metadata = JSON.parse(readFileSync(path, "utf8"));
968
+ safeInstallRelativePath(metadata?.name);
969
+ if (typeof metadata.version !== "string" || !skillVersionPattern.test(metadata.version)) throw new Error("invalid version");
970
+ if (installationTarget(target, metadata.name).path !== target.path) throw new Error("package name does not match its directory");
971
+ } catch (error) {
972
+ throw new Error(`Invalid installation metadata at ${path}: ${error.message}`);
973
+ }
974
+ return metadata;
975
+ }
976
+
977
+ function assertInstallDestination(target, name) {
978
+ const existing = installedMetadata(target);
979
+ if (existing && (existing.kind ?? "skill") !== (target.kind ?? "skill")) throw new Error(`Destination contains a different resource kind: ${target.path}`);
980
+ if (existing && existing.name !== name) throw new Error(`Destination belongs to ${existing.name}, not ${name}: ${target.path}`);
981
+ }
982
+
983
+ function listInstalled(args, options) {
984
+ const installations = [];
985
+ const query = args.join(" ").toLowerCase();
986
+ for (const root of installRoots(options, isMarketplace)) {
987
+ if (!existsSync(root.root)) continue;
988
+ const directories = readdirSync(root.root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
989
+ const paths = root.legacy
990
+ ? directories.flatMap((scope) => readdirSync(join(root.root, scope.name), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join(root.root, scope.name, entry.name)))
991
+ : directories.map((entry) => join(root.root, entry.name));
992
+ for (const path of paths) {
993
+ try {
994
+ const metadata = installedMetadata({ ...root, path });
995
+ if (!metadata || (metadata.kind ?? "skill") !== "skill" || !metadata.name.toLowerCase().includes(query)) continue;
996
+ installations.push({ ...metadata, client: root.client, path });
997
+ } catch (error) {
998
+ console.warn(`Warning: ${error.message}`);
999
+ }
1000
+ }
1001
+ }
1002
+ installations.sort((a, b) => a.name.localeCompare(b.name) || a.client.localeCompare(b.client));
1003
+ const result = options.limit ? installations.slice(0, options.limit) : installations;
1004
+ if (options.json) console.log(JSON.stringify(result, null, 2));
1005
+ else if (!result.length) console.log("No installed skills found.");
1006
+ else for (const item of result) console.log(`${item.name}@${item.version} (${item.client})\n ${item.path}`);
1007
+ }
1008
+
1009
+ async function uninstall(args, options) {
1010
+ if (options.kind !== "skill") return uninstallResources(args, options);
1011
+ if (!args.length) throw new Error(`Usage: ${cliName} uninstall <skill> [more-skills...] [--client <clients>|--target <dir>]`);
1012
+ const specs = args.map(parseSkillSpec);
1013
+ await selectOperationClients(options);
1014
+ const plan = new Map();
1015
+ // Validate all requested installations before removing any of them.
1016
+ for (const spec of specs) {
1017
+ for (const target of resolveInstallTargets(spec.name, options)) {
1018
+ const metadata = installedMetadata(target);
1019
+ if (!metadata) {
1020
+ if (pathStats(target.path)) throw new Error(`Refusing to remove a directory without installation metadata: ${target.path}`);
1021
+ plan.set(target.path, { name: spec.name, client: target.client, path: target.path, status: "not-installed" });
1022
+ continue;
1023
+ }
1024
+ if ((metadata.kind ?? "skill") !== "skill") throw new Error(`Destination contains a different resource kind: ${target.path}`);
1025
+ if (metadata.name !== spec.name) throw new Error(`Destination belongs to ${metadata.name}, not ${spec.name}: ${target.path}`);
1026
+ assertRequestedVersion(spec.version, metadata);
1027
+ plan.set(target.path, { name: metadata.name, version: metadata.version, client: target.client, path: target.path, status: "removed" });
1028
+ }
1029
+ }
1030
+ for (const item of plan.values()) {
1031
+ if (item.status === "removed") rmSync(item.path, { recursive: true });
1032
+ if (!options.json) console.log(item.status === "removed" ? `Uninstalled ${item.name}@${item.version} from ${item.path} (${item.client})` : `Not installed: ${item.name} (${item.client})`);
1033
+ }
1034
+ if (options.json) console.log(JSON.stringify([...plan.values()], null, 2));
1035
+ }
1036
+
1037
+ function readRegistry() {
1038
+ return JSON.parse(readFileSync(registryPath, "utf8"));
1039
+ }
1040
+
1041
+ function writeRegistry(registry) {
1042
+ writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`);
1043
+ }
1044
+
1045
+ function localList(query) {
1046
+ const registry = readRegistry();
1047
+ const normalized = query.trim().toLowerCase();
1048
+ return registry.skills.filter((skill) => {
1049
+ if (!normalized) return true;
1050
+ return [skill.name, skill.displayName, skill.summary, skill.category, ...(skill.tags ?? [])]
1051
+ .join(" ")
1052
+ .toLowerCase()
1053
+ .includes(normalized);
1054
+ });
1055
+ }
1056
+
1057
+ function findSkill(name) {
1058
+ const registry = readRegistry();
1059
+ const skill = registry.skills.find((item) => item.name === name);
1060
+ if (!skill) fail(`Skill not found: ${name}`);
1061
+ return skill;
1062
+ }
1063
+
1064
+ function readLocalManifest(packageDir) {
1065
+ const manifestPath = join(packageDir, "cpm.json");
1066
+
1067
+ if (!existsSync(manifestPath)) fail(`Missing manifest: ${manifestPath}`);
1068
+
1069
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
1070
+ const required = ["name", "displayName", "version", "summary", "description", "author", "category", "entry"];
1071
+ const missing = required.filter((key) => !manifest[key]);
1072
+ if (missing.length > 0) fail(`Manifest missing fields: ${missing.join(", ")}`);
1073
+ if (manifest.entry !== "SKILL.md") {
1074
+ throw new Error("Manifest entry must be the root-level SKILL.md for client compatibility");
1075
+ }
1076
+ safeInstallRelativePath(manifest.name);
1077
+ if (typeof manifest.version !== "string" || !skillVersionPattern.test(manifest.version)) throw new Error(`Invalid skill version: ${manifest.version}`);
1078
+ const skillPath = resolveLocalSkillEntry(packageDir, manifest.entry);
1079
+ return {
1080
+ ...manifest,
1081
+ readme: readFileSync(skillPath, "utf8")
1082
+ };
1083
+ }
1084
+
1085
+ function resolveLocalSkillEntry(packageDir, entry) {
1086
+ if (
1087
+ typeof entry !== "string" ||
1088
+ entry === "" ||
1089
+ entry.includes("\0") ||
1090
+ entry.includes("\\") ||
1091
+ isAbsolute(entry) ||
1092
+ /^[A-Za-z]:/.test(entry) ||
1093
+ entry.split("/").some((segment) => segment === "" || segment === "." || segment === "..")
1094
+ ) {
1095
+ throw new Error(`Unsafe manifest entry: ${String(entry)}`);
1096
+ }
1097
+
1098
+ const segments = entry.split("/");
1099
+ let current = resolve(packageDir);
1100
+ for (const [index, segment] of segments.entries()) {
1101
+ if (!readdirSync(current).includes(segment)) {
1102
+ throw new Error(`Missing skill entry with exact casing: ${entry}`);
1103
+ }
1104
+ current = join(current, segment);
1105
+ const stats = lstatSync(current);
1106
+ if (index < segments.length - 1 && !stats.isDirectory()) {
1107
+ throw new Error(`Skill entry parent must be a regular directory: ${entry}`);
1108
+ }
1109
+ if (index === segments.length - 1 && !stats.isFile()) {
1110
+ throw new Error(`Skill entry must be a regular file: ${entry}`);
1111
+ }
1112
+ }
1113
+ return current;
1114
+ }
1115
+
1116
+ async function apiList(registry, query, token, requestedLimit) {
1117
+ const defaultPageSize = Number.isInteger(requestedLimit) ? requestedLimit : 100;
1118
+ const collected = [];
1119
+ const categories = new Set();
1120
+ let firstResponse;
1121
+ let page = 1;
1122
+ let nextPageSize = defaultPageSize;
1123
+ let total;
1124
+ let previousSignature;
1125
+
1126
+ // New registries expose total/pageSize so clients can walk the complete
1127
+ // public catalogue. Older registries may ignore these query parameters;
1128
+ // when their response has no pagination metadata we retain one-page
1129
+ // compatibility instead of issuing an unbounded sequence of duplicate
1130
+ // requests.
1131
+ while (true) {
1132
+ const params = new URLSearchParams();
1133
+ if (query.trim()) params.set("q", query.trim());
1134
+ // Keep the first request compatible with registries that predate the
1135
+ // count-aware pagination fields. Subsequent requests opt into paging.
1136
+ if (page > 1) {
1137
+ params.set("page", String(page));
1138
+ params.set("pageSize", String(nextPageSize));
1139
+ }
1140
+ const response = await apiFetch(registry, `/api/skills?${params.toString()}`, undefined, token);
1141
+ if (!firstResponse) firstResponse = response;
1142
+ const batch = Array.isArray(response?.skills) ? response.skills : [];
1143
+ for (const category of Array.isArray(response?.categories) ? response.categories : []) {
1144
+ if (typeof category === "string" && category.trim()) categories.add(category);
1145
+ }
1146
+
1147
+ const responseTotal = typeof response?.total === "number" || (typeof response?.total === "string" && response.total.trim() !== "") ? Number(response.total) : NaN;
1148
+ const hasTotal = Number.isSafeInteger(responseTotal) && responseTotal >= 0;
1149
+ const responsePageSize = typeof response?.pageSize === "number" || (typeof response?.pageSize === "string" && response.pageSize.trim() !== "") ? Number(response.pageSize) : NaN;
1150
+ if (Number.isSafeInteger(responsePageSize) && responsePageSize > 0) nextPageSize = responsePageSize;
1151
+ const responsePage = typeof response?.page === "number" || (typeof response?.page === "string" && response.page.trim() !== "") ? Number(response.page) : NaN;
1152
+ const hasPageMetadata = Number.isSafeInteger(responsePage) && responsePage >= 1;
1153
+ const signature = JSON.stringify(batch);
1154
+ if (page > 1 && signature === previousSignature) break;
1155
+ previousSignature = signature;
1156
+ collected.push(...batch);
1157
+ if (hasTotal) total = responseTotal;
1158
+
1159
+ if (!hasTotal || batch.length === 0) break;
1160
+ if (Number.isInteger(requestedLimit) && collected.length >= requestedLimit) break;
1161
+ if (collected.length >= total) break;
1162
+ if (hasPageMetadata && responsePage !== page) break;
1163
+ page += 1;
1164
+ }
1165
+
1166
+ const result = firstResponse && typeof firstResponse === "object" ? { ...firstResponse } : {};
1167
+ result.skills = Number.isInteger(requestedLimit) ? collected.slice(0, requestedLimit) : collected;
1168
+ if (categories.size > 0) result.categories = [...categories].sort();
1169
+ if (Number.isSafeInteger(total)) result.total = total;
1170
+ return result;
1171
+ }
1172
+
1173
+ async function apiInfo(registry, name, token) {
1174
+ return apiFetch(registry, `/api/skills/-/resolve?name=${encodeURIComponent(name)}`, undefined, token);
1175
+ }
1176
+
1177
+ async function apiInfoWithOwnerFallback(registry, name, token, requestedVersion) {
1178
+ let publicSkill;
1179
+ try {
1180
+ publicSkill = await apiInfo(registry, name, token);
1181
+ if (!requestedVersion || publicSkill?.version === requestedVersion || !token) return publicSkill;
1182
+ } catch (error) {
1183
+ if (!(error instanceof RegistryError) || error.status !== 404 || !token) throw error;
1184
+ }
1185
+ const owned = await apiFindOwnedSkill(registry, name, token);
1186
+ if (!owned) {
1187
+ if (publicSkill) return publicSkill;
1188
+ throw new RegistryError(`Skill not found: ${name}`, 404);
1189
+ }
1190
+ const pkg = await apiOwnedPackage(registry, owned.id, token);
1191
+ assertOwnedPackageIdentity(pkg, owned, name);
1192
+ return pkg.skill;
1193
+ }
1194
+
1195
+ async function apiPackage(registry, name, token) {
1196
+ return apiFetch(registry, `/api/skills/-/package?name=${encodeURIComponent(name)}`, undefined, token);
1197
+ }
1198
+
1199
+ async function apiPackageWithOwnerFallback(registry, name, token, requestedVersion) {
1200
+ let publicPackage;
1201
+ try {
1202
+ publicPackage = await apiPackage(registry, name, token);
1203
+ if (!requestedVersion || publicPackage?.skill?.version === requestedVersion || !token) return publicPackage;
1204
+ } catch (error) {
1205
+ if (!(error instanceof RegistryError) || error.status !== 404 || !token) throw error;
1206
+ }
1207
+ const owned = await apiFindOwnedSkill(registry, name, token);
1208
+ if (!owned) {
1209
+ if (publicPackage) return publicPackage;
1210
+ throw new RegistryError(`Skill not found: ${name}`, 404);
1211
+ }
1212
+ const pkg = await apiOwnedPackage(registry, owned.id, token);
1213
+ assertOwnedPackageIdentity(pkg, owned, name);
1214
+ return pkg;
1215
+ }
1216
+
1217
+ async function apiOwnedPackage(registry, id, token) {
1218
+ return apiFetch(registry, `/api/me/skills/${encodeURIComponent(id)}/package`, undefined, token);
1219
+ }
1220
+
1221
+ function assertOwnedPackageIdentity(pkg, owned, requestedName) {
1222
+ if (!pkg?.skill || String(pkg.skill.id ?? "") !== String(owned.id) || pkg.skill.name !== requestedName) {
1223
+ throw new Error("Registry returned a package that does not match the authenticated owner's Skill");
1224
+ }
1225
+ }
1226
+
1227
+ async function apiDownload(registry, name, token) {
1228
+ return apiFetch(registry, `/api/skills/-/download?name=${encodeURIComponent(name)}`, { method: "POST" }, token);
1229
+ }
1230
+
1231
+ async function apiFindOwnedSkillIfSupported(registry, name, token) {
1232
+ try {
1233
+ return await apiFindOwnedSkill(registry, name, token);
1234
+ } catch (error) {
1235
+ if (error instanceof RegistryError && [404, 405].includes(error.status)) return undefined;
1236
+ throw error;
1237
+ }
1238
+ }
1239
+
1240
+ async function apiFindOwnedSkill(registry, name, token) {
1241
+ const data = await apiFetch(registry, "/api/me/skills", undefined, token);
1242
+ const matches = Array.isArray(data?.skills) ? data.skills.filter((item) => item?.name === name && item?.id) : [];
1243
+ if (matches.length > 1) throw new Error(`Registry returned duplicate owned Skills named ${name}`);
1244
+ return matches[0] ? { ...matches[0], id: String(matches[0].id) } : undefined;
1245
+ }
1246
+
1247
+ async function apiUploadFiles(registry, manifest, files, token, skillId) {
1248
+ const form = new FormData();
1249
+ const paths = Object.keys(files).sort();
1250
+ form.append("paths", JSON.stringify(paths));
1251
+ form.append("visibility", "private");
1252
+ if (skillId) form.append("skillId", String(skillId));
1253
+ paths.forEach((path, index) => form.append(`file:${index}`, new Blob([files[path]]), `file-${index}`));
1254
+ try {
1255
+ return await apiFetch(registry, "/api/me/skills/files", { method: "POST", body: form, signal: AbortSignal.timeout(300_000) }, token);
1256
+ } catch (error) {
1257
+ if (!(error instanceof RegistryError) || ![404, 405].includes(error.status)) throw error;
1258
+ throw new Error("Registry does not support direct file uploads; upgrade the API server before publishing this package");
1259
+ }
1260
+ }
1261
+
1262
+ async function apiFetch(registry, path, init = {}, token) {
1263
+ let response;
1264
+ const headers = new Headers(init.headers);
1265
+ if (token) headers.set("Authorization", `Bearer ${token}`);
1266
+ try {
1267
+ response = await fetch(`${trimSlash(registry)}${path}`, {
1268
+ ...init,
1269
+ headers,
1270
+ signal: init.signal ?? AbortSignal.timeout(20_000)
1271
+ });
1272
+ } catch (error) {
1273
+ const cause = error?.cause?.message ? ` (${error.cause.message})` : "";
1274
+ throw new Error(`Registry request failed: ${error.message}${cause}`);
1275
+ }
1276
+ if (!response.ok) {
1277
+ let message = `Registry request failed: ${response.status}`;
1278
+ try {
1279
+ const error = await response.json();
1280
+ if (error?.error) message = error.error;
1281
+ } catch {
1282
+ // Keep status message when the server does not return JSON.
1283
+ }
1284
+ if (response.status === 401) message += `. Run "${cliName} login --registry ${registry}" to sign in again, or replace YEP_TOKEN/--token if set.`;
1285
+ throw new RegistryError(message, response.status);
1286
+ }
1287
+ if (response.status === 204) return undefined;
1288
+ return response.json();
1289
+ }
1290
+
1291
+ class RegistryError extends Error {
1292
+ constructor(message, status) {
1293
+ super(message);
1294
+ this.status = status;
1295
+ }
1296
+ }
1297
+
1298
+ async function recordDownload(registry, name, token, success = "downloaded successfully") {
1299
+ try {
1300
+ await apiDownload(registry, name, token);
1301
+ } catch (error) {
1302
+ console.warn(`Warning: ${success}, but could not update the download count: ${error.message}`);
1303
+ }
1304
+ }
1305
+
1306
+ function shouldRecordDownload(skill) {
1307
+ return skill?.visibility !== "private" && !["draft", "pending", "rejected"].includes(skill?.status);
1308
+ }
1309
+
1310
+ function collectPackageFiles(packageDir) {
1311
+ const files = Object.create(null);
1312
+ const seenPaths = new Map();
1313
+ let count = 0;
1314
+ let totalBytes = 0;
1315
+
1316
+ function visit(directory, prefix = "") {
1317
+ for (const name of readdirSync(directory).sort()) {
1318
+ const packagePath = prefix ? `${prefix}/${name}` : name;
1319
+ if (shouldIgnorePackagePath(packagePath)) continue;
1320
+ safePackageOutput(packageDir, packagePath);
1321
+ const diskPath = join(directory, name);
1322
+ const stats = lstatSync(diskPath);
1323
+ const pathKey = packagePath.toLowerCase();
1324
+ if (seenPaths.has(pathKey)) throw new Error(`Package paths differ only by case: ${seenPaths.get(pathKey)} and ${packagePath}`);
1325
+ seenPaths.set(pathKey, packagePath);
1326
+ if (stats.isSymbolicLink()) throw new Error(`Package cannot contain symbolic links: ${packagePath}`);
1327
+ if (stats.isDirectory()) {
1328
+ visit(diskPath, packagePath);
1329
+ continue;
1330
+ }
1331
+ if (!stats.isFile()) continue;
1332
+ count++;
1333
+ totalBytes += stats.size;
1334
+ if (count > maxPackageFiles) throw new Error(`Package exceeds ${maxPackageFiles} files`);
1335
+ if (stats.size > maxPackageFileBytes) {
1336
+ throw new Error(`Package file exceeds ${maxPackageFileBytes / 1024 / 1024} MiB: ${packagePath}`);
1337
+ }
1338
+ if (totalBytes > maxExpandedPackageBytes) {
1339
+ throw new Error(`Package expands beyond ${maxExpandedPackageBytes / 1024 / 1024} MiB`);
1340
+ }
1341
+ files[packagePath] = new Uint8Array(readFileSync(diskPath));
1342
+ }
1343
+ }
1344
+
1345
+ visit(packageDir);
1346
+ return files;
1347
+ }
1348
+
1349
+ function shouldIgnorePackagePath(packagePath) {
1350
+ const segments = packagePath.split("/");
1351
+ const name = basename(packagePath);
1352
+ return segments.includes(".git") || segments.includes("node_modules") || name === ".DS_Store" || name === "yep-installed.json" || name === ".yep.json" || name === "cpm-installed.json" || name === ".skills-center.json" || name === ".env" || name.startsWith(".env.");
1353
+ }
1354
+
1355
+ function safePackageOutput(target, packagePath) {
1356
+ const segments = typeof packagePath === "string" ? packagePath.split("/") : [];
1357
+ if (
1358
+ typeof packagePath !== "string" ||
1359
+ packagePath === "" ||
1360
+ Buffer.byteLength(packagePath, "utf8") > maxPackagePathBytes ||
1361
+ packagePath.includes("\0") ||
1362
+ packagePath.includes("\\") ||
1363
+ isAbsolute(packagePath) ||
1364
+ /^[A-Za-z]:/.test(packagePath) ||
1365
+ /[\u0000-\u001f\u007f-\u009f]/u.test(packagePath) ||
1366
+ segments.some((segment) => segment === "" || segment === "." || segment === ".." || !isPortablePackageSegment(segment))
1367
+ ) {
1368
+ throw new Error(`Unsafe package path: ${String(packagePath)}`);
1369
+ }
1370
+ const root = resolve(target);
1371
+ const output = resolve(root, packagePath);
1372
+ const rel = relative(root, output);
1373
+ if (rel === "" || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(rel)) {
1374
+ throw new Error(`Unsafe package path: ${packagePath}`);
1375
+ }
1376
+ return output;
1377
+ }
1378
+
1379
+ function isPortablePackageSegment(segment) {
1380
+ if (
1381
+ Buffer.byteLength(segment, "utf8") > maxPackageSegmentBytes ||
1382
+ segment.endsWith(".") ||
1383
+ segment.endsWith(" ") ||
1384
+ /[<>:"|?*]/u.test(segment)
1385
+ ) {
1386
+ return false;
1387
+ }
1388
+ const base = segment.split(".", 1)[0].toUpperCase();
1389
+ return !["CON", "PRN", "AUX", "NUL"].includes(base) && !/^(?:COM|LPT)[1-9]$/.test(base);
1390
+ }
1391
+
1392
+ function validateRemotePackage(pkg, installRoot, requestedName, targetPath, normalizeSkillEntry = true) {
1393
+ if (!pkg || typeof pkg !== "object" || !pkg.skill || typeof pkg.skill !== "object") {
1394
+ throw new Error("Invalid remote package metadata");
1395
+ }
1396
+ for (const field of ["name", "displayName", "version", "entry"]) {
1397
+ if (typeof pkg.skill[field] !== "string" || pkg.skill[field].trim() === "") {
1398
+ throw new Error(`Invalid remote package skill.${field}`);
1399
+ }
1400
+ }
1401
+ if (pkg.skill.name !== requestedName) throw new Error(`Registry returned package ${pkg.skill.name} for requested ${requestedName}`);
1402
+ const requestedEntry = pkg.skill.entry;
1403
+ safePackageOutput(targetPath ? resolve(targetPath) : installRoot, requestedEntry);
1404
+ safeInstallRelativePath(pkg.skill.name);
1405
+ if (!skillVersionPattern.test(pkg.skill.version)) throw new Error(`Invalid remote package skill.version: ${pkg.skill.version}`);
1406
+ if (!Array.isArray(pkg.files)) throw new Error("Invalid remote package files");
1407
+ if (pkg.files.length === 0) throw new Error("Remote package contains no files");
1408
+ if (pkg.files.length > maxPackageFiles) throw new Error(`Package exceeds ${maxPackageFiles} files`);
1409
+
1410
+ const target = targetPath ? resolve(targetPath) : join(installRoot, safeInstallRelativePath(pkg.skill.name));
1411
+ safePackageOutput(target, requestedEntry);
1412
+ const files = [];
1413
+ const seenPaths = new Set();
1414
+ const exactPaths = new Set();
1415
+ let totalBytes = 0;
1416
+ for (const file of pkg.files) {
1417
+ if (!file || typeof file !== "object") throw new Error("Invalid remote package file");
1418
+ safePackageOutput(target, file.path);
1419
+ const pathKey = file.path.toLowerCase();
1420
+ if (seenPaths.has(pathKey)) throw new Error(`Duplicate package path: ${file.path}`);
1421
+ seenPaths.add(pathKey);
1422
+ exactPaths.add(file.path);
1423
+
1424
+ const content = decodePackageContent(file.contentBase64, file.path);
1425
+ totalBytes += content.length;
1426
+ if (totalBytes > maxExpandedPackageBytes) {
1427
+ throw new Error(`Package expands beyond ${maxExpandedPackageBytes / 1024 / 1024} MiB`);
1428
+ }
1429
+ files.push({ path: file.path, pathKey, content });
1430
+ }
1431
+
1432
+ const sortedPaths = files.map((file) => file.pathKey).sort();
1433
+ for (let index = 0; index < sortedPaths.length - 1; index++) {
1434
+ if (sortedPaths[index + 1].startsWith(`${sortedPaths[index]}/`)) {
1435
+ throw new Error(`Conflicting package paths: ${sortedPaths[index]} and ${sortedPaths[index + 1]}`);
1436
+ }
1437
+ }
1438
+ if (!exactPaths.has(requestedEntry)) throw new Error(`Remote package is missing its entry file: ${requestedEntry}`);
1439
+ // Older registries may point at docs/SKILL.md. Normalize that entry at install time so
1440
+ // Codex and Claude both receive the root-level SKILL.md they discover natively.
1441
+ if (normalizeSkillEntry && requestedEntry !== "SKILL.md") {
1442
+ const entryFile = files.find((file) => file.path === requestedEntry);
1443
+ if (!entryFile) throw new Error(`Remote package is missing its entry file: ${requestedEntry}`);
1444
+ const normalizedFiles = files.filter((file) => file.path !== requestedEntry && file.path !== "SKILL.md");
1445
+ normalizedFiles.push({ ...entryFile, path: "SKILL.md" });
1446
+ return { skill: { ...pkg.skill, entry: "SKILL.md" }, target, files: normalizedFiles };
1447
+ }
1448
+ return { skill: pkg.skill, target, files };
1449
+ }
1450
+
1451
+ function decodePackageContent(contentBase64, packagePath) {
1452
+ const maximumBase64Length = Math.ceil(maxPackageFileBytes / 3) * 4;
1453
+ if (
1454
+ typeof contentBase64 !== "string" ||
1455
+ contentBase64.length > maximumBase64Length ||
1456
+ contentBase64.length % 4 !== 0 ||
1457
+ !hasValidBase64CharactersAndPadding(contentBase64)
1458
+ ) {
1459
+ throw new Error(`Invalid base64 content for package file: ${String(packagePath)}`);
1460
+ }
1461
+ const content = Buffer.from(contentBase64, "base64");
1462
+ if (content.toString("base64") !== contentBase64) {
1463
+ throw new Error(`Invalid base64 content for package file: ${packagePath}`);
1464
+ }
1465
+ if (content.length > maxPackageFileBytes) {
1466
+ throw new Error(`Package file exceeds ${maxPackageFileBytes / 1024 / 1024} MiB: ${packagePath}`);
1467
+ }
1468
+ return content;
1469
+ }
1470
+
1471
+ function hasValidBase64CharactersAndPadding(value) {
1472
+ let padding = 0;
1473
+ if (value.endsWith("=")) padding++;
1474
+ if (value.endsWith("==")) padding++;
1475
+ const contentLength = value.length - padding;
1476
+
1477
+ for (let index = 0; index < contentLength; index++) {
1478
+ const code = value.charCodeAt(index);
1479
+ const isBase64Character =
1480
+ (code >= 65 && code <= 90) ||
1481
+ (code >= 97 && code <= 122) ||
1482
+ (code >= 48 && code <= 57) ||
1483
+ code === 43 ||
1484
+ code === 47;
1485
+ if (!isBase64Character) return false;
1486
+ }
1487
+ for (let index = contentLength; index < value.length; index++) {
1488
+ if (value.charCodeAt(index) !== 61) return false;
1489
+ }
1490
+ return padding <= 2;
1491
+ }
1492
+
1493
+ function installRemotePackage(target, files, skill, registry, client = "local") {
1494
+ installPackageAtomically(target, skill, registry, (staging) => {
1495
+ for (const file of files) {
1496
+ const output = safePackageOutput(staging, file.path);
1497
+ mkdirSync(dirname(output), { recursive: true });
1498
+ writeFileSync(output, file.content);
1499
+ }
1500
+ }, client);
1501
+ }
1502
+
1503
+ function installLocalPackage(target, source, skill, client = "local") {
1504
+ installPackageAtomically(target, skill, registryPath, (staging) => copyDir(source, staging), client);
1505
+ }
1506
+
1507
+ function installPackageAtomically(target, skill, registry, populate, client = "local") {
1508
+ const parent = dirname(target);
1509
+ mkdirSync(parent, { recursive: true });
1510
+ const staging = mkdtempSync(join(parent, `.${basename(target)}.tmp-`));
1511
+ try {
1512
+ populate(staging);
1513
+ writeInstalledManifest(staging, skill, registry, client);
1514
+ replaceInstalledDirectory(staging, target);
1515
+ } finally {
1516
+ rmSync(staging, { recursive: true, force: true });
1517
+ }
1518
+ }
1519
+
1520
+ function replaceInstalledDirectory(staging, target) {
1521
+ if (!existsSync(target)) {
1522
+ renameSync(staging, target);
1523
+ return;
1524
+ }
1525
+
1526
+ const previous = `${staging}.previous`;
1527
+ renameSync(target, previous);
1528
+ try {
1529
+ renameSync(staging, target);
1530
+ } catch (error) {
1531
+ renameSync(previous, target);
1532
+ throw error;
1533
+ }
1534
+
1535
+ try {
1536
+ rmSync(previous, { recursive: true, force: true });
1537
+ } catch (error) {
1538
+ renameSync(target, staging);
1539
+ renameSync(previous, target);
1540
+ throw error;
1541
+ }
1542
+ }
1543
+
1544
+ function writeInstalledManifest(target, skill, registry, client = "local") {
1545
+ writeFileSync(
1546
+ join(target, client === "local" ? "yep-installed.json" : ".yep.json"),
1547
+ JSON.stringify(
1548
+ {
1549
+ name: skill.name,
1550
+ version: skill.version,
1551
+ installedAt: new Date().toISOString(),
1552
+ registry,
1553
+ client,
1554
+ ...(skill.kind ? { kind: skill.kind, revisionId: skill.currentRevisionId } : {})
1555
+ },
1556
+ null,
1557
+ 2
1558
+ )
1559
+ );
1560
+ }
1561
+
1562
+ function requiredArg(value, usage) {
1563
+ if (!value || value.startsWith("--")) fail(usage);
1564
+ return value;
1565
+ }
1566
+
1567
+ function safePackageDir(name) {
1568
+ safeInstallRelativePath(name);
1569
+ return encodeURIComponent(name);
1570
+ }
1571
+
1572
+ function safeInstallRelativePath(name) {
1573
+ if (typeof name !== "string") throw new Error(`Unsafe package name: ${String(name)}`);
1574
+ const match = /^@([a-z0-9][a-z0-9._-]{2,31})\/([a-z0-9][a-z0-9._-]{1,63})$/.exec(name);
1575
+ if (!match) throw new Error(`Unsafe package name: ${name}`);
1576
+ return join(match[1], match[2]);
1577
+ }
1578
+
1579
+ function copyDir(source, target) {
1580
+ mkdirSync(target, { recursive: true });
1581
+ for (const entry of readdirSync(source)) {
1582
+ const from = join(source, entry);
1583
+ const to = join(target, entry);
1584
+ const stats = lstatSync(from);
1585
+ if (stats.isSymbolicLink()) throw new Error(`Package cannot contain symbolic links: ${from}`);
1586
+ if (stats.isDirectory()) {
1587
+ copyDir(from, to);
1588
+ } else if (stats.isFile()) {
1589
+ writeFileSync(to, readFileSync(from));
1590
+ } else {
1591
+ throw new Error(`Unsupported package entry: ${from}`);
1592
+ }
1593
+ }
1594
+ }
1595
+
1596
+ function requireRegistryToken(token, registry, operation) {
1597
+ if (typeof token === "string" && token.trim()) return token.trim();
1598
+ throw new Error(`Authentication is required to ${operation}. Run "${cliName} login --registry ${registry}" or set YEP_TOKEN.`);
1599
+ }
1600
+
1601
+ function normalizeRegistry(value) {
1602
+ let url;
1603
+ try {
1604
+ url = new URL(String(value));
1605
+ } catch {
1606
+ throw new Error(`Invalid registry URL: ${String(value)}`);
1607
+ }
1608
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
1609
+ throw new Error("Registry URL must use HTTP(S) and cannot contain credentials, a query, or a fragment");
1610
+ }
1611
+ return trimSlash(url.toString());
1612
+ }
1613
+
1614
+ function trimSlash(value) {
1615
+ return value.replace(/\/+$/, "");
1616
+ }
1617
+
1618
+ function fail(message) {
1619
+ console.error(message);
1620
+ process.exit(1);
1621
+ }