@agent-nexus/csreg 0.1.4 → 0.1.6

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/dist/index.js CHANGED
@@ -1,206 +1,32 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ ApiClient,
4
+ CliError,
5
+ MAX_ARCHIVE_SIZE,
6
+ extract,
7
+ findClaudeSkillsDir,
8
+ formatTable,
9
+ getApiUrl,
10
+ getAuthToken,
11
+ handleError,
12
+ info,
13
+ pack,
14
+ parseManifest,
15
+ pushCommand,
16
+ runValidation,
17
+ setConfig,
18
+ spinner,
19
+ success,
20
+ validateCommand,
21
+ warn
22
+ } from "./chunk-7UFQ62TS.js";
2
23
 
3
24
  // src/index.ts
4
- import { Command as Command12 } from "commander";
25
+ import { Command as Command11 } from "commander";
5
26
 
6
27
  // src/commands/login.ts
7
28
  import { Command } from "commander";
8
29
  import { input } from "@inquirer/prompts";
9
-
10
- // src/config.ts
11
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
12
- import { join } from "path";
13
- import { homedir } from "os";
14
- var CONFIG_DIR = join(homedir(), ".config", "csreg");
15
- var CONFIG_PATH = join(CONFIG_DIR, "config.json");
16
- function getConfig() {
17
- if (!existsSync(CONFIG_PATH)) {
18
- return {};
19
- }
20
- try {
21
- const raw = readFileSync(CONFIG_PATH, "utf-8");
22
- return JSON.parse(raw);
23
- } catch {
24
- return {};
25
- }
26
- }
27
- function setConfig(updates) {
28
- const current = getConfig();
29
- const merged = { ...current, ...updates };
30
- mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
31
- writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
32
- }
33
- function getApiUrl() {
34
- return process.env.CSREG_API_URL ?? getConfig().apiUrl ?? "https://www.csreg.nexus";
35
- }
36
- function getAuthToken() {
37
- return process.env.CSREG_TOKEN ?? getConfig().token;
38
- }
39
-
40
- // src/lib/errors.ts
41
- import chalk from "chalk";
42
- var CliError = class extends Error {
43
- suggestions;
44
- constructor(message, suggestions = []) {
45
- super(message);
46
- this.name = "CliError";
47
- this.suggestions = suggestions;
48
- }
49
- };
50
- function handleError(err) {
51
- if (err instanceof CliError) {
52
- console.error(chalk.red("Error:") + " " + err.message);
53
- if (err.suggestions.length > 0) {
54
- console.error("");
55
- console.error(chalk.yellow("Suggestions:"));
56
- for (const suggestion of err.suggestions) {
57
- console.error(" " + chalk.dim("-") + " " + suggestion);
58
- }
59
- }
60
- } else if (err instanceof Error) {
61
- console.error(chalk.red("Error:") + " " + err.message);
62
- } else {
63
- console.error(chalk.red("Error:") + " " + String(err));
64
- }
65
- process.exit(1);
66
- }
67
-
68
- // src/api-client.ts
69
- var MAX_RETRIES = 3;
70
- var BASE_DELAY_MS = 500;
71
- var ApiClient = class {
72
- baseUrl;
73
- token;
74
- constructor() {
75
- this.baseUrl = getApiUrl();
76
- this.token = getAuthToken();
77
- }
78
- buildUrl(path, query) {
79
- const url = new URL(path, this.baseUrl);
80
- if (query) {
81
- for (const [key, value] of Object.entries(query)) {
82
- url.searchParams.set(key, value);
83
- }
84
- }
85
- return url.toString();
86
- }
87
- buildHeaders(extra) {
88
- const headers = {
89
- "Content-Type": "application/json",
90
- "Accept": "application/json",
91
- ...extra
92
- };
93
- if (this.token) {
94
- headers["Authorization"] = `Bearer ${this.token}`;
95
- }
96
- return headers;
97
- }
98
- async handleResponse(response) {
99
- if (response.ok) {
100
- if (response.status === 204) {
101
- return void 0;
102
- }
103
- return response.json();
104
- }
105
- let errorBody;
106
- try {
107
- errorBody = await response.json();
108
- } catch {
109
- }
110
- const detail = errorBody?.detail ?? response.statusText;
111
- const title = errorBody?.title ?? `HTTP ${response.status}`;
112
- const suggestions = [];
113
- if (response.status === 401) {
114
- suggestions.push("Run `csreg login` to authenticate.");
115
- } else if (response.status === 403) {
116
- suggestions.push("You may not have permission for this action.");
117
- } else if (response.status === 404) {
118
- suggestions.push("Check that the skill name and scope are correct.");
119
- }
120
- throw new CliError(`${title}: ${detail}`, suggestions);
121
- }
122
- async requestWithRetry(method, path, opts) {
123
- const url = this.buildUrl(path, opts?.query);
124
- const headers = this.buildHeaders(opts?.headers);
125
- let lastError;
126
- for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
127
- try {
128
- const response = await fetch(url, {
129
- method,
130
- headers,
131
- body: opts?.body !== void 0 ? JSON.stringify(opts.body) : void 0
132
- });
133
- if (response.status >= 500 && attempt < MAX_RETRIES - 1) {
134
- const delay = BASE_DELAY_MS * Math.pow(2, attempt);
135
- await new Promise((resolve7) => setTimeout(resolve7, delay));
136
- continue;
137
- }
138
- return await this.handleResponse(response);
139
- } catch (error2) {
140
- lastError = error2;
141
- if (error2 instanceof CliError) {
142
- throw error2;
143
- }
144
- if (attempt < MAX_RETRIES - 1) {
145
- const delay = BASE_DELAY_MS * Math.pow(2, attempt);
146
- await new Promise((resolve7) => setTimeout(resolve7, delay));
147
- continue;
148
- }
149
- }
150
- }
151
- throw lastError instanceof CliError ? lastError : new CliError(`Request failed: ${String(lastError)}`, [
152
- "Check your internet connection.",
153
- "The API server may be unavailable."
154
- ]);
155
- }
156
- async get(path, opts) {
157
- return this.requestWithRetry("GET", path, opts);
158
- }
159
- async post(path, opts) {
160
- return this.requestWithRetry("POST", path, opts);
161
- }
162
- async put(path, opts) {
163
- return this.requestWithRetry("PUT", path, opts);
164
- }
165
- async patch(path, opts) {
166
- return this.requestWithRetry("PATCH", path, opts);
167
- }
168
- async delete(path, opts) {
169
- return this.requestWithRetry("DELETE", path, opts);
170
- }
171
- };
172
-
173
- // src/lib/output.ts
174
- import chalk2 from "chalk";
175
- import ora from "ora";
176
- import Table from "cli-table3";
177
- function formatTable(headers, rows) {
178
- const table = new Table({
179
- head: headers.map((h) => chalk2.bold.cyan(h)),
180
- style: { head: [], border: [] }
181
- });
182
- for (const row of rows) {
183
- table.push(row);
184
- }
185
- return table.toString();
186
- }
187
- function spinner(text) {
188
- return ora({ text, color: "cyan" }).start();
189
- }
190
- function success(msg) {
191
- console.log(chalk2.green("\u2713") + " " + msg);
192
- }
193
- function error(msg) {
194
- console.error(chalk2.red("\u2717") + " " + msg);
195
- }
196
- function warn(msg) {
197
- console.log(chalk2.yellow("!") + " " + msg);
198
- }
199
- function info(msg) {
200
- console.log(chalk2.blue("i") + " " + msg);
201
- }
202
-
203
- // src/commands/login.ts
204
30
  var loginCommand = new Command("login").description("Authenticate with the Skills Registry").option("--token <token>", "Provide auth token directly").action(async (opts) => {
205
31
  try {
206
32
  let token = opts.token;
@@ -260,8 +86,8 @@ var whoamiCommand = new Command3("whoami").description("Display the currently au
260
86
  // src/commands/init.ts
261
87
  import { Command as Command4 } from "commander";
262
88
  import { input as input2, confirm } from "@inquirer/prompts";
263
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
264
- import { join as join2, resolve } from "path";
89
+ import { mkdirSync, writeFileSync, existsSync } from "fs";
90
+ import { join, resolve } from "path";
265
91
  var initCommand = new Command4("init").description("Initialize a new skill project").argument("[dir]", "Directory to initialize the skill in").action(async (dir) => {
266
92
  try {
267
93
  const name = await input2({
@@ -298,18 +124,18 @@ var initCommand = new Command4("init").description("Initialize a new skill proje
298
124
  message: "User-invocable (show in /slash command menu)?",
299
125
  default: true
300
126
  });
301
- const defaultDir = dir ?? join2(".claude", "skills", name.trim());
127
+ const defaultDir = dir ?? join(".claude", "skills", name.trim());
302
128
  const chosenDir = await input2({
303
129
  message: "Directory to create skill in:",
304
130
  default: defaultDir
305
131
  });
306
132
  const targetDir = resolve(chosenDir.trim());
307
- if (existsSync2(targetDir)) {
133
+ if (existsSync(targetDir)) {
308
134
  throw new CliError(`Directory already exists: ${targetDir}`, [
309
135
  "Choose a different name or directory."
310
136
  ]);
311
137
  }
312
- mkdirSync2(targetDir, { recursive: true });
138
+ mkdirSync(targetDir, { recursive: true });
313
139
  const frontmatterLines = [
314
140
  "---",
315
141
  `name: ${name.trim()}`,
@@ -335,8 +161,8 @@ when this skill is invoked.
335
161
  You can use \`$ARGUMENTS\` to reference arguments passed by the user.
336
162
  For example: \`/my-skill some-argument\` makes \`$ARGUMENTS\` = "some-argument".
337
163
  `;
338
- writeFileSync2(
339
- join2(targetDir, "SKILL.md"),
164
+ writeFileSync(
165
+ join(targetDir, "SKILL.md"),
340
166
  skillContent,
341
167
  "utf-8"
342
168
  );
@@ -357,315 +183,14 @@ For example: \`/my-skill some-argument\` makes \`$ARGUMENTS\` = "some-argument".
357
183
  }
358
184
  });
359
185
 
360
- // src/commands/validate.ts
361
- import { Command as Command5 } from "commander";
362
- import { resolve as resolve3, join as join5, basename } from "path";
363
- import { existsSync as existsSync5, statSync, readdirSync as readdirSync2, lstatSync } from "fs";
364
-
365
- // src/lib/manifest.ts
366
- import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
367
- import { join as join3 } from "path";
368
- import { parse as parseYaml } from "yaml";
369
- var SKILL_FILE = "SKILL.md";
370
- function parseManifest(dir) {
371
- const skillPath = join3(dir, SKILL_FILE);
372
- if (!existsSync3(skillPath)) {
373
- throw new CliError(`No ${SKILL_FILE} found in ${dir}`, [
374
- "Run `csreg init` to create a new skill.",
375
- "A valid skill requires a SKILL.md file with YAML frontmatter."
376
- ]);
377
- }
378
- const raw = readFileSync2(skillPath, "utf-8");
379
- const frontmatterMatch = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
380
- if (!frontmatterMatch) {
381
- throw new CliError(`${SKILL_FILE} is missing YAML frontmatter.`, [
382
- "SKILL.md must start with --- delimited YAML frontmatter.",
383
- "Example:\n ---\n name: my-skill\n description: Does something\n ---\n \n Your instructions here."
384
- ]);
385
- }
386
- const [, frontmatterRaw, body] = frontmatterMatch;
387
- let parsed;
388
- try {
389
- parsed = parseYaml(frontmatterRaw);
390
- } catch (err) {
391
- throw new CliError(`Failed to parse ${SKILL_FILE} frontmatter: ${String(err)}`, [
392
- "Check that the YAML between --- delimiters is valid."
393
- ]);
394
- }
395
- if (!parsed || typeof parsed !== "object") {
396
- throw new CliError(`${SKILL_FILE} frontmatter is empty or not an object.`, [
397
- 'Add at least a "name" and "description" field.'
398
- ]);
399
- }
400
- return { ...parsed, _body: body.trim() };
401
- }
402
- function validateManifest(manifest) {
403
- const errors = [];
404
- if (!manifest.name || typeof manifest.name !== "string") {
405
- errors.push('Missing or invalid "name" field in frontmatter.');
406
- } else {
407
- if (manifest.name.length > 64) {
408
- errors.push('"name" must be at most 64 characters.');
409
- }
410
- if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(manifest.name)) {
411
- errors.push('"name" must be lowercase alphanumeric with hyphens, not starting or ending with a hyphen.');
412
- }
413
- if (manifest.name.includes("--")) {
414
- errors.push('"name" must not contain consecutive hyphens (--).');
415
- }
416
- }
417
- if (!manifest.description || typeof manifest.description !== "string") {
418
- errors.push('Missing "description" field. Claude uses this to decide when to invoke the skill.');
419
- } else if (manifest.description.length > 1024) {
420
- errors.push('"description" must be at most 1024 characters.');
421
- }
422
- if (manifest.version !== void 0) {
423
- const semverRegex = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$/;
424
- if (!semverRegex.test(manifest.version)) {
425
- errors.push('"version" must be valid semver (e.g., 1.0.0).');
426
- }
427
- }
428
- if (manifest.scope !== void 0 && typeof manifest.scope === "string") {
429
- if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(manifest.scope)) {
430
- errors.push('"scope" must be lowercase alphanumeric with hyphens.');
431
- }
432
- }
433
- if (manifest["allowed-tools"] !== void 0 && typeof manifest["allowed-tools"] !== "string") {
434
- errors.push('"allowed-tools" must be a string (space-delimited list of tools).');
435
- }
436
- if (manifest["disable-model-invocation"] !== void 0 && typeof manifest["disable-model-invocation"] !== "boolean") {
437
- errors.push('"disable-model-invocation" must be a boolean.');
438
- }
439
- if (manifest["user-invocable"] !== void 0 && typeof manifest["user-invocable"] !== "boolean") {
440
- errors.push('"user-invocable" must be a boolean.');
441
- }
442
- return errors;
443
- }
444
-
445
- // src/lib/discovery.ts
446
- import { resolve as resolve2, join as join4, dirname } from "path";
447
- import { existsSync as existsSync4, readdirSync } from "fs";
448
- function findClaudeSkillsDir() {
449
- let dir = resolve2(".");
450
- const parts = dir.split("/");
451
- const claudeIdx = parts.lastIndexOf(".claude");
452
- if (claudeIdx >= 0) {
453
- const claudeRoot = parts.slice(0, claudeIdx + 1).join("/");
454
- return join4(claudeRoot, "skills");
455
- }
456
- while (dir !== dirname(dir)) {
457
- const claudeDir = join4(dir, ".claude");
458
- if (existsSync4(claudeDir)) {
459
- return join4(claudeDir, "skills");
460
- }
461
- dir = dirname(dir);
462
- }
463
- return null;
464
- }
465
- function discoverSkillDirs(parentDir) {
466
- if (!existsSync4(parentDir)) return [];
467
- const dirs = [];
468
- const entries = readdirSync(parentDir, { withFileTypes: true });
469
- for (const entry of entries) {
470
- if (!entry.isDirectory()) continue;
471
- if (entry.name.startsWith(".")) continue;
472
- const skillMd = join4(parentDir, entry.name, "SKILL.md");
473
- if (existsSync4(skillMd)) {
474
- dirs.push(join4(parentDir, entry.name));
475
- }
476
- }
477
- return dirs.sort();
478
- }
479
-
480
- // src/commands/validate.ts
481
- var MAX_FILE_COUNT = 100;
482
- var MAX_FILE_SIZE = 500 * 1024;
483
- var MAX_TOTAL_SIZE = 2 * 1024 * 1024;
484
- function collectFiles(dir, prefix = "") {
485
- const files = [];
486
- const entries = readdirSync2(dir, { withFileTypes: true });
487
- for (const entry of entries) {
488
- if (entry.name === "node_modules" || entry.name === ".git") continue;
489
- const fullPath = join5(dir, entry.name);
490
- const lstat = lstatSync(fullPath);
491
- if (lstat.isSymbolicLink()) continue;
492
- const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
493
- if (entry.isDirectory()) {
494
- files.push(...collectFiles(fullPath, relativePath));
495
- } else {
496
- const stat = statSync(fullPath);
497
- files.push({ path: relativePath, size: stat.size });
498
- }
499
- }
500
- return files;
501
- }
502
- function runValidation(dir) {
503
- const errors = [];
504
- const warnings = [];
505
- if (!existsSync5(join5(dir, "SKILL.md"))) {
506
- errors.push("Missing SKILL.md \u2014 every skill must have a SKILL.md file with YAML frontmatter.");
507
- return { valid: false, errors, warnings };
508
- }
509
- const manifest = parseManifest(dir);
510
- const manifestErrors = validateManifest(manifest);
511
- errors.push(...manifestErrors);
512
- if (!manifest._body) {
513
- warnings.push("SKILL.md has no instructions body after the frontmatter. Add skill instructions below the --- delimiter.");
514
- }
515
- if (!manifest.version) {
516
- warnings.push('No "version" in frontmatter. Required for publishing to the registry (e.g., version: "1.0.0").');
517
- }
518
- if (!manifest.scope) {
519
- warnings.push('No "scope" in frontmatter. Required for publishing to the registry (e.g., scope: my-team).');
520
- }
521
- const dirName = dir.split("/").pop();
522
- if (manifest.name && dirName && dirName !== manifest.name) {
523
- warnings.push(`Directory name "${dirName}" doesn't match skill name "${manifest.name}". They should match per the Agent Skills spec.`);
524
- }
525
- const files = collectFiles(dir);
526
- if (files.length > MAX_FILE_COUNT) {
527
- errors.push(`Too many files: ${files.length} (max ${MAX_FILE_COUNT}).`);
528
- }
529
- let totalSize = 0;
530
- for (const file of files) {
531
- totalSize += file.size;
532
- if (file.size > MAX_FILE_SIZE) {
533
- errors.push(`File too large: ${file.path} (${(file.size / 1024).toFixed(1)}KB, max 500KB).`);
534
- }
535
- }
536
- if (totalSize > MAX_TOTAL_SIZE) {
537
- errors.push(`Total size too large: ${(totalSize / 1024 / 1024).toFixed(2)}MB (max 2MB).`);
538
- }
539
- return { valid: errors.length === 0, errors, warnings };
540
- }
541
- var validateCommand = new Command5("validate").description("Validate a skill package").argument("[dir]", "Skill directory", ".").option("--all", "Validate all skills in .claude/skills/").action(async (dir, opts) => {
542
- try {
543
- if (opts.all) {
544
- const skillsDir = findClaudeSkillsDir();
545
- if (!skillsDir) {
546
- throw new CliError("Cannot find .claude/skills/ directory.", [
547
- "Run this from a project with a .claude/ directory."
548
- ]);
549
- }
550
- const skillDirs = discoverSkillDirs(skillsDir);
551
- if (skillDirs.length === 0) {
552
- throw new CliError(`No skills found in ${skillsDir}/`, [
553
- "Skills must be directories containing a SKILL.md file."
554
- ]);
555
- }
556
- console.log(`Validating ${skillDirs.length} skill(s) in ${skillsDir}/
557
- `);
558
- let passed = 0;
559
- let failed = 0;
560
- for (const skillDir of skillDirs) {
561
- const name = basename(skillDir);
562
- const result2 = runValidation(skillDir);
563
- if (result2.valid) {
564
- success(`${name}`);
565
- passed++;
566
- } else {
567
- error(`${name}`);
568
- for (const e of result2.errors) {
569
- console.error(` ${e}`);
570
- }
571
- failed++;
572
- }
573
- for (const w of result2.warnings) {
574
- warn(` ${name}: ${w}`);
575
- }
576
- }
577
- console.log("");
578
- if (failed === 0) {
579
- success(`All ${passed} skill(s) are valid.`);
580
- } else {
581
- throw new CliError(`${failed}/${passed + failed} skill(s) failed validation.`);
582
- }
583
- return;
584
- }
585
- const resolved = resolve3(dir);
586
- if (!existsSync5(resolved)) {
587
- throw new CliError(`Directory not found: ${resolved}`);
588
- }
589
- const result = runValidation(resolved);
590
- for (const w of result.warnings) {
591
- warn(w);
592
- }
593
- if (result.valid) {
594
- success("Skill is valid.");
595
- } else {
596
- for (const e of result.errors) {
597
- error(e);
598
- }
599
- throw new CliError("Validation failed.", [
600
- "Fix the errors above and try again."
601
- ]);
602
- }
603
- } catch (err) {
604
- handleError(err);
605
- }
606
- });
607
-
608
186
  // src/commands/pack.ts
609
- import { Command as Command6 } from "commander";
610
- import { resolve as resolve4 } from "path";
611
- import { existsSync as existsSync6 } from "fs";
612
-
613
- // src/lib/archive.ts
614
- import { statSync as statSync2 } from "fs";
615
- import { readFile } from "fs/promises";
616
- import { createHash } from "crypto";
617
- import { join as join6, basename as basename2 } from "path";
618
- import { create as tarCreate, extract as tarExtract } from "tar";
619
- var MAX_ARCHIVE_SIZE = 10 * 1024 * 1024;
620
- async function computeSha256(filePath) {
621
- const data = await readFile(filePath);
622
- return createHash("sha256").update(data).digest("hex");
623
- }
624
- async function pack(dir, outputPath) {
625
- const dirName = basename2(dir);
626
- const archivePath = outputPath ?? join6(dir, "..", `${dirName}.tar.gz`);
627
- await tarCreate(
628
- {
629
- gzip: true,
630
- file: archivePath,
631
- cwd: join6(dir, "..")
632
- },
633
- [dirName]
634
- );
635
- const sha256 = await computeSha256(archivePath);
636
- const stat = statSync2(archivePath);
637
- if (stat.size > MAX_ARCHIVE_SIZE) {
638
- throw new CliError(
639
- `Archive size ${(stat.size / 1024 / 1024).toFixed(1)}MB exceeds the 10MB limit.`,
640
- ["Remove unnecessary files or assets to reduce the package size."]
641
- );
642
- }
643
- return {
644
- path: archivePath,
645
- sha256,
646
- size: stat.size
647
- };
648
- }
649
- async function extract(archivePath, outputDir, expectedSha256) {
650
- const actualSha256 = await computeSha256(archivePath);
651
- if (actualSha256 !== expectedSha256) {
652
- throw new CliError(
653
- `SHA-256 mismatch: expected ${expectedSha256}, got ${actualSha256}`,
654
- ["The archive may be corrupted or tampered with.", "Try downloading again."]
655
- );
656
- }
657
- await tarExtract({
658
- file: archivePath,
659
- cwd: outputDir,
660
- filter: (path) => !path.includes("..")
661
- });
662
- }
663
-
664
- // src/commands/pack.ts
665
- var packCommand = new Command6("pack").description("Pack a skill into a tarball").argument("[dir]", "Skill directory", ".").action(async (dir) => {
187
+ import { Command as Command5 } from "commander";
188
+ import { resolve as resolve2 } from "path";
189
+ import { existsSync as existsSync2 } from "fs";
190
+ var packCommand = new Command5("pack").description("Pack a skill into a tarball").argument("[dir]", "Skill directory", ".").action(async (dir) => {
666
191
  try {
667
- const resolved = resolve4(dir);
668
- if (!existsSync6(resolved)) {
192
+ const resolved = resolve2(dir);
193
+ if (!existsSync2(resolved)) {
669
194
  throw new CliError(`Directory not found: ${resolved}`);
670
195
  }
671
196
  const validation = runValidation(resolved);
@@ -688,194 +213,71 @@ var packCommand = new Command6("pack").description("Pack a skill into a tarball"
688
213
  }
689
214
  });
690
215
 
691
- // src/commands/push.ts
692
- import { Command as Command7 } from "commander";
693
- import { resolve as resolve5, join as join7, basename as basename3 } from "path";
694
- import { existsSync as existsSync7, readFileSync as readFileSync3, statSync as statSync3, readdirSync as readdirSync3, lstatSync as lstatSync2 } from "fs";
695
- import { createHash as createHash2 } from "crypto";
696
- function collectFileTree(dir, prefix = "") {
697
- const files = [];
698
- const entries = readdirSync3(dir, { withFileTypes: true });
699
- for (const entry of entries) {
700
- if (entry.name === "node_modules" || entry.name === ".git") continue;
701
- const fullPath = join7(dir, entry.name);
702
- const lstat = lstatSync2(fullPath);
703
- if (lstat.isSymbolicLink()) continue;
704
- const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
705
- if (entry.isDirectory()) {
706
- files.push(...collectFileTree(fullPath, relativePath));
707
- } else {
708
- const stat = statSync3(fullPath);
709
- const content = readFileSync3(fullPath);
710
- const sha256 = createHash2("sha256").update(content).digest("hex");
711
- files.push({ path: relativePath, size: stat.size, sha256 });
712
- }
713
- }
714
- return files;
715
- }
716
- async function pushSingle(resolved) {
717
- const spin = spinner("Validating skill...");
718
- const validation = runValidation(resolved);
719
- if (!validation.valid) {
720
- spin.fail("Validation failed.");
721
- for (const e of validation.errors) {
722
- console.error(" " + e);
723
- }
724
- throw new CliError("Fix validation errors before pushing.");
725
- }
726
- spin.succeed("Validation passed.");
727
- const manifest = parseManifest(resolved);
728
- if (!manifest.version) {
729
- throw new CliError('Missing "version" in SKILL.md frontmatter.', [
730
- 'Add a version field: version: "1.0.0"'
731
- ]);
732
- }
733
- if (!manifest.scope) {
734
- throw new CliError('Missing "scope" in SKILL.md frontmatter.', [
735
- "Add a scope field: scope: my-team"
736
- ]);
737
- }
738
- spin.start("Packing skill...");
739
- const archive = await pack(resolved);
740
- spin.succeed(`Packed (${(archive.size / 1024).toFixed(1)}KB).`);
741
- const client = new ApiClient();
742
- spin.start("Checking registry...");
743
- try {
744
- await client.get(`/api/v1/skills/${manifest.scope}/${manifest.name}`);
745
- spin.succeed("Skill found in registry.");
746
- } catch {
747
- spin.start(`Creating skill ${manifest.scope}/${manifest.name}...`);
748
- await client.post("/api/v1/skills", {
749
- body: {
750
- scope: manifest.scope,
751
- slug: manifest.name,
752
- displayName: manifest.name,
753
- description: manifest.description || "",
754
- tags: manifest.tags || []
755
- }
756
- });
757
- spin.succeed(`Created skill ${manifest.scope}/${manifest.name}.`);
758
- }
759
- spin.start("Preparing version...");
760
- const fileTree = collectFileTree(resolved);
761
- const prepared = await client.post(
762
- `/api/v1/skills/${manifest.scope}/${manifest.name}/versions`,
763
- {
764
- body: {
765
- version: manifest.version,
766
- fileTree,
767
- archiveSha256: archive.sha256,
768
- archiveSize: archive.size,
769
- entryPoint: "SKILL.md",
770
- manifestJson: {
771
- name: manifest.name,
772
- description: manifest.description,
773
- version: manifest.version,
774
- scope: manifest.scope,
775
- "allowed-tools": manifest["allowed-tools"],
776
- "argument-hint": manifest["argument-hint"],
777
- "disable-model-invocation": manifest["disable-model-invocation"],
778
- "user-invocable": manifest["user-invocable"],
779
- tags: manifest.tags
780
- }
781
- }
782
- }
783
- );
784
- spin.succeed("Version prepared.");
785
- spin.start("Uploading archive...");
786
- const archiveData = readFileSync3(archive.path);
787
- const uploadResponse = await fetch(prepared.uploadUrl, {
788
- method: "PUT",
789
- headers: {
790
- "Content-Type": "application/gzip",
791
- "Content-Length": String(archive.size)
792
- },
793
- body: archiveData
794
- });
795
- if (!uploadResponse.ok) {
796
- spin.fail("Upload failed.");
797
- throw new CliError(`Upload failed with status ${uploadResponse.status}.`, [
798
- "Try again. If the problem persists, contact support."
799
- ]);
800
- }
801
- spin.succeed("Archive uploaded.");
802
- spin.start("Finalizing version...");
803
- await client.post(
804
- `/api/v1/skills/${manifest.scope}/${manifest.name}/versions/${manifest.version}/finalize`,
805
- {
806
- body: { status: "published" }
807
- }
808
- );
809
- spin.succeed("Version finalized.");
810
- return `${manifest.scope}/${manifest.name}@${manifest.version}`;
216
+ // src/commands/release.ts
217
+ import { Command as Command6 } from "commander";
218
+ import { select, input as input3 } from "@inquirer/prompts";
219
+ import { readFileSync, writeFileSync as writeFileSync2 } from "fs";
220
+ import { join as join2, resolve as resolve3 } from "path";
221
+ function bumpPatch(version) {
222
+ const parts = version.replace(/^"(.*)"$/, "$1").split(".");
223
+ if (parts.length < 3) return version;
224
+ parts[2] = String(parseInt(parts[2], 10) + 1);
225
+ return parts.join(".");
811
226
  }
812
- var pushCommand = new Command7("push").description("Publish a skill to the registry").argument("[dir]", "Skill directory", ".").option("--all", "Push all skills in .claude/skills/").action(async (dir, opts) => {
227
+ var releaseCommand = new Command6("release").description("Bump the version in SKILL.md and push to the registry").argument("[dir]", "Skill directory", ".").action(async (dir) => {
813
228
  try {
814
- if (!getAuthToken()) {
815
- throw new CliError("Not logged in.", ["Run `csreg login` to authenticate."]);
229
+ const resolved = resolve3(dir);
230
+ const manifest = parseManifest(resolved);
231
+ if (!manifest.version) {
232
+ throw new CliError('Missing "version" in SKILL.md frontmatter.', [
233
+ 'Add a version field: version: "1.0.0"'
234
+ ]);
816
235
  }
817
- if (opts.all) {
818
- const skillsDir = findClaudeSkillsDir();
819
- if (!skillsDir) {
820
- throw new CliError("Cannot find .claude/skills/ directory.", [
821
- "Run this from a project with a .claude/ directory."
822
- ]);
823
- }
824
- const skillDirs = discoverSkillDirs(skillsDir);
825
- if (skillDirs.length === 0) {
826
- throw new CliError(`No skills found in ${skillsDir}/`, [
827
- "Skills must be directories containing a SKILL.md file."
828
- ]);
829
- }
830
- console.log(`Pushing ${skillDirs.length} skill(s) from ${skillsDir}/
831
- `);
832
- let published = 0;
833
- let failed = 0;
834
- for (const skillDir of skillDirs) {
835
- const name = basename3(skillDir);
836
- console.log(`
837
- --- ${name} ---
838
- `);
839
- try {
840
- const ref2 = await pushSingle(skillDir);
841
- success(`Published ${ref2}`);
842
- published++;
843
- } catch (err) {
844
- const msg = err instanceof Error ? err.message : String(err);
845
- warn(`Failed to push ${name}: ${msg}`);
846
- failed++;
847
- }
848
- }
849
- console.log(`
850
- ${"\u2500".repeat(40)}
851
- `);
852
- if (failed === 0) {
853
- success(`All ${published} skill(s) published.`);
854
- } else {
855
- success(`Published ${published}/${published + failed} skill(s).`);
856
- if (failed > 0) {
857
- warn(`${failed} skill(s) failed. Check the errors above.`);
236
+ const currentVersion = manifest.version;
237
+ const nextPatch = bumpPatch(currentVersion);
238
+ info(`Current version: ${currentVersion}`);
239
+ const chosen = await select({
240
+ message: "Version to release:",
241
+ choices: [
242
+ { name: `${nextPatch} (patch bump)`, value: nextPatch },
243
+ { name: `${currentVersion} (current)`, value: currentVersion },
244
+ { name: "Enter manually", value: "__manual__" }
245
+ ]
246
+ });
247
+ let newVersion = chosen;
248
+ if (chosen === "__manual__") {
249
+ newVersion = await input3({
250
+ message: "Version:",
251
+ validate: (value) => {
252
+ const semver = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$/;
253
+ if (!semver.test(value.trim())) return "Must be valid semver (e.g., 1.2.3).";
254
+ return true;
858
255
  }
859
- }
860
- return;
861
- }
862
- const resolved = resolve5(dir);
863
- if (!existsSync7(resolved)) {
864
- throw new CliError(`Directory not found: ${resolved}`);
865
- }
866
- const ref = await pushSingle(resolved);
867
- console.log("");
868
- success(`Published ${ref}`);
256
+ });
257
+ newVersion = newVersion.trim();
258
+ }
259
+ if (newVersion !== currentVersion) {
260
+ const skillPath = join2(resolved, "SKILL.md");
261
+ const raw = readFileSync(skillPath, "utf-8");
262
+ const updated = raw.replace(
263
+ /^(version:\s*)"?[^"\n]+"?\s*$/m,
264
+ `$1"${newVersion}"`
265
+ );
266
+ writeFileSync2(skillPath, updated, "utf-8");
267
+ info(`Updated version to ${newVersion}`);
268
+ }
269
+ const { pushCommand: pushCommand2 } = await import("./push-E2IC4IC3.js");
270
+ await pushCommand2.parseAsync([dir], { from: "user" });
869
271
  } catch (err) {
870
272
  handleError(err);
871
273
  }
872
274
  });
873
275
 
874
276
  // src/commands/pull.ts
875
- import { Command as Command8 } from "commander";
876
- import { resolve as resolve6, join as join8, dirname as dirname2 } from "path";
877
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, existsSync as existsSync8, readFileSync as readFileSync4, renameSync } from "fs";
878
- import { createHash as createHash3 } from "crypto";
277
+ import { Command as Command7 } from "commander";
278
+ import { resolve as resolve4, join as join3, dirname } from "path";
279
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, existsSync as existsSync3, readFileSync as readFileSync2, renameSync } from "fs";
280
+ import { createHash } from "crypto";
879
281
  import { confirm as confirm2 } from "@inquirer/prompts";
880
282
  function parseSkillRef(ref) {
881
283
  const cleaned = ref.startsWith("@") ? ref.slice(1) : ref;
@@ -901,12 +303,12 @@ function parseSkillRef(ref) {
901
303
  };
902
304
  }
903
305
  function readSkillsConfig() {
904
- let dir = resolve6(".");
905
- while (dir !== dirname2(dir)) {
906
- const configPath = join8(dir, ".claude", "skills.json");
907
- if (existsSync8(configPath)) {
306
+ let dir = resolve4(".");
307
+ while (dir !== dirname(dir)) {
308
+ const configPath = join3(dir, ".claude", "skills.json");
309
+ if (existsSync3(configPath)) {
908
310
  try {
909
- const raw = readFileSync4(configPath, "utf-8");
311
+ const raw = readFileSync2(configPath, "utf-8");
910
312
  return JSON.parse(raw);
911
313
  } catch {
912
314
  throw new CliError("Failed to parse .claude/skills.json", [
@@ -914,7 +316,7 @@ function readSkillsConfig() {
914
316
  ]);
915
317
  }
916
318
  }
917
- dir = dirname2(dir);
319
+ dir = dirname(dir);
918
320
  }
919
321
  return null;
920
322
  }
@@ -944,7 +346,7 @@ async function pullSkill(scope, name, version, targetDir) {
944
346
  `Archive size ${(archiveData.length / 1024 / 1024).toFixed(1)}MB exceeds the 10MB limit.`
945
347
  );
946
348
  }
947
- const actualSha = createHash3("sha256").update(archiveData).digest("hex");
349
+ const actualSha = createHash("sha256").update(archiveData).digest("hex");
948
350
  if (actualSha !== downloadInfo.archiveSha256) {
949
351
  spin.fail("Integrity check failed.");
950
352
  throw new CliError(
@@ -953,17 +355,17 @@ async function pullSkill(scope, name, version, targetDir) {
953
355
  );
954
356
  }
955
357
  spin.succeed("Downloaded and verified.");
956
- mkdirSync3(targetDir, { recursive: true });
358
+ mkdirSync2(targetDir, { recursive: true });
957
359
  spin.start("Installing...");
958
- const tempDir = join8(dirname2(targetDir), `.csreg-tmp-${Date.now()}`);
959
- mkdirSync3(tempDir, { recursive: true });
960
- const tempArchive = join8(tempDir, `${name}.tar.gz`);
360
+ const tempDir = join3(dirname(targetDir), `.csreg-tmp-${Date.now()}`);
361
+ mkdirSync2(tempDir, { recursive: true });
362
+ const tempArchive = join3(tempDir, `${name}.tar.gz`);
961
363
  writeFileSync3(tempArchive, archiveData);
962
364
  try {
963
365
  await extract(tempArchive, tempDir, downloadInfo.archiveSha256);
964
- const extractedDir = join8(tempDir, name);
965
- const finalDir = join8(targetDir, name);
966
- if (existsSync8(finalDir)) {
366
+ const extractedDir = join3(tempDir, name);
367
+ const finalDir = join3(targetDir, name);
368
+ if (existsSync3(finalDir)) {
967
369
  const { rmSync } = await import("fs");
968
370
  rmSync(finalDir, { recursive: true });
969
371
  }
@@ -978,7 +380,7 @@ async function pullSkill(scope, name, version, targetDir) {
978
380
  }
979
381
  }
980
382
  }
981
- var pullCommand = new Command8("pull").description("Download and install a skill").argument("[ref]", "Skill reference (@scope/name[@version])").option("--all", "Pull all skills listed in .claude/skills.json").option("--path <dir>", "Custom install directory (overrides auto-detection)").action(async (ref, opts) => {
383
+ var pullCommand = new Command7("pull").description("Download and install a skill").argument("[ref]", "Skill reference (@scope/name[@version])").option("--all", "Pull all skills listed in .claude/skills.json").option("--path <dir>", "Custom install directory (overrides auto-detection)").action(async (ref, opts) => {
982
384
  try {
983
385
  if (opts.all) {
984
386
  const config = readSkillsConfig();
@@ -992,7 +394,7 @@ var pullCommand = new Command8("pull").description("Download and install a skill
992
394
  info("No skills listed in .claude/skills.json. Nothing to pull.");
993
395
  return;
994
396
  }
995
- const skillsDir = opts.path ? resolve6(opts.path) : findClaudeSkillsDir();
397
+ const skillsDir = opts.path ? resolve4(opts.path) : findClaudeSkillsDir();
996
398
  if (!skillsDir) {
997
399
  throw new CliError("Cannot find .claude/ directory.", [
998
400
  "Run this from a project with a .claude/ directory, or use --path."
@@ -1026,54 +428,54 @@ var pullCommand = new Command8("pull").description("Download and install a skill
1026
428
  const { scope, name, version } = parseSkillRef(ref);
1027
429
  let targetDir;
1028
430
  if (opts.path) {
1029
- targetDir = resolve6(opts.path);
431
+ targetDir = resolve4(opts.path);
1030
432
  } else {
1031
433
  const detected = findClaudeSkillsDir();
1032
434
  if (detected) {
1033
435
  targetDir = detected;
1034
- const finalPath = join8(detected, name);
436
+ const finalPath = join3(detected, name);
1035
437
  const ok = await confirm2({
1036
438
  message: `Install to ${finalPath}?`,
1037
439
  default: true
1038
440
  });
1039
441
  if (!ok) {
1040
- const { input: input3 } = await import("@inquirer/prompts");
1041
- const custom = await input3({
442
+ const { input: input4 } = await import("@inquirer/prompts");
443
+ const custom = await input4({
1042
444
  message: "Custom install path:",
1043
- default: join8(".", ".claude", "skills")
445
+ default: join3(".", ".claude", "skills")
1044
446
  });
1045
- targetDir = resolve6(custom);
447
+ targetDir = resolve4(custom);
1046
448
  }
1047
449
  } else {
1048
- targetDir = join8(resolve6("."), ".claude", "skills");
450
+ targetDir = join3(resolve4("."), ".claude", "skills");
1049
451
  info(`No .claude/ directory found. Will create ${targetDir}/`);
1050
452
  const ok = await confirm2({
1051
- message: `Install to ${join8(targetDir, name)}?`,
453
+ message: `Install to ${join3(targetDir, name)}?`,
1052
454
  default: true
1053
455
  });
1054
456
  if (!ok) {
1055
- const { input: input3 } = await import("@inquirer/prompts");
1056
- const custom = await input3({
457
+ const { input: input4 } = await import("@inquirer/prompts");
458
+ const custom = await input4({
1057
459
  message: "Custom install path:",
1058
460
  default: targetDir
1059
461
  });
1060
- targetDir = resolve6(custom);
462
+ targetDir = resolve4(custom);
1061
463
  }
1062
464
  }
1063
465
  }
1064
466
  const result = await pullSkill(scope, name, version, targetDir);
1065
467
  console.log("");
1066
468
  success(`Pulled ${scope}/${name}@${result.version}`);
1067
- info(`Skill is ready at ${join8(targetDir, name)}/SKILL.md`);
469
+ info(`Skill is ready at ${join3(targetDir, name)}/SKILL.md`);
1068
470
  } catch (err) {
1069
471
  handleError(err);
1070
472
  }
1071
473
  });
1072
474
 
1073
475
  // src/commands/info.ts
1074
- import { Command as Command9 } from "commander";
1075
- import chalk3 from "chalk";
1076
- var infoCommand = new Command9("info").description("Display details about a skill").argument("<ref>", "Skill reference (scope/name)").action(async (ref) => {
476
+ import { Command as Command8 } from "commander";
477
+ import chalk from "chalk";
478
+ var infoCommand = new Command8("info").description("Display details about a skill").argument("<ref>", "Skill reference (scope/name)").action(async (ref) => {
1077
479
  try {
1078
480
  const cleaned = ref.startsWith("@") ? ref.slice(1) : ref;
1079
481
  const slashIndex = cleaned.indexOf("/");
@@ -1087,7 +489,7 @@ var infoCommand = new Command9("info").description("Display details about a skil
1087
489
  const client = new ApiClient();
1088
490
  const skill = await client.get(`/api/v1/skills/${scope}/${name}`);
1089
491
  console.log("");
1090
- console.log(chalk3.bold(`${skill.scope}/${skill.name}`) + chalk3.dim(` @ ${skill.latestVersion}`));
492
+ console.log(chalk.bold(`${skill.scope}/${skill.name}`) + chalk.dim(` @ ${skill.latestVersion}`));
1091
493
  console.log("");
1092
494
  if (skill.description) {
1093
495
  console.log(skill.description);
@@ -1108,7 +510,7 @@ var infoCommand = new Command9("info").description("Display details about a skil
1108
510
  }
1109
511
  const maxLabel = Math.max(...fields.map(([label]) => label.length));
1110
512
  for (const [label, value] of fields) {
1111
- console.log(` ${chalk3.cyan(label.padEnd(maxLabel + 2))}${value}`);
513
+ console.log(` ${chalk.cyan(label.padEnd(maxLabel + 2))}${value}`);
1112
514
  }
1113
515
  console.log("");
1114
516
  } catch (err) {
@@ -1117,8 +519,8 @@ var infoCommand = new Command9("info").description("Display details about a skil
1117
519
  });
1118
520
 
1119
521
  // src/commands/versions.ts
1120
- import { Command as Command10 } from "commander";
1121
- var versionsCommand = new Command10("versions").description("List all versions of a skill").argument("<ref>", "Skill reference (scope/name)").action(async (ref) => {
522
+ import { Command as Command9 } from "commander";
523
+ var versionsCommand = new Command9("versions").description("List all versions of a skill").argument("<ref>", "Skill reference (scope/name)").action(async (ref) => {
1122
524
  try {
1123
525
  const cleaned = ref.startsWith("@") ? ref.slice(1) : ref;
1124
526
  const slashIndex = cleaned.indexOf("/");
@@ -1152,8 +554,8 @@ var versionsCommand = new Command10("versions").description("List all versions o
1152
554
  });
1153
555
 
1154
556
  // src/commands/search.ts
1155
- import { Command as Command11 } from "commander";
1156
- var searchCommand = new Command11("search").description("Search for skills in the registry").argument("<query>", "Search query").option("-t, --type <type>", "Filter by skill type").option("-l, --limit <limit>", "Max results", "20").action(async (query, opts) => {
557
+ import { Command as Command10 } from "commander";
558
+ var searchCommand = new Command10("search").description("Search for skills in the registry").argument("<query>", "Search query").option("-t, --type <type>", "Filter by skill type").option("-l, --limit <limit>", "Max results", "20").action(async (query, opts) => {
1157
559
  try {
1158
560
  const client = new ApiClient();
1159
561
  const queryParams = {
@@ -1186,7 +588,7 @@ var searchCommand = new Command11("search").description("Search for skills in th
1186
588
  });
1187
589
 
1188
590
  // src/index.ts
1189
- var program = new Command12();
591
+ var program = new Command11();
1190
592
  program.name("csreg").description("Claude Skills Registry CLI").version("0.1.0");
1191
593
  program.addCommand(loginCommand);
1192
594
  program.addCommand(logoutCommand);
@@ -1195,6 +597,7 @@ program.addCommand(initCommand);
1195
597
  program.addCommand(validateCommand);
1196
598
  program.addCommand(packCommand);
1197
599
  program.addCommand(pushCommand);
600
+ program.addCommand(releaseCommand);
1198
601
  program.addCommand(pullCommand);
1199
602
  program.addCommand(infoCommand);
1200
603
  program.addCommand(versionsCommand);