skills-atlas-cli 0.4.1 → 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 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
 
@@ -113,11 +145,12 @@ skills-atlas hook on # enable (skills-atlas hook off / status)
113
145
  Registers a Claude Code `UserPromptSubmit` hook. When what you ask matches the
114
146
  territory of a catalog skill you don't have, the hook hands Claude a short
115
147
  shortlist of candidates and **Claude decides** whether any genuinely fits — and
116
- if so offers to install + activate it. You don't have to know the skill exists.
117
- The split is deliberate: the hook does **recall** (a distinctive-word match
118
- against the catalog, so the right skill is on the table), Claude does
119
- **precision** (it understands your intent and stays silent unless one truly
120
- fits, or searches further itself). It's:
148
+ if so, explains **what it does and why it fits your task**, then offers a choice:
149
+ use it now, see what it covers first (`skills-atlas info`), or skip. You don't
150
+ have to know the skill exists. The split is deliberate: the hook does **recall**
151
+ (a distinctive-word match against the catalog, so the right skill is on the
152
+ table), Claude does **precision** (it understands your intent and stays silent
153
+ unless one truly fits, or searches further itself). It's:
121
154
 
122
155
  - **off by default** — you turn it on explicitly; `hook off` removes it cleanly.
123
156
  - **quiet** — only fires on a distinctive match (greetings and generic actions
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills-atlas-cli",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Search, install and learn AI agent skills from the terminal — powered by the Skills Atlas catalog.",
5
5
  "bin": {
6
6
  "skills-atlas": "bin/skills.js",
package/src/args.js CHANGED
@@ -21,6 +21,8 @@ const ALL = {
21
21
  zh: { type: 'boolean' },
22
22
  all: { type: 'boolean' },
23
23
  inline: { type: 'boolean' },
24
+ archetype: { type: 'string' },
25
+ update: { type: 'boolean' },
24
26
  help: { type: 'boolean', short: 'h' },
25
27
  };
26
28
 
@@ -69,7 +69,13 @@ module.exports = async function hook(argv) {
69
69
  if (values.json) { console.log(JSON.stringify({ enabled: sub === 'on', settings: p })); return; }
70
70
  console.log(`${green('✓')} autopilot ${sub === 'on' ? 'enabled' : 'disabled'} ${dim(p)}`);
71
71
  if (sub === 'on') {
72
- console.log(dim('Claude now gets a one-line skill suggestion when your prompt strongly matches a catalog skill.'));
73
- console.log(dim('needs `skills-atlas` on PATH (npm i -g skills-atlas-cli). turn off: skills-atlas hook off'));
72
+ console.log('\nHow it works: when what you ask lines up with a skill you don\'t have yet, Claude');
73
+ console.log('quietly gets a shortlist and only if one truly fits explains it and offers a choice:');
74
+ console.log(dim(' you: "run a pre-mortem before we launch"'));
75
+ console.log(dim(' claude: "that\'s exactly what the pre-mortem skill does — it stress-tests your plan'));
76
+ console.log(dim(' before launch. use it now / see what it covers / skip?"'));
77
+ console.log(dim('\nIt stays silent on greetings and generic asks, never repeats a skill, and Claude makes'));
78
+ console.log(dim('the final call on relevance. Nothing leaves your machine.'));
79
+ console.log(dim('\nneeds `skills-atlas` on PATH (npm i -g skills-atlas-cli). turn off: skills-atlas hook off'));
74
80
  }
75
81
  };
@@ -275,12 +275,16 @@ module.exports = async function install(argv) {
275
275
  if (values.inline) {
276
276
  const body = readSkillMd(result.dest);
277
277
  if (body) {
278
- console.log('\n' + dim('─── SKILL.md (active for this task follow it now) ───'));
278
+ console.log('\n' + dim('─── SKILL.md the skill\'s own instructions; apply them to the task now ───'));
279
279
  console.log(body.trim());
280
280
  console.log(dim('─── end SKILL.md ───'));
281
281
  }
282
- console.log(dim('\n(the folder is also installed for future sessions)'));
282
+ console.log(`\n${green('✓')} ${bold(skill)} is now active — use the instructions above for the task at hand.`);
283
+ console.log(dim(` installed at ${fsu.tildify(result.dest)} (auto-loads in new sessions) · what it does: skills-atlas info ${skill} · remove: skills-atlas remove ${skill}`));
283
284
  } else {
284
- console.log(dim('\nStart a new Claude Code session to load the skill, then invoke it by name.'));
285
+ console.log(`\n${bold(skill)} is installed but not loaded yet. To use it:`);
286
+ console.log(dim(` • now, in this session: skills-atlas use ${skill}`));
287
+ console.log(dim(' • or start a new Claude Code session — it auto-loads from ~/.claude/skills/'));
288
+ console.log(dim(` what it does: skills-atlas info ${skill} · remove: skills-atlas remove ${skill}`));
285
289
  }
286
290
  };
@@ -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
+ };
@@ -63,16 +63,19 @@ module.exports = async function suggest() {
63
63
 
64
64
  const lines = candidates.map(c => {
65
65
  const uc = (c.row.use_case_en || c.row.use_case || '').replace(/\s+/g, ' ').trim().slice(0, 80);
66
- return `- ${c.skill}${uc ? ` — ${uc}` : ''} (install+activate: \`skills-atlas use ${c.skill} --yes\`)`;
66
+ return `- ${c.skill}${uc ? ` — ${uc}` : ''} (details: \`skills-atlas info ${c.skill}\` · use now: \`skills-atlas use ${c.skill} --yes\`)`;
67
67
  }).join('\n');
68
68
  const ctx =
69
69
  `[Skills Atlas autopilot] The user may be doing something one of these installable agent ` +
70
70
  `skills is built for. Judge for yourself — do NOT mention any of this unless one of them ` +
71
71
  `genuinely fits what they actually asked:\n${lines}\n` +
72
- `If one clearly fits, briefly offer it and (with the user's ok) install it via its command. ` +
73
- `If none fit but the task plainly needs a specialized skill, you may run ` +
74
- `\`skills-atlas search "<short intent>"\` to look further. If nothing fits, say nothing about ` +
75
- `this at all do not mention this hook, these skills, or that a suggestion was made.`;
72
+ `If one genuinely fits, DON'T just name it: in one line tell the user what it does and why it ` +
73
+ `fits THIS task (it's a curated skill from the Skills Atlas catalog, not something you made up), ` +
74
+ `then let them choose — activate it now (\`skills-atlas use <skill> --yes\` installs + applies it ` +
75
+ `immediately), see what it covers first (\`skills-atlas info <skill>\`), or skip and you'll just ` +
76
+ `do the task yourself. If none fit but the task plainly needs a specialized skill, you may run ` +
77
+ `\`skills-atlas search "<short intent>"\` to look further. If nothing fits, say nothing about this ` +
78
+ `at all — don't mention this hook, these skills, or that a suggestion was made.`;
76
79
  console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: ctx } }));
77
80
 
78
81
  state.lastSuggestedCount = state.count;
@@ -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 };