skills-atlas-cli 0.5.0 → 0.6.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/README.md +17 -0
- package/bin/skills.js +3 -1
- package/package.json +1 -1
- package/src/args.js +1 -0
- package/src/commands/registry.js +66 -0
- package/src/commands/update.js +12 -6
- package/src/data.js +49 -8
- package/src/index-build.js +3 -0
- package/src/merge.js +17 -0
- package/src/registry.js +78 -0
package/README.md
CHANGED
|
@@ -125,6 +125,23 @@ On top of that, `install` can place a skill straight into `.claude/skills/`:
|
|
|
125
125
|
The catalog ships inside the package and works offline. `skills-atlas update` pulls
|
|
126
126
|
the latest from the public feed (cached under `~/.cache/skills-atlas/`).
|
|
127
127
|
|
|
128
|
+
## Private / org catalog sources
|
|
129
|
+
|
|
130
|
+
Point the CLI at your organization's own catalog — a `data.json` in the same
|
|
131
|
+
schema — so internal skills show up in `search` / `info` / `install` / `kit`
|
|
132
|
+
alongside the public Atlas:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
skills-atlas registry add https://skills.acme.internal/data.json # or a local path
|
|
136
|
+
skills-atlas registry list
|
|
137
|
+
skills-atlas registry remove https://skills.acme.internal/data.json
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Private skills **merge** with the public catalog (a private source wins a same-name
|
|
141
|
+
clash). Sources are cached locally and merged offline. For a private URL behind
|
|
142
|
+
auth, set `SKILLS_ATLAS_TOKEN` (sent as a Bearer header); in CI,
|
|
143
|
+
`SKILLS_ATLAS_SOURCES=url1,url2` adds sources without touching config.
|
|
144
|
+
|
|
128
145
|
## In Claude Code
|
|
129
146
|
|
|
130
147
|
A thin [Claude Code plugin](./plugin) lets Claude do all of this in-conversation —
|
package/bin/skills.js
CHANGED
|
@@ -11,6 +11,7 @@ const outdated = require('../src/commands/outdated');
|
|
|
11
11
|
const doctor = require('../src/commands/doctor');
|
|
12
12
|
const kit = require('../src/commands/kit');
|
|
13
13
|
const sync = require('../src/commands/sync');
|
|
14
|
+
const registry = require('../src/commands/registry');
|
|
14
15
|
const suggest = require('../src/commands/suggest');
|
|
15
16
|
const hook = require('../src/commands/hook');
|
|
16
17
|
const update = require('../src/commands/update');
|
|
@@ -19,7 +20,7 @@ const { categories, list } = require('../src/commands/categories');
|
|
|
19
20
|
const VERSION = require('../package.json').version;
|
|
20
21
|
// `use` = install + activate inline (emit the SKILL.md so an agent follows it now).
|
|
21
22
|
const use = argv => install([...argv, '--inline']);
|
|
22
|
-
const commands = { search, info, install, use, kit, sync, installed, upgrade, remove, outdated, doctor, suggest, hook, update, categories, list };
|
|
23
|
+
const commands = { search, info, install, use, kit, sync, installed, upgrade, remove, outdated, doctor, suggest, hook, update, categories, list, registry };
|
|
23
24
|
|
|
24
25
|
const HELP = `skills-atlas — search, install & manage AI agent skills
|
|
25
26
|
|
|
@@ -47,6 +48,7 @@ catalog:
|
|
|
47
48
|
update refresh the catalog from the public data feed
|
|
48
49
|
categories list the top-level categories
|
|
49
50
|
list [category] list skill groups (optionally within one category)
|
|
51
|
+
registry add/list/remove a private catalog source (org-internal skills)
|
|
50
52
|
|
|
51
53
|
global flags: --zh (中文 output; English by default), --json (machine output), -h/--help
|
|
52
54
|
docs: https://zita-go.github.io/Skills-Atlas/`;
|
package/package.json
CHANGED
package/src/args.js
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// `skills-atlas registry add|list|remove` — manage private catalog sources.
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { parse } = require('../args');
|
|
5
|
+
const { fetchSource, counts, PUBLIC_URL } = require('../data');
|
|
6
|
+
const registry = require('../registry');
|
|
7
|
+
const { green, dim, cyan, bold } = require('../format');
|
|
8
|
+
|
|
9
|
+
const HELP = `usage: skills-atlas registry <add|list|remove> [url|path]
|
|
10
|
+
|
|
11
|
+
Manage private catalog sources (a data.json in the same schema). Their skills
|
|
12
|
+
merge into search / info / install / kit; a private source wins a name clash.
|
|
13
|
+
|
|
14
|
+
add <url|path> add a source (--name <label>); fetches + caches it now
|
|
15
|
+
list show the public default + your private sources
|
|
16
|
+
remove <url|path> drop a source
|
|
17
|
+
|
|
18
|
+
A source can be an https URL or a local file path. For a private URL behind auth,
|
|
19
|
+
set SKILLS_ATLAS_TOKEN (sent as a Bearer header). CI: SKILLS_ATLAS_SOURCES=url1,url2`;
|
|
20
|
+
|
|
21
|
+
module.exports = async function registryCmd(argv) {
|
|
22
|
+
const { values, positionals } = parse(argv, ['json', 'name']);
|
|
23
|
+
if (values.help) { console.log(HELP); return; }
|
|
24
|
+
const sub = positionals[0] || 'list';
|
|
25
|
+
const url = positionals[1];
|
|
26
|
+
|
|
27
|
+
if (sub === 'list') {
|
|
28
|
+
const srcs = registry.effectiveSources();
|
|
29
|
+
if (values.json) { console.log(JSON.stringify(srcs, null, 2)); return; }
|
|
30
|
+
console.log(`${bold('public')} ${PUBLIC_URL} ${dim('(default)')}`);
|
|
31
|
+
if (!srcs.length) { console.log(dim('no private sources. add one: skills-atlas registry add <url|path>')); return; }
|
|
32
|
+
for (const s of srcs) {
|
|
33
|
+
const cached = registry.readCachedSource(s.url);
|
|
34
|
+
const n = cached ? Object.keys(cached.vendors || {}).length : 0;
|
|
35
|
+
console.log(`${green('•')} ${s.name ? bold(s.name) + ' ' : ''}${s.url} ${dim(cached ? n + ' vendors' : 'not fetched — run skills-atlas update')}`);
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (sub === 'add') {
|
|
41
|
+
if (!url) { console.error('usage: skills-atlas registry add <url|path>'); process.exitCode = 1; return; }
|
|
42
|
+
try {
|
|
43
|
+
const d = await fetchSource(url);
|
|
44
|
+
registry.cacheSource(url, d);
|
|
45
|
+
registry.addSource(url, { name: values.name });
|
|
46
|
+
const c = counts(d);
|
|
47
|
+
if (values.json) { console.log(JSON.stringify({ added: url, counts: c })); return; }
|
|
48
|
+
console.log(`${green('✓')} added private source ${cyan(url)} ${dim(`(${c.vendors} vendors, ${c.groups} groups)`)}`);
|
|
49
|
+
console.log(dim('its skills now appear in search / info / install / kit.'));
|
|
50
|
+
} catch (e) { console.error(`failed to add ${url}: ${e.message}`); process.exitCode = 1; }
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (sub === 'remove') {
|
|
55
|
+
if (!url) { console.error('usage: skills-atlas registry remove <url|path>'); process.exitCode = 1; return; }
|
|
56
|
+
const removed = registry.removeSource(url);
|
|
57
|
+
registry.removeCachedSource(url);
|
|
58
|
+
if (values.json) { console.log(JSON.stringify({ removed: removed ? url : null })); if (!removed) process.exitCode = 1; return; }
|
|
59
|
+
if (removed) console.log(`${green('✓')} removed ${url}`);
|
|
60
|
+
else { console.error(`not a configured source: ${url}`); process.exitCode = 1; }
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.error(`unknown registry subcommand '${sub}'. use: add | list | remove`);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
};
|
package/src/commands/update.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { parse } = require('../args');
|
|
4
|
-
const { refreshData } = require('../data');
|
|
4
|
+
const { refreshData, refreshSources } = require('../data');
|
|
5
5
|
|
|
6
6
|
module.exports = async function update(argv) {
|
|
7
7
|
const { values } = parse(argv, ['json']);
|
|
@@ -12,14 +12,20 @@ module.exports = async function update(argv) {
|
|
|
12
12
|
|
|
13
13
|
try {
|
|
14
14
|
const res = await refreshData();
|
|
15
|
-
|
|
15
|
+
const sources = await refreshSources();
|
|
16
|
+
if (values.json) { console.log(JSON.stringify({ ...res, sources }, null, 2)); return; }
|
|
16
17
|
if (!res.changed) {
|
|
17
18
|
console.log(`catalog already up to date (${res.counts.vendors} vendors, ${res.counts.groups} groups).`);
|
|
18
|
-
|
|
19
|
+
} else {
|
|
20
|
+
const p = res.prevCounts;
|
|
21
|
+
const was = p ? ` ${'(was ' + p.vendors + ' vendors / ' + p.groups + ' groups)'}` : '';
|
|
22
|
+
console.log(`updated: ${res.counts.sections} sections, ${res.counts.groups} groups, ${res.counts.vendors} vendors.${was}`);
|
|
23
|
+
}
|
|
24
|
+
if (sources.length) {
|
|
25
|
+
const ok = sources.filter(s => s.ok).length;
|
|
26
|
+
const failed = sources.filter(s => !s.ok).map(s => s.url);
|
|
27
|
+
console.log(`private sources: ${ok}/${sources.length} refreshed${failed.length ? ' (failed: ' + failed.join(', ') + ')' : ''}.`);
|
|
19
28
|
}
|
|
20
|
-
const p = res.prevCounts;
|
|
21
|
-
const was = p ? ` ${'(was ' + p.vendors + ' vendors / ' + p.groups + ' groups)'}` : '';
|
|
22
|
-
console.log(`updated: ${res.counts.sections} sections, ${res.counts.groups} groups, ${res.counts.vendors} vendors.${was}`);
|
|
23
29
|
} catch (e) {
|
|
24
30
|
console.error(`update failed: ${e.message}`);
|
|
25
31
|
process.exitCode = 1;
|
package/src/data.js
CHANGED
|
@@ -9,6 +9,9 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const os = require('os');
|
|
11
11
|
|
|
12
|
+
const registry = require('./registry');
|
|
13
|
+
const { mergeCatalogs } = require('./merge');
|
|
14
|
+
|
|
12
15
|
const PUBLIC_URL = 'https://zita-go.github.io/Skills-Atlas/data.json';
|
|
13
16
|
const UA = 'skills-atlas-cli';
|
|
14
17
|
const STALE_DAYS = 30;
|
|
@@ -75,13 +78,15 @@ function maybeStaleNudge(fromCache) {
|
|
|
75
78
|
|
|
76
79
|
function loadData({ quiet = false } = {}) {
|
|
77
80
|
const cached = tryReadJSON(cacheFile());
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
let base, info;
|
|
82
|
+
if (isValid(cached)) { base = cached; info = { source: 'cache', fromCache: true }; if (!quiet) maybeStaleNudge(true); }
|
|
83
|
+
else { const b = loadBundled(); base = b.data; info = { source: b.source, fromCache: false }; if (!quiet) maybeStaleNudge(false); }
|
|
84
|
+
|
|
85
|
+
const overlays = registry.effectiveSources()
|
|
86
|
+
.map(s => registry.readCachedSource(s.url))
|
|
87
|
+
.filter(isValid);
|
|
88
|
+
if (!overlays.length) return { data: base, ...info };
|
|
89
|
+
return { data: mergeCatalogs(base, overlays), source: `${info.source}+${overlays.length}private`, fromCache: info.fromCache };
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
function ensureDir(d) {
|
|
@@ -147,4 +152,40 @@ async function refreshData() {
|
|
|
147
152
|
};
|
|
148
153
|
}
|
|
149
154
|
|
|
150
|
-
|
|
155
|
+
// Fetch (or read, for a local path / file:// URL) a private catalog source and
|
|
156
|
+
// validate it. Honors SKILLS_ATLAS_TOKEN (Bearer) and SKILLS_ATLAS_TIMEOUT_MS.
|
|
157
|
+
async function fetchSource(src) {
|
|
158
|
+
if (!/^https?:\/\//i.test(src)) { // local file path or file:// URL
|
|
159
|
+
const p = src.replace(/^file:\/\//, '');
|
|
160
|
+
let d;
|
|
161
|
+
try { d = JSON.parse(fs.readFileSync(p, 'utf8')); } catch (e) { throw new Error(`${src}: ${e.message}`); }
|
|
162
|
+
if (!isValid(d)) throw new Error(`${src}: not a valid catalog (sections/vendors)`);
|
|
163
|
+
return d;
|
|
164
|
+
}
|
|
165
|
+
const headers = { 'User-Agent': UA, Accept: 'application/json' };
|
|
166
|
+
if (process.env.SKILLS_ATLAS_TOKEN) headers.Authorization = `Bearer ${process.env.SKILLS_ATLAS_TOKEN}`;
|
|
167
|
+
const ac = new AbortController();
|
|
168
|
+
const timeoutMs = Number(process.env.SKILLS_ATLAS_TIMEOUT_MS) || 25000;
|
|
169
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
170
|
+
let res;
|
|
171
|
+
try { res = await fetch(src, { headers, signal: ac.signal }); }
|
|
172
|
+
catch (e) { throw new Error(e.name === 'AbortError' ? `timed out fetching ${src}` : `network error fetching ${src}: ${e.message}`); }
|
|
173
|
+
finally { clearTimeout(timer); }
|
|
174
|
+
if (!res.ok) throw new Error(`fetch failed: HTTP ${res.status} ${res.statusText} (${src})`);
|
|
175
|
+
let d;
|
|
176
|
+
try { d = JSON.parse(await res.text()); } catch { throw new Error(`${src}: not valid JSON`); }
|
|
177
|
+
if (!isValid(d)) throw new Error(`${src}: not a valid catalog (sections/vendors)`);
|
|
178
|
+
return d;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Refresh every configured private source into its local cache (best-effort).
|
|
182
|
+
async function refreshSources() {
|
|
183
|
+
const out = [];
|
|
184
|
+
for (const s of registry.effectiveSources()) {
|
|
185
|
+
try { const d = await fetchSource(s.url); registry.cacheSource(s.url, d); out.push({ url: s.url, ok: true, counts: counts(d) }); }
|
|
186
|
+
catch (e) { out.push({ url: s.url, ok: false, error: e.message }); }
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = { loadData, refreshData, fetchSource, refreshSources, counts, cacheDir, PUBLIC_URL };
|
package/src/index-build.js
CHANGED
|
@@ -53,6 +53,9 @@ function vendorsFor(skillIndex, skillName) {
|
|
|
53
53
|
if (id && !seen.has(id)) seen.set(id, e);
|
|
54
54
|
}
|
|
55
55
|
return [...seen.values()].sort((a, b) => {
|
|
56
|
+
const pa = a.vendor && a.vendor._private ? 1 : 0;
|
|
57
|
+
const pb = b.vendor && b.vendor._private ? 1 : 0;
|
|
58
|
+
if (pa !== pb) return pb - pa; // private sources first (registry override)
|
|
56
59
|
const da = skillDocPath(a.vendor, a.skill) ? 1 : 0;
|
|
57
60
|
const db = skillDocPath(b.vendor, b.skill) ? 1 : 0;
|
|
58
61
|
if (da !== db) return db - da;
|
package/src/merge.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Merge a base catalog with private overlays. Private vendors are tagged `_private`
|
|
2
|
+
// so `vendorsFor` ranks them first on a same-name clash. Pure — returns a new object,
|
|
3
|
+
// never mutates inputs, and the result lives only in memory (never cached).
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
function mergeCatalogs(base, overlays = []) {
|
|
7
|
+
const vendors = { ...(base.vendors || {}) };
|
|
8
|
+
const sections = [...(base.sections || [])];
|
|
9
|
+
for (const ov of overlays) {
|
|
10
|
+
if (!ov) continue;
|
|
11
|
+
for (const [id, v] of Object.entries(ov.vendors || {})) vendors[id] = { ...v, _private: true };
|
|
12
|
+
for (const s of (ov.sections || [])) sections.push(s);
|
|
13
|
+
}
|
|
14
|
+
return { sections, vendors };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { mergeCatalogs };
|
package/src/registry.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Private catalog sources ("registry"): user config + per-source local cache, so
|
|
2
|
+
// an org can point the CLI at its own data.json alongside the public Atlas.
|
|
3
|
+
// Self-contained (own XDG path helpers) to avoid a require cycle with data.js.
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
|
|
11
|
+
function configDir() {
|
|
12
|
+
const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
13
|
+
return path.join(base, 'skills-atlas');
|
|
14
|
+
}
|
|
15
|
+
const configFile = () => path.join(configDir(), 'config.json');
|
|
16
|
+
|
|
17
|
+
function cacheDir() {
|
|
18
|
+
const base = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
19
|
+
return path.join(base, 'skills-atlas');
|
|
20
|
+
}
|
|
21
|
+
const sourcesDir = () => path.join(cacheDir(), 'sources');
|
|
22
|
+
|
|
23
|
+
function readConfig() {
|
|
24
|
+
try {
|
|
25
|
+
const c = JSON.parse(fs.readFileSync(configFile(), 'utf8'));
|
|
26
|
+
if (!Array.isArray(c.sources)) c.sources = [];
|
|
27
|
+
return c;
|
|
28
|
+
} catch { return { version: 1, sources: [] }; }
|
|
29
|
+
}
|
|
30
|
+
function writeConfig(c) {
|
|
31
|
+
fs.mkdirSync(configDir(), { recursive: true });
|
|
32
|
+
fs.writeFileSync(configFile(), JSON.stringify(c, null, 2) + '\n');
|
|
33
|
+
}
|
|
34
|
+
function addSource(url, opts = {}) {
|
|
35
|
+
const c = readConfig();
|
|
36
|
+
const existing = c.sources.find(s => s.url === url);
|
|
37
|
+
if (existing) { if (opts.name) existing.name = opts.name; }
|
|
38
|
+
else c.sources.push({ url, name: opts.name || null, addedAt: new Date().toISOString() });
|
|
39
|
+
writeConfig(c);
|
|
40
|
+
}
|
|
41
|
+
function removeSource(url) {
|
|
42
|
+
const c = readConfig();
|
|
43
|
+
const before = c.sources.length;
|
|
44
|
+
c.sources = c.sources.filter(s => s.url !== url);
|
|
45
|
+
writeConfig(c);
|
|
46
|
+
return c.sources.length < before;
|
|
47
|
+
}
|
|
48
|
+
const listSources = () => readConfig().sources;
|
|
49
|
+
|
|
50
|
+
const envSources = () => (process.env.SKILLS_ATLAS_SOURCES || '')
|
|
51
|
+
.split(',').map(s => s.trim()).filter(Boolean);
|
|
52
|
+
|
|
53
|
+
function effectiveSources() {
|
|
54
|
+
const out = [], seen = new Set();
|
|
55
|
+
for (const s of listSources()) if (s.url && !seen.has(s.url)) { seen.add(s.url); out.push({ url: s.url, name: s.name || null }); }
|
|
56
|
+
for (const u of envSources()) if (!seen.has(u)) { seen.add(u); out.push({ url: u, name: null }); }
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sourceCachePath(url) {
|
|
61
|
+
const h = crypto.createHash('sha1').update(url).digest('hex').slice(0, 16);
|
|
62
|
+
return path.join(sourcesDir(), `${h}.json`);
|
|
63
|
+
}
|
|
64
|
+
function cacheSource(url, dataObj) {
|
|
65
|
+
fs.mkdirSync(sourcesDir(), { recursive: true });
|
|
66
|
+
fs.writeFileSync(sourceCachePath(url), JSON.stringify(dataObj));
|
|
67
|
+
}
|
|
68
|
+
function readCachedSource(url) {
|
|
69
|
+
try { return JSON.parse(fs.readFileSync(sourceCachePath(url), 'utf8')); } catch { return null; }
|
|
70
|
+
}
|
|
71
|
+
function removeCachedSource(url) {
|
|
72
|
+
try { fs.rmSync(sourceCachePath(url), { force: true }); } catch { /* ignore */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
configDir, configFile, readConfig, writeConfig, addSource, removeSource, listSources,
|
|
77
|
+
effectiveSources, sourceCachePath, cacheSource, readCachedSource, removeCachedSource,
|
|
78
|
+
};
|