skillforge-kit 0.2.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.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,665 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import pc from "picocolors";
6
+ import ora from "ora";
7
+ import { mkdir, writeFile as writeFile2, readdir, rm, readFile as readFile2, stat } from "fs/promises";
8
+ import { join as join3, dirname as dirname2 } from "path";
9
+ import { createInterface } from "readline";
10
+ import {
11
+ searchSkillRepos,
12
+ listSkillsInRepo,
13
+ downloadSkillFiles,
14
+ generateAgentsMd,
15
+ parseSkillMd
16
+ } from "skillforge-core";
17
+
18
+ // src/config.ts
19
+ import Conf from "conf";
20
+ import { homedir } from "os";
21
+ import { join } from "path";
22
+ var config = new Conf({
23
+ projectName: "skillforge",
24
+ defaults: {
25
+ defaultTarget: "project"
26
+ }
27
+ });
28
+ function getConfig() {
29
+ return {
30
+ registry: config.get("registry"),
31
+ token: config.get("token"),
32
+ githubToken: config.get("githubToken"),
33
+ defaultTarget: config.get("defaultTarget")
34
+ };
35
+ }
36
+ function setConfig(key, value) {
37
+ config.set(key, value);
38
+ }
39
+ function getTargetDir(target, skillName, global = false) {
40
+ const home = homedir();
41
+ if (global) {
42
+ const dirs2 = {
43
+ cursor: join(home, ".cursor", "skills", skillName),
44
+ claude: join(home, ".claude", "skills", skillName),
45
+ project: join(home, ".agent", "skills", skillName)
46
+ };
47
+ return dirs2[target];
48
+ }
49
+ const dirs = {
50
+ cursor: join(process.cwd(), ".cursor", "skills", skillName),
51
+ claude: join(process.cwd(), ".claude", "skills", skillName),
52
+ project: join(process.cwd(), ".agent", "skills", skillName)
53
+ };
54
+ return dirs[target];
55
+ }
56
+
57
+ // src/lockfile.ts
58
+ import { readFile, writeFile } from "fs/promises";
59
+ import { join as join2, dirname } from "path";
60
+ var LOCK_FILE = "skillforge.lock.json";
61
+ async function getLockFilePath(target, global) {
62
+ const skillsDir = dirname(getTargetDir(target, "dummy", global));
63
+ return join2(dirname(skillsDir), LOCK_FILE);
64
+ }
65
+ async function readLockFile(lockPath) {
66
+ try {
67
+ const content = await readFile(lockPath, "utf-8");
68
+ return JSON.parse(content);
69
+ } catch {
70
+ return { version: 1, skills: {} };
71
+ }
72
+ }
73
+ async function writeLockFile(lockPath, lock) {
74
+ await writeFile(lockPath, JSON.stringify(lock, null, 2));
75
+ }
76
+ async function recordInstall(target, global, skillName, source, version, commit) {
77
+ const lockPath = await getLockFilePath(target, global);
78
+ const lock = await readLockFile(lockPath);
79
+ const now = (/* @__PURE__ */ new Date()).toISOString();
80
+ const existing = lock.skills[skillName];
81
+ lock.skills[skillName] = {
82
+ name: skillName,
83
+ source,
84
+ version,
85
+ commit,
86
+ installedAt: existing?.installedAt || now,
87
+ updatedAt: now
88
+ };
89
+ await writeLockFile(lockPath, lock);
90
+ }
91
+ async function removeFromLock(target, global, skillName) {
92
+ const lockPath = await getLockFilePath(target, global);
93
+ const lock = await readLockFile(lockPath);
94
+ delete lock.skills[skillName];
95
+ await writeLockFile(lockPath, lock);
96
+ }
97
+ async function getAllInstalled(target, global) {
98
+ const lockPath = await getLockFilePath(target, global);
99
+ const lock = await readLockFile(lockPath);
100
+ return lock.skills;
101
+ }
102
+
103
+ // src/index.ts
104
+ async function confirm(message) {
105
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
106
+ return new Promise((resolve) => {
107
+ rl.question(`${message} (y/N): `, (answer) => {
108
+ rl.close();
109
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
110
+ });
111
+ });
112
+ }
113
+ async function fetchRegistry(path, options = {}) {
114
+ const config2 = getConfig();
115
+ if (!config2.registry) return null;
116
+ const headers = { "Content-Type": "application/json" };
117
+ if (options.token || config2.token) {
118
+ headers.Authorization = `Bearer ${options.token || config2.token}`;
119
+ }
120
+ const res = await fetch(`${config2.registry}${path}`, { headers });
121
+ if (!res.ok) return null;
122
+ return res.json();
123
+ }
124
+ function getRegistryAuthOrExit(overrideToken) {
125
+ const config2 = getConfig();
126
+ const token = overrideToken || config2.token;
127
+ if (!config2.registry) {
128
+ console.log(pc.yellow("No registry configured. Use: skify config set registry <url>"));
129
+ process.exit(1);
130
+ }
131
+ if (!token) {
132
+ console.log(pc.yellow("No token configured. Use: skify config set token <token>"));
133
+ process.exit(1);
134
+ }
135
+ return { registry: config2.registry, token };
136
+ }
137
+ var program = new Command();
138
+ program.name("skillforge").description("Agent Skills Kit - install & manage AI agent skills").version("0.1.0");
139
+ program.command("browse").description("Browse skills from private registry").option("-q, --query <query>", "Search query").action(async (options) => {
140
+ const config2 = getConfig();
141
+ if (!config2.registry) {
142
+ console.log(pc.yellow("No registry configured. Use: skify config set registry <url>"));
143
+ return;
144
+ }
145
+ const spinner = ora("Fetching from registry...").start();
146
+ try {
147
+ const query = options.query ? `?q=${encodeURIComponent(options.query)}` : "";
148
+ const data = await fetchRegistry(`/api/skills${query}`);
149
+ spinner.stop();
150
+ if (!data?.skills?.length) {
151
+ console.log(pc.yellow("No skills found in registry."));
152
+ return;
153
+ }
154
+ console.log(pc.bold(`
155
+ Skills in registry:
156
+ `));
157
+ for (const skill of data.skills) {
158
+ const stars = skill.stars ? pc.dim(`\u2B50 ${skill.stars}`) : "";
159
+ const installs = skill.installs ? pc.dim(`\u{1F4E6} ${skill.installs}`) : "";
160
+ console.log(` ${pc.green(skill.name)} ${stars} ${installs}`);
161
+ if (skill.description) {
162
+ console.log(` ${pc.dim(skill.description)}`);
163
+ }
164
+ }
165
+ } catch (err) {
166
+ spinner.fail("Failed to fetch registry");
167
+ console.error(pc.red(String(err)));
168
+ process.exit(1);
169
+ }
170
+ });
171
+ program.command("search <query>").description("Search for skills on GitHub").option("-t, --token <token>", "GitHub token for API access").action(async (query, options) => {
172
+ const spinner = ora("Searching skills...").start();
173
+ try {
174
+ const config2 = getConfig();
175
+ const skills = await searchSkillRepos(query, options.token || config2.githubToken);
176
+ spinner.stop();
177
+ if (skills.length === 0) {
178
+ console.log(pc.yellow("No skills found."));
179
+ return;
180
+ }
181
+ console.log(pc.bold(`
182
+ Found ${skills.length} skill repositories:
183
+ `));
184
+ for (const skill of skills) {
185
+ console.log(` ${pc.cyan(skill.repo)} ${pc.dim(`\u2B50 ${skill.stars}`)}`);
186
+ if (skill.description) {
187
+ console.log(` ${pc.dim(skill.description)}`);
188
+ }
189
+ }
190
+ console.log(pc.dim(`
191
+ Use ${pc.cyan("skify list <repo>")} to see skills in a repo`));
192
+ } catch (err) {
193
+ spinner.fail("Search failed");
194
+ console.error(pc.red(String(err)));
195
+ process.exit(1);
196
+ }
197
+ });
198
+ program.command("list [repo]").description("List skills in a GitHub repo, or list installed skills").option("-t, --token <token>", "GitHub token").option("-p, --path <path>", "Skills directory path", "skills").option("--agent <name>", "Target agent (cursor/claude/project)", "project").option("-g, --global", "Use global skills").action(async (repo, options) => {
199
+ const target = options.agent;
200
+ if (!repo) {
201
+ const installed = await getAllInstalled(target, options.global);
202
+ const names = Object.keys(installed);
203
+ if (names.length === 0) {
204
+ console.log(pc.yellow("No skills installed."));
205
+ return;
206
+ }
207
+ console.log(pc.bold(`
208
+ Installed skills:
209
+ `));
210
+ for (const name of names) {
211
+ const info = installed[name];
212
+ const version = info.version ? pc.dim(`v${info.version}`) : "";
213
+ const source = pc.dim(`\u2190 ${info.source}`);
214
+ console.log(` ${pc.green(name)} ${version} ${source}`);
215
+ }
216
+ return;
217
+ }
218
+ const spinner = ora("Fetching skills...").start();
219
+ try {
220
+ const config2 = getConfig();
221
+ const skills = await listSkillsInRepo(repo, options.token || config2.githubToken, options.path);
222
+ spinner.stop();
223
+ if (skills.length === 0) {
224
+ console.log(pc.yellow("No skills found in this repo."));
225
+ return;
226
+ }
227
+ console.log(pc.bold(`
228
+ Skills in ${pc.cyan(repo)}:
229
+ `));
230
+ for (const skill of skills) {
231
+ console.log(` - ${pc.green(skill)}`);
232
+ }
233
+ console.log(pc.dim(`
234
+ Use ${pc.cyan(`skify add ${repo}/<skill>`)} to install`));
235
+ } catch (err) {
236
+ spinner.fail("Failed to list skills");
237
+ console.error(pc.red(String(err)));
238
+ process.exit(1);
239
+ }
240
+ });
241
+ async function installFromRegistry(owner, repo, skillName, target, global) {
242
+ const config2 = getConfig();
243
+ if (!config2.registry) return false;
244
+ try {
245
+ const headers = {};
246
+ if (config2.token) {
247
+ headers.Authorization = `Bearer ${config2.token}`;
248
+ }
249
+ const res = await fetch(`${config2.registry}/api/download/${owner}/${repo}/${skillName}`, { headers });
250
+ if (!res.ok) return false;
251
+ const data = await res.json();
252
+ if (!data.files || Object.keys(data.files).length === 0) return false;
253
+ const targetDir = getTargetDir(target, skillName, global);
254
+ await mkdir(targetDir, { recursive: true });
255
+ for (const [path, content] of Object.entries(data.files)) {
256
+ const fullPath = join3(targetDir, path);
257
+ await mkdir(dirname2(fullPath), { recursive: true });
258
+ await writeFile2(fullPath, content);
259
+ }
260
+ await recordInstall(target, global, skillName, `registry:${owner}/${repo}/${skillName}`);
261
+ const installRes = await fetch(`${config2.registry}/api/skills/${owner}/${repo}/${skillName}/install`, {
262
+ method: "POST",
263
+ headers
264
+ });
265
+ return true;
266
+ } catch {
267
+ return false;
268
+ }
269
+ }
270
+ async function installFromGitHub(repo, skillName, skillsPath, target, global, token) {
271
+ const files = await downloadSkillFiles(repo, skillName, skillsPath, token);
272
+ const targetDir = getTargetDir(target, skillName, global);
273
+ await mkdir(targetDir, { recursive: true });
274
+ for (const [path, content] of Object.entries(files)) {
275
+ const fullPath = join3(targetDir, path);
276
+ await mkdir(dirname2(fullPath), { recursive: true });
277
+ await writeFile2(fullPath, content);
278
+ }
279
+ const skillMdPath = join3(targetDir, "SKILL.md");
280
+ try {
281
+ const content = await readFile2(skillMdPath, "utf-8");
282
+ const meta = parseSkillMd(content);
283
+ await recordInstall(target, global, skillName, `${repo}/${skillsPath}/${skillName}`, meta.version);
284
+ } catch {
285
+ await recordInstall(target, global, skillName, `${repo}/${skillsPath}/${skillName}`);
286
+ }
287
+ }
288
+ program.command("add <source>").description("Install a skill (tries registry first, then GitHub)").option("-g, --global", "Install globally").option("--agent <name>", "Target agent (cursor/claude/project)", "project").option("-t, --token <token>", "GitHub token").option("-p, --path <path>", "Skills directory path in repo", "skills").option("--github", "Force install from GitHub").action(async (source, options) => {
289
+ const spinner = ora("Installing skill...").start();
290
+ try {
291
+ const config2 = getConfig();
292
+ const githubToken = options.token || config2.githubToken;
293
+ const target = options.agent;
294
+ const parts = source.split("/");
295
+ let owner;
296
+ let repo;
297
+ let skillName;
298
+ if (parts.length === 3) {
299
+ owner = parts[0];
300
+ repo = parts[1];
301
+ skillName = parts[2];
302
+ } else if (parts.length === 2) {
303
+ owner = parts[0];
304
+ repo = parts[1];
305
+ skillName = void 0;
306
+ } else if (parts.length === 1) {
307
+ skillName = parts[0];
308
+ owner = "";
309
+ repo = "";
310
+ } else {
311
+ throw new Error("Invalid source format. Use skill-name, owner/repo, or owner/repo/skill");
312
+ }
313
+ if (skillName && !options.github && config2.registry) {
314
+ spinner.text = "Checking registry...";
315
+ if (owner && repo) {
316
+ const installed = await installFromRegistry(owner, repo, skillName, target, options.global);
317
+ if (installed) {
318
+ const targetDir = getTargetDir(target, skillName, options.global);
319
+ spinner.succeed(`Installed ${pc.green(skillName)} from registry to ${pc.dim(targetDir)}`);
320
+ return;
321
+ }
322
+ } else {
323
+ const data = await fetchRegistry(`/api/skills?q=${encodeURIComponent(skillName)}`);
324
+ if (data?.skills?.length > 0) {
325
+ const skill = data.skills.find((s) => s.name === skillName) || data.skills[0];
326
+ const installed = await installFromRegistry(skill.owner, skill.repo, skill.name, target, options.global);
327
+ if (installed) {
328
+ const targetDir = getTargetDir(target, skill.name, options.global);
329
+ spinner.succeed(`Installed ${pc.green(skill.name)} from registry to ${pc.dim(targetDir)}`);
330
+ return;
331
+ }
332
+ }
333
+ }
334
+ spinner.text = "Not in registry, trying GitHub...";
335
+ }
336
+ if (!owner || !repo) {
337
+ throw new Error("Skill not found in registry. Use owner/repo/skill format for GitHub.");
338
+ }
339
+ const fullRepo = `${owner}/${repo}`;
340
+ if (skillName) {
341
+ await installFromGitHub(fullRepo, skillName, options.path, target, options.global, githubToken);
342
+ const targetDir = getTargetDir(target, skillName, options.global);
343
+ spinner.succeed(`Installed ${pc.green(skillName)} from GitHub to ${pc.dim(targetDir)}`);
344
+ } else {
345
+ const skills = await listSkillsInRepo(fullRepo, githubToken, options.path);
346
+ if (skills.length === 0) {
347
+ const files = await downloadSkillFiles(fullRepo, void 0, "", githubToken);
348
+ const repoName = fullRepo.split("/").pop();
349
+ const targetDir = getTargetDir(target, repoName, options.global);
350
+ await mkdir(targetDir, { recursive: true });
351
+ for (const [path, content] of Object.entries(files)) {
352
+ const fullPath = join3(targetDir, path);
353
+ await mkdir(dirname2(fullPath), { recursive: true });
354
+ await writeFile2(fullPath, content);
355
+ }
356
+ await recordInstall(target, options.global, repoName, fullRepo);
357
+ spinner.succeed(`Installed ${pc.green(repoName)} from GitHub to ${pc.dim(targetDir)}`);
358
+ } else {
359
+ spinner.text = `Installing ${skills.length} skills from GitHub...`;
360
+ for (const skill of skills) {
361
+ await installFromGitHub(fullRepo, skill, options.path, target, options.global, githubToken);
362
+ console.log(` ${pc.green("\u2713")} ${skill}`);
363
+ }
364
+ spinner.succeed(`Installed ${skills.length} skills from GitHub`);
365
+ }
366
+ }
367
+ } catch (err) {
368
+ spinner.fail("Installation failed");
369
+ console.error(pc.red(String(err)));
370
+ process.exit(1);
371
+ }
372
+ });
373
+ program.command("update [name]").description("Update installed skills (all or specific)").option("--agent <name>", "Target agent (cursor/claude/project)", "project").option("-g, --global", "Use global skills").option("-t, --token <token>", "GitHub token").action(async (name, options) => {
374
+ const spinner = ora("Updating skills...").start();
375
+ try {
376
+ const config2 = getConfig();
377
+ const githubToken = options.token || config2.githubToken;
378
+ const target = options.agent;
379
+ const installed = await getAllInstalled(target, options.global);
380
+ const toUpdate = name ? [name] : Object.keys(installed);
381
+ if (toUpdate.length === 0) {
382
+ spinner.warn("No skills to update");
383
+ return;
384
+ }
385
+ let updated = 0;
386
+ for (const skillName of toUpdate) {
387
+ const info = installed[skillName];
388
+ if (!info) {
389
+ console.log(` ${pc.yellow("\u26A0")} ${skillName} not found in lockfile`);
390
+ continue;
391
+ }
392
+ spinner.text = `Updating ${skillName}...`;
393
+ if (info.source.startsWith("registry:")) {
394
+ const parts = info.source.replace("registry:", "").split("/");
395
+ if (parts.length >= 3) {
396
+ await installFromRegistry(parts[0], parts[1], parts[2], target, options.global);
397
+ console.log(` ${pc.green("\u2713")} ${skillName}`);
398
+ updated++;
399
+ continue;
400
+ }
401
+ }
402
+ const sourceParts = info.source.split("/");
403
+ if (sourceParts.length < 3) {
404
+ console.log(` ${pc.yellow("\u26A0")} ${skillName} has invalid source: ${info.source}`);
405
+ continue;
406
+ }
407
+ const repo = `${sourceParts[0]}/${sourceParts[1]}`;
408
+ const skillsPath = sourceParts.slice(2, -1).join("/") || "skills";
409
+ await installFromGitHub(repo, skillName, skillsPath, target, options.global, githubToken);
410
+ console.log(` ${pc.green("\u2713")} ${skillName}`);
411
+ updated++;
412
+ }
413
+ spinner.succeed(`Updated ${updated} skill(s)`);
414
+ } catch (err) {
415
+ spinner.fail("Update failed");
416
+ console.error(pc.red(String(err)));
417
+ process.exit(1);
418
+ }
419
+ });
420
+ program.command("read <name>").description("Read and output a skill (for agent consumption)").option("--agent <name>", "Target agent (cursor/claude/project)", "project").option("-g, --global", "Read from global installation").action(async (name, options) => {
421
+ try {
422
+ const target = options.agent;
423
+ const targetDir = getTargetDir(target, name, options.global);
424
+ const skillMdPath = join3(targetDir, "SKILL.md");
425
+ const content = await readFile2(skillMdPath, "utf-8");
426
+ const skill = parseSkillMd(content);
427
+ console.log(`
428
+ ${"=".repeat(60)}`);
429
+ console.log(`SKILL: ${skill.name}`);
430
+ console.log(`BASE_DIR: ${targetDir}`);
431
+ console.log("=".repeat(60));
432
+ console.log(skill.body);
433
+ console.log("=".repeat(60) + "\n");
434
+ } catch {
435
+ console.error(pc.red(`Skill "${name}" not found`));
436
+ process.exit(1);
437
+ }
438
+ });
439
+ program.command("remove <name>").description("Remove an installed skill").option("--agent <name>", "Target agent (cursor/claude/project)", "project").option("-g, --global", "Remove from global installation").action(async (name, options) => {
440
+ const spinner = ora(`Removing ${name}...`).start();
441
+ try {
442
+ const target = options.agent;
443
+ const targetDir = getTargetDir(target, name, options.global);
444
+ await rm(targetDir, { recursive: true, force: true });
445
+ await removeFromLock(target, options.global, name);
446
+ spinner.succeed(`Removed ${pc.green(name)}`);
447
+ } catch (err) {
448
+ spinner.fail("Removal failed");
449
+ console.error(pc.red(String(err)));
450
+ process.exit(1);
451
+ }
452
+ });
453
+ program.command("sync").description("Generate AGENTS.md from installed skills").option("-o, --output <path>", "Output file path", "AGENTS.md").option("--agent <name>", "Target agent (cursor/claude/project)", "project").option("-g, --global", "Use global skills").action(async (options) => {
454
+ const spinner = ora("Syncing skills...").start();
455
+ try {
456
+ const target = options.agent;
457
+ const skillsDir = dirname2(getTargetDir(target, "dummy", options.global));
458
+ let entries = [];
459
+ try {
460
+ entries = await readdir(skillsDir);
461
+ } catch {
462
+ spinner.fail("No skills directory found");
463
+ return;
464
+ }
465
+ const skills = [];
466
+ for (const entry of entries) {
467
+ const skillMdPath = join3(skillsDir, entry, "SKILL.md");
468
+ try {
469
+ await stat(skillMdPath);
470
+ const content = await readFile2(skillMdPath, "utf-8");
471
+ skills.push(parseSkillMd(content));
472
+ } catch {
473
+ continue;
474
+ }
475
+ }
476
+ if (skills.length === 0) {
477
+ spinner.warn("No skills found to sync");
478
+ return;
479
+ }
480
+ const agentsMd = generateAgentsMd(skills);
481
+ await writeFile2(options.output, agentsMd);
482
+ spinner.succeed(`Synced ${skills.length} skills to ${pc.cyan(options.output)}`);
483
+ } catch (err) {
484
+ spinner.fail("Sync failed");
485
+ console.error(pc.red(String(err)));
486
+ process.exit(1);
487
+ }
488
+ });
489
+ program.command("publish <path>", { hidden: true }).description("Publish a local skill to private registry").action(async (source) => {
490
+ const config2 = getConfig();
491
+ if (!config2.registry) {
492
+ console.log(pc.yellow("No registry configured. Use: skify config set registry <url>"));
493
+ process.exit(1);
494
+ }
495
+ if (!config2.token) {
496
+ console.log(pc.yellow("No token configured. Use: skify config set token <token>"));
497
+ process.exit(1);
498
+ }
499
+ const skillMdPath = source.endsWith("SKILL.md") ? source : join3(source, "SKILL.md");
500
+ const content = await readFile2(skillMdPath, "utf-8");
501
+ const pathParts = source.split("/");
502
+ const skillName = pathParts[pathParts.length - 1] || pathParts[pathParts.length - 2];
503
+ const meta = parseSkillMd(content);
504
+ const displayName = meta.name || skillName;
505
+ const confirmed = await confirm(`Publish "${displayName}" to registry?`);
506
+ if (!confirmed) {
507
+ console.log(pc.yellow("Cancelled."));
508
+ process.exit(0);
509
+ }
510
+ const spinner = ora("Publishing skill...").start();
511
+ try {
512
+ spinner.text = "Publishing to registry...";
513
+ const res = await fetch(`${config2.registry}/api/admin/skills`, {
514
+ method: "POST",
515
+ headers: {
516
+ "Content-Type": "application/json",
517
+ Authorization: `Bearer ${config2.token}`
518
+ },
519
+ body: JSON.stringify({
520
+ owner: "local",
521
+ repo: "skills",
522
+ name: displayName,
523
+ description: meta.description,
524
+ tags: meta.tags,
525
+ content
526
+ })
527
+ });
528
+ if (!res.ok) {
529
+ const err = await res.text();
530
+ throw new Error(`Registry error: ${err}`);
531
+ }
532
+ spinner.succeed(`Published ${pc.green(displayName)} to registry`);
533
+ } catch (err) {
534
+ spinner.fail("Publish failed");
535
+ console.error(pc.red(String(err)));
536
+ process.exit(1);
537
+ }
538
+ });
539
+ program.command("unpublish <name>", { hidden: true }).description("Remove a skill from private registry").option("-o, --owner <owner>", "Owner name", "local").option("-r, --repo <repo>", "Repo name", "skills").action(async (name, options) => {
540
+ const config2 = getConfig();
541
+ if (!config2.registry || !config2.token) {
542
+ console.log(pc.yellow("Registry and token required"));
543
+ process.exit(1);
544
+ }
545
+ const confirmed = await confirm(`Remove "${name}" from registry?`);
546
+ if (!confirmed) {
547
+ console.log(pc.yellow("Cancelled."));
548
+ process.exit(0);
549
+ }
550
+ const spinner = ora("Removing from registry...").start();
551
+ try {
552
+ const res = await fetch(`${config2.registry}/api/admin/skills/${options.owner}/${options.repo}/${name}`, {
553
+ method: "DELETE",
554
+ headers: { Authorization: `Bearer ${config2.token}` }
555
+ });
556
+ if (!res.ok) throw new Error("Failed to remove");
557
+ spinner.succeed(`Removed ${pc.green(name)} from registry`);
558
+ } catch (err) {
559
+ spinner.fail("Removal failed");
560
+ console.error(pc.red(String(err)));
561
+ process.exit(1);
562
+ }
563
+ });
564
+ program.command("token <action> [value]").description("Manage registry API tokens (list/create/revoke)").option("-n, --name <name>", "Token name for create").option("-p, --permissions <permissions>", "Comma-separated permissions (read,publish,admin)", "read").option("-t, --token <token>", "Admin token override").action(async (action, value, options) => {
565
+ const { registry, token } = getRegistryAuthOrExit(options.token);
566
+ const headers = {
567
+ "Content-Type": "application/json",
568
+ Authorization: `Bearer ${token}`
569
+ };
570
+ if (action === "list") {
571
+ const spinner = ora("Fetching tokens...").start();
572
+ try {
573
+ const res = await fetch(`${registry}/api/admin/tokens`, { headers });
574
+ if (!res.ok) {
575
+ throw new Error(`${res.status} ${await res.text()}`);
576
+ }
577
+ const data = await res.json();
578
+ spinner.stop();
579
+ if (!data.tokens?.length) {
580
+ console.log(pc.yellow("No tokens found."));
581
+ return;
582
+ }
583
+ console.log(pc.bold("\nRegistry tokens:\n"));
584
+ for (const t of data.tokens) {
585
+ console.log(` ${pc.green(t.name)} ${pc.dim(t.id)}`);
586
+ console.log(` permissions: ${pc.cyan(t.permissions.join(","))}`);
587
+ if (t.createdAt) console.log(` created: ${pc.dim(t.createdAt)}`);
588
+ }
589
+ return;
590
+ } catch (err) {
591
+ spinner.fail("Failed to fetch tokens");
592
+ console.error(pc.red(String(err)));
593
+ process.exit(1);
594
+ }
595
+ }
596
+ if (action === "create") {
597
+ const name = options.name || value;
598
+ if (!name) {
599
+ console.log(pc.yellow("Usage: skify token create <name> --permissions read,publish"));
600
+ process.exit(1);
601
+ }
602
+ const permissions = String(options.permissions || "read").split(",").map((p) => p.trim()).filter(Boolean);
603
+ const spinner = ora("Creating token...").start();
604
+ try {
605
+ const res = await fetch(`${registry}/api/admin/tokens`, {
606
+ method: "POST",
607
+ headers,
608
+ body: JSON.stringify({ name, permissions })
609
+ });
610
+ if (!res.ok) {
611
+ throw new Error(`${res.status} ${await res.text()}`);
612
+ }
613
+ const data = await res.json();
614
+ spinner.succeed(`Created token ${pc.green(data.token.name)} (${pc.dim(data.token.id)})`);
615
+ console.log(pc.bold("\nToken value (shown once):"));
616
+ console.log(pc.cyan(data.token.value));
617
+ return;
618
+ } catch (err) {
619
+ spinner.fail("Failed to create token");
620
+ console.error(pc.red(String(err)));
621
+ process.exit(1);
622
+ }
623
+ }
624
+ if (action === "revoke") {
625
+ const tokenId = value;
626
+ if (!tokenId) {
627
+ console.log(pc.yellow("Usage: skify token revoke <token-id>"));
628
+ process.exit(1);
629
+ }
630
+ const spinner = ora("Revoking token...").start();
631
+ try {
632
+ const res = await fetch(`${registry}/api/admin/tokens/${tokenId}/revoke`, {
633
+ method: "POST",
634
+ headers
635
+ });
636
+ if (!res.ok) {
637
+ throw new Error(`${res.status} ${await res.text()}`);
638
+ }
639
+ spinner.succeed(`Revoked token ${pc.green(tokenId)}`);
640
+ return;
641
+ } catch (err) {
642
+ spinner.fail("Failed to revoke token");
643
+ console.error(pc.red(String(err)));
644
+ process.exit(1);
645
+ }
646
+ }
647
+ console.log(pc.yellow("Usage: skify token list | create <name> --permissions read,publish | revoke <token-id>"));
648
+ process.exit(1);
649
+ });
650
+ program.command("config <action> [key] [value]").description("Manage configuration (get/set registry, token, githubToken, defaultTarget)").action(async (action, key, value) => {
651
+ if (action === "get") {
652
+ const config2 = getConfig();
653
+ if (key) {
654
+ console.log(config2[key] || "");
655
+ } else {
656
+ console.log(JSON.stringify(config2, null, 2));
657
+ }
658
+ } else if (action === "set" && key && value) {
659
+ setConfig(key, value);
660
+ console.log(pc.green(`Set ${key} = ${value}`));
661
+ } else {
662
+ console.log(pc.yellow("Usage: skify config get [key] | set <key> <value>"));
663
+ }
664
+ });
665
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "skillforge-kit",
3
+ "version": "0.2.1",
4
+ "type": "module",
5
+ "files": [
6
+ "dist"
7
+ ],
8
+ "bin": {
9
+ "skillforge": "./dist/index.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsup src/index.ts --format esm --dts",
13
+ "dev": "tsup src/index.ts --format esm --watch"
14
+ },
15
+ "dependencies": {
16
+ "skillforge-core": "^0.2.1",
17
+ "commander": "^12.1.0",
18
+ "picocolors": "^1.1.0",
19
+ "ora": "^8.1.0",
20
+ "conf": "^13.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "tsup": "^8.3.0",
24
+ "typescript": "^5.7.0",
25
+ "@types/node": "^22.0.0"
26
+ }
27
+ }