skillsforest 0.1.1 → 0.2.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/bin/skillsforest.js +2 -2
- package/package.json +1 -1
- package/src/commands/add.js +13 -6
- package/src/commands/search.js +2 -2
- package/src/lib/registry.js +29 -23
package/bin/skillsforest.js
CHANGED
|
@@ -13,8 +13,8 @@ program
|
|
|
13
13
|
.version('0.1.0');
|
|
14
14
|
|
|
15
15
|
program
|
|
16
|
-
.command('add <slug>')
|
|
17
|
-
.description('Install a skill by slug from the registry')
|
|
16
|
+
.command('add <owner/slug>')
|
|
17
|
+
.description('Install a skill by owner/slug from the registry (e.g. owner/my-skill)')
|
|
18
18
|
.option('--agent <type>', 'Agent type: cursor, claude, codex, generic')
|
|
19
19
|
.option('--path <dir>', 'Custom install path (relative to cwd)')
|
|
20
20
|
.action(addCommand);
|
package/package.json
CHANGED
package/src/commands/add.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const readline = require('readline');
|
|
2
|
-
const { download } = require('../lib/registry');
|
|
2
|
+
const { download, parseRegistryIdentifier } = require('../lib/registry');
|
|
3
3
|
const { detectAgent, getSkillsDir } = require('../lib/detector');
|
|
4
4
|
const { install } = require('../lib/installer');
|
|
5
5
|
|
|
@@ -14,7 +14,12 @@ function promptAgent() {
|
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
async function addCommand(
|
|
17
|
+
async function addCommand(registrySlug, opts) {
|
|
18
|
+
if (!parseRegistryIdentifier(registrySlug)) {
|
|
19
|
+
console.error('Use owner/slug format (e.g. npx skillsforest add owner/my-skill).');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
let agent = opts.agent;
|
|
19
24
|
const cwd = process.cwd();
|
|
20
25
|
|
|
@@ -34,15 +39,17 @@ async function addCommand(slug, opts) {
|
|
|
34
39
|
const targetDir = opts.path ? require('path').resolve(cwd, opts.path) : null;
|
|
35
40
|
|
|
36
41
|
try {
|
|
37
|
-
const data = await download(
|
|
42
|
+
const data = await download(registrySlug);
|
|
38
43
|
if (!data.files || !Array.isArray(data.files)) {
|
|
39
44
|
throw new Error('Invalid response: missing files');
|
|
40
45
|
}
|
|
41
|
-
const skillDir = install(
|
|
46
|
+
const skillDir = install(data.skill, data.files, agent, targetDir);
|
|
42
47
|
console.log(`Installed "${data.skill}" (v${data.version || '1.0.0'}) to ${skillDir}`);
|
|
43
48
|
} catch (err) {
|
|
44
|
-
if (err.status ===
|
|
45
|
-
console.error(
|
|
49
|
+
if (err.status === 400) {
|
|
50
|
+
console.error(err.message);
|
|
51
|
+
} else if (err.status === 404) {
|
|
52
|
+
console.error(`Skill "${registrySlug}" not found. It may be private (use "skillsforest login") or the identifier may be wrong.`);
|
|
46
53
|
} else if (err.status === 403) {
|
|
47
54
|
console.error('Access denied. Token may be missing or invalid (try "skillsforest login").');
|
|
48
55
|
} else {
|
package/src/commands/search.js
CHANGED
|
@@ -13,10 +13,10 @@ async function searchCommand(query) {
|
|
|
13
13
|
console.log('No skills found.');
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
|
-
console.log(formatRow('
|
|
16
|
+
console.log(formatRow('REGISTRY SLUG', 'DESCRIPTION', 'AUTHOR'));
|
|
17
17
|
console.log('-'.repeat(24 + 62 + 17));
|
|
18
18
|
for (const s of items) {
|
|
19
|
-
console.log(formatRow(s.slug || s.name, s.description, s.author));
|
|
19
|
+
console.log(formatRow(s.registry_slug || s.slug || s.name, s.description, s.author));
|
|
20
20
|
}
|
|
21
21
|
const total = data.meta?.total ?? items.length;
|
|
22
22
|
if (total > items.length) {
|
package/src/lib/registry.js
CHANGED
|
@@ -10,9 +10,8 @@ async function fetchRegistry(pathname, options = {}) {
|
|
|
10
10
|
headers.Authorization = `Bearer ${token}`;
|
|
11
11
|
}
|
|
12
12
|
const res = await fetch(url, { ...options, headers });
|
|
13
|
-
const body = await res.text();
|
|
14
|
-
|
|
15
13
|
if (!res.ok) {
|
|
14
|
+
const body = await res.text();
|
|
16
15
|
let msg = `Registry request failed: ${res.status} ${res.statusText}`;
|
|
17
16
|
try {
|
|
18
17
|
const j = JSON.parse(body);
|
|
@@ -24,23 +23,7 @@ async function fetchRegistry(pathname, options = {}) {
|
|
|
24
23
|
err.status = res.status;
|
|
25
24
|
throw err;
|
|
26
25
|
}
|
|
27
|
-
|
|
28
|
-
const contentType = res.headers.get('content-type') || '';
|
|
29
|
-
if (!contentType.includes('application/json')) {
|
|
30
|
-
const preview = body.trimStart().slice(0, 80);
|
|
31
|
-
throw new Error(
|
|
32
|
-
`Registry returned non-JSON (${contentType || 'unknown type'}). ` +
|
|
33
|
-
(body.trimStart().startsWith('<')
|
|
34
|
-
? 'Got HTML — the API URL may be wrong or the service may be returning an error page.'
|
|
35
|
-
: `Body preview: ${preview}`)
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
try {
|
|
39
|
-
return JSON.parse(body);
|
|
40
|
-
} catch (e) {
|
|
41
|
-
const preview = body.trimStart().slice(0, 80);
|
|
42
|
-
throw new Error(`Invalid JSON from registry. Body preview: ${preview}`);
|
|
43
|
-
}
|
|
26
|
+
return res.json();
|
|
44
27
|
}
|
|
45
28
|
|
|
46
29
|
async function search(query, params = {}) {
|
|
@@ -48,16 +31,39 @@ async function search(query, params = {}) {
|
|
|
48
31
|
return fetchRegistry(`/?${sp}`);
|
|
49
32
|
}
|
|
50
33
|
|
|
51
|
-
|
|
52
|
-
|
|
34
|
+
function parseRegistryIdentifier(registrySlug) {
|
|
35
|
+
const parts = registrySlug.split('/');
|
|
36
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return { owner: parts[0], slug: parts[1] };
|
|
53
40
|
}
|
|
54
41
|
|
|
55
|
-
async function
|
|
56
|
-
|
|
42
|
+
async function show(registrySlug) {
|
|
43
|
+
const id = parseRegistryIdentifier(registrySlug);
|
|
44
|
+
if (!id) {
|
|
45
|
+
const err = new Error('Use owner/slug format (e.g. owner/my-skill)');
|
|
46
|
+
err.status = 400;
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
const pathname = `/${encodeURIComponent(id.owner)}/${encodeURIComponent(id.slug)}`;
|
|
50
|
+
return fetchRegistry(pathname);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function download(registrySlug) {
|
|
54
|
+
const id = parseRegistryIdentifier(registrySlug);
|
|
55
|
+
if (!id) {
|
|
56
|
+
const err = new Error('Use owner/slug format (e.g. owner/my-skill)');
|
|
57
|
+
err.status = 400;
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
const pathname = `/${encodeURIComponent(id.owner)}/${encodeURIComponent(id.slug)}/download`;
|
|
61
|
+
return fetchRegistry(pathname);
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
module.exports = {
|
|
60
65
|
search,
|
|
61
66
|
show,
|
|
62
67
|
download,
|
|
68
|
+
parseRegistryIdentifier,
|
|
63
69
|
};
|