@agent-nexus/csreg 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1179 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command12 } from "commander";
5
+
6
+ // src/commands/login.ts
7
+ import { Command } from "commander";
8
+ 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 });
31
+ writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + "\n", "utf-8");
32
+ }
33
+ function getApiUrl() {
34
+ return process.env.CSREG_API_URL ?? getConfig().apiUrl ?? "http://localhost:3000";
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
+ var loginCommand = new Command("login").description("Authenticate with the Skills Registry").option("--token <token>", "Provide auth token directly").action(async (opts) => {
205
+ try {
206
+ let token = opts.token;
207
+ if (!token) {
208
+ const apiUrl = getApiUrl();
209
+ info(`Open this URL to authenticate:`);
210
+ console.log(` ${apiUrl}/cli/auth`);
211
+ console.log("");
212
+ token = await input({
213
+ message: "Paste your auth token:",
214
+ validate: (value) => {
215
+ if (!value.trim()) return "Token is required.";
216
+ return true;
217
+ }
218
+ });
219
+ token = token.trim();
220
+ }
221
+ setConfig({ token });
222
+ const client = new ApiClient();
223
+ const user = await client.get(
224
+ "/api/v1/users/me"
225
+ );
226
+ success(`Logged in as ${user.displayName} (${user.email})`);
227
+ } catch (err) {
228
+ handleError(err);
229
+ }
230
+ });
231
+
232
+ // src/commands/logout.ts
233
+ import { Command as Command2 } from "commander";
234
+ var logoutCommand = new Command2("logout").description("Remove stored authentication credentials").action(async () => {
235
+ try {
236
+ setConfig({ token: void 0 });
237
+ success("Logged out successfully.");
238
+ } catch (err) {
239
+ handleError(err);
240
+ }
241
+ });
242
+
243
+ // src/commands/whoami.ts
244
+ import { Command as Command3 } from "commander";
245
+ var whoamiCommand = new Command3("whoami").description("Display the currently authenticated user").action(async () => {
246
+ try {
247
+ if (!getAuthToken()) {
248
+ throw new CliError("Not logged in.", ["Run `csreg login` to authenticate."]);
249
+ }
250
+ const client = new ApiClient();
251
+ const user = await client.get(
252
+ "/api/v1/users/me"
253
+ );
254
+ success(`Logged in as ${user.displayName} (${user.email})`);
255
+ } catch (err) {
256
+ handleError(err);
257
+ }
258
+ });
259
+
260
+ // src/commands/init.ts
261
+ import { Command as Command4 } from "commander";
262
+ 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";
265
+ var initCommand = new Command4("init").description("Initialize a new skill project").argument("[dir]", "Directory to initialize the skill in").action(async (dir) => {
266
+ try {
267
+ const name = await input2({
268
+ message: "Skill name:",
269
+ validate: (value) => {
270
+ if (!value.trim()) return "Name is required.";
271
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(value.trim())) {
272
+ return "Must be lowercase alphanumeric with hyphens (e.g., my-skill).";
273
+ }
274
+ if (value.trim().length > 64) {
275
+ return "Name must be at most 64 characters.";
276
+ }
277
+ if (value.includes("--")) {
278
+ return "Name must not contain consecutive hyphens (--).";
279
+ }
280
+ return true;
281
+ }
282
+ });
283
+ const description = await input2({
284
+ message: "Description (what this skill does and when to use it):",
285
+ validate: (value) => {
286
+ if (!value.trim()) return "Description is required \u2014 Claude uses it to decide when to invoke the skill.";
287
+ return true;
288
+ }
289
+ });
290
+ const scope = await input2({
291
+ message: "Scope (your team/username, for registry publishing):",
292
+ validate: (value) => {
293
+ if (!value.trim()) return "Scope is required for publishing.";
294
+ return true;
295
+ }
296
+ });
297
+ const userInvocable = await confirm({
298
+ message: "User-invocable (show in /slash command menu)?",
299
+ default: true
300
+ });
301
+ const targetDir = resolve(dir ?? name.trim());
302
+ if (existsSync2(targetDir)) {
303
+ throw new CliError(`Directory already exists: ${targetDir}`, [
304
+ "Choose a different name or directory."
305
+ ]);
306
+ }
307
+ mkdirSync2(targetDir, { recursive: true });
308
+ const frontmatterLines = [
309
+ "---",
310
+ `name: ${name.trim()}`,
311
+ `description: ${description.trim()}`,
312
+ `version: "0.1.0"`,
313
+ `scope: ${scope.trim()}`
314
+ ];
315
+ if (!userInvocable) {
316
+ frontmatterLines.push("user-invocable: false");
317
+ }
318
+ frontmatterLines.push("---");
319
+ const skillContent = `${frontmatterLines.join("\n")}
320
+
321
+ # ${name.trim()}
322
+
323
+ ${description.trim()}
324
+
325
+ ## Instructions
326
+
327
+ Add your skill instructions here. This is the prompt that Claude will follow
328
+ when this skill is invoked.
329
+
330
+ You can use \`$ARGUMENTS\` to reference arguments passed by the user.
331
+ For example: \`/my-skill some-argument\` makes \`$ARGUMENTS\` = "some-argument".
332
+ `;
333
+ writeFileSync2(
334
+ join2(targetDir, "SKILL.md"),
335
+ skillContent,
336
+ "utf-8"
337
+ );
338
+ console.log("");
339
+ success(`Created skill "${scope.trim()}/${name.trim()}" in ${targetDir}`);
340
+ info("Files created:");
341
+ console.log(" - SKILL.md");
342
+ console.log("");
343
+ info("Next steps:");
344
+ console.log(" 1. Edit SKILL.md with your skill instructions");
345
+ console.log(" 2. Run `csreg validate` to check your skill");
346
+ console.log(" 3. Run `csreg push` to publish to the registry");
347
+ console.log("");
348
+ info("To use locally without publishing:");
349
+ console.log(` cp -r ${targetDir} .claude/skills/${name.trim()}`);
350
+ } catch (err) {
351
+ handleError(err);
352
+ }
353
+ });
354
+
355
+ // src/commands/validate.ts
356
+ import { Command as Command5 } from "commander";
357
+ import { resolve as resolve3, join as join5, basename } from "path";
358
+ import { existsSync as existsSync5, statSync, readdirSync as readdirSync2 } from "fs";
359
+
360
+ // src/lib/manifest.ts
361
+ import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
362
+ import { join as join3 } from "path";
363
+ import { parse as parseYaml } from "yaml";
364
+ var SKILL_FILE = "SKILL.md";
365
+ function parseManifest(dir) {
366
+ const skillPath = join3(dir, SKILL_FILE);
367
+ if (!existsSync3(skillPath)) {
368
+ throw new CliError(`No ${SKILL_FILE} found in ${dir}`, [
369
+ "Run `csreg init` to create a new skill.",
370
+ "A valid skill requires a SKILL.md file with YAML frontmatter."
371
+ ]);
372
+ }
373
+ const raw = readFileSync2(skillPath, "utf-8");
374
+ const frontmatterMatch = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
375
+ if (!frontmatterMatch) {
376
+ throw new CliError(`${SKILL_FILE} is missing YAML frontmatter.`, [
377
+ "SKILL.md must start with --- delimited YAML frontmatter.",
378
+ "Example:\n ---\n name: my-skill\n description: Does something\n ---\n \n Your instructions here."
379
+ ]);
380
+ }
381
+ const [, frontmatterRaw, body] = frontmatterMatch;
382
+ let parsed;
383
+ try {
384
+ parsed = parseYaml(frontmatterRaw);
385
+ } catch (err) {
386
+ throw new CliError(`Failed to parse ${SKILL_FILE} frontmatter: ${String(err)}`, [
387
+ "Check that the YAML between --- delimiters is valid."
388
+ ]);
389
+ }
390
+ if (!parsed || typeof parsed !== "object") {
391
+ throw new CliError(`${SKILL_FILE} frontmatter is empty or not an object.`, [
392
+ 'Add at least a "name" and "description" field.'
393
+ ]);
394
+ }
395
+ return { ...parsed, _body: body.trim() };
396
+ }
397
+ function validateManifest(manifest) {
398
+ const errors = [];
399
+ if (!manifest.name || typeof manifest.name !== "string") {
400
+ errors.push('Missing or invalid "name" field in frontmatter.');
401
+ } else {
402
+ if (manifest.name.length > 64) {
403
+ errors.push('"name" must be at most 64 characters.');
404
+ }
405
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(manifest.name)) {
406
+ errors.push('"name" must be lowercase alphanumeric with hyphens, not starting or ending with a hyphen.');
407
+ }
408
+ if (manifest.name.includes("--")) {
409
+ errors.push('"name" must not contain consecutive hyphens (--).');
410
+ }
411
+ }
412
+ if (!manifest.description || typeof manifest.description !== "string") {
413
+ errors.push('Missing "description" field. Claude uses this to decide when to invoke the skill.');
414
+ } else if (manifest.description.length > 1024) {
415
+ errors.push('"description" must be at most 1024 characters.');
416
+ }
417
+ if (manifest.version !== void 0) {
418
+ const semverRegex = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$/;
419
+ if (!semverRegex.test(manifest.version)) {
420
+ errors.push('"version" must be valid semver (e.g., 1.0.0).');
421
+ }
422
+ }
423
+ if (manifest.scope !== void 0 && typeof manifest.scope === "string") {
424
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(manifest.scope)) {
425
+ errors.push('"scope" must be lowercase alphanumeric with hyphens.');
426
+ }
427
+ }
428
+ if (manifest["allowed-tools"] !== void 0 && typeof manifest["allowed-tools"] !== "string") {
429
+ errors.push('"allowed-tools" must be a string (space-delimited list of tools).');
430
+ }
431
+ if (manifest["disable-model-invocation"] !== void 0 && typeof manifest["disable-model-invocation"] !== "boolean") {
432
+ errors.push('"disable-model-invocation" must be a boolean.');
433
+ }
434
+ if (manifest["user-invocable"] !== void 0 && typeof manifest["user-invocable"] !== "boolean") {
435
+ errors.push('"user-invocable" must be a boolean.');
436
+ }
437
+ return errors;
438
+ }
439
+
440
+ // src/lib/discovery.ts
441
+ import { resolve as resolve2, join as join4, dirname } from "path";
442
+ import { existsSync as existsSync4, readdirSync } from "fs";
443
+ function findClaudeSkillsDir() {
444
+ let dir = resolve2(".");
445
+ const parts = dir.split("/");
446
+ const claudeIdx = parts.lastIndexOf(".claude");
447
+ if (claudeIdx >= 0) {
448
+ const claudeRoot = parts.slice(0, claudeIdx + 1).join("/");
449
+ return join4(claudeRoot, "skills");
450
+ }
451
+ while (dir !== dirname(dir)) {
452
+ const claudeDir = join4(dir, ".claude");
453
+ if (existsSync4(claudeDir)) {
454
+ return join4(claudeDir, "skills");
455
+ }
456
+ dir = dirname(dir);
457
+ }
458
+ return null;
459
+ }
460
+ function discoverSkillDirs(parentDir) {
461
+ if (!existsSync4(parentDir)) return [];
462
+ const dirs = [];
463
+ const entries = readdirSync(parentDir, { withFileTypes: true });
464
+ for (const entry of entries) {
465
+ if (!entry.isDirectory()) continue;
466
+ if (entry.name.startsWith(".")) continue;
467
+ const skillMd = join4(parentDir, entry.name, "SKILL.md");
468
+ if (existsSync4(skillMd)) {
469
+ dirs.push(join4(parentDir, entry.name));
470
+ }
471
+ }
472
+ return dirs.sort();
473
+ }
474
+
475
+ // src/commands/validate.ts
476
+ var MAX_FILE_COUNT = 100;
477
+ var MAX_FILE_SIZE = 500 * 1024;
478
+ var MAX_TOTAL_SIZE = 2 * 1024 * 1024;
479
+ function collectFiles(dir, prefix = "") {
480
+ const files = [];
481
+ const entries = readdirSync2(dir, { withFileTypes: true });
482
+ for (const entry of entries) {
483
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
484
+ const fullPath = join5(dir, entry.name);
485
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
486
+ if (entry.isDirectory()) {
487
+ files.push(...collectFiles(fullPath, relativePath));
488
+ } else {
489
+ const stat = statSync(fullPath);
490
+ files.push({ path: relativePath, size: stat.size });
491
+ }
492
+ }
493
+ return files;
494
+ }
495
+ function runValidation(dir) {
496
+ const errors = [];
497
+ const warnings = [];
498
+ if (!existsSync5(join5(dir, "SKILL.md"))) {
499
+ errors.push("Missing SKILL.md \u2014 every skill must have a SKILL.md file with YAML frontmatter.");
500
+ return { valid: false, errors, warnings };
501
+ }
502
+ const manifest = parseManifest(dir);
503
+ const manifestErrors = validateManifest(manifest);
504
+ errors.push(...manifestErrors);
505
+ if (!manifest._body) {
506
+ warnings.push("SKILL.md has no instructions body after the frontmatter. Add skill instructions below the --- delimiter.");
507
+ }
508
+ if (!manifest.version) {
509
+ warnings.push('No "version" in frontmatter. Required for publishing to the registry (e.g., version: "1.0.0").');
510
+ }
511
+ if (!manifest.scope) {
512
+ warnings.push('No "scope" in frontmatter. Required for publishing to the registry (e.g., scope: my-team).');
513
+ }
514
+ const dirName = dir.split("/").pop();
515
+ if (manifest.name && dirName && dirName !== manifest.name) {
516
+ warnings.push(`Directory name "${dirName}" doesn't match skill name "${manifest.name}". They should match per the Agent Skills spec.`);
517
+ }
518
+ const files = collectFiles(dir);
519
+ if (files.length > MAX_FILE_COUNT) {
520
+ errors.push(`Too many files: ${files.length} (max ${MAX_FILE_COUNT}).`);
521
+ }
522
+ let totalSize = 0;
523
+ for (const file of files) {
524
+ totalSize += file.size;
525
+ if (file.size > MAX_FILE_SIZE) {
526
+ errors.push(`File too large: ${file.path} (${(file.size / 1024).toFixed(1)}KB, max 500KB).`);
527
+ }
528
+ }
529
+ if (totalSize > MAX_TOTAL_SIZE) {
530
+ errors.push(`Total size too large: ${(totalSize / 1024 / 1024).toFixed(2)}MB (max 2MB).`);
531
+ }
532
+ return { valid: errors.length === 0, errors, warnings };
533
+ }
534
+ 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) => {
535
+ try {
536
+ if (opts.all) {
537
+ const skillsDir = findClaudeSkillsDir();
538
+ if (!skillsDir) {
539
+ throw new CliError("Cannot find .claude/skills/ directory.", [
540
+ "Run this from a project with a .claude/ directory."
541
+ ]);
542
+ }
543
+ const skillDirs = discoverSkillDirs(skillsDir);
544
+ if (skillDirs.length === 0) {
545
+ throw new CliError(`No skills found in ${skillsDir}/`, [
546
+ "Skills must be directories containing a SKILL.md file."
547
+ ]);
548
+ }
549
+ console.log(`Validating ${skillDirs.length} skill(s) in ${skillsDir}/
550
+ `);
551
+ let passed = 0;
552
+ let failed = 0;
553
+ for (const skillDir of skillDirs) {
554
+ const name = basename(skillDir);
555
+ const result2 = runValidation(skillDir);
556
+ if (result2.valid) {
557
+ success(`${name}`);
558
+ passed++;
559
+ } else {
560
+ error(`${name}`);
561
+ for (const e of result2.errors) {
562
+ console.error(` ${e}`);
563
+ }
564
+ failed++;
565
+ }
566
+ for (const w of result2.warnings) {
567
+ warn(` ${name}: ${w}`);
568
+ }
569
+ }
570
+ console.log("");
571
+ if (failed === 0) {
572
+ success(`All ${passed} skill(s) are valid.`);
573
+ } else {
574
+ throw new CliError(`${failed}/${passed + failed} skill(s) failed validation.`);
575
+ }
576
+ return;
577
+ }
578
+ const resolved = resolve3(dir);
579
+ if (!existsSync5(resolved)) {
580
+ throw new CliError(`Directory not found: ${resolved}`);
581
+ }
582
+ const result = runValidation(resolved);
583
+ for (const w of result.warnings) {
584
+ warn(w);
585
+ }
586
+ if (result.valid) {
587
+ success("Skill is valid.");
588
+ } else {
589
+ for (const e of result.errors) {
590
+ error(e);
591
+ }
592
+ throw new CliError("Validation failed.", [
593
+ "Fix the errors above and try again."
594
+ ]);
595
+ }
596
+ } catch (err) {
597
+ handleError(err);
598
+ }
599
+ });
600
+
601
+ // src/commands/pack.ts
602
+ import { Command as Command6 } from "commander";
603
+ import { resolve as resolve4 } from "path";
604
+ import { existsSync as existsSync6 } from "fs";
605
+
606
+ // src/lib/archive.ts
607
+ import { statSync as statSync2 } from "fs";
608
+ import { readFile } from "fs/promises";
609
+ import { createHash } from "crypto";
610
+ import { join as join6, basename as basename2 } from "path";
611
+ import { create as tarCreate, extract as tarExtract } from "tar";
612
+ async function computeSha256(filePath) {
613
+ const data = await readFile(filePath);
614
+ return createHash("sha256").update(data).digest("hex");
615
+ }
616
+ async function pack(dir, outputPath) {
617
+ const dirName = basename2(dir);
618
+ const archivePath = outputPath ?? join6(dir, "..", `${dirName}.tar.gz`);
619
+ await tarCreate(
620
+ {
621
+ gzip: true,
622
+ file: archivePath,
623
+ cwd: join6(dir, "..")
624
+ },
625
+ [dirName]
626
+ );
627
+ const sha256 = await computeSha256(archivePath);
628
+ const stat = statSync2(archivePath);
629
+ return {
630
+ path: archivePath,
631
+ sha256,
632
+ size: stat.size
633
+ };
634
+ }
635
+ async function extract(archivePath, outputDir, expectedSha256) {
636
+ const actualSha256 = await computeSha256(archivePath);
637
+ if (actualSha256 !== expectedSha256) {
638
+ throw new CliError(
639
+ `SHA-256 mismatch: expected ${expectedSha256}, got ${actualSha256}`,
640
+ ["The archive may be corrupted or tampered with.", "Try downloading again."]
641
+ );
642
+ }
643
+ await tarExtract({
644
+ file: archivePath,
645
+ cwd: outputDir
646
+ });
647
+ }
648
+
649
+ // src/commands/pack.ts
650
+ var packCommand = new Command6("pack").description("Pack a skill into a tarball").argument("[dir]", "Skill directory", ".").action(async (dir) => {
651
+ try {
652
+ const resolved = resolve4(dir);
653
+ if (!existsSync6(resolved)) {
654
+ throw new CliError(`Directory not found: ${resolved}`);
655
+ }
656
+ const validation = runValidation(resolved);
657
+ if (!validation.valid) {
658
+ for (const e of validation.errors) {
659
+ console.error(e);
660
+ }
661
+ throw new CliError("Validation failed. Fix errors before packing.", [
662
+ "Run `csreg validate` for details."
663
+ ]);
664
+ }
665
+ const result = await pack(resolved);
666
+ console.log("");
667
+ success("Skill packed successfully.");
668
+ info(`Archive: ${result.path}`);
669
+ info(`Size: ${(result.size / 1024).toFixed(1)}KB`);
670
+ info(`SHA-256: ${result.sha256}`);
671
+ } catch (err) {
672
+ handleError(err);
673
+ }
674
+ });
675
+
676
+ // src/commands/push.ts
677
+ import { Command as Command7 } from "commander";
678
+ import { resolve as resolve5, join as join7, basename as basename3 } from "path";
679
+ import { existsSync as existsSync7, readFileSync as readFileSync3, statSync as statSync3, readdirSync as readdirSync3 } from "fs";
680
+ import { createHash as createHash2 } from "crypto";
681
+ function collectFileTree(dir, prefix = "") {
682
+ const files = [];
683
+ const entries = readdirSync3(dir, { withFileTypes: true });
684
+ for (const entry of entries) {
685
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
686
+ const fullPath = join7(dir, entry.name);
687
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
688
+ if (entry.isDirectory()) {
689
+ files.push(...collectFileTree(fullPath, relativePath));
690
+ } else {
691
+ const stat = statSync3(fullPath);
692
+ const content = readFileSync3(fullPath);
693
+ const sha256 = createHash2("sha256").update(content).digest("hex");
694
+ files.push({ path: relativePath, size: stat.size, sha256 });
695
+ }
696
+ }
697
+ return files;
698
+ }
699
+ async function pushSingle(resolved) {
700
+ const spin = spinner("Validating skill...");
701
+ const validation = runValidation(resolved);
702
+ if (!validation.valid) {
703
+ spin.fail("Validation failed.");
704
+ for (const e of validation.errors) {
705
+ console.error(" " + e);
706
+ }
707
+ throw new CliError("Fix validation errors before pushing.");
708
+ }
709
+ spin.succeed("Validation passed.");
710
+ const manifest = parseManifest(resolved);
711
+ if (!manifest.version) {
712
+ throw new CliError('Missing "version" in SKILL.md frontmatter.', [
713
+ 'Add a version field: version: "1.0.0"'
714
+ ]);
715
+ }
716
+ if (!manifest.scope) {
717
+ throw new CliError('Missing "scope" in SKILL.md frontmatter.', [
718
+ "Add a scope field: scope: my-team"
719
+ ]);
720
+ }
721
+ spin.start("Packing skill...");
722
+ const archive = await pack(resolved);
723
+ spin.succeed(`Packed (${(archive.size / 1024).toFixed(1)}KB).`);
724
+ const client = new ApiClient();
725
+ spin.start("Checking registry...");
726
+ try {
727
+ await client.get(`/api/v1/skills/${manifest.scope}/${manifest.name}`);
728
+ spin.succeed("Skill found in registry.");
729
+ } catch {
730
+ spin.start(`Creating skill ${manifest.scope}/${manifest.name}...`);
731
+ await client.post("/api/v1/skills", {
732
+ body: {
733
+ scope: manifest.scope,
734
+ slug: manifest.name,
735
+ displayName: manifest.name,
736
+ description: manifest.description || "",
737
+ tags: manifest.tags || []
738
+ }
739
+ });
740
+ spin.succeed(`Created skill ${manifest.scope}/${manifest.name}.`);
741
+ }
742
+ spin.start("Preparing version...");
743
+ const fileTree = collectFileTree(resolved);
744
+ const prepared = await client.post(
745
+ `/api/v1/skills/${manifest.scope}/${manifest.name}/versions`,
746
+ {
747
+ body: {
748
+ version: manifest.version,
749
+ fileTree,
750
+ archiveSha256: archive.sha256,
751
+ archiveSize: archive.size,
752
+ entryPoint: "SKILL.md",
753
+ manifestJson: {
754
+ name: manifest.name,
755
+ description: manifest.description,
756
+ version: manifest.version,
757
+ scope: manifest.scope,
758
+ "allowed-tools": manifest["allowed-tools"],
759
+ "argument-hint": manifest["argument-hint"],
760
+ "disable-model-invocation": manifest["disable-model-invocation"],
761
+ "user-invocable": manifest["user-invocable"],
762
+ tags: manifest.tags
763
+ }
764
+ }
765
+ }
766
+ );
767
+ spin.succeed("Version prepared.");
768
+ spin.start("Uploading archive...");
769
+ const archiveData = readFileSync3(archive.path);
770
+ const uploadResponse = await fetch(prepared.uploadUrl, {
771
+ method: "PUT",
772
+ headers: {
773
+ "Content-Type": "application/gzip",
774
+ "Content-Length": String(archive.size)
775
+ },
776
+ body: archiveData
777
+ });
778
+ if (!uploadResponse.ok) {
779
+ spin.fail("Upload failed.");
780
+ throw new CliError(`Upload failed with status ${uploadResponse.status}.`, [
781
+ "Try again. If the problem persists, contact support."
782
+ ]);
783
+ }
784
+ spin.succeed("Archive uploaded.");
785
+ spin.start("Finalizing version...");
786
+ await client.post(
787
+ `/api/v1/skills/${manifest.scope}/${manifest.name}/versions/${manifest.version}/finalize`,
788
+ {
789
+ body: { status: "published" }
790
+ }
791
+ );
792
+ spin.succeed("Version finalized.");
793
+ return `${manifest.scope}/${manifest.name}@${manifest.version}`;
794
+ }
795
+ 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) => {
796
+ try {
797
+ if (!getAuthToken()) {
798
+ throw new CliError("Not logged in.", ["Run `csreg login` to authenticate."]);
799
+ }
800
+ if (opts.all) {
801
+ const skillsDir = findClaudeSkillsDir();
802
+ if (!skillsDir) {
803
+ throw new CliError("Cannot find .claude/skills/ directory.", [
804
+ "Run this from a project with a .claude/ directory."
805
+ ]);
806
+ }
807
+ const skillDirs = discoverSkillDirs(skillsDir);
808
+ if (skillDirs.length === 0) {
809
+ throw new CliError(`No skills found in ${skillsDir}/`, [
810
+ "Skills must be directories containing a SKILL.md file."
811
+ ]);
812
+ }
813
+ console.log(`Pushing ${skillDirs.length} skill(s) from ${skillsDir}/
814
+ `);
815
+ let published = 0;
816
+ let failed = 0;
817
+ for (const skillDir of skillDirs) {
818
+ const name = basename3(skillDir);
819
+ console.log(`
820
+ --- ${name} ---
821
+ `);
822
+ try {
823
+ const ref2 = await pushSingle(skillDir);
824
+ success(`Published ${ref2}`);
825
+ published++;
826
+ } catch (err) {
827
+ const msg = err instanceof Error ? err.message : String(err);
828
+ warn(`Failed to push ${name}: ${msg}`);
829
+ failed++;
830
+ }
831
+ }
832
+ console.log(`
833
+ ${"\u2500".repeat(40)}
834
+ `);
835
+ if (failed === 0) {
836
+ success(`All ${published} skill(s) published.`);
837
+ } else {
838
+ success(`Published ${published}/${published + failed} skill(s).`);
839
+ if (failed > 0) {
840
+ warn(`${failed} skill(s) failed. Check the errors above.`);
841
+ }
842
+ }
843
+ return;
844
+ }
845
+ const resolved = resolve5(dir);
846
+ if (!existsSync7(resolved)) {
847
+ throw new CliError(`Directory not found: ${resolved}`);
848
+ }
849
+ const ref = await pushSingle(resolved);
850
+ console.log("");
851
+ success(`Published ${ref}`);
852
+ } catch (err) {
853
+ handleError(err);
854
+ }
855
+ });
856
+
857
+ // src/commands/pull.ts
858
+ import { Command as Command8 } from "commander";
859
+ import { resolve as resolve6, join as join8, dirname as dirname2 } from "path";
860
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, existsSync as existsSync8, readFileSync as readFileSync4, renameSync } from "fs";
861
+ import { createHash as createHash3 } from "crypto";
862
+ import { confirm as confirm2 } from "@inquirer/prompts";
863
+ function parseSkillRef(ref) {
864
+ const cleaned = ref.startsWith("@") ? ref.slice(1) : ref;
865
+ const atIndex = cleaned.lastIndexOf("@");
866
+ let scopeAndName;
867
+ let version;
868
+ if (atIndex > 0) {
869
+ scopeAndName = cleaned.slice(0, atIndex);
870
+ version = cleaned.slice(atIndex + 1);
871
+ } else {
872
+ scopeAndName = cleaned;
873
+ }
874
+ const slashIndex = scopeAndName.indexOf("/");
875
+ if (slashIndex < 0) {
876
+ throw new CliError(`Invalid skill reference: ${ref}`, [
877
+ "Use the format: @scope/name or @scope/name@version"
878
+ ]);
879
+ }
880
+ return {
881
+ scope: scopeAndName.slice(0, slashIndex),
882
+ name: scopeAndName.slice(slashIndex + 1),
883
+ version
884
+ };
885
+ }
886
+ function readSkillsConfig() {
887
+ let dir = resolve6(".");
888
+ while (dir !== dirname2(dir)) {
889
+ const configPath = join8(dir, ".claude", "skills.json");
890
+ if (existsSync8(configPath)) {
891
+ try {
892
+ const raw = readFileSync4(configPath, "utf-8");
893
+ return JSON.parse(raw);
894
+ } catch {
895
+ throw new CliError("Failed to parse .claude/skills.json", [
896
+ "Check that the file is valid JSON."
897
+ ]);
898
+ }
899
+ }
900
+ dir = dirname2(dir);
901
+ }
902
+ return null;
903
+ }
904
+ async function pullSkill(scope, name, version, targetDir) {
905
+ const spin = spinner(`Fetching ${scope}/${name}${version ? `@${version}` : ""}...`);
906
+ const client = new ApiClient();
907
+ const query = {};
908
+ if (version) {
909
+ query.version = version;
910
+ }
911
+ const downloadInfo = await client.get(
912
+ `/api/v1/skills/${scope}/${name}/download`,
913
+ { query }
914
+ );
915
+ spin.succeed(`Found ${scope}/${name}@${downloadInfo.version}`);
916
+ spin.start("Downloading archive...");
917
+ const response = await fetch(downloadInfo.downloadUrl);
918
+ if (!response.ok) {
919
+ spin.fail("Download failed.");
920
+ throw new CliError(`Download failed with status ${response.status}.`);
921
+ }
922
+ const arrayBuffer = await response.arrayBuffer();
923
+ const archiveData = Buffer.from(arrayBuffer);
924
+ const actualSha = createHash3("sha256").update(archiveData).digest("hex");
925
+ if (actualSha !== downloadInfo.archiveSha256) {
926
+ spin.fail("Integrity check failed.");
927
+ throw new CliError(
928
+ `SHA-256 mismatch: expected ${downloadInfo.archiveSha256}, got ${actualSha}`,
929
+ ["The archive may be corrupted. Try again."]
930
+ );
931
+ }
932
+ spin.succeed("Downloaded and verified.");
933
+ mkdirSync3(targetDir, { recursive: true });
934
+ spin.start("Installing...");
935
+ const tempDir = join8(dirname2(targetDir), `.csreg-tmp-${Date.now()}`);
936
+ mkdirSync3(tempDir, { recursive: true });
937
+ const tempArchive = join8(tempDir, `${name}.tar.gz`);
938
+ writeFileSync3(tempArchive, archiveData);
939
+ try {
940
+ await extract(tempArchive, tempDir, downloadInfo.archiveSha256);
941
+ const extractedDir = join8(tempDir, name);
942
+ const finalDir = join8(targetDir, name);
943
+ if (existsSync8(finalDir)) {
944
+ const { rmSync } = await import("fs");
945
+ rmSync(finalDir, { recursive: true });
946
+ }
947
+ renameSync(extractedDir, finalDir);
948
+ spin.succeed(`Installed to ${finalDir}`);
949
+ return { version: downloadInfo.version };
950
+ } finally {
951
+ try {
952
+ const { rmSync } = await import("fs");
953
+ rmSync(tempDir, { recursive: true, force: true });
954
+ } catch {
955
+ }
956
+ }
957
+ }
958
+ 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) => {
959
+ try {
960
+ if (opts.all) {
961
+ const config = readSkillsConfig();
962
+ if (!config) {
963
+ throw new CliError("No .claude/skills.json found.", [
964
+ 'Create .claude/skills.json with a "skills" array.',
965
+ 'Example:\n {\n "skills": [\n { "ref": "@nexus/newsletter" },\n { "ref": "@nexus/code-review", "version": "1.2.0" }\n ]\n }'
966
+ ]);
967
+ }
968
+ if (!config.skills || config.skills.length === 0) {
969
+ info("No skills listed in .claude/skills.json. Nothing to pull.");
970
+ return;
971
+ }
972
+ const skillsDir = opts.path ? resolve6(opts.path) : findClaudeSkillsDir();
973
+ if (!skillsDir) {
974
+ throw new CliError("Cannot find .claude/ directory.", [
975
+ "Run this from a project with a .claude/ directory, or use --path."
976
+ ]);
977
+ }
978
+ console.log(`Installing ${config.skills.length} skill(s) to ${skillsDir}/
979
+ `);
980
+ let installed = 0;
981
+ for (const entry of config.skills) {
982
+ try {
983
+ const { scope: scope2, name: name2, version: version2 } = parseSkillRef(entry.ref);
984
+ const ver = entry.version || version2;
985
+ await pullSkill(scope2, name2, ver, skillsDir);
986
+ installed++;
987
+ console.log("");
988
+ } catch (err) {
989
+ warn(`Failed to pull ${entry.ref}: ${err instanceof Error ? err.message : String(err)}`);
990
+ console.log("");
991
+ }
992
+ }
993
+ console.log("");
994
+ success(`Installed ${installed}/${config.skills.length} skill(s).`);
995
+ return;
996
+ }
997
+ if (!ref) {
998
+ throw new CliError("Missing skill reference.", [
999
+ "Usage: csreg pull @scope/name[@version]",
1000
+ "Or use: csreg pull --all (to pull from .claude/skills.json)"
1001
+ ]);
1002
+ }
1003
+ const { scope, name, version } = parseSkillRef(ref);
1004
+ let targetDir;
1005
+ if (opts.path) {
1006
+ targetDir = resolve6(opts.path);
1007
+ } else {
1008
+ const detected = findClaudeSkillsDir();
1009
+ if (detected) {
1010
+ targetDir = detected;
1011
+ const finalPath = join8(detected, name);
1012
+ const ok = await confirm2({
1013
+ message: `Install to ${finalPath}?`,
1014
+ default: true
1015
+ });
1016
+ if (!ok) {
1017
+ const { input: input3 } = await import("@inquirer/prompts");
1018
+ const custom = await input3({
1019
+ message: "Custom install path:",
1020
+ default: join8(".", ".claude", "skills")
1021
+ });
1022
+ targetDir = resolve6(custom);
1023
+ }
1024
+ } else {
1025
+ targetDir = join8(resolve6("."), ".claude", "skills");
1026
+ info(`No .claude/ directory found. Will create ${targetDir}/`);
1027
+ const ok = await confirm2({
1028
+ message: `Install to ${join8(targetDir, name)}?`,
1029
+ default: true
1030
+ });
1031
+ if (!ok) {
1032
+ const { input: input3 } = await import("@inquirer/prompts");
1033
+ const custom = await input3({
1034
+ message: "Custom install path:",
1035
+ default: targetDir
1036
+ });
1037
+ targetDir = resolve6(custom);
1038
+ }
1039
+ }
1040
+ }
1041
+ const result = await pullSkill(scope, name, version, targetDir);
1042
+ console.log("");
1043
+ success(`Pulled ${scope}/${name}@${result.version}`);
1044
+ info(`Skill is ready at ${join8(targetDir, name)}/SKILL.md`);
1045
+ } catch (err) {
1046
+ handleError(err);
1047
+ }
1048
+ });
1049
+
1050
+ // src/commands/info.ts
1051
+ import { Command as Command9 } from "commander";
1052
+ import chalk3 from "chalk";
1053
+ var infoCommand = new Command9("info").description("Display details about a skill").argument("<ref>", "Skill reference (scope/name)").action(async (ref) => {
1054
+ try {
1055
+ const cleaned = ref.startsWith("@") ? ref.slice(1) : ref;
1056
+ const slashIndex = cleaned.indexOf("/");
1057
+ if (slashIndex < 0) {
1058
+ throw new CliError(`Invalid skill reference: ${ref}`, [
1059
+ "Use the format: @scope/name"
1060
+ ]);
1061
+ }
1062
+ const scope = cleaned.slice(0, slashIndex);
1063
+ const name = cleaned.slice(slashIndex + 1);
1064
+ const client = new ApiClient();
1065
+ const skill = await client.get(`/api/v1/skills/${scope}/${name}`);
1066
+ console.log("");
1067
+ console.log(chalk3.bold(`${skill.scope}/${skill.name}`) + chalk3.dim(` @ ${skill.latestVersion}`));
1068
+ console.log("");
1069
+ if (skill.description) {
1070
+ console.log(skill.description);
1071
+ console.log("");
1072
+ }
1073
+ const fields = [
1074
+ ["Type", skill.type],
1075
+ ["Latest Version", skill.latestVersion],
1076
+ ["Downloads", String(skill.totalDownloads)],
1077
+ ["Created", new Date(skill.createdAt).toLocaleDateString()],
1078
+ ["Updated", new Date(skill.updatedAt).toLocaleDateString()]
1079
+ ];
1080
+ if (skill.author) {
1081
+ fields.push(["Author", `${skill.author.displayName} (${skill.author.email})`]);
1082
+ }
1083
+ if (skill.tags.length > 0) {
1084
+ fields.push(["Tags", skill.tags.join(", ")]);
1085
+ }
1086
+ const maxLabel = Math.max(...fields.map(([label]) => label.length));
1087
+ for (const [label, value] of fields) {
1088
+ console.log(` ${chalk3.cyan(label.padEnd(maxLabel + 2))}${value}`);
1089
+ }
1090
+ console.log("");
1091
+ } catch (err) {
1092
+ handleError(err);
1093
+ }
1094
+ });
1095
+
1096
+ // src/commands/versions.ts
1097
+ import { Command as Command10 } from "commander";
1098
+ var versionsCommand = new Command10("versions").description("List all versions of a skill").argument("<ref>", "Skill reference (scope/name)").action(async (ref) => {
1099
+ try {
1100
+ const cleaned = ref.startsWith("@") ? ref.slice(1) : ref;
1101
+ const slashIndex = cleaned.indexOf("/");
1102
+ if (slashIndex < 0) {
1103
+ throw new CliError(`Invalid skill reference: ${ref}`, [
1104
+ "Use the format: @scope/name"
1105
+ ]);
1106
+ }
1107
+ const scope = cleaned.slice(0, slashIndex);
1108
+ const name = cleaned.slice(slashIndex + 1);
1109
+ const client = new ApiClient();
1110
+ const data = await client.get(
1111
+ `/api/v1/skills/${scope}/${name}/versions`
1112
+ );
1113
+ if (data.versions.length === 0) {
1114
+ console.log("No versions found.");
1115
+ return;
1116
+ }
1117
+ const rows = data.versions.map((v) => [
1118
+ v.version,
1119
+ v.status,
1120
+ new Date(v.createdAt).toLocaleDateString(),
1121
+ String(v.fileCount)
1122
+ ]);
1123
+ console.log("");
1124
+ console.log(formatTable(["Version", "Status", "Published", "Files"], rows));
1125
+ console.log("");
1126
+ } catch (err) {
1127
+ handleError(err);
1128
+ }
1129
+ });
1130
+
1131
+ // src/commands/search.ts
1132
+ import { Command as Command11 } from "commander";
1133
+ 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) => {
1134
+ try {
1135
+ const client = new ApiClient();
1136
+ const queryParams = {
1137
+ q: query,
1138
+ limit: opts.limit
1139
+ };
1140
+ if (opts.type) {
1141
+ queryParams.type = opts.type;
1142
+ }
1143
+ const data = await client.get("/api/v1/skills", {
1144
+ query: queryParams
1145
+ });
1146
+ if (data.skills.length === 0) {
1147
+ info(`No skills found for "${query}".`);
1148
+ return;
1149
+ }
1150
+ const rows = data.skills.map((s) => [
1151
+ `${s.scope}/${s.name}`,
1152
+ s.description?.slice(0, 50) ?? "",
1153
+ String(s.totalDownloads),
1154
+ s.latestVersion
1155
+ ]);
1156
+ console.log("");
1157
+ console.log(formatTable(["Skill", "Description", "Downloads", "Version"], rows));
1158
+ console.log("");
1159
+ info(`Showing ${data.skills.length} of ${data.total} results.`);
1160
+ } catch (err) {
1161
+ handleError(err);
1162
+ }
1163
+ });
1164
+
1165
+ // src/index.ts
1166
+ var program = new Command12();
1167
+ program.name("csreg").description("Claude Skills Registry CLI").version("0.1.0");
1168
+ program.addCommand(loginCommand);
1169
+ program.addCommand(logoutCommand);
1170
+ program.addCommand(whoamiCommand);
1171
+ program.addCommand(initCommand);
1172
+ program.addCommand(validateCommand);
1173
+ program.addCommand(packCommand);
1174
+ program.addCommand(pushCommand);
1175
+ program.addCommand(pullCommand);
1176
+ program.addCommand(infoCommand);
1177
+ program.addCommand(versionsCommand);
1178
+ program.addCommand(searchCommand);
1179
+ program.parse();