@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 +118 -0
- package/dist/index.js +24 -6
- package/package.json +17 -3
- package/src/api-client.ts +0 -145
- package/src/commands/info.ts +0 -71
- package/src/commands/init.ts +0 -112
- package/src/commands/login.ts +0 -43
- package/src/commands/logout.ts +0 -15
- package/src/commands/pack.ts +0 -40
- package/src/commands/pull.ts +0 -276
- package/src/commands/push.ts +0 -228
- package/src/commands/search.ts +0 -58
- package/src/commands/validate.ts +0 -171
- package/src/commands/versions.ts +0 -56
- package/src/commands/whoami.ts +0 -24
- package/src/config.ts +0 -38
- package/src/index.ts +0 -34
- package/src/lib/archive.ts +0 -59
- package/src/lib/discovery.ts +0 -51
- package/src/lib/errors.ts +0 -29
- package/src/lib/manifest.ts +0 -143
- package/src/lib/output.ts +0 -34
- package/tsconfig.json +0 -17
- package/tsup.config.ts +0 -22
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 ?? "
|
|
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.
|
|
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": "
|
|
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
|
-
}
|
package/src/commands/info.ts
DELETED
|
@@ -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
|
-
});
|
package/src/commands/init.ts
DELETED
|
@@ -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
|
-
});
|
package/src/commands/login.ts
DELETED
|
@@ -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
|
-
});
|
package/src/commands/logout.ts
DELETED
|
@@ -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
|
-
});
|
package/src/commands/pack.ts
DELETED
|
@@ -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
|
-
});
|