@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.d.ts +1 -0
- package/dist/index.js +1179 -0
- package/package.json +36 -0
- package/src/api-client.ts +145 -0
- package/src/commands/info.ts +71 -0
- package/src/commands/init.ts +112 -0
- package/src/commands/login.ts +43 -0
- package/src/commands/logout.ts +15 -0
- package/src/commands/pack.ts +40 -0
- package/src/commands/pull.ts +276 -0
- package/src/commands/push.ts +228 -0
- package/src/commands/search.ts +58 -0
- package/src/commands/validate.ts +171 -0
- package/src/commands/versions.ts +56 -0
- package/src/commands/whoami.ts +24 -0
- package/src/config.ts +38 -0
- package/src/index.ts +34 -0
- package/src/lib/archive.ts +59 -0
- package/src/lib/discovery.ts +51 -0
- package/src/lib/errors.ts +29 -0
- package/src/lib/manifest.ts +143 -0
- package/src/lib/output.ts +34 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +22 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { resolve, join, dirname } from 'node:path';
|
|
3
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, renameSync } from 'node:fs';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { confirm } from '@inquirer/prompts';
|
|
6
|
+
import { ApiClient } from '../api-client.js';
|
|
7
|
+
import { extract } from '../lib/archive.js';
|
|
8
|
+
import { findClaudeSkillsDir } from '../lib/discovery.js';
|
|
9
|
+
import { spinner, success, info, warn } from '../lib/output.js';
|
|
10
|
+
import { handleError, CliError } from '../lib/errors.js';
|
|
11
|
+
|
|
12
|
+
interface DownloadResponse {
|
|
13
|
+
downloadUrl: string;
|
|
14
|
+
archiveSha256: string;
|
|
15
|
+
archiveSizeBytes: number;
|
|
16
|
+
version: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SkillLockEntry {
|
|
20
|
+
ref: string;
|
|
21
|
+
version?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface SkillsConfig {
|
|
25
|
+
skills: SkillLockEntry[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseSkillRef(ref: string): { scope: string; name: string; version?: string } {
|
|
29
|
+
// Strip leading @ (e.g., @nexus/newsletter → nexus/newsletter)
|
|
30
|
+
const cleaned = ref.startsWith('@') ? ref.slice(1) : ref;
|
|
31
|
+
|
|
32
|
+
// Check for version suffix: nexus/newsletter@1.0.0
|
|
33
|
+
const atIndex = cleaned.lastIndexOf('@');
|
|
34
|
+
let scopeAndName: string;
|
|
35
|
+
let version: string | undefined;
|
|
36
|
+
|
|
37
|
+
if (atIndex > 0) {
|
|
38
|
+
scopeAndName = cleaned.slice(0, atIndex);
|
|
39
|
+
version = cleaned.slice(atIndex + 1);
|
|
40
|
+
} else {
|
|
41
|
+
scopeAndName = cleaned;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const slashIndex = scopeAndName.indexOf('/');
|
|
45
|
+
if (slashIndex < 0) {
|
|
46
|
+
throw new CliError(`Invalid skill reference: ${ref}`, [
|
|
47
|
+
'Use the format: @scope/name or @scope/name@version',
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
scope: scopeAndName.slice(0, slashIndex),
|
|
53
|
+
name: scopeAndName.slice(slashIndex + 1),
|
|
54
|
+
version,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Read the skills config file (.claude/skills.json).
|
|
60
|
+
*/
|
|
61
|
+
function readSkillsConfig(): SkillsConfig | null {
|
|
62
|
+
// Look for .claude/skills.json by walking up
|
|
63
|
+
let dir = resolve('.');
|
|
64
|
+
while (dir !== dirname(dir)) {
|
|
65
|
+
const configPath = join(dir, '.claude', 'skills.json');
|
|
66
|
+
if (existsSync(configPath)) {
|
|
67
|
+
try {
|
|
68
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
69
|
+
return JSON.parse(raw) as SkillsConfig;
|
|
70
|
+
} catch {
|
|
71
|
+
throw new CliError('Failed to parse .claude/skills.json', [
|
|
72
|
+
'Check that the file is valid JSON.',
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
dir = dirname(dir);
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Pull a single skill to a target directory.
|
|
83
|
+
*/
|
|
84
|
+
async function pullSkill(
|
|
85
|
+
scope: string,
|
|
86
|
+
name: string,
|
|
87
|
+
version: string | undefined,
|
|
88
|
+
targetDir: string,
|
|
89
|
+
): Promise<{ version: string }> {
|
|
90
|
+
const spin = spinner(`Fetching ${scope}/${name}${version ? `@${version}` : ''}...`);
|
|
91
|
+
|
|
92
|
+
const client = new ApiClient();
|
|
93
|
+
const query: Record<string, string> = {};
|
|
94
|
+
if (version) {
|
|
95
|
+
query.version = version;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const downloadInfo = await client.get<DownloadResponse>(
|
|
99
|
+
`/api/v1/skills/${scope}/${name}/download`,
|
|
100
|
+
{ query },
|
|
101
|
+
);
|
|
102
|
+
spin.succeed(`Found ${scope}/${name}@${downloadInfo.version}`);
|
|
103
|
+
|
|
104
|
+
// Download the archive
|
|
105
|
+
spin.start('Downloading archive...');
|
|
106
|
+
const response = await fetch(downloadInfo.downloadUrl);
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
spin.fail('Download failed.');
|
|
109
|
+
throw new CliError(`Download failed with status ${response.status}.`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
113
|
+
const archiveData = Buffer.from(arrayBuffer);
|
|
114
|
+
|
|
115
|
+
// Verify SHA-256
|
|
116
|
+
const actualSha = createHash('sha256').update(archiveData).digest('hex');
|
|
117
|
+
if (actualSha !== downloadInfo.archiveSha256) {
|
|
118
|
+
spin.fail('Integrity check failed.');
|
|
119
|
+
throw new CliError(
|
|
120
|
+
`SHA-256 mismatch: expected ${downloadInfo.archiveSha256}, got ${actualSha}`,
|
|
121
|
+
['The archive may be corrupted. Try again.'],
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
spin.succeed('Downloaded and verified.');
|
|
125
|
+
|
|
126
|
+
// Ensure target directory exists
|
|
127
|
+
mkdirSync(targetDir, { recursive: true });
|
|
128
|
+
|
|
129
|
+
// Write temp file and extract to a temp location, then move
|
|
130
|
+
spin.start('Installing...');
|
|
131
|
+
const tempDir = join(dirname(targetDir), `.csreg-tmp-${Date.now()}`);
|
|
132
|
+
mkdirSync(tempDir, { recursive: true });
|
|
133
|
+
const tempArchive = join(tempDir, `${name}.tar.gz`);
|
|
134
|
+
writeFileSync(tempArchive, archiveData);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
await extract(tempArchive, tempDir, downloadInfo.archiveSha256);
|
|
138
|
+
|
|
139
|
+
// The archive extracts to tempDir/<name>/ — move contents to targetDir
|
|
140
|
+
const extractedDir = join(tempDir, name);
|
|
141
|
+
const finalDir = join(targetDir, name);
|
|
142
|
+
|
|
143
|
+
// Remove existing skill directory if it exists (update)
|
|
144
|
+
if (existsSync(finalDir)) {
|
|
145
|
+
// Remove recursively
|
|
146
|
+
const { rmSync } = await import('node:fs');
|
|
147
|
+
rmSync(finalDir, { recursive: true });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
renameSync(extractedDir, finalDir);
|
|
151
|
+
spin.succeed(`Installed to ${finalDir}`);
|
|
152
|
+
|
|
153
|
+
return { version: downloadInfo.version };
|
|
154
|
+
} finally {
|
|
155
|
+
// Clean up temp dir
|
|
156
|
+
try {
|
|
157
|
+
const { rmSync } = await import('node:fs');
|
|
158
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
159
|
+
} catch {
|
|
160
|
+
// ignore cleanup errors
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const pullCommand = new Command('pull')
|
|
166
|
+
.description('Download and install a skill')
|
|
167
|
+
.argument('[ref]', 'Skill reference (@scope/name[@version])')
|
|
168
|
+
.option('--all', 'Pull all skills listed in .claude/skills.json')
|
|
169
|
+
.option('--path <dir>', 'Custom install directory (overrides auto-detection)')
|
|
170
|
+
.action(async (ref: string | undefined, opts: { all?: boolean; path?: string }) => {
|
|
171
|
+
try {
|
|
172
|
+
// --- Mode: pull --all from skills.json ---
|
|
173
|
+
if (opts.all) {
|
|
174
|
+
const config = readSkillsConfig();
|
|
175
|
+
if (!config) {
|
|
176
|
+
throw new CliError('No .claude/skills.json found.', [
|
|
177
|
+
'Create .claude/skills.json with a "skills" array.',
|
|
178
|
+
'Example:\n {\n "skills": [\n { "ref": "@nexus/newsletter" },\n { "ref": "@nexus/code-review", "version": "1.2.0" }\n ]\n }',
|
|
179
|
+
]);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!config.skills || config.skills.length === 0) {
|
|
183
|
+
info('No skills listed in .claude/skills.json. Nothing to pull.');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Determine target
|
|
188
|
+
const skillsDir = opts.path ? resolve(opts.path) : findClaudeSkillsDir();
|
|
189
|
+
if (!skillsDir) {
|
|
190
|
+
throw new CliError('Cannot find .claude/ directory.', [
|
|
191
|
+
'Run this from a project with a .claude/ directory, or use --path.',
|
|
192
|
+
]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log(`Installing ${config.skills.length} skill(s) to ${skillsDir}/\n`);
|
|
196
|
+
|
|
197
|
+
let installed = 0;
|
|
198
|
+
for (const entry of config.skills) {
|
|
199
|
+
try {
|
|
200
|
+
const { scope, name, version } = parseSkillRef(entry.ref);
|
|
201
|
+
const ver = entry.version || version;
|
|
202
|
+
await pullSkill(scope, name, ver, skillsDir);
|
|
203
|
+
installed++;
|
|
204
|
+
console.log('');
|
|
205
|
+
} catch (err) {
|
|
206
|
+
warn(`Failed to pull ${entry.ref}: ${err instanceof Error ? err.message : String(err)}`);
|
|
207
|
+
console.log('');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log('');
|
|
212
|
+
success(`Installed ${installed}/${config.skills.length} skill(s).`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --- Mode: pull single skill ---
|
|
217
|
+
if (!ref) {
|
|
218
|
+
throw new CliError('Missing skill reference.', [
|
|
219
|
+
'Usage: csreg pull @scope/name[@version]',
|
|
220
|
+
'Or use: csreg pull --all (to pull from .claude/skills.json)',
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const { scope, name, version } = parseSkillRef(ref);
|
|
225
|
+
|
|
226
|
+
// Determine install target
|
|
227
|
+
let targetDir: string;
|
|
228
|
+
|
|
229
|
+
if (opts.path) {
|
|
230
|
+
targetDir = resolve(opts.path);
|
|
231
|
+
} else {
|
|
232
|
+
const detected = findClaudeSkillsDir();
|
|
233
|
+
|
|
234
|
+
if (detected) {
|
|
235
|
+
targetDir = detected;
|
|
236
|
+
const finalPath = join(detected, name);
|
|
237
|
+
const ok = await confirm({
|
|
238
|
+
message: `Install to ${finalPath}?`,
|
|
239
|
+
default: true,
|
|
240
|
+
});
|
|
241
|
+
if (!ok) {
|
|
242
|
+
const { input } = await import('@inquirer/prompts');
|
|
243
|
+
const custom = await input({
|
|
244
|
+
message: 'Custom install path:',
|
|
245
|
+
default: join('.', '.claude', 'skills'),
|
|
246
|
+
});
|
|
247
|
+
targetDir = resolve(custom);
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
// No .claude/ found — default to .claude/skills/ in cwd
|
|
251
|
+
targetDir = join(resolve('.'), '.claude', 'skills');
|
|
252
|
+
info(`No .claude/ directory found. Will create ${targetDir}/`);
|
|
253
|
+
const ok = await confirm({
|
|
254
|
+
message: `Install to ${join(targetDir, name)}?`,
|
|
255
|
+
default: true,
|
|
256
|
+
});
|
|
257
|
+
if (!ok) {
|
|
258
|
+
const { input } = await import('@inquirer/prompts');
|
|
259
|
+
const custom = await input({
|
|
260
|
+
message: 'Custom install path:',
|
|
261
|
+
default: targetDir,
|
|
262
|
+
});
|
|
263
|
+
targetDir = resolve(custom);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const result = await pullSkill(scope, name, version, targetDir);
|
|
269
|
+
|
|
270
|
+
console.log('');
|
|
271
|
+
success(`Pulled ${scope}/${name}@${result.version}`);
|
|
272
|
+
info(`Skill is ready at ${join(targetDir, name)}/SKILL.md`);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
handleError(err);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { resolve, join, basename } from 'node:path';
|
|
3
|
+
import { existsSync, readFileSync, statSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { runValidation } from './validate.js';
|
|
6
|
+
import { pack as archivePack } from '../lib/archive.js';
|
|
7
|
+
import { parseManifest } from '../lib/manifest.js';
|
|
8
|
+
import { findClaudeSkillsDir, discoverSkillDirs } from '../lib/discovery.js';
|
|
9
|
+
import { ApiClient } from '../api-client.js';
|
|
10
|
+
import { spinner, success, warn } from '../lib/output.js';
|
|
11
|
+
import { handleError, CliError } from '../lib/errors.js';
|
|
12
|
+
import { getAuthToken } from '../config.js';
|
|
13
|
+
|
|
14
|
+
interface PrepareResponse {
|
|
15
|
+
versionId: string;
|
|
16
|
+
uploadUrl: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function collectFileTree(dir: string, prefix = ''): Array<{ path: string; size: number; sha256: string }> {
|
|
20
|
+
const files: Array<{ path: string; size: number; sha256: string }> = [];
|
|
21
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
22
|
+
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
25
|
+
const fullPath = join(dir, entry.name);
|
|
26
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
27
|
+
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
files.push(...collectFileTree(fullPath, relativePath));
|
|
30
|
+
} else {
|
|
31
|
+
const stat = statSync(fullPath);
|
|
32
|
+
const content = readFileSync(fullPath);
|
|
33
|
+
const sha256 = createHash('sha256').update(content).digest('hex');
|
|
34
|
+
files.push({ path: relativePath, size: stat.size, sha256 });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return files;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Push a single skill directory to the registry.
|
|
42
|
+
* Returns the published version string, or throws on failure.
|
|
43
|
+
*/
|
|
44
|
+
async function pushSingle(resolved: string): Promise<string> {
|
|
45
|
+
// Step 1: Validate
|
|
46
|
+
const spin = spinner('Validating skill...');
|
|
47
|
+
const validation = runValidation(resolved);
|
|
48
|
+
if (!validation.valid) {
|
|
49
|
+
spin.fail('Validation failed.');
|
|
50
|
+
for (const e of validation.errors) {
|
|
51
|
+
console.error(' ' + e);
|
|
52
|
+
}
|
|
53
|
+
throw new CliError('Fix validation errors before pushing.');
|
|
54
|
+
}
|
|
55
|
+
spin.succeed('Validation passed.');
|
|
56
|
+
|
|
57
|
+
// Step 2: Parse manifest from SKILL.md frontmatter
|
|
58
|
+
const manifest = parseManifest(resolved);
|
|
59
|
+
|
|
60
|
+
if (!manifest.version) {
|
|
61
|
+
throw new CliError('Missing "version" in SKILL.md frontmatter.', [
|
|
62
|
+
'Add a version field: version: "1.0.0"',
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
if (!manifest.scope) {
|
|
66
|
+
throw new CliError('Missing "scope" in SKILL.md frontmatter.', [
|
|
67
|
+
'Add a scope field: scope: my-team',
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Step 3: Pack
|
|
72
|
+
spin.start('Packing skill...');
|
|
73
|
+
const archive = await archivePack(resolved);
|
|
74
|
+
spin.succeed(`Packed (${(archive.size / 1024).toFixed(1)}KB).`);
|
|
75
|
+
|
|
76
|
+
// Step 4: Ensure skill exists in registry (auto-create on first push)
|
|
77
|
+
const client = new ApiClient();
|
|
78
|
+
spin.start('Checking registry...');
|
|
79
|
+
try {
|
|
80
|
+
await client.get(`/api/v1/skills/${manifest.scope}/${manifest.name}`);
|
|
81
|
+
spin.succeed('Skill found in registry.');
|
|
82
|
+
} catch {
|
|
83
|
+
spin.start(`Creating skill ${manifest.scope}/${manifest.name}...`);
|
|
84
|
+
await client.post('/api/v1/skills', {
|
|
85
|
+
body: {
|
|
86
|
+
scope: manifest.scope,
|
|
87
|
+
slug: manifest.name,
|
|
88
|
+
displayName: manifest.name,
|
|
89
|
+
description: manifest.description || '',
|
|
90
|
+
tags: manifest.tags || [],
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
spin.succeed(`Created skill ${manifest.scope}/${manifest.name}.`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Step 5: Prepare version
|
|
97
|
+
spin.start('Preparing version...');
|
|
98
|
+
const fileTree = collectFileTree(resolved);
|
|
99
|
+
|
|
100
|
+
const prepared = await client.post<PrepareResponse>(
|
|
101
|
+
`/api/v1/skills/${manifest.scope}/${manifest.name}/versions`,
|
|
102
|
+
{
|
|
103
|
+
body: {
|
|
104
|
+
version: manifest.version,
|
|
105
|
+
fileTree,
|
|
106
|
+
archiveSha256: archive.sha256,
|
|
107
|
+
archiveSize: archive.size,
|
|
108
|
+
entryPoint: 'SKILL.md',
|
|
109
|
+
manifestJson: {
|
|
110
|
+
name: manifest.name,
|
|
111
|
+
description: manifest.description,
|
|
112
|
+
version: manifest.version,
|
|
113
|
+
scope: manifest.scope,
|
|
114
|
+
'allowed-tools': manifest['allowed-tools'],
|
|
115
|
+
'argument-hint': manifest['argument-hint'],
|
|
116
|
+
'disable-model-invocation': manifest['disable-model-invocation'],
|
|
117
|
+
'user-invocable': manifest['user-invocable'],
|
|
118
|
+
tags: manifest.tags,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
spin.succeed('Version prepared.');
|
|
124
|
+
|
|
125
|
+
// Step 6: Upload to presigned URL
|
|
126
|
+
spin.start('Uploading archive...');
|
|
127
|
+
const archiveData = readFileSync(archive.path);
|
|
128
|
+
const uploadResponse = await fetch(prepared.uploadUrl, {
|
|
129
|
+
method: 'PUT',
|
|
130
|
+
headers: {
|
|
131
|
+
'Content-Type': 'application/gzip',
|
|
132
|
+
'Content-Length': String(archive.size),
|
|
133
|
+
},
|
|
134
|
+
body: archiveData,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (!uploadResponse.ok) {
|
|
138
|
+
spin.fail('Upload failed.');
|
|
139
|
+
throw new CliError(`Upload failed with status ${uploadResponse.status}.`, [
|
|
140
|
+
'Try again. If the problem persists, contact support.',
|
|
141
|
+
]);
|
|
142
|
+
}
|
|
143
|
+
spin.succeed('Archive uploaded.');
|
|
144
|
+
|
|
145
|
+
// Step 7: Finalize
|
|
146
|
+
spin.start('Finalizing version...');
|
|
147
|
+
await client.post(
|
|
148
|
+
`/api/v1/skills/${manifest.scope}/${manifest.name}/versions/${manifest.version}/finalize`,
|
|
149
|
+
{
|
|
150
|
+
body: { status: 'published' },
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
spin.succeed('Version finalized.');
|
|
154
|
+
|
|
155
|
+
return `${manifest.scope}/${manifest.name}@${manifest.version}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const pushCommand = new Command('push')
|
|
159
|
+
.description('Publish a skill to the registry')
|
|
160
|
+
.argument('[dir]', 'Skill directory', '.')
|
|
161
|
+
.option('--all', 'Push all skills in .claude/skills/')
|
|
162
|
+
.action(async (dir: string, opts: { all?: boolean }) => {
|
|
163
|
+
try {
|
|
164
|
+
if (!getAuthToken()) {
|
|
165
|
+
throw new CliError('Not logged in.', ['Run `csreg login` to authenticate.']);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// --- Mode: push --all ---
|
|
169
|
+
if (opts.all) {
|
|
170
|
+
const skillsDir = findClaudeSkillsDir();
|
|
171
|
+
if (!skillsDir) {
|
|
172
|
+
throw new CliError('Cannot find .claude/skills/ directory.', [
|
|
173
|
+
'Run this from a project with a .claude/ directory.',
|
|
174
|
+
]);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const skillDirs = discoverSkillDirs(skillsDir);
|
|
178
|
+
if (skillDirs.length === 0) {
|
|
179
|
+
throw new CliError(`No skills found in ${skillsDir}/`, [
|
|
180
|
+
'Skills must be directories containing a SKILL.md file.',
|
|
181
|
+
]);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(`Pushing ${skillDirs.length} skill(s) from ${skillsDir}/\n`);
|
|
185
|
+
|
|
186
|
+
let published = 0;
|
|
187
|
+
let failed = 0;
|
|
188
|
+
|
|
189
|
+
for (const skillDir of skillDirs) {
|
|
190
|
+
const name = basename(skillDir);
|
|
191
|
+
console.log(`\n--- ${name} ---\n`);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const ref = await pushSingle(skillDir);
|
|
195
|
+
success(`Published ${ref}`);
|
|
196
|
+
published++;
|
|
197
|
+
} catch (err) {
|
|
198
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
199
|
+
warn(`Failed to push ${name}: ${msg}`);
|
|
200
|
+
failed++;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log(`\n${'─'.repeat(40)}\n`);
|
|
205
|
+
if (failed === 0) {
|
|
206
|
+
success(`All ${published} skill(s) published.`);
|
|
207
|
+
} else {
|
|
208
|
+
success(`Published ${published}/${published + failed} skill(s).`);
|
|
209
|
+
if (failed > 0) {
|
|
210
|
+
warn(`${failed} skill(s) failed. Check the errors above.`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --- Mode: push single ---
|
|
217
|
+
const resolved = resolve(dir);
|
|
218
|
+
if (!existsSync(resolved)) {
|
|
219
|
+
throw new CliError(`Directory not found: ${resolved}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const ref = await pushSingle(resolved);
|
|
223
|
+
console.log('');
|
|
224
|
+
success(`Published ${ref}`);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
handleError(err);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { ApiClient } from '../api-client.js';
|
|
3
|
+
import { formatTable, info } from '../lib/output.js';
|
|
4
|
+
import { handleError } from '../lib/errors.js';
|
|
5
|
+
|
|
6
|
+
interface SearchResult {
|
|
7
|
+
scope: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
totalDownloads: number;
|
|
11
|
+
latestVersion: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface SearchResponse {
|
|
15
|
+
skills: SearchResult[];
|
|
16
|
+
total: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const searchCommand = new Command('search')
|
|
20
|
+
.description('Search for skills in the registry')
|
|
21
|
+
.argument('<query>', 'Search query')
|
|
22
|
+
.option('-t, --type <type>', 'Filter by skill type')
|
|
23
|
+
.option('-l, --limit <limit>', 'Max results', '20')
|
|
24
|
+
.action(async (query: string, opts: { type?: string; limit: string }) => {
|
|
25
|
+
try {
|
|
26
|
+
const client = new ApiClient();
|
|
27
|
+
const queryParams: Record<string, string> = {
|
|
28
|
+
q: query,
|
|
29
|
+
limit: opts.limit,
|
|
30
|
+
};
|
|
31
|
+
if (opts.type) {
|
|
32
|
+
queryParams.type = opts.type;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const data = await client.get<SearchResponse>('/api/v1/skills', {
|
|
36
|
+
query: queryParams,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (data.skills.length === 0) {
|
|
40
|
+
info(`No skills found for "${query}".`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const rows = data.skills.map(s => [
|
|
45
|
+
`${s.scope}/${s.name}`,
|
|
46
|
+
s.description?.slice(0, 50) ?? '',
|
|
47
|
+
String(s.totalDownloads),
|
|
48
|
+
s.latestVersion,
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(formatTable(['Skill', 'Description', 'Downloads', 'Version'], rows));
|
|
53
|
+
console.log('');
|
|
54
|
+
info(`Showing ${data.skills.length} of ${data.total} results.`);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
handleError(err);
|
|
57
|
+
}
|
|
58
|
+
});
|