skills-atlas-cli 0.1.1 → 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 +12 -7
- package/package.json +1 -1
- package/src/args.js +1 -0
- package/src/commands/info.js +5 -4
- package/src/commands/install.js +37 -8
- package/src/format.js +61 -32
- package/src/fsutil.js +9 -1
- package/src/index-build.js +16 -1
package/README.md
CHANGED
|
@@ -59,16 +59,21 @@ After installing a skill, start a new Claude Code session to load it.
|
|
|
59
59
|
|
|
60
60
|
## How install works
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
The real value is the **catalog**: `search` / `info` / `categories` work fully
|
|
63
|
+
offline and map *which* skill fits — function-organized, bilingual, tagged with
|
|
64
|
+
use-case / when-to-use / personas / ⛓ chains. That's what `npx skills add` and
|
|
65
|
+
GitHub search don't give you.
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
On top of that, `install` can place a skill straight into `.claude/skills/`:
|
|
68
|
+
|
|
69
|
+
- For a repo that exposes a **per-skill folder**, it downloads only that folder
|
|
70
|
+
(via the repo archive — **no GitHub API rate limit**) into
|
|
71
|
+
`<target>/.claude/skills/<skill>/`, not the whole repo.
|
|
67
72
|
- Several sources? The best installable one is auto-picked — `--source <id>` to
|
|
68
73
|
choose, `--yes` for non-interactive runs.
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
-
|
|
74
|
+
- Other sources (whole-repo / marketplace) print their official command instead
|
|
75
|
+
(e.g. `npx skills add owner/repo`).
|
|
76
|
+
- `GITHUB_TOKEN` is only needed if you fall back to the API and hit its 60/h limit.
|
|
72
77
|
|
|
73
78
|
## Keeping the catalog fresh
|
|
74
79
|
|
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
|
@@ -7,7 +7,7 @@ const { buildIndices, vendorsFor, suggestSkills, skillDocPath } = require('../in
|
|
|
7
7
|
const { listSkillFiles, fetchRaw, fetchSkillFolderTar } = require('../github');
|
|
8
8
|
const fsu = require('../fsutil');
|
|
9
9
|
const { confirm, choose } = require('../prompt');
|
|
10
|
-
const { buildInfo, renderInfo, bold, dim, cyan, green, stars, safeAlt } = require('../format');
|
|
10
|
+
const { buildInfo, infoForRow, renderInfo, bold, dim, cyan, green, stars, safeAlt } = require('../format');
|
|
11
11
|
|
|
12
12
|
const HELP = `usage: skills-atlas install <skill> [options]
|
|
13
13
|
|
|
@@ -20,8 +20,9 @@ options:
|
|
|
20
20
|
--dry-run show what would download, write nothing
|
|
21
21
|
--json machine-readable output
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
Downloads the repo archive (no GitHub API rate limit). Only if that fetch fails
|
|
24
|
+
does it fall back to the GitHub API (60/h unauthenticated) — set GITHUB_TOKEN to
|
|
25
|
+
raise that fallback to 5000/h.`;
|
|
25
26
|
|
|
26
27
|
function resolveGlobal(values) {
|
|
27
28
|
if (values.project) return false;
|
|
@@ -79,6 +80,17 @@ module.exports = async function install(argv) {
|
|
|
79
80
|
chosen = candidates[i];
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
// Heads-up when --yes auto-picked among semantically different groups.
|
|
84
|
+
if (values.yes && candidates.length > 1) {
|
|
85
|
+
const groups = [...new Set(candidates.map(c => c.row && c.row.group).filter(Boolean))];
|
|
86
|
+
if (groups.length > 1) {
|
|
87
|
+
const picked = chosen.row && chosen.row.group;
|
|
88
|
+
const otherGroups = groups.filter(g => g !== picked).join('; ');
|
|
89
|
+
const st = stars(chosen.source.stars);
|
|
90
|
+
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.`));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
82
94
|
const v = chosen.vendor || {};
|
|
83
95
|
const src = chosen.source;
|
|
84
96
|
const skill = chosen.skill || name; // canonical skill name from the index
|
|
@@ -138,6 +150,15 @@ module.exports = async function install(argv) {
|
|
|
138
150
|
process.exitCode = 1;
|
|
139
151
|
return;
|
|
140
152
|
}
|
|
153
|
+
if (values.json) {
|
|
154
|
+
console.log(JSON.stringify({
|
|
155
|
+
skill: name, mode: 'folder', dest,
|
|
156
|
+
files: listing.files.length,
|
|
157
|
+
scripts: fsu.scriptFiles(listing.files.map(f => f.rel)).length,
|
|
158
|
+
branch: listing.branchUsed, dryRun: true,
|
|
159
|
+
}));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
141
162
|
console.log(`would install ${listing.files.length} file(s) to ${fsu.tildify(dest)} (branch ${listing.branchUsed}):`);
|
|
142
163
|
listing.files.forEach(f => console.log(` ${f.rel}`));
|
|
143
164
|
if (listing.note) console.log(dim(' ' + listing.note));
|
|
@@ -149,7 +170,7 @@ module.exports = async function install(argv) {
|
|
|
149
170
|
// Primary: one repo-archive download from codeload (not GitHub-API rate-limited);
|
|
150
171
|
// extract only this skill's folder. Fallback: tree API + raw per file.
|
|
151
172
|
const tmp = fsu.mkdtemp();
|
|
152
|
-
let branchUsed, fileCount, note = null;
|
|
173
|
+
let branchUsed, fileCount, note = null, scripts = [];
|
|
153
174
|
try {
|
|
154
175
|
let folder = null;
|
|
155
176
|
try {
|
|
@@ -180,6 +201,7 @@ module.exports = async function install(argv) {
|
|
|
180
201
|
});
|
|
181
202
|
if (!hasSkillMd) throw new Error('no SKILL.md found in downloaded folder');
|
|
182
203
|
fileCount = rels.length;
|
|
204
|
+
scripts = fsu.scriptFiles(rels);
|
|
183
205
|
fsu.swapDir(tmp, dest);
|
|
184
206
|
} catch (e) {
|
|
185
207
|
fsu.rmrf(tmp);
|
|
@@ -191,16 +213,23 @@ module.exports = async function install(argv) {
|
|
|
191
213
|
if (values.json) {
|
|
192
214
|
console.log(JSON.stringify({
|
|
193
215
|
skill: name, mode: 'folder', source: src.name,
|
|
194
|
-
dest, files: fileCount, branch: branchUsed,
|
|
216
|
+
dest, files: fileCount, scripts: scripts.length, branch: branchUsed,
|
|
195
217
|
}));
|
|
196
218
|
return;
|
|
197
219
|
}
|
|
198
220
|
|
|
199
221
|
console.log(`\n${green('✓')} installed ${bold(skill)} → ${fsu.tildify(dest)} ${dim(`(${fileCount} file(s) from ${src.name}@${branchUsed})`)}`);
|
|
200
222
|
if (note) console.log(dim(' ' + note));
|
|
223
|
+
console.log(dim(` source: ${src.name}@${branchUsed} — branch HEAD, not a pinned commit; review before use`));
|
|
224
|
+
if (scripts.length) {
|
|
225
|
+
const show = scripts.slice(0, 6).join(', ') + (scripts.length > 6 ? ', …' : '');
|
|
226
|
+
console.log(dim(` ⚠ includes ${scripts.length} script file(s): ${show}`));
|
|
227
|
+
}
|
|
201
228
|
|
|
202
|
-
// usage guidance
|
|
203
|
-
const
|
|
204
|
-
|
|
229
|
+
// usage guidance — scoped by row identity to the exact group you installed from
|
|
230
|
+
const guide = chosen.row
|
|
231
|
+
? infoForRow(skill, chosen.row, data.vendors)
|
|
232
|
+
: buildInfo(skill, { skillIndex: idx.skillIndex, vendors: data.vendors });
|
|
233
|
+
console.log(renderInfo(guide, { en: !values.zh, all: true }));
|
|
205
234
|
console.log(dim('\nStart a new Claude Code session to load the skill, then invoke it by name.'));
|
|
206
235
|
};
|
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/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]);
|