skills-atlas-cli 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/README.md +19 -8
- package/bin/skills.js +4 -2
- package/package.json +1 -1
- package/src/args.js +1 -0
- package/src/commands/info.js +5 -4
- package/src/commands/install.js +138 -48
- package/src/commands/installed.js +46 -0
- package/src/format.js +61 -32
- package/src/fsutil.js +9 -1
- package/src/github.js +32 -2
- package/src/index-build.js +16 -1
- package/src/manifest.js +56 -0
package/README.md
CHANGED
|
@@ -46,29 +46,40 @@ skills-atlas info brainstorming
|
|
|
46
46
|
# 📥 Install it (into .claude/skills/)
|
|
47
47
|
skills-atlas install brainstorming # → ~/.claude/skills/ (default, all projects)
|
|
48
48
|
skills-atlas install brainstorming --project # → ./.claude/skills/ (this project only)
|
|
49
|
+
skills-atlas install brainstorming --chain # install the whole ⛓ workflow it belongs to
|
|
49
50
|
skills-atlas install brainstorming --dry-run # preview the files, write nothing
|
|
50
51
|
|
|
51
|
-
# 🗂️
|
|
52
|
+
# 🗂️ Manage & browse
|
|
53
|
+
skills-atlas installed # what you've installed (global + project)
|
|
52
54
|
skills-atlas categories # the 20 top-level categories
|
|
53
55
|
skills-atlas list marketing # skill groups within a category
|
|
54
56
|
skills-atlas update # pull the latest catalog
|
|
55
57
|
```
|
|
56
58
|
|
|
59
|
+
**⛓ Workflows, not just skills.** Many skills belong to a curated chain (e.g.
|
|
60
|
+
`brainstorming → writing-plans → executing-plans → …`). `install <skill> --chain`
|
|
61
|
+
installs the whole pipeline in one archive download, ready to run in order.
|
|
62
|
+
|
|
57
63
|
Output is English by default; add `--zh` for Chinese, or `--json` to any command for machine-readable output.
|
|
58
64
|
After installing a skill, start a new Claude Code session to load it.
|
|
59
65
|
|
|
60
66
|
## How install works
|
|
61
67
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
The real value is the **catalog**: `search` / `info` / `categories` work fully
|
|
69
|
+
offline and map *which* skill fits — function-organized, bilingual, tagged with
|
|
70
|
+
use-case / when-to-use / personas / ⛓ chains. That's what `npx skills add` and
|
|
71
|
+
GitHub search don't give you.
|
|
72
|
+
|
|
73
|
+
On top of that, `install` can place a skill straight into `.claude/skills/`:
|
|
65
74
|
|
|
66
|
-
-
|
|
75
|
+
- For a repo that exposes a **per-skill folder**, it downloads only that folder
|
|
76
|
+
(via the repo archive — **no GitHub API rate limit**) into
|
|
77
|
+
`<target>/.claude/skills/<skill>/`, not the whole repo.
|
|
67
78
|
- Several sources? The best installable one is auto-picked — `--source <id>` to
|
|
68
79
|
choose, `--yes` for non-interactive runs.
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
-
|
|
80
|
+
- Other sources (whole-repo / marketplace) print their official command instead
|
|
81
|
+
(e.g. `npx skills add owner/repo`).
|
|
82
|
+
- `GITHUB_TOKEN` is only needed if you fall back to the API and hit its 60/h limit.
|
|
72
83
|
|
|
73
84
|
## Keeping the catalog fresh
|
|
74
85
|
|
package/bin/skills.js
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
const search = require('../src/commands/search');
|
|
5
5
|
const info = require('../src/commands/info');
|
|
6
6
|
const install = require('../src/commands/install');
|
|
7
|
+
const installed = require('../src/commands/installed');
|
|
7
8
|
const update = require('../src/commands/update');
|
|
8
9
|
const { categories, list } = require('../src/commands/categories');
|
|
9
10
|
|
|
10
11
|
const VERSION = require('../package.json').version;
|
|
11
|
-
const commands = { search, info, install, update, categories, list };
|
|
12
|
+
const commands = { search, info, install, installed, update, categories, list };
|
|
12
13
|
|
|
13
14
|
const HELP = `skills-atlas — search, install & learn AI agent skills
|
|
14
15
|
|
|
@@ -17,7 +18,8 @@ usage: skills-atlas <command> [args]
|
|
|
17
18
|
commands:
|
|
18
19
|
search <query> find skills (filters: -c category, -p persona, -t type, --chain)
|
|
19
20
|
info <skill> show description, usage guidance, sources & install command
|
|
20
|
-
install <skill> download the skill into .claude/skills/ (--
|
|
21
|
+
install <skill> download the skill into .claude/skills/ (--chain for the whole workflow)
|
|
22
|
+
installed list skills you've installed (global + project)
|
|
21
23
|
update refresh the catalog from the public data feed
|
|
22
24
|
categories list the top-level categories
|
|
23
25
|
list [category] list skill groups (optionally within one category)
|
package/package.json
CHANGED
package/src/args.js
CHANGED
package/src/commands/info.js
CHANGED
|
@@ -5,13 +5,14 @@ const { loadData } = require('../data');
|
|
|
5
5
|
const { buildIndices, suggestSkills } = require('../index-build');
|
|
6
6
|
const { buildInfo, renderInfo } = require('../format');
|
|
7
7
|
|
|
8
|
-
const HELP = `usage: skills-atlas info <skill> [--json] [--zh]
|
|
8
|
+
const HELP = `usage: skills-atlas info <skill> [--all] [--json] [--zh]
|
|
9
9
|
|
|
10
10
|
Show a skill's description, use case, when-to-use, personas, source repo(s)
|
|
11
|
-
(stars / license / type), the in-repo SKILL.md path, and the install command
|
|
11
|
+
(stars / license / type), the in-repo SKILL.md path, and the install command.
|
|
12
|
+
Leads with the most relevant group; --all expands same-named skills in others.`;
|
|
12
13
|
|
|
13
14
|
module.exports = async function info(argv) {
|
|
14
|
-
const { values, positionals } = parse(argv, ['json']);
|
|
15
|
+
const { values, positionals } = parse(argv, ['json', 'all']);
|
|
15
16
|
if (values.help) { console.log(HELP); return; }
|
|
16
17
|
|
|
17
18
|
const name = positionals[0];
|
|
@@ -42,5 +43,5 @@ module.exports = async function info(argv) {
|
|
|
42
43
|
console.log(JSON.stringify(infoObj, null, 2));
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
|
-
console.log(renderInfo(infoObj, { en: !values.zh }));
|
|
46
|
+
console.log(renderInfo(infoObj, { en: !values.zh, all: values.all }));
|
|
46
47
|
};
|
package/src/commands/install.js
CHANGED
|
@@ -4,10 +4,11 @@ const path = require('path');
|
|
|
4
4
|
const { parse } = require('../args');
|
|
5
5
|
const { loadData } = require('../data');
|
|
6
6
|
const { buildIndices, vendorsFor, suggestSkills, skillDocPath } = require('../index-build');
|
|
7
|
-
const { listSkillFiles,
|
|
7
|
+
const { listSkillFiles, getSkillFolder } = require('../github');
|
|
8
8
|
const fsu = require('../fsutil');
|
|
9
|
+
const manifest = require('../manifest');
|
|
9
10
|
const { confirm, choose } = require('../prompt');
|
|
10
|
-
const { buildInfo, renderInfo, bold, dim, cyan, green, stars, safeAlt } = require('../format');
|
|
11
|
+
const { buildInfo, infoForRow, renderInfo, bold, dim, cyan, green, stars, safeAlt } = require('../format');
|
|
11
12
|
|
|
12
13
|
const HELP = `usage: skills-atlas install <skill> [options]
|
|
13
14
|
|
|
@@ -17,11 +18,13 @@ options:
|
|
|
17
18
|
-s, --source <id> pick a source when a skill has several
|
|
18
19
|
-f, --force overwrite if already installed
|
|
19
20
|
-y, --yes non-interactive (auto-pick top source, assume yes)
|
|
21
|
+
--chain install the whole ⛓ workflow this skill belongs to
|
|
20
22
|
--dry-run show what would download, write nothing
|
|
21
23
|
--json machine-readable output
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
Downloads the repo archive (no GitHub API rate limit). Only if that fetch fails
|
|
26
|
+
does it fall back to the GitHub API (60/h unauthenticated) — set GITHUB_TOKEN to
|
|
27
|
+
raise that fallback to 5000/h.`;
|
|
25
28
|
|
|
26
29
|
function resolveGlobal(values) {
|
|
27
30
|
if (values.project) return false;
|
|
@@ -29,9 +32,84 @@ function resolveGlobal(values) {
|
|
|
29
32
|
return true; // default: global
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
const hasSkillMd = rels => rels.some(r => {
|
|
36
|
+
const x = r.toLowerCase();
|
|
37
|
+
return x === 'skill.md' || x.endsWith('/skill.md');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Download one skill's folder and atomically install it; records the manifest.
|
|
41
|
+
// Returns { dest, fileCount, scripts, branchUsed, note }. Throws on failure.
|
|
42
|
+
async function installFolder({ author, repo, branch, docPath, dest, targetRoot, skillName, src, row }) {
|
|
43
|
+
const folder = await getSkillFolder({ author, repo, branch, docPath });
|
|
44
|
+
const tmp = fsu.mkdtemp();
|
|
45
|
+
try {
|
|
46
|
+
for (const f of folder.files) fsu.writeFileMkdir(path.join(tmp, f.rel), f.data);
|
|
47
|
+
const rels = folder.files.map(f => f.rel);
|
|
48
|
+
if (!hasSkillMd(rels)) throw new Error('no SKILL.md found in downloaded folder');
|
|
49
|
+
fsu.swapDir(tmp, dest);
|
|
50
|
+
const scripts = fsu.scriptFiles(rels);
|
|
51
|
+
manifest.record(targetRoot, {
|
|
52
|
+
skill: skillName, source: src.name, repo, branch: folder.branchUsed,
|
|
53
|
+
group: row && row.group, category: row && row._cat,
|
|
54
|
+
files: rels.length, scripts: scripts.length, installedAt: new Date().toISOString(),
|
|
55
|
+
});
|
|
56
|
+
return { dest, fileCount: rels.length, scripts, branchUsed: folder.branchUsed, note: folder.note };
|
|
57
|
+
} catch (e) {
|
|
58
|
+
fsu.rmrf(tmp);
|
|
59
|
+
throw e;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Install every installable skill in a chain (workflow). One archive download
|
|
64
|
+
// serves them all (github archive cache).
|
|
65
|
+
async function installChain({ row, vendor, src, targetRoot, values }) {
|
|
66
|
+
const author = vendor.author || src.author;
|
|
67
|
+
const repo = vendor.repo || src.repo;
|
|
68
|
+
const branch = vendor.default_branch || src.default_branch || 'main';
|
|
69
|
+
|
|
70
|
+
const items = [], skipped = [];
|
|
71
|
+
for (const sk of row.skills || []) {
|
|
72
|
+
const dp = skillDocPath(vendor, sk);
|
|
73
|
+
if (dp) items.push({ name: sk, docPath: dp }); else skipped.push(sk);
|
|
74
|
+
}
|
|
75
|
+
if (!items.length) {
|
|
76
|
+
console.error(`no installable skills in this chain from ${src.name}.`);
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!values.json) console.log(`\ninstalling ${items.length}-skill chain from ${bold(src.name)}: ${dim(items.map(i => i.name).join(' → '))}`);
|
|
82
|
+
const installed = [], failed = [];
|
|
83
|
+
for (const it of items) {
|
|
84
|
+
const dest = path.join(targetRoot, it.name);
|
|
85
|
+
if (fsu.dirExists(dest) && !values.force) {
|
|
86
|
+
if (!values.json) console.log(dim(` • ${it.name} — already installed (use --force to overwrite)`));
|
|
87
|
+
installed.push(it.name);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const r = await installFolder({ author, repo, branch, docPath: it.docPath, dest, targetRoot, skillName: it.name, src, row });
|
|
92
|
+
if (!values.json) console.log(` ${green('✓')} ${it.name} ${dim(`(${r.fileCount} files)`)}`);
|
|
93
|
+
installed.push(it.name);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
if (!values.json) console.log(dim(` ✗ ${it.name} — ${e.message}`));
|
|
96
|
+
failed.push(it.name);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (values.json) {
|
|
101
|
+
console.log(JSON.stringify({ mode: 'chain', group: row.group, dest: targetRoot, installed, failed, skipped }));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (skipped.length) console.log(dim(` (skipped ${skipped.length}: ${skipped.join(', ')} — no per-skill folder)`));
|
|
105
|
+
console.log(`\n${green('✓')} chain ready — ${installed.length} skill(s) in ${fsu.tildify(targetRoot)}`);
|
|
106
|
+
console.log(dim(` run in order: ${(row.skills || []).join(' → ')}`));
|
|
107
|
+
console.log(dim('\nStart a new Claude Code session to load them, then run the workflow in order.'));
|
|
108
|
+
}
|
|
109
|
+
|
|
32
110
|
module.exports = async function install(argv) {
|
|
33
111
|
const { values, positionals } = parse(argv,
|
|
34
|
-
['global', 'project', 'source', 'force', 'yes', 'dry-run', 'json']);
|
|
112
|
+
['global', 'project', 'source', 'force', 'yes', 'chain', 'dry-run', 'json']);
|
|
35
113
|
if (values.help) { console.log(HELP); return; }
|
|
36
114
|
|
|
37
115
|
const name = positionals[0];
|
|
@@ -79,6 +157,17 @@ module.exports = async function install(argv) {
|
|
|
79
157
|
chosen = candidates[i];
|
|
80
158
|
}
|
|
81
159
|
|
|
160
|
+
// Heads-up when --yes auto-picked among semantically different groups.
|
|
161
|
+
if (values.yes && candidates.length > 1) {
|
|
162
|
+
const groups = [...new Set(candidates.map(c => c.row && c.row.group).filter(Boolean))];
|
|
163
|
+
if (groups.length > 1) {
|
|
164
|
+
const picked = chosen.row && chosen.row.group;
|
|
165
|
+
const otherGroups = groups.filter(g => g !== picked).join('; ');
|
|
166
|
+
const st = stars(chosen.source.stars);
|
|
167
|
+
console.error(dim(`note: '${name}' exists in ${groups.length} groups; auto-picked ${chosen.source.name}${st ? ' ' + st : ''} (${picked || '?'}). also in: ${otherGroups} — use --source to choose.`));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
82
171
|
const v = chosen.vendor || {};
|
|
83
172
|
const src = chosen.source;
|
|
84
173
|
const skill = chosen.skill || name; // canonical skill name from the index
|
|
@@ -113,6 +202,21 @@ module.exports = async function install(argv) {
|
|
|
113
202
|
}
|
|
114
203
|
|
|
115
204
|
const targetRoot = fsu.installTargetDir({ global: resolveGlobal(values) });
|
|
205
|
+
const isChain = Boolean(chosen.row && chosen.row.chain && (chosen.row.skills || []).length >= 2);
|
|
206
|
+
|
|
207
|
+
// --- chain: install the whole workflow ---
|
|
208
|
+
if (values.chain) {
|
|
209
|
+
if (!isChain) {
|
|
210
|
+
console.log(dim(`'${skill}' isn't part of a multi-skill chain; installing it alone.`));
|
|
211
|
+
} else if (values['dry-run']) {
|
|
212
|
+
console.log(`would install the ${chosen.row.skills.length}-skill chain: ${chosen.row.skills.join(' → ')}`);
|
|
213
|
+
return;
|
|
214
|
+
} else {
|
|
215
|
+
await installChain({ row: chosen.row, vendor: v, src, targetRoot, values });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
116
220
|
const dest = path.join(targetRoot, skill); // folder = skill name (strips repo nesting)
|
|
117
221
|
|
|
118
222
|
// --- already installed? ---
|
|
@@ -138,6 +242,15 @@ module.exports = async function install(argv) {
|
|
|
138
242
|
process.exitCode = 1;
|
|
139
243
|
return;
|
|
140
244
|
}
|
|
245
|
+
if (values.json) {
|
|
246
|
+
console.log(JSON.stringify({
|
|
247
|
+
skill: name, mode: 'folder', dest,
|
|
248
|
+
files: listing.files.length,
|
|
249
|
+
scripts: fsu.scriptFiles(listing.files.map(f => f.rel)).length,
|
|
250
|
+
branch: listing.branchUsed, dryRun: true,
|
|
251
|
+
}));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
141
254
|
console.log(`would install ${listing.files.length} file(s) to ${fsu.tildify(dest)} (branch ${listing.branchUsed}):`);
|
|
142
255
|
listing.files.forEach(f => console.log(` ${f.rel}`));
|
|
143
256
|
if (listing.note) console.log(dim(' ' + listing.note));
|
|
@@ -145,44 +258,11 @@ module.exports = async function install(argv) {
|
|
|
145
258
|
return;
|
|
146
259
|
}
|
|
147
260
|
|
|
148
|
-
// --- download
|
|
149
|
-
|
|
150
|
-
// extract only this skill's folder. Fallback: tree API + raw per file.
|
|
151
|
-
const tmp = fsu.mkdtemp();
|
|
152
|
-
let branchUsed, fileCount, note = null;
|
|
261
|
+
// --- single skill: download (archive → API fallback), record, report ---
|
|
262
|
+
let result;
|
|
153
263
|
try {
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
folder = await fetchSkillFolderTar({ author, repo, branch, docPath });
|
|
157
|
-
} catch (e) {
|
|
158
|
-
note = `archive download failed (${e.message}); fell back to the GitHub API`;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
let rels;
|
|
162
|
-
if (folder && folder.files.length) {
|
|
163
|
-
branchUsed = folder.branchUsed;
|
|
164
|
-
for (const f of folder.files) fsu.writeFileMkdir(path.join(tmp, f.rel), f.data);
|
|
165
|
-
rels = folder.files.map(f => f.rel);
|
|
166
|
-
} else {
|
|
167
|
-
const listing = await listSkillFiles({ author, repo, branch, docPath });
|
|
168
|
-
branchUsed = listing.branchUsed;
|
|
169
|
-
if (listing.note) note = listing.note;
|
|
170
|
-
for (const f of listing.files) {
|
|
171
|
-
const buf = await fetchRaw(author, repo, listing.branchUsed, f.path);
|
|
172
|
-
fsu.writeFileMkdir(path.join(tmp, f.rel), buf);
|
|
173
|
-
}
|
|
174
|
-
rels = listing.files.map(f => f.rel);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const hasSkillMd = rels.some(rel => {
|
|
178
|
-
const r = rel.toLowerCase();
|
|
179
|
-
return r === 'skill.md' || r.endsWith('/skill.md');
|
|
180
|
-
});
|
|
181
|
-
if (!hasSkillMd) throw new Error('no SKILL.md found in downloaded folder');
|
|
182
|
-
fileCount = rels.length;
|
|
183
|
-
fsu.swapDir(tmp, dest);
|
|
264
|
+
result = await installFolder({ author, repo, branch, docPath, dest, targetRoot, skillName: skill, src, row: chosen.row });
|
|
184
265
|
} catch (e) {
|
|
185
|
-
fsu.rmrf(tmp);
|
|
186
266
|
console.error(`install failed: ${e.message}`);
|
|
187
267
|
process.exitCode = 1;
|
|
188
268
|
return;
|
|
@@ -190,17 +270,27 @@ module.exports = async function install(argv) {
|
|
|
190
270
|
|
|
191
271
|
if (values.json) {
|
|
192
272
|
console.log(JSON.stringify({
|
|
193
|
-
skill
|
|
194
|
-
dest, files: fileCount, branch: branchUsed,
|
|
273
|
+
skill, mode: 'folder', source: src.name,
|
|
274
|
+
dest: result.dest, files: result.fileCount, scripts: result.scripts.length, branch: result.branchUsed,
|
|
195
275
|
}));
|
|
196
276
|
return;
|
|
197
277
|
}
|
|
198
278
|
|
|
199
|
-
console.log(`\n${green('✓')} installed ${bold(skill)} → ${fsu.tildify(dest)} ${dim(`(${fileCount} file(s) from ${src.name}@${branchUsed})`)}`);
|
|
200
|
-
if (note) console.log(dim(' ' + note));
|
|
279
|
+
console.log(`\n${green('✓')} installed ${bold(skill)} → ${fsu.tildify(result.dest)} ${dim(`(${result.fileCount} file(s) from ${src.name}@${result.branchUsed})`)}`);
|
|
280
|
+
if (result.note) console.log(dim(' ' + result.note));
|
|
281
|
+
console.log(dim(` source: ${src.name}@${result.branchUsed} — branch HEAD, not a pinned commit; review before use`));
|
|
282
|
+
if (result.scripts.length) {
|
|
283
|
+
const show = result.scripts.slice(0, 6).join(', ') + (result.scripts.length > 6 ? ', …' : '');
|
|
284
|
+
console.log(dim(` ⚠ includes ${result.scripts.length} script file(s): ${show}`));
|
|
285
|
+
}
|
|
286
|
+
if (isChain) {
|
|
287
|
+
console.log(dim(` ⛓ part of a ${chosen.row.skills.length}-skill workflow — install all: skills-atlas install ${skill} --chain`));
|
|
288
|
+
}
|
|
201
289
|
|
|
202
|
-
// usage guidance
|
|
203
|
-
const
|
|
204
|
-
|
|
290
|
+
// usage guidance — scoped by row identity to the exact group you installed from
|
|
291
|
+
const guide = chosen.row
|
|
292
|
+
? infoForRow(skill, chosen.row, data.vendors)
|
|
293
|
+
: buildInfo(skill, { skillIndex: idx.skillIndex, vendors: data.vendors });
|
|
294
|
+
console.log(renderInfo(guide, { en: !values.zh, all: true }));
|
|
205
295
|
console.log(dim('\nStart a new Claude Code session to load the skill, then invoke it by name.'));
|
|
206
296
|
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parse } = require('../args');
|
|
4
|
+
const fsu = require('../fsutil');
|
|
5
|
+
const manifest = require('../manifest');
|
|
6
|
+
const { bold, dim, cyan } = require('../format');
|
|
7
|
+
|
|
8
|
+
const HELP = `usage: skills-atlas installed [--global|--project] [--json]
|
|
9
|
+
|
|
10
|
+
List skills installed by skills-atlas (from the per-target manifest).
|
|
11
|
+
Default: both global (~/.claude/skills) and project (./.claude/skills).`;
|
|
12
|
+
|
|
13
|
+
module.exports = async function installed(argv) {
|
|
14
|
+
const { values } = parse(argv, ['global', 'project', 'json']);
|
|
15
|
+
if (values.help) { console.log(HELP); return; }
|
|
16
|
+
|
|
17
|
+
const scopes = [];
|
|
18
|
+
if (values.project && !values.global) {
|
|
19
|
+
scopes.push({ name: 'project', root: fsu.installTargetDir({ global: false }) });
|
|
20
|
+
} else if (values.global && !values.project) {
|
|
21
|
+
scopes.push({ name: 'global', root: fsu.installTargetDir({ global: true }) });
|
|
22
|
+
} else {
|
|
23
|
+
scopes.push({ name: 'global', root: fsu.installTargetDir({ global: true }) });
|
|
24
|
+
scopes.push({ name: 'project', root: fsu.installTargetDir({ global: false }) });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const out = scopes.map(s => ({ scope: s.name, root: s.root, skills: manifest.list(s.root) }));
|
|
28
|
+
|
|
29
|
+
if (values.json) { console.log(JSON.stringify(out, null, 2)); return; }
|
|
30
|
+
|
|
31
|
+
let total = 0;
|
|
32
|
+
for (const s of out) {
|
|
33
|
+
if (!s.skills.length) continue;
|
|
34
|
+
console.log(`\n${bold(s.scope)} ${dim(fsu.tildify(s.root))}`);
|
|
35
|
+
for (const e of s.skills.slice().sort((a, b) => a.skill.localeCompare(b.skill))) {
|
|
36
|
+
total++;
|
|
37
|
+
const when = e.installedAt ? e.installedAt.slice(0, 10) : '';
|
|
38
|
+
const meta = [`${e.source || '?'}@${e.branch || '?'}`, e.group, when].filter(Boolean).join(' · ');
|
|
39
|
+
console.log(` ${cyan(e.skill)} ${dim(meta)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!total) {
|
|
43
|
+
console.log('no skills installed by skills-atlas yet.');
|
|
44
|
+
console.log(dim('find one: skills-atlas search <query> → skills-atlas install <skill>'));
|
|
45
|
+
}
|
|
46
|
+
};
|
package/src/format.js
CHANGED
|
@@ -55,47 +55,70 @@ const PERSONA_EN = {
|
|
|
55
55
|
'研究': 'Research', '营销': 'Marketing', '设计': 'Design', '运营': 'Ops', '通用': 'General',
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
//
|
|
58
|
+
// Rank a row best-first: top stars, then total stars, then source count — so a
|
|
59
|
+
// star tie (one popular source shared across namesake groups) breaks meaningfully.
|
|
60
|
+
function rowRank(r) {
|
|
61
|
+
const ss = (r.sources || []).map(s => s.stars || 0);
|
|
62
|
+
return { max: Math.max(0, ...ss), sum: ss.reduce((a, b) => a + b, 0), n: ss.length };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The per-group info object for one row.
|
|
66
|
+
function groupOf(r, skillName, vendors) {
|
|
67
|
+
return {
|
|
68
|
+
group: r.group,
|
|
69
|
+
group_en: r.group_en,
|
|
70
|
+
category: r._cat,
|
|
71
|
+
category_en: r._catEn,
|
|
72
|
+
chain: r.chain,
|
|
73
|
+
description: r.description,
|
|
74
|
+
description_en: r.description_en,
|
|
75
|
+
use_case: r.use_case,
|
|
76
|
+
use_case_en: r.use_case_en,
|
|
77
|
+
when_to_use: r.when_to_use,
|
|
78
|
+
when_to_use_en: r.when_to_use_en,
|
|
79
|
+
personas: r.personas || [],
|
|
80
|
+
sources: (r.sources || []).map(s => {
|
|
81
|
+
const v = vendors[s.name] || {};
|
|
82
|
+
const docPath = skillDocPath(v, skillName);
|
|
83
|
+
return {
|
|
84
|
+
id: s.name,
|
|
85
|
+
url: s.url,
|
|
86
|
+
stars: s.stars,
|
|
87
|
+
license: (v.skill_licenses && v.skill_licenses[skillName]) || s.license || null,
|
|
88
|
+
type: s.type,
|
|
89
|
+
path: docPath || null,
|
|
90
|
+
install: s.install || v.install || null,
|
|
91
|
+
};
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Structured info for a skill (also the machine-readable --json shape). Groups
|
|
97
|
+
// are ordered best-first so the most relevant one leads.
|
|
59
98
|
function buildInfo(skillName, { skillIndex, vendors }) {
|
|
60
|
-
const rows = rowsFor(skillIndex, skillName)
|
|
99
|
+
const rows = rowsFor(skillIndex, skillName).slice().sort((a, b) => {
|
|
100
|
+
const A = rowRank(a), B = rowRank(b);
|
|
101
|
+
return B.max - A.max || B.sum - A.sum || B.n - A.n;
|
|
102
|
+
});
|
|
61
103
|
return {
|
|
62
104
|
skill: skillName,
|
|
63
105
|
found: rows.length > 0,
|
|
64
|
-
groups: rows.map(r => (
|
|
65
|
-
group: r.group,
|
|
66
|
-
group_en: r.group_en,
|
|
67
|
-
category: r._cat,
|
|
68
|
-
category_en: r._catEn,
|
|
69
|
-
chain: r.chain,
|
|
70
|
-
description: r.description,
|
|
71
|
-
description_en: r.description_en,
|
|
72
|
-
use_case: r.use_case,
|
|
73
|
-
use_case_en: r.use_case_en,
|
|
74
|
-
when_to_use: r.when_to_use,
|
|
75
|
-
when_to_use_en: r.when_to_use_en,
|
|
76
|
-
personas: r.personas || [],
|
|
77
|
-
sources: (r.sources || []).map(s => {
|
|
78
|
-
const v = vendors[s.name] || {};
|
|
79
|
-
const docPath = skillDocPath(v, skillName);
|
|
80
|
-
return {
|
|
81
|
-
id: s.name,
|
|
82
|
-
url: s.url,
|
|
83
|
-
stars: s.stars,
|
|
84
|
-
license: (v.skill_licenses && v.skill_licenses[skillName]) || s.license || null,
|
|
85
|
-
type: s.type,
|
|
86
|
-
path: docPath || null,
|
|
87
|
-
install: s.install || v.install || null,
|
|
88
|
-
};
|
|
89
|
-
}),
|
|
90
|
-
})),
|
|
106
|
+
groups: rows.map(r => groupOf(r, skillName, vendors)),
|
|
91
107
|
};
|
|
92
108
|
}
|
|
93
109
|
|
|
94
|
-
|
|
110
|
+
// Single-group info for one specific row (scoped post-install guidance — scopes
|
|
111
|
+
// by row identity, so two namesake rows with the same group name don't both show).
|
|
112
|
+
function infoForRow(skillName, row, vendors) {
|
|
113
|
+
return { skill: skillName, found: true, groups: [groupOf(row, skillName, vendors)] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function renderInfo(info, { en = false, all = false } = {}) {
|
|
95
117
|
const pick = (zh, e) => (en ? (e || zh) : (zh || e)) || '';
|
|
96
118
|
const out = [];
|
|
97
119
|
out.push(`\n${bold(green(info.skill))}${info.groups.some(g => g.chain) ? ' ' + cyan('⛓') : ''}`);
|
|
98
|
-
|
|
120
|
+
const shown = all ? info.groups : info.groups.slice(0, 1);
|
|
121
|
+
for (const g of shown) {
|
|
99
122
|
out.push(` ${dim('group:')} ${pick(g.group, g.group_en)} ${dim('[' + pick(g.category, g.category_en) + ']')}`);
|
|
100
123
|
const desc = pick(g.description, g.description_en);
|
|
101
124
|
if (desc) out.push(` ${desc}`);
|
|
@@ -119,9 +142,15 @@ function renderInfo(info, { en = false } = {}) {
|
|
|
119
142
|
}
|
|
120
143
|
}
|
|
121
144
|
}
|
|
145
|
+
if (!all && info.groups.length > 1) {
|
|
146
|
+
const others = info.groups.slice(1).map(g => pick(g.group, g.group_en));
|
|
147
|
+
out.push(dim(` + ${others.length} other group(s) with a same-named skill: ${others.join('; ')}`));
|
|
148
|
+
out.push(dim(` (run \`skills-atlas info ${info.skill} --all\` to expand)`));
|
|
149
|
+
}
|
|
122
150
|
return out.join('\n');
|
|
123
151
|
}
|
|
124
152
|
|
|
125
153
|
module.exports = {
|
|
126
|
-
bold, dim, green, cyan, yellow, stars, safeAlt, text, renderRow,
|
|
154
|
+
bold, dim, green, cyan, yellow, stars, safeAlt, text, renderRow,
|
|
155
|
+
buildInfo, infoForRow, renderInfo,
|
|
127
156
|
};
|
package/src/fsutil.js
CHANGED
|
@@ -46,7 +46,15 @@ function tildify(p) {
|
|
|
46
46
|
return p.startsWith(home) ? '~' + p.slice(home.length) : p;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// Executable/script files in a skill folder, by extension (for the install
|
|
50
|
+
// transparency heads-up). Conservative: extension-based, won't catch every
|
|
51
|
+
// extensionless executable, so the provenance note is always shown regardless.
|
|
52
|
+
const SCRIPT_RE = /\.(sh|bash|zsh|fish|command|js|cjs|mjs|ts|tsx|jsx|py|rb|pl|ps1|psm1|bat|cmd)$/i;
|
|
53
|
+
function scriptFiles(rels) {
|
|
54
|
+
return (rels || []).filter(r => SCRIPT_RE.test(r));
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
module.exports = {
|
|
50
58
|
installTargetDir, dirExists, ensureDir, mkdtemp,
|
|
51
|
-
writeFileMkdir, rmrf, swapDir, tildify,
|
|
59
|
+
writeFileMkdir, rmrf, swapDir, tildify, scriptFiles,
|
|
52
60
|
};
|
package/src/github.js
CHANGED
|
@@ -180,12 +180,19 @@ function extractTarGz(gzBuf, folderPath) {
|
|
|
180
180
|
return out;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// One archive per repo@ref per process — so installing a whole chain (many
|
|
184
|
+
// skills from one repo) downloads the archive ONCE.
|
|
185
|
+
const _archiveCache = new Map();
|
|
183
186
|
async function fetchArchive(author, repo, ref) {
|
|
187
|
+
const key = `${author}/${repo}@${ref}`;
|
|
188
|
+
if (_archiveCache.has(key)) return _archiveCache.get(key);
|
|
184
189
|
const url = `https://codeload.github.com/${author}/${repo}/tar.gz/${ref}`;
|
|
185
190
|
const res = await fetchT(url, { headers: { 'User-Agent': UA } });
|
|
186
191
|
if (res.status === 404) { const e = new Error(`archive not found: ${ref}`); e.code = 'NOT_FOUND'; throw e; }
|
|
187
192
|
if (!res.ok) throw new Error(`codeload HTTP ${res.status}`);
|
|
188
|
-
|
|
193
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
194
|
+
_archiveCache.set(key, buf);
|
|
195
|
+
return buf;
|
|
189
196
|
}
|
|
190
197
|
|
|
191
198
|
// Resolve a skill's folder via the repo archive. Returns { files:[{rel,data}],
|
|
@@ -212,4 +219,27 @@ async function fetchSkillFolderTar({ author, repo, branch, docPath }) {
|
|
|
212
219
|
return { files, branchUsed, source: 'archive' };
|
|
213
220
|
}
|
|
214
221
|
|
|
215
|
-
|
|
222
|
+
// Resolve a skill's folder CONTENTS — archive first (no API budget), tree+raw
|
|
223
|
+
// fallback. Returns { files:[{rel,data}], branchUsed, source, note }.
|
|
224
|
+
async function getSkillFolder({ author, repo, branch, docPath }) {
|
|
225
|
+
let archiveErr = null;
|
|
226
|
+
try {
|
|
227
|
+
const folder = await fetchSkillFolderTar({ author, repo, branch, docPath });
|
|
228
|
+
if (folder && folder.files.length) {
|
|
229
|
+
return { files: folder.files, branchUsed: folder.branchUsed, source: 'archive', note: null };
|
|
230
|
+
}
|
|
231
|
+
} catch (e) {
|
|
232
|
+
archiveErr = e.message;
|
|
233
|
+
}
|
|
234
|
+
const listing = await listSkillFiles({ author, repo, branch, docPath });
|
|
235
|
+
const files = [];
|
|
236
|
+
for (const f of listing.files) {
|
|
237
|
+
files.push({ rel: f.rel, data: await fetchRaw(author, repo, listing.branchUsed, f.path) });
|
|
238
|
+
}
|
|
239
|
+
const note = archiveErr ? `archive unavailable (${archiveErr}); used the GitHub API` : listing.note;
|
|
240
|
+
return { files, branchUsed: listing.branchUsed, source: 'api', note };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = {
|
|
244
|
+
listSkillFiles, fetchRaw, fetchTree, fetchSkillFolderTar, extractTarGz, getSkillFolder,
|
|
245
|
+
};
|
package/src/index-build.js
CHANGED
|
@@ -88,12 +88,27 @@ function levenshtein(a, b) {
|
|
|
88
88
|
|
|
89
89
|
// Nearest skill names for a not-found query.
|
|
90
90
|
function suggestSkills(skillIndex, query, max = 5) {
|
|
91
|
-
const q = String(query).toLowerCase();
|
|
91
|
+
const q = String(query).toLowerCase().trim();
|
|
92
|
+
if (!q) return [];
|
|
92
93
|
const names = [...skillIndex.keys()];
|
|
94
|
+
|
|
95
|
+
// 1) substring either way — high-signal.
|
|
93
96
|
const sub = names.filter(n => n.includes(q) || q.includes(n));
|
|
94
97
|
if (sub.length) return sub.slice(0, max);
|
|
98
|
+
|
|
99
|
+
// 2) share a meaningful word with the query (helps multi-word misses).
|
|
100
|
+
const qtokens = q.split(/[^a-z0-9一-鿿]+/)
|
|
101
|
+
.filter(t => (/[一-鿿]/.test(t) ? t.length >= 2 : t.length >= 3));
|
|
102
|
+
if (qtokens.length) {
|
|
103
|
+
const overlap = names.filter(n => qtokens.some(t => n.includes(t)));
|
|
104
|
+
if (overlap.length) return overlap.slice(0, max);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 3) fuzzy, but GATED to close edits only — return nothing rather than noise.
|
|
108
|
+
const limit = Math.max(1, Math.floor(q.length * 0.34));
|
|
95
109
|
return names
|
|
96
110
|
.map(n => [n, levenshtein(q, n)])
|
|
111
|
+
.filter(([, d]) => d <= limit)
|
|
97
112
|
.sort((a, b) => a[1] - b[1])
|
|
98
113
|
.slice(0, max)
|
|
99
114
|
.map(s => s[0]);
|
package/src/manifest.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Per-target install ledger: <skills-root>/.skills-atlas.json. Turns the opaque
|
|
2
|
+
// .claude/skills directory into a managed inventory — the keystone that lets
|
|
3
|
+
// `installed` / future `outdated` / `upgrade` / `remove` / `sync` work.
|
|
4
|
+
// (A dotfile, so Claude Code never loads it as a skill.)
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const FILE = '.skills-atlas.json';
|
|
11
|
+
|
|
12
|
+
const fileFor = root => path.join(root, FILE);
|
|
13
|
+
|
|
14
|
+
function read(root) {
|
|
15
|
+
try {
|
|
16
|
+
const m = JSON.parse(fs.readFileSync(fileFor(root), 'utf8'));
|
|
17
|
+
if (!m.skills) m.skills = {};
|
|
18
|
+
return m;
|
|
19
|
+
} catch {
|
|
20
|
+
return { version: 1, skills: {} };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function write(root, m) {
|
|
25
|
+
fs.mkdirSync(root, { recursive: true });
|
|
26
|
+
fs.writeFileSync(fileFor(root), JSON.stringify(m, null, 2) + '\n');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Upsert one installed skill.
|
|
30
|
+
function record(root, entry) {
|
|
31
|
+
const m = read(root);
|
|
32
|
+
m.skills[entry.skill] = {
|
|
33
|
+
source: entry.source || null,
|
|
34
|
+
repo: entry.repo || null,
|
|
35
|
+
branch: entry.branch || null,
|
|
36
|
+
group: entry.group || null,
|
|
37
|
+
category: entry.category || null,
|
|
38
|
+
files: entry.files ?? null,
|
|
39
|
+
scripts: entry.scripts ?? 0,
|
|
40
|
+
installedAt: entry.installedAt || null,
|
|
41
|
+
};
|
|
42
|
+
write(root, m);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function remove(root, skill) {
|
|
46
|
+
const m = read(root);
|
|
47
|
+
if (m.skills[skill]) { delete m.skills[skill]; write(root, m); return true; }
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function list(root) {
|
|
52
|
+
const m = read(root);
|
|
53
|
+
return Object.entries(m.skills).map(([skill, v]) => ({ skill, ...v }));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { read, write, record, remove, list, fileFor, FILE };
|