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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills-atlas-cli",
3
- "version": "0.4.2",
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
 
@@ -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 };