skills-atlas-cli 0.4.2 → 0.5.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 +32 -0
- package/bin/skills.js +5 -1
- package/package.json +1 -1
- package/src/args.js +2 -0
- package/src/commands/kit.js +136 -0
- package/src/commands/sync.js +91 -0
- package/src/detect.js +47 -0
- package/src/kitmanifest.js +36 -0
- package/src/kits.js +62 -0
package/README.md
CHANGED
|
@@ -32,6 +32,29 @@ npm install -g skills-atlas-cli # adds the `skills-atlas` command (alias: `
|
|
|
32
32
|
|
|
33
33
|
Or run it without installing: `npx skills-atlas-cli search seo`
|
|
34
34
|
|
|
35
|
+
## How you'll use it
|
|
36
|
+
|
|
37
|
+
Three ways to reach the same catalog — pick whichever fits the moment:
|
|
38
|
+
|
|
39
|
+
| Mode | Best when | Get going |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| **Manual** | you want to browse and grab skills yourself | `skills-atlas search <task>` → `skills-atlas use <skill>` |
|
|
42
|
+
| **In Claude Code** | you'd rather just ask Claude in-conversation | install the [plugin](#in-claude-code), then describe your task |
|
|
43
|
+
| **🤖 Autopilot** | you want the right skill to find *you* | `skills-atlas hook on` — Claude offers a fitting skill as you work |
|
|
44
|
+
|
|
45
|
+
**60-second quickstart:**
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install -g skills-atlas-cli
|
|
49
|
+
skills-atlas search "stress test my launch plan" # → pre-mortem tops the results
|
|
50
|
+
skills-atlas use pre-mortem # install + activate now (prints its SKILL.md)
|
|
51
|
+
skills-atlas hook on # optional — let the right skill find you from here on
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`use` drops the skill into `~/.claude/skills/` and prints it for the task at hand; it
|
|
55
|
+
auto-loads in new Claude Code sessions. With autopilot on, you don't even need to
|
|
56
|
+
`search` — describe your task and Claude surfaces the skill if one fits.
|
|
57
|
+
|
|
35
58
|
## Usage
|
|
36
59
|
|
|
37
60
|
```bash
|
|
@@ -57,6 +80,10 @@ skills-atlas upgrade brainstorming # re-fetch to latest (--all; won'
|
|
|
57
80
|
skills-atlas remove brainstorming # delete it
|
|
58
81
|
skills-atlas doctor # health check: orphans, drift, license/script risks
|
|
59
82
|
|
|
83
|
+
# 📦 Set up a whole project at once
|
|
84
|
+
skills-atlas kit # detect this project & install the right skills for it
|
|
85
|
+
skills-atlas sync # reproduce a project's kit (skills-atlas.kit.json)
|
|
86
|
+
|
|
60
87
|
# 🌐 Catalog
|
|
61
88
|
skills-atlas categories # the 20 top-level categories
|
|
62
89
|
skills-atlas list marketing # skill groups within a category
|
|
@@ -67,6 +94,11 @@ skills-atlas update # pull the latest catalog
|
|
|
67
94
|
`brainstorming → writing-plans → executing-plans → …`). `install <skill> --chain`
|
|
68
95
|
installs the whole pipeline in one archive download, ready to run in order.
|
|
69
96
|
|
|
97
|
+
**📦 Project kits.** `skills-atlas kit` detects what this project is (frontend / backend /
|
|
98
|
+
data / infra) and installs a tailored set — a universal dev workflow plus archetype
|
|
99
|
+
add-ons — into `./.claude/skills/`, then writes a committable `skills-atlas.kit.json`.
|
|
100
|
+
A teammate runs `skills-atlas sync` to reproduce it exactly.
|
|
101
|
+
|
|
70
102
|
Output is English by default; add `--zh` for Chinese, or `--json` to any command for machine-readable output.
|
|
71
103
|
After installing a skill, start a new Claude Code session to load it.
|
|
72
104
|
|
package/bin/skills.js
CHANGED
|
@@ -9,6 +9,8 @@ const upgrade = require('../src/commands/upgrade');
|
|
|
9
9
|
const remove = require('../src/commands/remove');
|
|
10
10
|
const outdated = require('../src/commands/outdated');
|
|
11
11
|
const doctor = require('../src/commands/doctor');
|
|
12
|
+
const kit = require('../src/commands/kit');
|
|
13
|
+
const sync = require('../src/commands/sync');
|
|
12
14
|
const suggest = require('../src/commands/suggest');
|
|
13
15
|
const hook = require('../src/commands/hook');
|
|
14
16
|
const update = require('../src/commands/update');
|
|
@@ -17,7 +19,7 @@ const { categories, list } = require('../src/commands/categories');
|
|
|
17
19
|
const VERSION = require('../package.json').version;
|
|
18
20
|
// `use` = install + activate inline (emit the SKILL.md so an agent follows it now).
|
|
19
21
|
const use = argv => install([...argv, '--inline']);
|
|
20
|
-
const commands = { search, info, install, use, installed, upgrade, remove, outdated, doctor, suggest, hook, update, categories, list };
|
|
22
|
+
const commands = { search, info, install, use, kit, sync, installed, upgrade, remove, outdated, doctor, suggest, hook, update, categories, list };
|
|
21
23
|
|
|
22
24
|
const HELP = `skills-atlas — search, install & manage AI agent skills
|
|
23
25
|
|
|
@@ -35,6 +37,8 @@ manage what you've installed:
|
|
|
35
37
|
upgrade [skill] re-fetch to the latest (--all; refuses to clobber local edits)
|
|
36
38
|
remove <skill> delete an installed skill
|
|
37
39
|
doctor health check: orphans, drift, missing SKILL.md, license/script risks
|
|
40
|
+
kit set up the right skills for THIS project (detect + install)
|
|
41
|
+
sync reproduce a project's kit from skills-atlas.kit.json
|
|
38
42
|
|
|
39
43
|
autopilot (opt-in):
|
|
40
44
|
hook on|off|status proactively suggest a skill in Claude when your prompt fits one
|
package/package.json
CHANGED
package/src/args.js
CHANGED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// `skills-atlas kit` — detect the project's archetype(s), propose a curated skill
|
|
2
|
+
// kit (core dev workflow + add-ons), install to project scope after confirmation,
|
|
3
|
+
// and write a committable skills-atlas.kit.json. Reuses installer/manifest/index.
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { parse } = require('../args');
|
|
8
|
+
const { loadData } = require('../data');
|
|
9
|
+
const { buildIndices, vendorsFor, skillDocPath } = require('../index-build');
|
|
10
|
+
const { installFolder } = require('../installer');
|
|
11
|
+
const fsu = require('../fsutil');
|
|
12
|
+
const km = require('../kitmanifest');
|
|
13
|
+
const { detect } = require('../detect');
|
|
14
|
+
const { buildKitPlan, ARCHETYPE_NAMES } = require('../kits');
|
|
15
|
+
const { confirm } = require('../prompt');
|
|
16
|
+
const { bold, dim, green, cyan } = require('../format');
|
|
17
|
+
|
|
18
|
+
const HELP = `usage: skills-atlas kit [options]
|
|
19
|
+
|
|
20
|
+
Detect this project's type, propose a curated skill kit, install it to
|
|
21
|
+
./.claude/skills/, and write a committable skills-atlas.kit.json.
|
|
22
|
+
|
|
23
|
+
options:
|
|
24
|
+
--archetype <name> override detection (${ARCHETYPE_NAMES.join(', ')})
|
|
25
|
+
--global install to ~/.claude/skills/ instead of the project
|
|
26
|
+
--yes install the proposed kit without prompting
|
|
27
|
+
--dry-run show the proposed kit; write nothing
|
|
28
|
+
--json machine-readable output`;
|
|
29
|
+
|
|
30
|
+
module.exports = async function kit(argv) {
|
|
31
|
+
const { values } = parse(argv, ['global', 'project', 'yes', 'dry-run', 'json', 'archetype']);
|
|
32
|
+
if (values.help) { console.log(HELP); return; }
|
|
33
|
+
|
|
34
|
+
const projectRoot = process.cwd();
|
|
35
|
+
const global = Boolean(values.global); // kit defaults to PROJECT scope
|
|
36
|
+
const targetRoot = fsu.installTargetDir({ global });
|
|
37
|
+
|
|
38
|
+
// 1. detect (or honor --archetype)
|
|
39
|
+
let archetypes, signals;
|
|
40
|
+
if (values.archetype) {
|
|
41
|
+
if (!ARCHETYPE_NAMES.includes(values.archetype)) {
|
|
42
|
+
console.error(`unknown archetype '${values.archetype}'. known: ${ARCHETYPE_NAMES.join(', ')}`);
|
|
43
|
+
process.exitCode = 1; return;
|
|
44
|
+
}
|
|
45
|
+
archetypes = [values.archetype]; signals = [`--archetype ${values.archetype}`];
|
|
46
|
+
} else {
|
|
47
|
+
({ archetypes, signals } = detect(projectRoot));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. resolve catalog + currently-installed set, build the plan
|
|
51
|
+
const { data } = loadData({ quiet: values.json });
|
|
52
|
+
const idx = buildIndices(data);
|
|
53
|
+
const installed = new Set(require('../manifest').list(targetRoot).map(e => e.skill));
|
|
54
|
+
const plan = buildKitPlan(archetypes, installed);
|
|
55
|
+
|
|
56
|
+
// resolve each skill to an installable source (skip ones with no folder)
|
|
57
|
+
const items = [];
|
|
58
|
+
for (const s of plan.skills) {
|
|
59
|
+
const chosen = vendorsFor(idx.skillIndex, s.name)[0];
|
|
60
|
+
const docPath = chosen && skillDocPath(chosen.vendor, s.name);
|
|
61
|
+
items.push({ ...s, chosen, docPath, installable: Boolean(docPath) });
|
|
62
|
+
}
|
|
63
|
+
const installable = items.filter(i => i.installable && !i.installed);
|
|
64
|
+
|
|
65
|
+
// human proposal (machine mode prints one JSON object at the end instead)
|
|
66
|
+
if (!values.json) {
|
|
67
|
+
console.log(`\nDetected: ${bold(archetypes.join(' + '))} ${dim(signals.join('; '))}`);
|
|
68
|
+
console.log(`Proposed kit — ${installable.length} new skill(s) → ${fsu.tildify(targetRoot)}\n`);
|
|
69
|
+
let group = null;
|
|
70
|
+
for (const i of items) {
|
|
71
|
+
if (i.group !== group) { group = i.group; console.log(` ${group === 'core' ? 'core dev workflow' : group}`); }
|
|
72
|
+
const mark = i.installed ? dim('✓ installed') : i.installable ? '' : dim('(whole-repo only — skipped)');
|
|
73
|
+
console.log(` • ${i.name.padEnd(26)} ${dim(i.reason)} ${mark}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const proposal = { archetypes, scope: global ? 'global' : 'project',
|
|
77
|
+
skills: items.map(i => ({ name: i.name, group: i.group, installed: i.installed, installable: i.installable })) };
|
|
78
|
+
|
|
79
|
+
if (values['dry-run']) {
|
|
80
|
+
if (values.json) console.log(JSON.stringify(proposal, null, 2));
|
|
81
|
+
else console.log(dim('\n(dry run — nothing written)'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!installable.length) {
|
|
85
|
+
if (values.json) console.log(JSON.stringify({ ...proposal, manifest: null, installedNow: [], failed: [] }, null, 2));
|
|
86
|
+
else console.log(green('\n✓ nothing to install — kit already satisfied.'));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!values.yes && !values.json) {
|
|
91
|
+
const ok = await confirm(`\nInstall ${installable.length} skill(s) and write ${km.FILE}?`, true);
|
|
92
|
+
if (!ok) { console.log('aborted.'); return; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 3. install (best-effort, like the chain installer)
|
|
96
|
+
const recorded = [];
|
|
97
|
+
const installedNow = [], failed = [];
|
|
98
|
+
for (const i of installable) {
|
|
99
|
+
const v = i.chosen.vendor, src = i.chosen.source;
|
|
100
|
+
const dest = path.join(targetRoot, i.name);
|
|
101
|
+
try {
|
|
102
|
+
const r = await installFolder({
|
|
103
|
+
author: v.author || src.author, repo: v.repo || src.repo,
|
|
104
|
+
branch: v.default_branch || src.default_branch || 'main',
|
|
105
|
+
docPath: i.docPath, dest, targetRoot, skillName: i.name,
|
|
106
|
+
source: src.name, group: i.chosen.row && i.chosen.row.group, category: i.chosen.row && i.chosen.row._cat,
|
|
107
|
+
});
|
|
108
|
+
if (!values.json) console.log(` ${green('✓')} ${i.name} ${dim(`(${r.fileCount} files)`)}`);
|
|
109
|
+
recorded.push({ name: i.name, source: src.name, ref: r.branchUsed, hash: r.hash, group: i.group });
|
|
110
|
+
installedNow.push(i.name);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
if (!values.json) console.log(` ${dim('✗ ' + i.name + ' — ' + e.message)}`);
|
|
113
|
+
failed.push({ name: i.name, error: e.message });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// include already-installed kit members in the manifest too (so it's complete)
|
|
118
|
+
for (const i of items.filter(x => x.installed)) {
|
|
119
|
+
const rec = require('../manifest').read(targetRoot).skills[i.name] || {};
|
|
120
|
+
recorded.push({ name: i.name, source: rec.source || null, ref: rec.branch || null, hash: rec.hash || null, group: i.group });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 4. write the committable kit manifest
|
|
124
|
+
const manifest = { version: 1, archetypes, scope: global ? 'global' : 'project', skills: recorded };
|
|
125
|
+
km.write(projectRoot, manifest);
|
|
126
|
+
if (failed.length) process.exitCode = 1; // surface partial/total install failure to callers/CI
|
|
127
|
+
|
|
128
|
+
if (values.json) {
|
|
129
|
+
console.log(JSON.stringify({ ...proposal, manifest: km.FILE, installedNow, failed }, null, 2));
|
|
130
|
+
} else {
|
|
131
|
+
console.log(`\n${green('✓')} kit ready — ${recorded.length} skill(s); wrote ${cyan(km.FILE)}`);
|
|
132
|
+
if (failed.length) console.log(dim(` ${failed.length} failed to install — see above.`));
|
|
133
|
+
console.log(dim(` teammates can reproduce it with: skills-atlas sync`));
|
|
134
|
+
console.log(dim(' start a new Claude Code session to load the skills.'));
|
|
135
|
+
}
|
|
136
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// `skills-atlas sync` — read skills-atlas.kit.json and install/upgrade the project
|
|
2
|
+
// to match it. Idempotent; for teammates and CI. Reuses installer + drift guard.
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { parse } = require('../args');
|
|
7
|
+
const { loadData } = require('../data');
|
|
8
|
+
const { buildIndices, vendorsFor, skillDocPath } = require('../index-build');
|
|
9
|
+
const { installFolder } = require('../installer');
|
|
10
|
+
const fsu = require('../fsutil');
|
|
11
|
+
const km = require('../kitmanifest');
|
|
12
|
+
const { green, dim, cyan } = require('../format');
|
|
13
|
+
|
|
14
|
+
const HELP = `usage: skills-atlas sync [options]
|
|
15
|
+
|
|
16
|
+
Read skills-atlas.kit.json and install/upgrade this project's skills to match it.
|
|
17
|
+
|
|
18
|
+
options:
|
|
19
|
+
--global sync to ~/.claude/skills/ instead of the project
|
|
20
|
+
--force overwrite skills with local edits
|
|
21
|
+
--update re-fetch every skill to latest and rewrite the manifest
|
|
22
|
+
--dry-run show what would change; write nothing
|
|
23
|
+
--json machine-readable output`;
|
|
24
|
+
|
|
25
|
+
module.exports = async function sync(argv) {
|
|
26
|
+
const { values } = parse(argv, ['global', 'project', 'force', 'update', 'dry-run', 'json']);
|
|
27
|
+
if (values.help) { console.log(HELP); return; }
|
|
28
|
+
|
|
29
|
+
const projectRoot = process.cwd();
|
|
30
|
+
const manifest = km.read(projectRoot);
|
|
31
|
+
if (!manifest) {
|
|
32
|
+
console.error(`no ${km.FILE} here. run \`skills-atlas kit\` first.`);
|
|
33
|
+
process.exitCode = 1; return;
|
|
34
|
+
}
|
|
35
|
+
const targetRoot = fsu.installTargetDir({ global: Boolean(values.global) });
|
|
36
|
+
|
|
37
|
+
const currentHash = name => {
|
|
38
|
+
const dest = path.join(targetRoot, name);
|
|
39
|
+
if (!fsu.dirExists(dest)) return null;
|
|
40
|
+
try { return fsu.hashDir(dest); } catch { return null; }
|
|
41
|
+
};
|
|
42
|
+
const actions = km.planSync(manifest, currentHash);
|
|
43
|
+
|
|
44
|
+
if (values['dry-run']) {
|
|
45
|
+
for (const a of actions) console.log(` ${a.action.padEnd(14)} ${a.name}`);
|
|
46
|
+
console.log(dim('\n(dry run — nothing written)'));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { data } = loadData({ quiet: values.json });
|
|
51
|
+
const idx = buildIndices(data);
|
|
52
|
+
const results = [];
|
|
53
|
+
|
|
54
|
+
for (const a of actions) {
|
|
55
|
+
const rec = { name: a.name, action: a.action };
|
|
56
|
+
if (a.action === 'up-to-date' && !values.update) { results.push({ ...rec, status: 'up-to-date' }); continue; }
|
|
57
|
+
if (a.action === 'local-changes' && !values.force) { results.push({ ...rec, status: 'local-changes' }); continue; }
|
|
58
|
+
|
|
59
|
+
const chosen = vendorsFor(idx.skillIndex, a.name).find(c => c.source.name === a.declared.source)
|
|
60
|
+
|| vendorsFor(idx.skillIndex, a.name)[0];
|
|
61
|
+
const docPath = chosen && skillDocPath(chosen.vendor, a.name);
|
|
62
|
+
if (!docPath) { results.push({ ...rec, status: 'no-folder' }); continue; }
|
|
63
|
+
|
|
64
|
+
const v = chosen.vendor, src = chosen.source;
|
|
65
|
+
const dest = path.join(targetRoot, a.name);
|
|
66
|
+
try {
|
|
67
|
+
const r = await installFolder({
|
|
68
|
+
author: v.author || src.author, repo: v.repo || src.repo,
|
|
69
|
+
branch: a.declared.ref || v.default_branch || src.default_branch || 'main',
|
|
70
|
+
docPath, dest, targetRoot, skillName: a.name,
|
|
71
|
+
source: src.name, group: chosen.row && chosen.row.group, category: chosen.row && chosen.row._cat,
|
|
72
|
+
});
|
|
73
|
+
results.push({ ...rec, status: 'installed', hash: r.hash });
|
|
74
|
+
if (values.update) { a.declared.hash = r.hash; a.declared.ref = r.branchUsed; a.declared.source = src.name; }
|
|
75
|
+
} catch (e) {
|
|
76
|
+
results.push({ ...rec, status: 'failed', error: e.message });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (values.update) km.write(projectRoot, manifest);
|
|
81
|
+
if (results.some(r => r.status === 'failed')) process.exitCode = 1; // CI-visible failure
|
|
82
|
+
|
|
83
|
+
if (values.json) { console.log(JSON.stringify(results, null, 2)); return; }
|
|
84
|
+
for (const r of results) {
|
|
85
|
+
const sym = r.status === 'installed' ? green('✓') : r.status === 'up-to-date' ? dim('=') : r.status === 'local-changes' ? '✋' : '✗';
|
|
86
|
+
console.log(` ${sym} ${r.name} ${dim(r.status + (r.error ? ': ' + r.error : ''))}`);
|
|
87
|
+
}
|
|
88
|
+
const n = results.filter(r => r.status === 'installed').length;
|
|
89
|
+
console.log(`\n${n} installed/updated from ${cyan(km.FILE)}.`);
|
|
90
|
+
if (results.some(r => r.status === 'local-changes')) console.log(dim('skipped local edits — re-run with --force.'));
|
|
91
|
+
};
|
package/src/detect.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Detect a project's archetype(s) from on-disk signals. Best-effort and dependency
|
|
2
|
+
// free; always returns at least ['generic']. Pure w.r.t. its `dir` argument.
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const readText = p => { try { return fs.readFileSync(p, 'utf8'); } catch { return null; } };
|
|
9
|
+
const readJson = p => { const t = readText(p); if (!t) return null; try { return JSON.parse(t); } catch { return null; } };
|
|
10
|
+
const listdir = d => { try { return fs.readdirSync(d); } catch { return []; } };
|
|
11
|
+
|
|
12
|
+
function detect(dir = process.cwd()) {
|
|
13
|
+
const archetypes = new Set();
|
|
14
|
+
const signals = [];
|
|
15
|
+
const add = (a, why) => { archetypes.add(a); signals.push(why); };
|
|
16
|
+
|
|
17
|
+
// --- Node / package.json ---
|
|
18
|
+
const pkg = readJson(path.join(dir, 'package.json'));
|
|
19
|
+
if (pkg) {
|
|
20
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
21
|
+
const names = Object.keys(deps);
|
|
22
|
+
const anyDep = res => names.some(d => res.some(re => re.test(d)));
|
|
23
|
+
if (anyDep([/^react(-dom)?$/, /^vue$/, /^svelte$/, /^next$/, /^nuxt$/, /^vite$/, /^@angular\//, /^solid-js$/]))
|
|
24
|
+
add('web-frontend', 'package.json: a frontend framework (react/vue/next/…)');
|
|
25
|
+
if (anyDep([/^express$/, /^fastify$/, /^@nestjs\//, /^koa$/, /^hapi$/]))
|
|
26
|
+
add('backend-service', 'package.json: a web-server framework');
|
|
27
|
+
if (pkg.bin) add('cli-library', 'package.json: bin field (CLI)');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// --- Python ---
|
|
31
|
+
const py = `${readText(path.join(dir, 'pyproject.toml')) || ''}\n${readText(path.join(dir, 'requirements.txt')) || ''}`.toLowerCase();
|
|
32
|
+
const hasNotebook = listdir(dir).some(f => f.endsWith('.ipynb'));
|
|
33
|
+
if (/pandas|numpy|scikit-learn|tensorflow|torch|jupyter/.test(py) || hasNotebook)
|
|
34
|
+
add('data-ml', 'python: data/ML libraries or notebooks');
|
|
35
|
+
if (/fastapi|flask|django/.test(py)) add('backend-service', 'python: a web framework');
|
|
36
|
+
|
|
37
|
+
// --- Infra ---
|
|
38
|
+
const entries = listdir(dir);
|
|
39
|
+
if (entries.some(f => f.endsWith('.tf')) ||
|
|
40
|
+
fs.existsSync(path.join(dir, 'Dockerfile')) ||
|
|
41
|
+
entries.some(f => /^docker-compose\.ya?ml$/.test(f)))
|
|
42
|
+
add('infra-devops', 'infra: terraform/docker files');
|
|
43
|
+
|
|
44
|
+
return { archetypes: archetypes.size ? [...archetypes] : ['generic'], signals };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { detect };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The committable, project-root kit declaration: skills-atlas.kit.json.
|
|
2
|
+
// Distinct from the per-dir install ledger (.skills-atlas.json): this is the
|
|
3
|
+
// *intent* ("the kit for this project is X"), committed and shared; `sync` applies it.
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const FILE = 'skills-atlas.kit.json';
|
|
10
|
+
const fileFor = root => path.join(root, FILE);
|
|
11
|
+
|
|
12
|
+
function read(root) {
|
|
13
|
+
try { return JSON.parse(fs.readFileSync(fileFor(root), 'utf8')); } catch { return null; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function write(root, m) {
|
|
17
|
+
fs.writeFileSync(fileFor(root), JSON.stringify(m, null, 2) + '\n');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Decide what `sync` should do per declared skill. `currentHash(name)` returns the
|
|
21
|
+
// on-disk content hash, or null/undefined if the skill isn't installed.
|
|
22
|
+
// not installed -> 'install'
|
|
23
|
+
// installed, hash matches -> 'up-to-date'
|
|
24
|
+
// installed, hash differs -> 'local-changes' (don't clobber edits without --force)
|
|
25
|
+
function planSync(manifest, currentHash) {
|
|
26
|
+
return (manifest.skills || []).map(s => {
|
|
27
|
+
const cur = currentHash(s.name);
|
|
28
|
+
let action;
|
|
29
|
+
if (!cur) action = 'install';
|
|
30
|
+
else if (!s.hash || cur === s.hash) action = 'up-to-date';
|
|
31
|
+
else action = 'local-changes';
|
|
32
|
+
return { name: s.name, action, declared: s };
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { FILE, fileFor, read, write, planSync };
|
package/src/kits.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Curated project kits: a universal core dev workflow plus per-archetype add-ons.
|
|
2
|
+
// Each skill carries a one-line reason. EVERY name here must exist in the catalog
|
|
3
|
+
// (enforced by test/kits.test.js — it already caught a non-existent 'openapi').
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
// [skillName, reason]
|
|
7
|
+
const CORE = [
|
|
8
|
+
['brainstorming', 'turn an idea into a spec'],
|
|
9
|
+
['writing-plans', 'break the work into a plan'],
|
|
10
|
+
['executing-plans', 'run the plan task by task'],
|
|
11
|
+
['systematic-debugging', 'root-cause a stubborn bug'],
|
|
12
|
+
['test-driven-development', 'write tests first'],
|
|
13
|
+
['requesting-code-review', 'get a structured review'],
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const ARCHETYPES = {
|
|
17
|
+
'web-frontend': [
|
|
18
|
+
['frontend-design', 'component/layout guidance'],
|
|
19
|
+
['web-design-guidelines', 'accessibility + performance'],
|
|
20
|
+
['webapp-testing', 'browser/E2E testing'],
|
|
21
|
+
],
|
|
22
|
+
'backend-service': [
|
|
23
|
+
['sql-queries', 'natural-language → SQL'],
|
|
24
|
+
['mcp-builder', 'build an MCP server'],
|
|
25
|
+
],
|
|
26
|
+
'data-ml': [
|
|
27
|
+
['data-analysis', 'analyze datasets'],
|
|
28
|
+
['exploratory-data-analysis', 'EDA workflow'],
|
|
29
|
+
],
|
|
30
|
+
'infra-devops': [
|
|
31
|
+
['security-best-practices', 'secure coding + threat modeling'],
|
|
32
|
+
['using-git-worktrees', 'isolated workspace workflow'],
|
|
33
|
+
],
|
|
34
|
+
'cli-library': [],
|
|
35
|
+
'generic': [],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const ARCHETYPE_NAMES = Object.keys(ARCHETYPES);
|
|
39
|
+
|
|
40
|
+
// archetypes[] -> [{ name, reason, group }] (core first, then add-ons, deduped)
|
|
41
|
+
function kitFor(archetypes) {
|
|
42
|
+
const out = CORE.map(([name, reason]) => ({ name, reason, group: 'core' }));
|
|
43
|
+
const seen = new Set(out.map(s => s.name));
|
|
44
|
+
for (const a of archetypes || []) {
|
|
45
|
+
for (const [name, reason] of (ARCHETYPES[a] || [])) {
|
|
46
|
+
if (!seen.has(name)) { seen.add(name); out.push({ name, reason, group: a }); }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Add an `installed` flag (from a set of already-present skill names).
|
|
53
|
+
function buildKitPlan(archetypes, installedNames = new Set()) {
|
|
54
|
+
const skills = kitFor(archetypes).map(s => ({ ...s, installed: installedNames.has(s.name) }));
|
|
55
|
+
return { archetypes: [...archetypes], skills };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const allKitSkills = () => [
|
|
59
|
+
...new Set([...CORE.map(c => c[0]), ...Object.values(ARCHETYPES).flat().map(s => s[0])]),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
module.exports = { CORE, ARCHETYPES, ARCHETYPE_NAMES, kitFor, buildKitPlan, allKitSkills };
|