claude-agent-skills 1.3.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 +65 -0
- package/bundled-skills/ask-matt/SKILL.md +61 -0
- package/bundled-skills/brainstorming/SKILL.md +159 -0
- package/bundled-skills/brainstorming/scripts/frame-template.html +213 -0
- package/bundled-skills/brainstorming/scripts/helper.js +167 -0
- package/bundled-skills/brainstorming/scripts/server.cjs +723 -0
- package/bundled-skills/brainstorming/scripts/start-server.sh +209 -0
- package/bundled-skills/brainstorming/scripts/stop-server.sh +120 -0
- package/bundled-skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/bundled-skills/brainstorming/visual-companion.md +298 -0
- package/bundled-skills/cavecrew/README.md +41 -0
- package/bundled-skills/cavecrew/SKILL.md +82 -0
- package/bundled-skills/caveman/README.md +48 -0
- package/bundled-skills/caveman/SKILL.md +78 -0
- package/bundled-skills/caveman-commit/README.md +44 -0
- package/bundled-skills/caveman-commit/SKILL.md +65 -0
- package/bundled-skills/caveman-compress/README.md +163 -0
- package/bundled-skills/caveman-compress/SECURITY.md +31 -0
- package/bundled-skills/caveman-compress/SKILL.md +111 -0
- package/bundled-skills/caveman-compress/scripts/__init__.py +9 -0
- package/bundled-skills/caveman-compress/scripts/__main__.py +3 -0
- package/bundled-skills/caveman-compress/scripts/benchmark.py +80 -0
- package/bundled-skills/caveman-compress/scripts/cli.py +85 -0
- package/bundled-skills/caveman-compress/scripts/compress.py +342 -0
- package/bundled-skills/caveman-compress/scripts/detect.py +121 -0
- package/bundled-skills/caveman-compress/scripts/validate.py +213 -0
- package/bundled-skills/caveman-help/README.md +38 -0
- package/bundled-skills/caveman-help/SKILL.md +63 -0
- package/bundled-skills/caveman-review/README.md +33 -0
- package/bundled-skills/caveman-review/SKILL.md +55 -0
- package/bundled-skills/caveman-stats/README.md +30 -0
- package/bundled-skills/caveman-stats/SKILL.md +10 -0
- package/bundled-skills/codebase-design/DEEPENING.md +37 -0
- package/bundled-skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/bundled-skills/codebase-design/SKILL.md +114 -0
- package/bundled-skills/council/SKILL.md +77 -0
- package/bundled-skills/diagnosing-bugs/SKILL.md +134 -0
- package/bundled-skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
- package/bundled-skills/dispatching-parallel-agents/SKILL.md +185 -0
- package/bundled-skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/bundled-skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/bundled-skills/domain-modeling/SKILL.md +74 -0
- package/bundled-skills/edit-article/SKILL.md +15 -0
- package/bundled-skills/executing-plans/SKILL.md +70 -0
- package/bundled-skills/finishing-a-development-branch/SKILL.md +241 -0
- package/bundled-skills/git-guardrails-claude-code/SKILL.md +95 -0
- package/bundled-skills/git-guardrails-claude-code/scripts/block-dangerous-git.sh +25 -0
- package/bundled-skills/grill-me/SKILL.md +7 -0
- package/bundled-skills/grill-with-docs/SKILL.md +7 -0
- package/bundled-skills/grilling/SKILL.md +10 -0
- package/bundled-skills/handoff/SKILL.md +16 -0
- package/bundled-skills/i-am-dumb/SKILL.md +57 -0
- package/bundled-skills/implement/SKILL.md +15 -0
- package/bundled-skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/bundled-skills/improve-codebase-architecture/SKILL.md +66 -0
- package/bundled-skills/migrate-to-shoehorn/SKILL.md +118 -0
- package/bundled-skills/obsidian-vault/SKILL.md +59 -0
- package/bundled-skills/ponytail/SKILL.md +117 -0
- package/bundled-skills/ponytail-audit/SKILL.md +50 -0
- package/bundled-skills/ponytail-debt/SKILL.md +59 -0
- package/bundled-skills/ponytail-gain/SKILL.md +51 -0
- package/bundled-skills/ponytail-help/SKILL.md +43 -0
- package/bundled-skills/ponytail-review/SKILL.md +51 -0
- package/bundled-skills/prototype/LOGIC.md +79 -0
- package/bundled-skills/prototype/SKILL.md +31 -0
- package/bundled-skills/prototype/UI.md +112 -0
- package/bundled-skills/receiving-code-review/SKILL.md +213 -0
- package/bundled-skills/requesting-code-review/SKILL.md +103 -0
- package/bundled-skills/requesting-code-review/code-reviewer.md +172 -0
- package/bundled-skills/resolving-merge-conflicts/SKILL.md +14 -0
- package/bundled-skills/scaffold-exercises/SKILL.md +106 -0
- package/bundled-skills/setup-matt-pocock-skills/SKILL.md +127 -0
- package/bundled-skills/setup-matt-pocock-skills/domain.md +51 -0
- package/bundled-skills/setup-matt-pocock-skills/issue-tracker-github.md +34 -0
- package/bundled-skills/setup-matt-pocock-skills/issue-tracker-gitlab.md +35 -0
- package/bundled-skills/setup-matt-pocock-skills/issue-tracker-local.md +19 -0
- package/bundled-skills/setup-matt-pocock-skills/triage-labels.md +15 -0
- package/bundled-skills/setup-pre-commit/SKILL.md +91 -0
- package/bundled-skills/subagent-driven-development/SKILL.md +418 -0
- package/bundled-skills/subagent-driven-development/implementer-prompt.md +139 -0
- package/bundled-skills/subagent-driven-development/scripts/review-package +44 -0
- package/bundled-skills/subagent-driven-development/scripts/sdd-workspace +22 -0
- package/bundled-skills/subagent-driven-development/scripts/task-brief +40 -0
- package/bundled-skills/subagent-driven-development/task-reviewer-prompt.md +188 -0
- package/bundled-skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/bundled-skills/systematic-debugging/SKILL.md +296 -0
- package/bundled-skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/bundled-skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/bundled-skills/systematic-debugging/defense-in-depth.md +122 -0
- package/bundled-skills/systematic-debugging/find-polluter.sh +63 -0
- package/bundled-skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/bundled-skills/systematic-debugging/test-academic.md +14 -0
- package/bundled-skills/systematic-debugging/test-pressure-1.md +58 -0
- package/bundled-skills/systematic-debugging/test-pressure-2.md +68 -0
- package/bundled-skills/systematic-debugging/test-pressure-3.md +69 -0
- package/bundled-skills/tdd/SKILL.md +108 -0
- package/bundled-skills/tdd/mocking.md +59 -0
- package/bundled-skills/tdd/refactoring.md +10 -0
- package/bundled-skills/tdd/tests.md +61 -0
- package/bundled-skills/teach/GLOSSARY-FORMAT.md +35 -0
- package/bundled-skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
- package/bundled-skills/teach/MISSION-FORMAT.md +31 -0
- package/bundled-skills/teach/RESOURCES-FORMAT.md +32 -0
- package/bundled-skills/teach/SKILL.md +140 -0
- package/bundled-skills/test-driven-development/SKILL.md +371 -0
- package/bundled-skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/bundled-skills/to-issues/SKILL.md +84 -0
- package/bundled-skills/to-prd/SKILL.md +75 -0
- package/bundled-skills/triage/AGENT-BRIEF.md +207 -0
- package/bundled-skills/triage/OUT-OF-SCOPE.md +105 -0
- package/bundled-skills/triage/SKILL.md +112 -0
- package/bundled-skills/using-git-worktrees/SKILL.md +202 -0
- package/bundled-skills/using-superpowers/SKILL.md +121 -0
- package/bundled-skills/using-superpowers/references/antigravity-tools.md +96 -0
- package/bundled-skills/using-superpowers/references/claude-code-tools.md +50 -0
- package/bundled-skills/using-superpowers/references/codex-tools.md +72 -0
- package/bundled-skills/using-superpowers/references/copilot-tools.md +49 -0
- package/bundled-skills/using-superpowers/references/gemini-tools.md +63 -0
- package/bundled-skills/using-superpowers/references/pi-tools.md +28 -0
- package/bundled-skills/verification-before-completion/SKILL.md +139 -0
- package/bundled-skills/writing-great-skills/GLOSSARY.md +195 -0
- package/bundled-skills/writing-great-skills/SKILL.md +82 -0
- package/bundled-skills/writing-plans/SKILL.md +174 -0
- package/bundled-skills/writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/bundled-skills/writing-skills/SKILL.md +689 -0
- package/bundled-skills/writing-skills/anthropic-best-practices.md +1150 -0
- package/bundled-skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/bundled-skills/writing-skills/graphviz-conventions.dot +172 -0
- package/bundled-skills/writing-skills/persuasion-principles.md +187 -0
- package/bundled-skills/writing-skills/render-graphs.js +168 -0
- package/bundled-skills/writing-skills/testing-skills-with-subagents.md +384 -0
- package/commands/add.js +97 -0
- package/commands/check.js +54 -0
- package/commands/exportSkills.js +30 -0
- package/commands/hub.js +52 -0
- package/commands/importSkills.js +68 -0
- package/commands/list.js +37 -0
- package/commands/remove.js +59 -0
- package/commands/sync.js +66 -0
- package/commands/update.js +70 -0
- package/index.js +100 -0
- package/lib/banner.js +108 -0
- package/lib/constants.js +10 -0
- package/lib/deps.js +51 -0
- package/lib/hash.js +26 -0
- package/lib/install.js +31 -0
- package/lib/lockfile.js +37 -0
- package/lib/prompts.js +50 -0
- package/lib/scope.js +19 -0
- package/lib/summary.js +108 -0
- package/lib/theme.js +11 -0
- package/package.json +43 -0
- package/skills.json +164 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { note, outro } from '@clack/prompts';
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
3
|
+
import { showIntro } from '../lib/banner.js';
|
|
4
|
+
import { readLock } from '../lib/lockfile.js';
|
|
5
|
+
import { resolveScope } from '../lib/scope.js';
|
|
6
|
+
import { pickScope } from '../lib/prompts.js';
|
|
7
|
+
|
|
8
|
+
export async function runExport(opts = {}) {
|
|
9
|
+
await showIntro({ skip: opts.skipIntro ?? true });
|
|
10
|
+
|
|
11
|
+
const scopeFlags = await pickScope(opts);
|
|
12
|
+
const scope = resolveScope({ global: scopeFlags.global });
|
|
13
|
+
const lock = await readLock(scope.lockPath);
|
|
14
|
+
|
|
15
|
+
if (!lock || !Object.keys(lock.skills).length) {
|
|
16
|
+
note(`No skills installed at ${scope.lockPath}.`, 'Nothing to export');
|
|
17
|
+
outro('');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const json = `${JSON.stringify(lock, null, 2)}\n`;
|
|
22
|
+
|
|
23
|
+
if (opts.file) {
|
|
24
|
+
writeFileSync(opts.file, json, 'utf8');
|
|
25
|
+
note(`Wrote ${Object.keys(lock.skills).length} skill(s) to ${opts.file}\nShare this file with teammates and have them run:\n claude-agent-skills import --file ${opts.file}`, 'Exported');
|
|
26
|
+
outro('Done.');
|
|
27
|
+
} else {
|
|
28
|
+
process.stdout.write(json);
|
|
29
|
+
}
|
|
30
|
+
}
|
package/commands/hub.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { select, isCancel, cancel, outro } from '@clack/prompts';
|
|
2
|
+
import updateNotifier from 'update-notifier';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { showIntro } from '../lib/banner.js';
|
|
7
|
+
import { CliCancel } from '../lib/prompts.js';
|
|
8
|
+
|
|
9
|
+
const req = createRequire(import.meta.url);
|
|
10
|
+
const pkg = req(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'));
|
|
11
|
+
updateNotifier({ pkg }).notify();
|
|
12
|
+
import { runAdd } from './add.js';
|
|
13
|
+
import { runUpdate } from './update.js';
|
|
14
|
+
import { runRemove } from './remove.js';
|
|
15
|
+
import { runList } from './list.js';
|
|
16
|
+
import { runSync } from './sync.js';
|
|
17
|
+
import { runCheck } from './check.js';
|
|
18
|
+
|
|
19
|
+
const SKIP = { skipIntro: true };
|
|
20
|
+
|
|
21
|
+
const MENU = [
|
|
22
|
+
{ value: 'add', label: 'Add Skill(s)' },
|
|
23
|
+
{ value: 'update', label: 'Update Existing Skill(s)' },
|
|
24
|
+
{ value: 'remove', label: 'Remove Existing Skill(s)' },
|
|
25
|
+
{ value: 'list', label: 'List Installed Skill(s)' },
|
|
26
|
+
{ value: 'sync', label: 'Sync/Restore Skills from Lockfile' },
|
|
27
|
+
{ value: 'check', label: 'Check Skill(s)' },
|
|
28
|
+
{ value: 'quit', label: 'Quit' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export async function runHub() {
|
|
32
|
+
await showIntro();
|
|
33
|
+
|
|
34
|
+
for (;;) {
|
|
35
|
+
const choice = await select({ message: 'What do you want to do?', options: MENU });
|
|
36
|
+
|
|
37
|
+
if (isCancel(choice)) { cancel('Cancelled.'); return; }
|
|
38
|
+
if (choice === 'quit') { outro('Goodbye.'); return; }
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
if (choice === 'add') await runAdd(SKIP);
|
|
42
|
+
if (choice === 'update') await runUpdate(SKIP);
|
|
43
|
+
if (choice === 'remove') await runRemove(SKIP);
|
|
44
|
+
if (choice === 'list') await runList(SKIP);
|
|
45
|
+
if (choice === 'sync') await runSync(SKIP);
|
|
46
|
+
if (choice === 'check') await runCheck(SKIP);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
if (e instanceof CliCancel) continue;
|
|
49
|
+
throw e;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { note, outro } from '@clack/prompts';
|
|
2
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { showIntro } from '../lib/banner.js';
|
|
6
|
+
import { readLock, writeLock, upsertSkill } from '../lib/lockfile.js';
|
|
7
|
+
import { resolveScope, ensureDirs } from '../lib/scope.js';
|
|
8
|
+
import { pickScope, confirmProceed } from '../lib/prompts.js';
|
|
9
|
+
import { materialize } from '../lib/install.js';
|
|
10
|
+
import { hashSkill } from '../lib/hash.js';
|
|
11
|
+
|
|
12
|
+
const BUNDLE_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'bundled-skills');
|
|
13
|
+
|
|
14
|
+
export async function runImport(opts = {}) {
|
|
15
|
+
await showIntro({ skip: opts.skipIntro ?? true });
|
|
16
|
+
|
|
17
|
+
const file = opts.file;
|
|
18
|
+
if (!file) {
|
|
19
|
+
console.error('Specify a file: claude-agent-skills import --file <lockfile.json>');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let imported;
|
|
24
|
+
try {
|
|
25
|
+
imported = JSON.parse(readFileSync(file, 'utf8'));
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error(`Could not read ${file}: ${e.message}`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const skills = Object.keys(imported.skills ?? {});
|
|
32
|
+
if (!skills.length) {
|
|
33
|
+
note('No skills found in the imported lockfile.', 'Empty');
|
|
34
|
+
outro('');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const scopeFlags = await pickScope(opts);
|
|
39
|
+
const scope = resolveScope({ global: scopeFlags.global });
|
|
40
|
+
|
|
41
|
+
note(`${skills.length} skill(s) from ${file}:\n${skills.join(', ')}`, 'Ready to import');
|
|
42
|
+
|
|
43
|
+
const proceed = await confirmProceed(`Install ${skills.length} skill(s)?`);
|
|
44
|
+
if (!proceed) { outro('Cancelled. No changes made.'); return; }
|
|
45
|
+
|
|
46
|
+
await ensureDirs(scope);
|
|
47
|
+
const lock = (await readLock(scope.lockPath)) ?? { version: 1, skills: {} };
|
|
48
|
+
const installed = [], skipped = [];
|
|
49
|
+
|
|
50
|
+
for (const name of skills.sort()) {
|
|
51
|
+
const src = join(BUNDLE_DIR, name);
|
|
52
|
+
if (!existsSync(src)) { skipped.push(name); continue; }
|
|
53
|
+
|
|
54
|
+
const entry = imported.skills[name];
|
|
55
|
+
const copy = entry?.linkType !== 'symlink';
|
|
56
|
+
await materialize({ src, dest: join(scope.skillsDir, name), copy });
|
|
57
|
+
if (scope.agentsSkillsDir) {
|
|
58
|
+
await materialize({ src, dest: join(scope.agentsSkillsDir, name), copy });
|
|
59
|
+
}
|
|
60
|
+
upsertSkill(lock, name, { computedHash: await hashSkill(src), linkType: copy ? 'copy' : 'symlink' });
|
|
61
|
+
installed.push(name);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await writeLock(scope.lockPath, lock);
|
|
65
|
+
|
|
66
|
+
if (skipped.length) note(`Skipped (not in bundle): ${skipped.join(', ')}`, 'Warning');
|
|
67
|
+
outro(`Imported ${installed.length} skill(s). Restart Claude Code.`);
|
|
68
|
+
}
|
package/commands/list.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { note, outro } from '@clack/prompts';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { showIntro } from '../lib/banner.js';
|
|
4
|
+
import { readLock } from '../lib/lockfile.js';
|
|
5
|
+
import { resolveScope } from '../lib/scope.js';
|
|
6
|
+
import { pickScope } from '../lib/prompts.js';
|
|
7
|
+
import { pathExists, isBroken } from '../lib/install.js';
|
|
8
|
+
import { renderListSummary } from '../lib/summary.js';
|
|
9
|
+
|
|
10
|
+
export async function runList(opts = {}) {
|
|
11
|
+
await showIntro({ skip: opts.skipIntro });
|
|
12
|
+
|
|
13
|
+
const scopeFlags = await pickScope(opts);
|
|
14
|
+
const scope = resolveScope({ global: scopeFlags.global });
|
|
15
|
+
const lock = await readLock(scope.lockPath);
|
|
16
|
+
|
|
17
|
+
if (!lock || !Object.keys(lock.skills).length) {
|
|
18
|
+
console.log(`No skills installed. Lockfile not found at: ${scope.lockPath}`);
|
|
19
|
+
outro('');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const rows = await Promise.all(
|
|
24
|
+
Object.entries(lock.skills).sort().map(async ([name, entry]) => {
|
|
25
|
+
const dest = join(scope.skillsDir, name);
|
|
26
|
+
const ex = await pathExists(dest);
|
|
27
|
+
const broken = ex && await isBroken(dest);
|
|
28
|
+
const status = ex && !broken ? 'ok' : broken ? 'broken' : 'missing';
|
|
29
|
+
return { name, linkType: entry.linkType, hash: entry.computedHash.slice(0, 8), status, healthy: status === 'ok' };
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
note(renderListSummary({
|
|
34
|
+
scope: scope.scope, skillsDir: scope.skillsDir, lockPath: scope.lockPath, rows,
|
|
35
|
+
}), 'Installed skills');
|
|
36
|
+
outro('');
|
|
37
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { multiselect, note, outro, isCancel } from '@clack/prompts';
|
|
2
|
+
import { rm } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { showIntro } from '../lib/banner.js';
|
|
5
|
+
import { readLock, writeLock, removeSkill } from '../lib/lockfile.js';
|
|
6
|
+
import { resolveScope } from '../lib/scope.js';
|
|
7
|
+
import { pickScope, confirmProceed, CliCancel } from '../lib/prompts.js';
|
|
8
|
+
import { pathExists } from '../lib/install.js';
|
|
9
|
+
import { renderRemoveSummary } from '../lib/summary.js';
|
|
10
|
+
|
|
11
|
+
export async function runRemove(opts = {}) {
|
|
12
|
+
await showIntro({ skip: opts.skipIntro });
|
|
13
|
+
|
|
14
|
+
const scopeFlags = await pickScope(opts);
|
|
15
|
+
const scope = resolveScope({ global: scopeFlags.global });
|
|
16
|
+
const lock = await readLock(scope.lockPath);
|
|
17
|
+
|
|
18
|
+
if (!lock || !Object.keys(lock.skills).length) {
|
|
19
|
+
console.log(`No skills installed. Lockfile not found at: ${scope.lockPath}`);
|
|
20
|
+
outro('');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const installed = Object.keys(lock.skills).sort();
|
|
25
|
+
|
|
26
|
+
const selected = await multiselect({
|
|
27
|
+
message: 'Select skills to remove:',
|
|
28
|
+
options: installed.map(n => ({ value: n, label: n })),
|
|
29
|
+
required: false,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (isCancel(selected)) throw new CliCancel();
|
|
33
|
+
if (!selected?.length) { outro('Nothing selected.'); return; }
|
|
34
|
+
|
|
35
|
+
if (!opts.yes) {
|
|
36
|
+
const proceed = await confirmProceed(`Remove ${selected.length} skill(s)?`);
|
|
37
|
+
if (!proceed) { outro('Cancelled. No changes made.'); return; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const removed = [], failed = [];
|
|
41
|
+
for (const name of selected) {
|
|
42
|
+
try {
|
|
43
|
+
const dest = join(scope.skillsDir, name);
|
|
44
|
+
if (await pathExists(dest)) await rm(dest, { recursive: true, force: true });
|
|
45
|
+
if (scope.agentsSkillsDir) {
|
|
46
|
+
const agentDest = join(scope.agentsSkillsDir, name);
|
|
47
|
+
if (await pathExists(agentDest)) await rm(agentDest, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
removeSkill(lock, name);
|
|
50
|
+
removed.push(name);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
failed.push({ name, error: e.message });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
await writeLock(scope.lockPath, lock);
|
|
56
|
+
|
|
57
|
+
note(renderRemoveSummary({ scope: scope.scope, skillsDir: scope.skillsDir, lockPath: scope.lockPath, removed, failed }), 'Removed');
|
|
58
|
+
outro(removed.length > 0 ? `Removed ${removed.length} skill(s). Restart Claude Code.` : 'Nothing removed.');
|
|
59
|
+
}
|
package/commands/sync.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { note, outro } from '@clack/prompts';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { showIntro } from '../lib/banner.js';
|
|
6
|
+
import { readLock, writeLock, upsertSkill } from '../lib/lockfile.js';
|
|
7
|
+
import { resolveScope, ensureDirs } from '../lib/scope.js';
|
|
8
|
+
import { pickScope } from '../lib/prompts.js';
|
|
9
|
+
import { materialize, pathExists, isBroken, diskType } from '../lib/install.js';
|
|
10
|
+
import { hashSkill } from '../lib/hash.js';
|
|
11
|
+
import { renderSyncSummary } from '../lib/summary.js';
|
|
12
|
+
|
|
13
|
+
const BUNDLE_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'bundled-skills');
|
|
14
|
+
|
|
15
|
+
export async function runSync(opts = {}) {
|
|
16
|
+
await showIntro({ skip: opts.skipIntro });
|
|
17
|
+
|
|
18
|
+
const scopeFlags = await pickScope(opts);
|
|
19
|
+
const scope = resolveScope({ global: scopeFlags.global });
|
|
20
|
+
const lock = await readLock(scope.lockPath);
|
|
21
|
+
|
|
22
|
+
if (!lock || !Object.keys(lock.skills).length) {
|
|
23
|
+
note(`No lockfile found at ${scope.lockPath}.\nRun "Add Skill(s)" first.`, 'Nothing to sync');
|
|
24
|
+
outro('');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await ensureDirs(scope);
|
|
29
|
+
const synced = [], ok = [];
|
|
30
|
+
|
|
31
|
+
for (const [name, entry] of Object.entries(lock.skills).sort()) {
|
|
32
|
+
const dest = join(scope.skillsDir, name);
|
|
33
|
+
const src = join(BUNDLE_DIR, name);
|
|
34
|
+
|
|
35
|
+
if (!existsSync(src)) {
|
|
36
|
+
console.warn(` Warning: "${name}" in lockfile but missing from bundle — skipping.`);
|
|
37
|
+
ok.push(name);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ex = await pathExists(dest);
|
|
42
|
+
const broken = ex && await isBroken(dest);
|
|
43
|
+
const onDisk = await diskType(dest);
|
|
44
|
+
const wantCopy = entry.linkType === 'copy';
|
|
45
|
+
const typeMismatch = onDisk !== 'missing' &&
|
|
46
|
+
((wantCopy && onDisk === 'symlink') || (!wantCopy && onDisk === 'copy'));
|
|
47
|
+
|
|
48
|
+
if (ex && !broken && !typeMismatch) { ok.push(name); continue; }
|
|
49
|
+
|
|
50
|
+
const lt = await materialize({ src, dest, copy: wantCopy });
|
|
51
|
+
if (scope.agentsSkillsDir) {
|
|
52
|
+
await materialize({ src, dest: join(scope.agentsSkillsDir, name), copy: wantCopy });
|
|
53
|
+
}
|
|
54
|
+
upsertSkill(lock, name, { computedHash: await hashSkill(src), linkType: lt });
|
|
55
|
+
synced.push(name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await writeLock(scope.lockPath, lock);
|
|
59
|
+
|
|
60
|
+
note(renderSyncSummary({
|
|
61
|
+
scope: scope.scope, skillsDir: scope.skillsDir, lockPath: scope.lockPath,
|
|
62
|
+
synced, ok,
|
|
63
|
+
}), 'Sync summary');
|
|
64
|
+
|
|
65
|
+
outro(synced.length > 0 ? `Synced ${synced.length} skill(s).` : 'All skills present.');
|
|
66
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { multiselect, note, outro, isCancel } from '@clack/prompts';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { showIntro } from '../lib/banner.js';
|
|
6
|
+
import { readLock, writeLock, upsertSkill } from '../lib/lockfile.js';
|
|
7
|
+
import { resolveScope, ensureDirs } from '../lib/scope.js';
|
|
8
|
+
import { pickScope, confirmProceed, CliCancel } from '../lib/prompts.js';
|
|
9
|
+
import { materialize } from '../lib/install.js';
|
|
10
|
+
import { hashSkill } from '../lib/hash.js';
|
|
11
|
+
import { renderUpdateSummary } from '../lib/summary.js';
|
|
12
|
+
|
|
13
|
+
const BUNDLE_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'bundled-skills');
|
|
14
|
+
|
|
15
|
+
export async function runUpdate(opts = {}) {
|
|
16
|
+
await showIntro({ skip: opts.skipIntro });
|
|
17
|
+
|
|
18
|
+
const scopeFlags = await pickScope(opts);
|
|
19
|
+
const scope = resolveScope({ global: scopeFlags.global });
|
|
20
|
+
const lock = await readLock(scope.lockPath);
|
|
21
|
+
|
|
22
|
+
if (!lock || !Object.keys(lock.skills).length) {
|
|
23
|
+
console.log(`No skills installed. Run "add" first.`);
|
|
24
|
+
outro('');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const updatable = [];
|
|
29
|
+
for (const [name, entry] of Object.entries(lock.skills).sort()) {
|
|
30
|
+
const bundledPath = join(BUNDLE_DIR, name);
|
|
31
|
+
if (!existsSync(bundledPath)) continue;
|
|
32
|
+
const bundledHash = await hashSkill(bundledPath);
|
|
33
|
+
if (bundledHash !== entry.computedHash) updatable.push({ name, bundledPath, linkType: entry.linkType });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!updatable.length) {
|
|
37
|
+
note('All installed skills are already up to date.', 'No updates available');
|
|
38
|
+
outro('Done.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const selected = await multiselect({
|
|
43
|
+
message: `Select skills to update (${updatable.length} available):`,
|
|
44
|
+
options: updatable.map(s => ({ value: s.name, label: s.name })),
|
|
45
|
+
required: false,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (isCancel(selected)) throw new CliCancel();
|
|
49
|
+
if (!selected?.length) { outro('Nothing selected.'); return; }
|
|
50
|
+
|
|
51
|
+
if (!opts.yes) {
|
|
52
|
+
const proceed = await confirmProceed(`Update ${selected.length} skill(s)?`);
|
|
53
|
+
if (!proceed) { outro('Cancelled. No changes made.'); return; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await ensureDirs(scope);
|
|
57
|
+
for (const name of selected) {
|
|
58
|
+
const skill = updatable.find(s => s.name === name);
|
|
59
|
+
const hash = await hashSkill(skill.bundledPath);
|
|
60
|
+
const lt = await materialize({ src: skill.bundledPath, dest: join(scope.skillsDir, name), copy: skill.linkType === 'copy' });
|
|
61
|
+
if (scope.agentsSkillsDir) {
|
|
62
|
+
await materialize({ src: skill.bundledPath, dest: join(scope.agentsSkillsDir, name), copy: skill.linkType === 'copy' });
|
|
63
|
+
}
|
|
64
|
+
upsertSkill(lock, name, { computedHash: hash, linkType: lt });
|
|
65
|
+
}
|
|
66
|
+
await writeLock(scope.lockPath, lock);
|
|
67
|
+
|
|
68
|
+
note(renderUpdateSummary({ scope: scope.scope, skillsDir: scope.skillsDir, lockPath: scope.lockPath, updated: selected }), 'Updated');
|
|
69
|
+
outro(`Done! ${selected.length} skill(s) updated. Restart Claude Code.`);
|
|
70
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { runHub } from './commands/hub.js';
|
|
7
|
+
import { runAdd } from './commands/add.js';
|
|
8
|
+
import { runUpdate } from './commands/update.js';
|
|
9
|
+
import { runRemove } from './commands/remove.js';
|
|
10
|
+
import { runList } from './commands/list.js';
|
|
11
|
+
import { runSync } from './commands/sync.js';
|
|
12
|
+
import { runCheck } from './commands/check.js';
|
|
13
|
+
import { runExport } from './commands/exportSkills.js';
|
|
14
|
+
import { runImport } from './commands/importSkills.js';
|
|
15
|
+
import { CliCancel } from './lib/prompts.js';
|
|
16
|
+
|
|
17
|
+
const req = createRequire(import.meta.url);
|
|
18
|
+
const { version } = req(join(dirname(fileURLToPath(import.meta.url)), 'package.json'));
|
|
19
|
+
|
|
20
|
+
const program = new Command()
|
|
21
|
+
.name('claude-skills')
|
|
22
|
+
.description("Install and manage Pavi's Claude Code skills")
|
|
23
|
+
.version(version);
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.command('hub', { isDefault: true })
|
|
27
|
+
.description('Interactive menu (default) — loops until Quit or Ctrl+C')
|
|
28
|
+
.action(() => runHub());
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command('add')
|
|
32
|
+
.description('Install skills')
|
|
33
|
+
.option('-g, --global', 'Install globally (~/.claude/skills)')
|
|
34
|
+
.option('-p, --project', 'Install to current project (.claude/skills)')
|
|
35
|
+
.option('--all', 'Install all skills without prompting')
|
|
36
|
+
.option('--skill <name...>', 'Specific skill(s) to install')
|
|
37
|
+
.option('--copy', 'Copy files (safe for npx)')
|
|
38
|
+
.option('--symlink', 'Create symlinks (persistent install only)')
|
|
39
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
40
|
+
.action(opts => runAdd(opts));
|
|
41
|
+
|
|
42
|
+
program
|
|
43
|
+
.command('update')
|
|
44
|
+
.description('Update installed skills to latest bundled version')
|
|
45
|
+
.option('-g, --global', 'Update global skills')
|
|
46
|
+
.option('-p, --project', 'Update project skills')
|
|
47
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
48
|
+
.action(opts => runUpdate(opts));
|
|
49
|
+
|
|
50
|
+
program
|
|
51
|
+
.command('remove')
|
|
52
|
+
.description('Remove installed skills')
|
|
53
|
+
.option('-g, --global', 'Remove from global')
|
|
54
|
+
.option('-p, --project', 'Remove from project')
|
|
55
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
56
|
+
.action(opts => runRemove(opts));
|
|
57
|
+
|
|
58
|
+
program
|
|
59
|
+
.command('list')
|
|
60
|
+
.description('List installed skills and their health status')
|
|
61
|
+
.option('-g, --global', 'List global skills')
|
|
62
|
+
.option('-p, --project', 'List project skills')
|
|
63
|
+
.action(opts => runList(opts));
|
|
64
|
+
|
|
65
|
+
program
|
|
66
|
+
.command('sync')
|
|
67
|
+
.description('Restore skills from lockfile')
|
|
68
|
+
.option('-g, --global', 'Sync global skills')
|
|
69
|
+
.option('-p, --project', 'Sync project skills')
|
|
70
|
+
.action(opts => runSync(opts));
|
|
71
|
+
|
|
72
|
+
program
|
|
73
|
+
.command('check')
|
|
74
|
+
.description('Check health and update status of installed skills')
|
|
75
|
+
.option('-g, --global', 'Check global skills')
|
|
76
|
+
.option('-p, --project', 'Check project skills')
|
|
77
|
+
.action(opts => runCheck(opts));
|
|
78
|
+
|
|
79
|
+
program
|
|
80
|
+
.command('export')
|
|
81
|
+
.description('Export lockfile for sharing with teammates')
|
|
82
|
+
.option('-g, --global', 'Export global lockfile')
|
|
83
|
+
.option('-p, --project', 'Export project lockfile')
|
|
84
|
+
.option('--file <path>', 'Write to file instead of stdout')
|
|
85
|
+
.action(opts => runExport(opts));
|
|
86
|
+
|
|
87
|
+
program
|
|
88
|
+
.command('import')
|
|
89
|
+
.description('Import a shared lockfile and install those skills')
|
|
90
|
+
.option('-g, --global', 'Install into global scope')
|
|
91
|
+
.option('-p, --project', 'Install into project scope')
|
|
92
|
+
.option('--file <path>', 'Lockfile to import (required)')
|
|
93
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
94
|
+
.action(opts => runImport(opts));
|
|
95
|
+
|
|
96
|
+
program.parseAsync().catch(err => {
|
|
97
|
+
if (err instanceof CliCancel) process.exit(0);
|
|
98
|
+
console.error(`\nError: ${err.message}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
});
|
package/lib/banner.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import ansis from 'ansis';
|
|
2
|
+
import { muted } from './theme.js';
|
|
3
|
+
import { REPO } from './constants.js';
|
|
4
|
+
|
|
5
|
+
const CLAUDE_LINES = [
|
|
6
|
+
' ██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗',
|
|
7
|
+
'██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝',
|
|
8
|
+
'██║ ██║ ███████║██║ ██║██║ ██║█████╗ ',
|
|
9
|
+
'██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝ ',
|
|
10
|
+
'╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗',
|
|
11
|
+
' ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const SKILLS_LINES = [
|
|
15
|
+
'███████╗██╗ ██╗██╗██╗ ██╗ ███████╗',
|
|
16
|
+
'██╔════╝██║ ██╔╝██║██║ ██║ ██╔════╝',
|
|
17
|
+
'███████╗█████╔╝ ██║██║ ██║ ███████╗',
|
|
18
|
+
'╚════██║██╔═██╗ ██║██║ ██║ ╚════██║',
|
|
19
|
+
'███████║██║ ██╗██║███████╗███████╗███████║',
|
|
20
|
+
'╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const ART = [...CLAUDE_LINES, ...SKILLS_LINES];
|
|
24
|
+
const MAX_WIDTH = Math.max(...ART.map(l => l.length));
|
|
25
|
+
|
|
26
|
+
// One pride stripe per art line (12 lines → 2 lines per colour)
|
|
27
|
+
const PRIDE = [
|
|
28
|
+
[220, 30, 30], // red
|
|
29
|
+
[220, 30, 30],
|
|
30
|
+
[255, 140, 0], // orange
|
|
31
|
+
[255, 140, 0],
|
|
32
|
+
[255, 210, 0], // yellow
|
|
33
|
+
[255, 210, 0],
|
|
34
|
+
[ 30, 185, 30], // green
|
|
35
|
+
[ 30, 185, 30],
|
|
36
|
+
[ 30, 100, 255], // blue
|
|
37
|
+
[ 30, 100, 255],
|
|
38
|
+
[150, 30, 230], // purple
|
|
39
|
+
[150, 30, 230],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
43
|
+
const col = (r, g, b) => s => ansis.rgb(Math.round(r), Math.round(g), Math.round(b))(s);
|
|
44
|
+
const up = n => process.stdout.write(`\x1b[${n}A`);
|
|
45
|
+
const wline = s => process.stdout.write(`\x1b[2K${s}\n`);
|
|
46
|
+
|
|
47
|
+
function clamp(v) { return Math.max(0, Math.min(255, Math.round(v))); }
|
|
48
|
+
|
|
49
|
+
/** Sheen sweep: all chars glow pride → white at the core → pride → dim */
|
|
50
|
+
function sheenColor(_ch, dist, [r, g, b]) {
|
|
51
|
+
if (dist <= 1) return col(255, 255, 255); // white-hot core
|
|
52
|
+
if (dist <= 3) return col(clamp(r*1.1+60), clamp(g*1.1+60), clamp(b*1.1+60)); // bright pride
|
|
53
|
+
if (dist <= 8) return col(r, g, b); // pure pride
|
|
54
|
+
if (dist <= 15) return col(r * 0.7, g * 0.7, b * 0.7); // dimmed
|
|
55
|
+
return col(r * 0.45, g * 0.45, b * 0.45); // dark base
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Static settled state: sine-arc so centre is brightest (metallic highlight) */
|
|
59
|
+
function staticColor(_ch, i, len, [r, g, b]) {
|
|
60
|
+
const arc = Math.sin((i / (len - 1 || 1)) * Math.PI); // 0 at edges, 1 at centre
|
|
61
|
+
const f = 0.38 + arc * 0.62; // 0.38 → 1.0
|
|
62
|
+
return col(r * f, g * f, b * f);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderFrame(getColor) {
|
|
66
|
+
ART.forEach((line, li) => {
|
|
67
|
+
const pride = PRIDE[li] ?? PRIDE[PRIDE.length - 1];
|
|
68
|
+
let out = '';
|
|
69
|
+
for (let i = 0; i < line.length; i++) {
|
|
70
|
+
const ch = line[i];
|
|
71
|
+
out += ch === ' ' ? ' ' : getColor(ch, i, line.length, pride)(ch);
|
|
72
|
+
}
|
|
73
|
+
wline(out);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function runSweep({ step = 3, fps = 55 } = {}) {
|
|
78
|
+
const delay = Math.round(1000 / fps);
|
|
79
|
+
for (let sx = -20; sx <= MAX_WIDTH + 20; sx += step) {
|
|
80
|
+
up(ART.length);
|
|
81
|
+
renderFrame((ch, i, _len, pride) => sheenColor(ch, Math.abs(i - sx), pride));
|
|
82
|
+
await sleep(delay);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function renderStatic() {
|
|
87
|
+
up(ART.length);
|
|
88
|
+
renderFrame(staticColor);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* showIntro() — full two-sweep entrance + subtitle (call ONCE at startup)
|
|
93
|
+
* showIntro({ skip:true }) — no-op (used inside sub-commands called from hub)
|
|
94
|
+
*/
|
|
95
|
+
export async function showIntro({ skip = false } = {}) {
|
|
96
|
+
if (skip) return;
|
|
97
|
+
|
|
98
|
+
process.stdout.write('\n'.repeat(ART.length)); // reserve block
|
|
99
|
+
|
|
100
|
+
await runSweep({ step: 3, fps: 55 }); // first sweep (~0.8 s)
|
|
101
|
+
await runSweep({ step: 3, fps: 55 }); // second sweep (~0.8 s)
|
|
102
|
+
await renderStatic(); // settle to metallic pride
|
|
103
|
+
|
|
104
|
+
const silver = s => ansis.rgb(190, 190, 190)(s);
|
|
105
|
+
process.stdout.write('\n');
|
|
106
|
+
process.stdout.write(silver('Agent Skills for Claude Code by Pavithran Francis') + '\n\n');
|
|
107
|
+
process.stdout.write(muted('Repository: ') + silver(REPO) + '\n\n');
|
|
108
|
+
}
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const req = createRequire(import.meta.url);
|
|
6
|
+
const pkg = req(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'));
|
|
7
|
+
|
|
8
|
+
export const VERSION = pkg.version;
|
|
9
|
+
export const PACKAGE_NAME = pkg.name;
|
|
10
|
+
export const REPO = 'https://github.com/Pavithran-Francis/skills';
|
package/lib/deps.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const req = createRequire(import.meta.url);
|
|
6
|
+
|
|
7
|
+
export function loadManifest() {
|
|
8
|
+
const manifestPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'skills.json');
|
|
9
|
+
try {
|
|
10
|
+
return req(manifestPath);
|
|
11
|
+
} catch {
|
|
12
|
+
return { skills: [], dependsOn: {} };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Topological expansion: given selected skill names, resolves all transitive
|
|
18
|
+
* dependencies and returns them in install order (deps before dependents).
|
|
19
|
+
* Returns { ordered, addedBy } where addedBy maps auto-added names → who pulled them in.
|
|
20
|
+
*/
|
|
21
|
+
export function expandDependencies(manifest, selected) {
|
|
22
|
+
const known = new Set(manifest.skills ?? []);
|
|
23
|
+
const selectedSet = new Set(selected);
|
|
24
|
+
const addedBy = new Map();
|
|
25
|
+
const visiting = new Set();
|
|
26
|
+
const visited = new Set();
|
|
27
|
+
const ordered = [];
|
|
28
|
+
|
|
29
|
+
function visit(name, parent) {
|
|
30
|
+
if (visited.has(name)) return;
|
|
31
|
+
if (visiting.has(name)) throw new Error(`Circular skill dependency involving: ${name}`);
|
|
32
|
+
visiting.add(name);
|
|
33
|
+
|
|
34
|
+
for (const dep of (manifest.dependsOn ?? {})[name] ?? []) {
|
|
35
|
+
if (!selectedSet.has(dep) && !addedBy.has(dep)) {
|
|
36
|
+
addedBy.set(dep, name);
|
|
37
|
+
}
|
|
38
|
+
visit(dep, name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
visiting.delete(name);
|
|
42
|
+
visited.add(name);
|
|
43
|
+
ordered.push(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const name of selected) {
|
|
47
|
+
visit(name, null);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { ordered, addedBy };
|
|
51
|
+
}
|