@agent-nexus/csreg 0.1.0 → 0.1.2

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/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # @agent-nexus/csreg
2
+
3
+ CLI for the Claude Skills Registry — publish, install, and manage reusable Claude skills.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @agent-nexus/csreg
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ # Authenticate with the registry
15
+ csreg login --token <your-token>
16
+
17
+ # Initialize a new skill project
18
+ csreg init my-skill
19
+
20
+ # Validate, pack, and publish
21
+ csreg validate
22
+ csreg push
23
+ ```
24
+
25
+ ## Commands
26
+
27
+ ### Authentication
28
+
29
+ | Command | Description |
30
+ | --- | --- |
31
+ | `csreg login [--token <token>]` | Authenticate with the Skills Registry |
32
+ | `csreg logout` | Remove stored authentication credentials |
33
+ | `csreg whoami` | Display the currently authenticated user |
34
+
35
+ ### Skill Development
36
+
37
+ | Command | Description |
38
+ | --- | --- |
39
+ | `csreg init [dir]` | Initialize a new skill project |
40
+ | `csreg validate [dir]` | Validate a skill package |
41
+ | `csreg pack [dir]` | Pack a skill into a tarball |
42
+ | `csreg push [dir]` | Publish a skill to the registry |
43
+
44
+ ### Skill Discovery & Installation
45
+
46
+ | Command | Description |
47
+ | --- | --- |
48
+ | `csreg search <query>` | Search for skills in the registry |
49
+ | `csreg info <scope/name>` | Display details about a skill |
50
+ | `csreg versions <scope/name>` | List all versions of a skill |
51
+ | `csreg pull [ref]` | Download and install a skill |
52
+
53
+ ## Usage
54
+
55
+ ### Publishing a Skill
56
+
57
+ ```bash
58
+ # Create a new skill
59
+ csreg init my-skill
60
+ cd my-skill
61
+
62
+ # Edit your skill files, then validate
63
+ csreg validate
64
+
65
+ # Publish to the registry
66
+ csreg push
67
+ ```
68
+
69
+ ### Installing a Skill
70
+
71
+ ```bash
72
+ # Install a specific skill
73
+ csreg pull @scope/skill-name
74
+
75
+ # Install a specific version
76
+ csreg pull @scope/skill-name@1.2.0
77
+
78
+ # Install all skills listed in .claude/skills.json
79
+ csreg pull --all
80
+ ```
81
+
82
+ ### Searching for Skills
83
+
84
+ ```bash
85
+ # Search by keyword
86
+ csreg search "code review"
87
+
88
+ # Filter by type
89
+ csreg search "linting" --type prompt
90
+
91
+ # Limit results
92
+ csreg search "testing" --limit 5
93
+ ```
94
+
95
+ ### Bulk Operations
96
+
97
+ ```bash
98
+ # Validate all skills in .claude/skills/
99
+ csreg validate --all
100
+
101
+ # Push all skills in .claude/skills/
102
+ csreg push --all
103
+ ```
104
+
105
+ ## Configuration
106
+
107
+ The CLI stores its configuration in `~/.config/csreg/config.json`.
108
+
109
+ You can override settings with environment variables:
110
+
111
+ | Variable | Description |
112
+ | --- | --- |
113
+ | `CSREG_API_URL` | Override the registry API URL |
114
+ | `CSREG_TOKEN` | Provide an auth token (skips stored credentials) |
115
+
116
+ ## License
117
+
118
+ MIT
package/dist/index.js CHANGED
@@ -27,11 +27,11 @@ function getConfig() {
27
27
  function setConfig(updates) {
28
28
  const current = getConfig();
29
29
  const merged = { ...current, ...updates };
30
- mkdirSync(CONFIG_DIR, { recursive: true });
31
- writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + "\n", "utf-8");
30
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
31
+ writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
32
32
  }
33
33
  function getApiUrl() {
34
- return process.env.CSREG_API_URL ?? getConfig().apiUrl ?? "http://localhost:3000";
34
+ return process.env.CSREG_API_URL ?? getConfig().apiUrl ?? "https://csreg.nexus";
35
35
  }
36
36
  function getAuthToken() {
37
37
  return process.env.CSREG_TOKEN ?? getConfig().token;
@@ -355,7 +355,7 @@ For example: \`/my-skill some-argument\` makes \`$ARGUMENTS\` = "some-argument".
355
355
  // src/commands/validate.ts
356
356
  import { Command as Command5 } from "commander";
357
357
  import { resolve as resolve3, join as join5, basename } from "path";
358
- import { existsSync as existsSync5, statSync, readdirSync as readdirSync2 } from "fs";
358
+ import { existsSync as existsSync5, statSync, readdirSync as readdirSync2, lstatSync } from "fs";
359
359
 
360
360
  // src/lib/manifest.ts
361
361
  import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
@@ -482,6 +482,8 @@ function collectFiles(dir, prefix = "") {
482
482
  for (const entry of entries) {
483
483
  if (entry.name === "node_modules" || entry.name === ".git") continue;
484
484
  const fullPath = join5(dir, entry.name);
485
+ const lstat = lstatSync(fullPath);
486
+ if (lstat.isSymbolicLink()) continue;
485
487
  const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
486
488
  if (entry.isDirectory()) {
487
489
  files.push(...collectFiles(fullPath, relativePath));
@@ -609,6 +611,7 @@ import { readFile } from "fs/promises";
609
611
  import { createHash } from "crypto";
610
612
  import { join as join6, basename as basename2 } from "path";
611
613
  import { create as tarCreate, extract as tarExtract } from "tar";
614
+ var MAX_ARCHIVE_SIZE = 10 * 1024 * 1024;
612
615
  async function computeSha256(filePath) {
613
616
  const data = await readFile(filePath);
614
617
  return createHash("sha256").update(data).digest("hex");
@@ -626,6 +629,12 @@ async function pack(dir, outputPath) {
626
629
  );
627
630
  const sha256 = await computeSha256(archivePath);
628
631
  const stat = statSync2(archivePath);
632
+ if (stat.size > MAX_ARCHIVE_SIZE) {
633
+ throw new CliError(
634
+ `Archive size ${(stat.size / 1024 / 1024).toFixed(1)}MB exceeds the 10MB limit.`,
635
+ ["Remove unnecessary files or assets to reduce the package size."]
636
+ );
637
+ }
629
638
  return {
630
639
  path: archivePath,
631
640
  sha256,
@@ -642,7 +651,8 @@ async function extract(archivePath, outputDir, expectedSha256) {
642
651
  }
643
652
  await tarExtract({
644
653
  file: archivePath,
645
- cwd: outputDir
654
+ cwd: outputDir,
655
+ filter: (path) => !path.includes("..")
646
656
  });
647
657
  }
648
658
 
@@ -676,7 +686,7 @@ var packCommand = new Command6("pack").description("Pack a skill into a tarball"
676
686
  // src/commands/push.ts
677
687
  import { Command as Command7 } from "commander";
678
688
  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";
689
+ import { existsSync as existsSync7, readFileSync as readFileSync3, statSync as statSync3, readdirSync as readdirSync3, lstatSync as lstatSync2 } from "fs";
680
690
  import { createHash as createHash2 } from "crypto";
681
691
  function collectFileTree(dir, prefix = "") {
682
692
  const files = [];
@@ -684,6 +694,8 @@ function collectFileTree(dir, prefix = "") {
684
694
  for (const entry of entries) {
685
695
  if (entry.name === "node_modules" || entry.name === ".git") continue;
686
696
  const fullPath = join7(dir, entry.name);
697
+ const lstat = lstatSync2(fullPath);
698
+ if (lstat.isSymbolicLink()) continue;
687
699
  const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
688
700
  if (entry.isDirectory()) {
689
701
  files.push(...collectFileTree(fullPath, relativePath));
@@ -921,6 +933,12 @@ async function pullSkill(scope, name, version, targetDir) {
921
933
  }
922
934
  const arrayBuffer = await response.arrayBuffer();
923
935
  const archiveData = Buffer.from(arrayBuffer);
936
+ if (archiveData.length > MAX_ARCHIVE_SIZE) {
937
+ spin.fail("Archive too large.");
938
+ throw new CliError(
939
+ `Archive size ${(archiveData.length / 1024 / 1024).toFixed(1)}MB exceeds the 10MB limit.`
940
+ );
941
+ }
924
942
  const actualSha = createHash3("sha256").update(archiveData).digest("hex");
925
943
  if (actualSha !== downloadInfo.archiveSha256) {
926
944
  spin.fail("Integrity check failed.");
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@agent-nexus/csreg",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "CLI for Claude Skills Registry",
5
+ "license": "MIT",
5
6
  "type": "module",
6
7
  "bin": {
7
8
  "csreg": "./dist/index.js"
@@ -9,7 +10,9 @@
9
10
  "scripts": {
10
11
  "build": "tsup",
11
12
  "dev": "tsx src/index.ts",
12
- "test": "vitest run"
13
+ "test": "vitest run",
14
+ "prepublishOnly": "npm run build",
15
+ "release": "npm version patch && npm publish"
13
16
  },
14
17
  "dependencies": {
15
18
  "commander": "^13.0.0",
@@ -22,8 +25,19 @@
22
25
  "cli-table3": "^0.6.0",
23
26
  "glob": "^11.0.0"
24
27
  },
28
+ "files": [
29
+ "dist",
30
+ "README.md"
31
+ ],
32
+ "keywords": [
33
+ "claude",
34
+ "skills",
35
+ "registry",
36
+ "cli",
37
+ "ai"
38
+ ],
25
39
  "publishConfig": {
26
- "access": "restricted"
40
+ "access": "public"
27
41
  },
28
42
  "devDependencies": {
29
43
  "tsup": "^8.0.0",
package/src/api-client.ts DELETED
@@ -1,145 +0,0 @@
1
- import { getApiUrl, getAuthToken } from './config.js';
2
- import { CliError } from './lib/errors.js';
3
-
4
- interface Rfc7807Error {
5
- type?: string;
6
- title?: string;
7
- detail?: string;
8
- status?: number;
9
- }
10
-
11
- interface RequestOptions {
12
- body?: unknown;
13
- headers?: Record<string, string>;
14
- query?: Record<string, string>;
15
- }
16
-
17
- const MAX_RETRIES = 3;
18
- const BASE_DELAY_MS = 500;
19
-
20
- export class ApiClient {
21
- private baseUrl: string;
22
- private token: string | undefined;
23
-
24
- constructor() {
25
- this.baseUrl = getApiUrl();
26
- this.token = getAuthToken();
27
- }
28
-
29
- private buildUrl(path: string, query?: Record<string, string>): string {
30
- const url = new URL(path, this.baseUrl);
31
- if (query) {
32
- for (const [key, value] of Object.entries(query)) {
33
- url.searchParams.set(key, value);
34
- }
35
- }
36
- return url.toString();
37
- }
38
-
39
- private buildHeaders(extra?: Record<string, string>): Record<string, string> {
40
- const headers: Record<string, string> = {
41
- 'Content-Type': 'application/json',
42
- 'Accept': 'application/json',
43
- ...extra,
44
- };
45
- if (this.token) {
46
- headers['Authorization'] = `Bearer ${this.token}`;
47
- }
48
- return headers;
49
- }
50
-
51
- private async handleResponse<T>(response: Response): Promise<T> {
52
- if (response.ok) {
53
- if (response.status === 204) {
54
- return undefined as T;
55
- }
56
- return response.json() as Promise<T>;
57
- }
58
-
59
- let errorBody: Rfc7807Error | undefined;
60
- try {
61
- errorBody = await response.json() as Rfc7807Error;
62
- } catch {
63
- // response body was not JSON
64
- }
65
-
66
- const detail = errorBody?.detail ?? response.statusText;
67
- const title = errorBody?.title ?? `HTTP ${response.status}`;
68
- const suggestions: string[] = [];
69
-
70
- if (response.status === 401) {
71
- suggestions.push('Run `csreg login` to authenticate.');
72
- } else if (response.status === 403) {
73
- suggestions.push('You may not have permission for this action.');
74
- } else if (response.status === 404) {
75
- suggestions.push('Check that the skill name and scope are correct.');
76
- }
77
-
78
- throw new CliError(`${title}: ${detail}`, suggestions);
79
- }
80
-
81
- private async requestWithRetry<T>(
82
- method: string,
83
- path: string,
84
- opts?: RequestOptions,
85
- ): Promise<T> {
86
- const url = this.buildUrl(path, opts?.query);
87
- const headers = this.buildHeaders(opts?.headers);
88
-
89
- let lastError: unknown;
90
- for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
91
- try {
92
- const response = await fetch(url, {
93
- method,
94
- headers,
95
- body: opts?.body !== undefined ? JSON.stringify(opts.body) : undefined,
96
- });
97
-
98
- if (response.status >= 500 && attempt < MAX_RETRIES - 1) {
99
- const delay = BASE_DELAY_MS * Math.pow(2, attempt);
100
- await new Promise(resolve => setTimeout(resolve, delay));
101
- continue;
102
- }
103
-
104
- return await this.handleResponse<T>(response);
105
- } catch (error) {
106
- lastError = error;
107
- if (error instanceof CliError) {
108
- throw error;
109
- }
110
- if (attempt < MAX_RETRIES - 1) {
111
- const delay = BASE_DELAY_MS * Math.pow(2, attempt);
112
- await new Promise(resolve => setTimeout(resolve, delay));
113
- continue;
114
- }
115
- }
116
- }
117
-
118
- throw lastError instanceof CliError
119
- ? lastError
120
- : new CliError(`Request failed: ${String(lastError)}`, [
121
- 'Check your internet connection.',
122
- 'The API server may be unavailable.',
123
- ]);
124
- }
125
-
126
- async get<T>(path: string, opts?: RequestOptions): Promise<T> {
127
- return this.requestWithRetry<T>('GET', path, opts);
128
- }
129
-
130
- async post<T>(path: string, opts?: RequestOptions): Promise<T> {
131
- return this.requestWithRetry<T>('POST', path, opts);
132
- }
133
-
134
- async put<T>(path: string, opts?: RequestOptions): Promise<T> {
135
- return this.requestWithRetry<T>('PUT', path, opts);
136
- }
137
-
138
- async patch<T>(path: string, opts?: RequestOptions): Promise<T> {
139
- return this.requestWithRetry<T>('PATCH', path, opts);
140
- }
141
-
142
- async delete<T>(path: string, opts?: RequestOptions): Promise<T> {
143
- return this.requestWithRetry<T>('DELETE', path, opts);
144
- }
145
- }
@@ -1,71 +0,0 @@
1
- import { Command } from 'commander';
2
- import chalk from 'chalk';
3
- import { ApiClient } from '../api-client.js';
4
- import { handleError, CliError } from '../lib/errors.js';
5
-
6
- interface SkillInfo {
7
- name: string;
8
- scope: string;
9
- description?: string;
10
- type: string;
11
- latestVersion: string;
12
- totalDownloads: number;
13
- tags: string[];
14
- createdAt: string;
15
- updatedAt: string;
16
- author?: { displayName: string; email: string };
17
- }
18
-
19
- export const infoCommand = new Command('info')
20
- .description('Display details about a skill')
21
- .argument('<ref>', 'Skill reference (scope/name)')
22
- .action(async (ref: string) => {
23
- try {
24
- const cleaned = ref.startsWith('@') ? ref.slice(1) : ref;
25
- const slashIndex = cleaned.indexOf('/');
26
- if (slashIndex < 0) {
27
- throw new CliError(`Invalid skill reference: ${ref}`, [
28
- 'Use the format: @scope/name',
29
- ]);
30
- }
31
-
32
- const scope = cleaned.slice(0, slashIndex);
33
- const name = cleaned.slice(slashIndex + 1);
34
-
35
- const client = new ApiClient();
36
- const skill = await client.get<SkillInfo>(`/api/v1/skills/${scope}/${name}`);
37
-
38
- console.log('');
39
- console.log(chalk.bold(`${skill.scope}/${skill.name}`) + chalk.dim(` @ ${skill.latestVersion}`));
40
- console.log('');
41
-
42
- if (skill.description) {
43
- console.log(skill.description);
44
- console.log('');
45
- }
46
-
47
- const fields: [string, string][] = [
48
- ['Type', skill.type],
49
- ['Latest Version', skill.latestVersion],
50
- ['Downloads', String(skill.totalDownloads)],
51
- ['Created', new Date(skill.createdAt).toLocaleDateString()],
52
- ['Updated', new Date(skill.updatedAt).toLocaleDateString()],
53
- ];
54
-
55
- if (skill.author) {
56
- fields.push(['Author', `${skill.author.displayName} (${skill.author.email})`]);
57
- }
58
-
59
- if (skill.tags.length > 0) {
60
- fields.push(['Tags', skill.tags.join(', ')]);
61
- }
62
-
63
- const maxLabel = Math.max(...fields.map(([label]) => label.length));
64
- for (const [label, value] of fields) {
65
- console.log(` ${chalk.cyan(label.padEnd(maxLabel + 2))}${value}`);
66
- }
67
- console.log('');
68
- } catch (err) {
69
- handleError(err);
70
- }
71
- });
@@ -1,112 +0,0 @@
1
- import { Command } from 'commander';
2
- import { input, confirm } from '@inquirer/prompts';
3
- import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
4
- import { join, resolve } from 'node:path';
5
- import { success, info } from '../lib/output.js';
6
- import { handleError, CliError } from '../lib/errors.js';
7
-
8
- export const initCommand = new Command('init')
9
- .description('Initialize a new skill project')
10
- .argument('[dir]', 'Directory to initialize the skill in')
11
- .action(async (dir?: string) => {
12
- try {
13
- const name = await input({
14
- message: 'Skill name:',
15
- validate: (value: string) => {
16
- if (!value.trim()) return 'Name is required.';
17
- if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(value.trim())) {
18
- return 'Must be lowercase alphanumeric with hyphens (e.g., my-skill).';
19
- }
20
- if (value.trim().length > 64) {
21
- return 'Name must be at most 64 characters.';
22
- }
23
- if (value.includes('--')) {
24
- return 'Name must not contain consecutive hyphens (--).';
25
- }
26
- return true;
27
- },
28
- });
29
-
30
- const description = await input({
31
- message: 'Description (what this skill does and when to use it):',
32
- validate: (value: string) => {
33
- if (!value.trim()) return 'Description is required — Claude uses it to decide when to invoke the skill.';
34
- return true;
35
- },
36
- });
37
-
38
- const scope = await input({
39
- message: 'Scope (your team/username, for registry publishing):',
40
- validate: (value: string) => {
41
- if (!value.trim()) return 'Scope is required for publishing.';
42
- return true;
43
- },
44
- });
45
-
46
- const userInvocable = await confirm({
47
- message: 'User-invocable (show in /slash command menu)?',
48
- default: true,
49
- });
50
-
51
- const targetDir = resolve(dir ?? name.trim());
52
-
53
- if (existsSync(targetDir)) {
54
- throw new CliError(`Directory already exists: ${targetDir}`, [
55
- 'Choose a different name or directory.',
56
- ]);
57
- }
58
-
59
- mkdirSync(targetDir, { recursive: true });
60
-
61
- // Build SKILL.md with YAML frontmatter
62
- const frontmatterLines = [
63
- '---',
64
- `name: ${name.trim()}`,
65
- `description: ${description.trim()}`,
66
- `version: "0.1.0"`,
67
- `scope: ${scope.trim()}`,
68
- ];
69
-
70
- if (!userInvocable) {
71
- frontmatterLines.push('user-invocable: false');
72
- }
73
-
74
- frontmatterLines.push('---');
75
-
76
- const skillContent = `${frontmatterLines.join('\n')}
77
-
78
- # ${name.trim()}
79
-
80
- ${description.trim()}
81
-
82
- ## Instructions
83
-
84
- Add your skill instructions here. This is the prompt that Claude will follow
85
- when this skill is invoked.
86
-
87
- You can use \`$ARGUMENTS\` to reference arguments passed by the user.
88
- For example: \`/my-skill some-argument\` makes \`$ARGUMENTS\` = "some-argument".
89
- `;
90
-
91
- writeFileSync(
92
- join(targetDir, 'SKILL.md'),
93
- skillContent,
94
- 'utf-8',
95
- );
96
-
97
- console.log('');
98
- success(`Created skill "${scope.trim()}/${name.trim()}" in ${targetDir}`);
99
- info('Files created:');
100
- console.log(' - SKILL.md');
101
- console.log('');
102
- info('Next steps:');
103
- console.log(' 1. Edit SKILL.md with your skill instructions');
104
- console.log(' 2. Run `csreg validate` to check your skill');
105
- console.log(' 3. Run `csreg push` to publish to the registry');
106
- console.log('');
107
- info('To use locally without publishing:');
108
- console.log(` cp -r ${targetDir} .claude/skills/${name.trim()}`);
109
- } catch (err) {
110
- handleError(err);
111
- }
112
- });
@@ -1,43 +0,0 @@
1
- import { Command } from 'commander';
2
- import { input } from '@inquirer/prompts';
3
- import { setConfig, getApiUrl } from '../config.js';
4
- import { ApiClient } from '../api-client.js';
5
- import { success, info } from '../lib/output.js';
6
- import { handleError } from '../lib/errors.js';
7
-
8
- export const loginCommand = new Command('login')
9
- .description('Authenticate with the Skills Registry')
10
- .option('--token <token>', 'Provide auth token directly')
11
- .action(async (opts: { token?: string }) => {
12
- try {
13
- let token = opts.token;
14
-
15
- if (!token) {
16
- const apiUrl = getApiUrl();
17
- info(`Open this URL to authenticate:`);
18
- console.log(` ${apiUrl}/cli/auth`);
19
- console.log('');
20
-
21
- token = await input({
22
- message: 'Paste your auth token:',
23
- validate: (value: string) => {
24
- if (!value.trim()) return 'Token is required.';
25
- return true;
26
- },
27
- });
28
- token = token.trim();
29
- }
30
-
31
- setConfig({ token });
32
-
33
- // Verify the token by calling whoami
34
- const client = new ApiClient();
35
- const user = await client.get<{ email: string; displayName: string }>(
36
- '/api/v1/users/me',
37
- );
38
-
39
- success(`Logged in as ${user.displayName} (${user.email})`);
40
- } catch (err) {
41
- handleError(err);
42
- }
43
- });
@@ -1,15 +0,0 @@
1
- import { Command } from 'commander';
2
- import { setConfig } from '../config.js';
3
- import { success } from '../lib/output.js';
4
- import { handleError } from '../lib/errors.js';
5
-
6
- export const logoutCommand = new Command('logout')
7
- .description('Remove stored authentication credentials')
8
- .action(async () => {
9
- try {
10
- setConfig({ token: undefined });
11
- success('Logged out successfully.');
12
- } catch (err) {
13
- handleError(err);
14
- }
15
- });
@@ -1,40 +0,0 @@
1
- import { Command } from 'commander';
2
- import { resolve } from 'node:path';
3
- import { existsSync } from 'node:fs';
4
- import { runValidation } from './validate.js';
5
- import { pack as archivePack } from '../lib/archive.js';
6
- import { success, info } from '../lib/output.js';
7
- import { handleError, CliError } from '../lib/errors.js';
8
-
9
- export const packCommand = new Command('pack')
10
- .description('Pack a skill into a tarball')
11
- .argument('[dir]', 'Skill directory', '.')
12
- .action(async (dir: string) => {
13
- try {
14
- const resolved = resolve(dir);
15
- if (!existsSync(resolved)) {
16
- throw new CliError(`Directory not found: ${resolved}`);
17
- }
18
-
19
- // Validate first
20
- const validation = runValidation(resolved);
21
- if (!validation.valid) {
22
- for (const e of validation.errors) {
23
- console.error(e);
24
- }
25
- throw new CliError('Validation failed. Fix errors before packing.', [
26
- 'Run `csreg validate` for details.',
27
- ]);
28
- }
29
-
30
- const result = await archivePack(resolved);
31
-
32
- console.log('');
33
- success('Skill packed successfully.');
34
- info(`Archive: ${result.path}`);
35
- info(`Size: ${(result.size / 1024).toFixed(1)}KB`);
36
- info(`SHA-256: ${result.sha256}`);
37
- } catch (err) {
38
- handleError(err);
39
- }
40
- });