@h1dr0n/skill-pool 0.1.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/LICENSE +21 -0
- package/README.md +296 -0
- package/bin/cli.js +157 -0
- package/package.json +41 -0
- package/skills/api/agents/backend-specialist.md +69 -0
- package/skills/api/agents/database-optimizer.md +176 -0
- package/skills/api/manifest.yaml +20 -0
- package/skills/api/rules/auth-security.md +45 -0
- package/skills/api/skills/api-patterns/SKILL.md +81 -0
- package/skills/api/skills/api-patterns/api-style.md +42 -0
- package/skills/api/skills/api-patterns/auth.md +24 -0
- package/skills/api/skills/api-patterns/documentation.md +26 -0
- package/skills/api/skills/api-patterns/graphql.md +41 -0
- package/skills/api/skills/api-patterns/rate-limiting.md +31 -0
- package/skills/api/skills/api-patterns/response.md +37 -0
- package/skills/api/skills/api-patterns/rest.md +40 -0
- package/skills/api/skills/api-patterns/scripts/api_validator.py +211 -0
- package/skills/api/skills/api-patterns/security-testing.md +122 -0
- package/skills/api/skills/api-patterns/trpc.md +41 -0
- package/skills/api/skills/api-patterns/versioning.md +22 -0
- package/skills/api/skills/database-patterns.md +126 -0
- package/skills/api/skills/deployment-patterns.md +105 -0
- package/skills/api/skills/docker-patterns.md +135 -0
- package/skills/common/agents/code-reviewer.md +78 -0
- package/skills/common/agents/planner.md +80 -0
- package/skills/common/agents/security-reviewer.md +82 -0
- package/skills/common/agents/software-architect.md +81 -0
- package/skills/common/manifest.yaml +25 -0
- package/skills/common/rules/coding-style.md +39 -0
- package/skills/common/rules/git-workflow.md +33 -0
- package/skills/common/rules/security.md +25 -0
- package/skills/common/skills/architecture/SKILL.md +55 -0
- package/skills/common/skills/architecture/context-discovery.md +43 -0
- package/skills/common/skills/architecture/examples.md +94 -0
- package/skills/common/skills/architecture/pattern-selection.md +68 -0
- package/skills/common/skills/architecture/patterns-reference.md +50 -0
- package/skills/common/skills/architecture/trade-off-analysis.md +77 -0
- package/skills/common/skills/brainstorming/SKILL.md +163 -0
- package/skills/common/skills/brainstorming/dynamic-questioning.md +350 -0
- package/skills/common/skills/clean-code.md +99 -0
- package/skills/common/skills/code-review-checklist.md +86 -0
- package/skills/common/skills/plan-writing/SKILL.md +152 -0
- package/skills/common/skills/skill-feedback.md +94 -0
- package/skills/common/skills/tdd-workflow.md +130 -0
- package/skills/common/skills/verification-loop.md +112 -0
- package/skills/cpp/agents/cpp-build-resolver.md +90 -0
- package/skills/cpp/agents/cpp-reviewer.md +72 -0
- package/skills/cpp/manifest.yaml +15 -0
- package/skills/cpp/skills/cpp-coding-standards.md +722 -0
- package/skills/cpp/skills/cpp-testing.md +323 -0
- package/skills/devops/agents/devops-automator.md +376 -0
- package/skills/devops/agents/sre.md +90 -0
- package/skills/devops/manifest.yaml +20 -0
- package/skills/devops/skills/deployment-patterns.md +427 -0
- package/skills/devops/skills/deployment-procedures/SKILL.md +241 -0
- package/skills/devops/skills/docker-patterns.md +364 -0
- package/skills/devops/skills/e2e-testing.md +326 -0
- package/skills/devops/skills/github-ops.md +144 -0
- package/skills/django/manifest.yaml +16 -0
- package/skills/django/skills/django-patterns.md +734 -0
- package/skills/django/skills/django-security.md +593 -0
- package/skills/django/skills/django-tdd.md +729 -0
- package/skills/django/skills/django-verification.md +469 -0
- package/skills/dotnet/agents/csharp-reviewer.md +101 -0
- package/skills/dotnet/manifest.yaml +14 -0
- package/skills/dotnet/skills/csharp-testing.md +321 -0
- package/skills/dotnet/skills/dotnet-patterns.md +321 -0
- package/skills/go/agents/code-reviewer.md +76 -0
- package/skills/go/agents/go-build-resolver.md +94 -0
- package/skills/go/agents/go-reviewer.md +76 -0
- package/skills/go/manifest.yaml +17 -0
- package/skills/go/rules/go-style.md +55 -0
- package/skills/go/skills/golang-patterns.md +674 -0
- package/skills/go/skills/golang-testing.md +720 -0
- package/skills/java/agents/java-build-resolver.md +153 -0
- package/skills/java/agents/java-reviewer.md +92 -0
- package/skills/java/manifest.yaml +18 -0
- package/skills/java/skills/java-coding-standards.md +147 -0
- package/skills/java/skills/jpa-patterns.md +151 -0
- package/skills/java/skills/springboot-patterns.md +314 -0
- package/skills/java/skills/springboot-security.md +272 -0
- package/skills/kotlin/agents/kotlin-build-resolver.md +118 -0
- package/skills/kotlin/agents/kotlin-reviewer.md +159 -0
- package/skills/kotlin/manifest.yaml +17 -0
- package/skills/kotlin/skills/kotlin-coroutines-flows.md +284 -0
- package/skills/kotlin/skills/kotlin-patterns.md +711 -0
- package/skills/kotlin/skills/kotlin-testing.md +824 -0
- package/skills/laravel/manifest.yaml +15 -0
- package/skills/laravel/skills/laravel-patterns.md +409 -0
- package/skills/laravel/skills/laravel-security.md +279 -0
- package/skills/laravel/skills/laravel-tdd.md +277 -0
- package/skills/laravel/skills/laravel-verification.md +173 -0
- package/skills/mobile/agents/dart-build-resolver.md +201 -0
- package/skills/mobile/agents/flutter-reviewer.md +243 -0
- package/skills/mobile/manifest.yaml +19 -0
- package/skills/mobile/skills/android-clean-architecture.md +339 -0
- package/skills/mobile/skills/dart-flutter-patterns.md +563 -0
- package/skills/mobile/skills/swiftui-patterns.md +259 -0
- package/skills/nestjs/manifest.yaml +13 -0
- package/skills/nestjs/skills/nestjs-patterns.md +230 -0
- package/skills/perl/manifest.yaml +13 -0
- package/skills/perl/skills/perl-patterns.md +504 -0
- package/skills/perl/skills/perl-security.md +503 -0
- package/skills/perl/skills/perl-testing.md +475 -0
- package/skills/python/agents/python-reviewer.md +98 -0
- package/skills/python/manifest.yaml +18 -0
- package/skills/python/rules/python-style.md +69 -0
- package/skills/python/skills/python-patterns/SKILL.md +441 -0
- package/skills/python/skills/python-patterns.md +90 -0
- package/skills/python/skills/python-testing.md +81 -0
- package/skills/rust/agents/rust-build-resolver.md +148 -0
- package/skills/rust/agents/rust-reviewer.md +94 -0
- package/skills/rust/manifest.yaml +16 -0
- package/skills/rust/rules/rust-style.md +107 -0
- package/skills/rust/skills/rust-patterns.md +499 -0
- package/skills/rust/skills/rust-testing.md +500 -0
- package/skills/security/agents/accessibility-auditor.md +316 -0
- package/skills/security/agents/security-reviewer.md +108 -0
- package/skills/security/manifest.yaml +19 -0
- package/skills/security/skills/red-team-tactics/SKILL.md +199 -0
- package/skills/security/skills/security-bounty-hunter.md +99 -0
- package/skills/security/skills/security-review.md +495 -0
- package/skills/security/skills/security-scan.md +165 -0
- package/skills/security/skills/vulnerability-scanner/SKILL.md +276 -0
- package/skills/security/skills/vulnerability-scanner/checklists.md +121 -0
- package/skills/security/skills/vulnerability-scanner/scripts/security_scan.py +458 -0
- package/skills/swift/manifest.yaml +16 -0
- package/skills/swift/skills/swift-actor-persistence.md +142 -0
- package/skills/swift/skills/swift-concurrency.md +216 -0
- package/skills/swift/skills/swift-protocol-di-testing.md +190 -0
- package/skills/swift/skills/swiftui-patterns.md +259 -0
- package/skills/unity/agents/game-designer.md +167 -0
- package/skills/unity/agents/unity-architect.md +52 -0
- package/skills/unity/agents/unity-editor-tool-developer.md +310 -0
- package/skills/unity/agents/unity-multiplayer-engineer.md +321 -0
- package/skills/unity/agents/unity-shader-graph-artist.md +269 -0
- package/skills/unity/manifest.yaml +21 -0
- package/skills/unity/rules/csharp-patterns.md +48 -0
- package/skills/unity/rules/unity-specific.md +53 -0
- package/skills/unity/skills/systematic-debugging.md +92 -0
- package/skills/unity/skills/unity-architecture.md +173 -0
- package/skills/unreal/agents/level-designer.md +208 -0
- package/skills/unreal/agents/technical-artist.md +229 -0
- package/skills/unreal/agents/unreal-multiplayer-architect.md +313 -0
- package/skills/unreal/agents/unreal-systems-engineer.md +310 -0
- package/skills/unreal/agents/unreal-technical-artist.md +256 -0
- package/skills/unreal/agents/unreal-world-builder.md +273 -0
- package/skills/unreal/manifest.yaml +21 -0
- package/skills/unreal/skills/unreal-patterns.md +183 -0
- package/skills/web/agents/frontend-specialist.md +71 -0
- package/skills/web/agents/ui-designer.md +383 -0
- package/skills/web/agents/ux-architect.md +469 -0
- package/skills/web/manifest.yaml +22 -0
- package/skills/web/rules/accessibility.md +54 -0
- package/skills/web/rules/css-performance.md +52 -0
- package/skills/web/skills/e2e-testing.md +132 -0
- package/skills/web/skills/frontend-design/SKILL.md +452 -0
- package/skills/web/skills/frontend-design/animation-guide.md +331 -0
- package/skills/web/skills/frontend-design/color-system.md +311 -0
- package/skills/web/skills/frontend-design/decision-trees.md +418 -0
- package/skills/web/skills/frontend-design/motion-graphics.md +306 -0
- package/skills/web/skills/frontend-design/scripts/accessibility_checker.py +183 -0
- package/skills/web/skills/frontend-design/scripts/ux_audit.py +722 -0
- package/skills/web/skills/frontend-design/typography-system.md +345 -0
- package/skills/web/skills/frontend-design/ux-psychology.md +1116 -0
- package/skills/web/skills/frontend-design/visual-effects.md +383 -0
- package/skills/web/skills/react-nextjs.md +135 -0
- package/skills/web/skills/tailwind-patterns/SKILL.md +269 -0
- package/src/adapters/antigravity.js +164 -0
- package/src/adapters/claude.js +188 -0
- package/src/adapters/cursor.js +161 -0
- package/src/adapters/index.js +67 -0
- package/src/adapters/windsurf.js +158 -0
- package/src/commands/add.js +266 -0
- package/src/commands/create.js +127 -0
- package/src/commands/diff.js +78 -0
- package/src/commands/info.js +88 -0
- package/src/commands/init.js +224 -0
- package/src/commands/install.js +90 -0
- package/src/commands/list.js +54 -0
- package/src/commands/remove.js +101 -0
- package/src/commands/targets.js +32 -0
- package/src/commands/update.js +57 -0
- package/src/core/manifest.js +57 -0
- package/src/core/plugins.js +86 -0
- package/src/core/resolver.js +84 -0
- package/src/core/tracker.js +49 -0
- package/src/utils/fs.js +80 -0
- package/src/utils/git.js +52 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
ensureDir,
|
|
4
|
+
writeFileAtomic,
|
|
5
|
+
readFileContent,
|
|
6
|
+
removeFile,
|
|
7
|
+
fileExists,
|
|
8
|
+
listDir,
|
|
9
|
+
copyDir,
|
|
10
|
+
removeDir,
|
|
11
|
+
isDirectory,
|
|
12
|
+
} from '../utils/fs.js';
|
|
13
|
+
import { getSkillsDir } from '../core/manifest.js';
|
|
14
|
+
|
|
15
|
+
const MARKER_PREFIX = '<!-- skill-pool:';
|
|
16
|
+
const MARKER_SUFFIX = ' -->';
|
|
17
|
+
const MARKER_FILE = '.skill-pool-marker';
|
|
18
|
+
|
|
19
|
+
function makeMarker(packName, version) {
|
|
20
|
+
return `${MARKER_PREFIX}${packName}:${version}${MARKER_SUFFIX}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseMarker(content) {
|
|
24
|
+
const match = content.match(/<!-- skill-pool:([^:]+):([^ ]+) -->/);
|
|
25
|
+
if (!match) return null;
|
|
26
|
+
return { pack: match[1], version: match[2] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function contentHasMarker(content, packName) {
|
|
30
|
+
const parsed = parseMarker(content);
|
|
31
|
+
return parsed && parsed.pack === packName;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildMdcContent(marker, packName, ruleContent) {
|
|
35
|
+
const frontmatter = [
|
|
36
|
+
'---',
|
|
37
|
+
`description: "skill-pool: ${packName} rules"`,
|
|
38
|
+
'globs: ""',
|
|
39
|
+
'alwaysApply: true',
|
|
40
|
+
'---',
|
|
41
|
+
].join('\n');
|
|
42
|
+
return `${frontmatter}\n${marker}\n\n${ruleContent}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function detect(projectDir) {
|
|
46
|
+
return fileExists(path.join(projectDir, '.cursor'));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function install(manifest, projectDir) {
|
|
50
|
+
const installed = [];
|
|
51
|
+
const skillsDir = getSkillsDir();
|
|
52
|
+
const marker = makeMarker(manifest.name, manifest.version);
|
|
53
|
+
|
|
54
|
+
// Rules -> .cursor/rules/<pack>-<name>.mdc
|
|
55
|
+
for (const rulePath of manifest.rules) {
|
|
56
|
+
const srcPath = path.join(skillsDir, manifest.name, rulePath);
|
|
57
|
+
const content = await readFileContent(srcPath);
|
|
58
|
+
const basename = path.basename(rulePath, path.extname(rulePath));
|
|
59
|
+
const rulesDir = path.join(projectDir, '.cursor', 'rules');
|
|
60
|
+
await ensureDir(rulesDir);
|
|
61
|
+
const destPath = path.join(rulesDir, `${manifest.name}-${basename}.mdc`);
|
|
62
|
+
await writeFileAtomic(destPath, buildMdcContent(marker, manifest.name, content));
|
|
63
|
+
installed.push(destPath);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Skills -> .cursor/skills/<pack>-<name>/
|
|
67
|
+
for (const skillPath of manifest.skills) {
|
|
68
|
+
const srcPath = path.join(skillsDir, manifest.name, skillPath);
|
|
69
|
+
const basename = path.basename(skillPath, path.extname(skillPath));
|
|
70
|
+
const destDir = path.join(projectDir, '.cursor', 'skills', `${manifest.name}-${basename}`);
|
|
71
|
+
|
|
72
|
+
if (await isDirectory(srcPath)) {
|
|
73
|
+
await copyDir(srcPath, destDir);
|
|
74
|
+
await writeFileAtomic(path.join(destDir, MARKER_FILE), marker);
|
|
75
|
+
installed.push(destDir);
|
|
76
|
+
} else {
|
|
77
|
+
await ensureDir(destDir);
|
|
78
|
+
const content = await readFileContent(srcPath);
|
|
79
|
+
await writeFileAtomic(path.join(destDir, 'SKILL.md'), `${marker}\n\n${content}`);
|
|
80
|
+
await writeFileAtomic(path.join(destDir, MARKER_FILE), marker);
|
|
81
|
+
installed.push(destDir);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Agents -> .cursor/rules/<pack>-agent-<name>.mdc
|
|
86
|
+
for (const agentPath of manifest.agents) {
|
|
87
|
+
const srcPath = path.join(skillsDir, manifest.name, agentPath);
|
|
88
|
+
const content = await readFileContent(srcPath);
|
|
89
|
+
const basename = path.basename(agentPath, path.extname(agentPath));
|
|
90
|
+
const rulesDir = path.join(projectDir, '.cursor', 'rules');
|
|
91
|
+
await ensureDir(rulesDir);
|
|
92
|
+
const destPath = path.join(rulesDir, `${manifest.name}-agent-${basename}.mdc`);
|
|
93
|
+
await writeFileAtomic(destPath, buildMdcContent(marker, manifest.name, content));
|
|
94
|
+
installed.push(destPath);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return installed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function remove(packName, projectDir) {
|
|
101
|
+
const removed = [];
|
|
102
|
+
|
|
103
|
+
// Remove rules + agent .mdc files
|
|
104
|
+
const rulesDir = path.join(projectDir, '.cursor', 'rules');
|
|
105
|
+
const ruleFiles = await listDir(rulesDir);
|
|
106
|
+
for (const file of ruleFiles) {
|
|
107
|
+
const filePath = path.join(rulesDir, file);
|
|
108
|
+
if (await isDirectory(filePath)) continue;
|
|
109
|
+
const content = await readFileContent(filePath);
|
|
110
|
+
if (contentHasMarker(content, packName)) {
|
|
111
|
+
await removeFile(filePath);
|
|
112
|
+
removed.push(filePath);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Remove skills
|
|
117
|
+
const skillsBase = path.join(projectDir, '.cursor', 'skills');
|
|
118
|
+
const skillDirs = await listDir(skillsBase);
|
|
119
|
+
for (const dir of skillDirs) {
|
|
120
|
+
const markerPath = path.join(skillsBase, dir, MARKER_FILE);
|
|
121
|
+
if (await fileExists(markerPath)) {
|
|
122
|
+
const content = await readFileContent(markerPath);
|
|
123
|
+
if (contentHasMarker(content, packName)) {
|
|
124
|
+
await removeDir(path.join(skillsBase, dir));
|
|
125
|
+
removed.push(path.join(skillsBase, dir));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return removed;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function list(projectDir) {
|
|
134
|
+
const results = [];
|
|
135
|
+
|
|
136
|
+
const rulesDir = path.join(projectDir, '.cursor', 'rules');
|
|
137
|
+
const ruleFiles = await listDir(rulesDir);
|
|
138
|
+
for (const file of ruleFiles) {
|
|
139
|
+
const filePath = path.join(rulesDir, file);
|
|
140
|
+
if (await isDirectory(filePath)) continue;
|
|
141
|
+
const content = await readFileContent(filePath);
|
|
142
|
+
const parsed = parseMarker(content);
|
|
143
|
+
if (parsed) {
|
|
144
|
+
const type = file.includes('-agent-') ? 'agent' : 'rule';
|
|
145
|
+
results.push({ ...parsed, file, type });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const skillsBase = path.join(projectDir, '.cursor', 'skills');
|
|
150
|
+
const skillDirs = await listDir(skillsBase);
|
|
151
|
+
for (const dir of skillDirs) {
|
|
152
|
+
const markerPath = path.join(skillsBase, dir, MARKER_FILE);
|
|
153
|
+
if (await fileExists(markerPath)) {
|
|
154
|
+
const content = await readFileContent(markerPath);
|
|
155
|
+
const parsed = parseMarker(content);
|
|
156
|
+
if (parsed) results.push({ ...parsed, file: dir, type: 'skill' });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as claude from './claude.js';
|
|
2
|
+
import * as cursor from './cursor.js';
|
|
3
|
+
import * as windsurf from './windsurf.js';
|
|
4
|
+
import * as antigravity from './antigravity.js';
|
|
5
|
+
import { loadPlugins } from '../core/plugins.js';
|
|
6
|
+
|
|
7
|
+
const adapters = {
|
|
8
|
+
claude,
|
|
9
|
+
cursor,
|
|
10
|
+
windsurf,
|
|
11
|
+
antigravity,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function getAdapter(name) {
|
|
15
|
+
const adapter = adapters[name];
|
|
16
|
+
if (!adapter) {
|
|
17
|
+
throw new Error(`Unknown target: "${name}". Available: ${Object.keys(adapters).join(', ')}`);
|
|
18
|
+
}
|
|
19
|
+
return adapter;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load custom adapters from project config and merge into the registry.
|
|
24
|
+
* Custom adapters do not overwrite built-in adapters.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} projectDir - absolute path to the project root
|
|
27
|
+
* @returns {Promise<Record<string, object>>} merged adapter map (built-in + custom)
|
|
28
|
+
*/
|
|
29
|
+
export async function loadCustomAdapters(projectDir) {
|
|
30
|
+
const custom = await loadPlugins(projectDir);
|
|
31
|
+
return { ...custom, ...adapters };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get an adapter by name, checking built-in adapters first, then plugins.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} name - adapter name
|
|
38
|
+
* @param {string} projectDir - absolute path to the project root
|
|
39
|
+
* @returns {Promise<object>} the adapter module
|
|
40
|
+
*/
|
|
41
|
+
export async function getAdapterWithPlugins(name, projectDir) {
|
|
42
|
+
if (adapters[name]) {
|
|
43
|
+
return adapters[name];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const custom = await loadPlugins(projectDir);
|
|
47
|
+
if (custom[name]) {
|
|
48
|
+
return custom[name];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const allNames = [...Object.keys(adapters), ...Object.keys(custom)];
|
|
52
|
+
throw new Error(`Unknown target: "${name}". Available: ${allNames.join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function detectTargets(projectDir) {
|
|
56
|
+
const detected = [];
|
|
57
|
+
for (const [name, adapter] of Object.entries(adapters)) {
|
|
58
|
+
if (await adapter.detect(projectDir)) {
|
|
59
|
+
detected.push(name);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return detected;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getAllTargetNames() {
|
|
66
|
+
return Object.keys(adapters);
|
|
67
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
ensureDir,
|
|
4
|
+
writeFileAtomic,
|
|
5
|
+
readFileContent,
|
|
6
|
+
removeFile,
|
|
7
|
+
fileExists,
|
|
8
|
+
listDir,
|
|
9
|
+
copyDir,
|
|
10
|
+
removeDir,
|
|
11
|
+
isDirectory,
|
|
12
|
+
} from '../utils/fs.js';
|
|
13
|
+
import { getSkillsDir } from '../core/manifest.js';
|
|
14
|
+
|
|
15
|
+
const MARKER_PREFIX = '<!-- skill-pool:';
|
|
16
|
+
const MARKER_SUFFIX = ' -->';
|
|
17
|
+
const MARKER_FILE = '.skill-pool-marker';
|
|
18
|
+
|
|
19
|
+
function makeMarker(packName, version) {
|
|
20
|
+
return `${MARKER_PREFIX}${packName}:${version}${MARKER_SUFFIX}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseMarker(line) {
|
|
24
|
+
if (!line.startsWith(MARKER_PREFIX)) return null;
|
|
25
|
+
const inner = line.slice(MARKER_PREFIX.length, line.indexOf(MARKER_SUFFIX));
|
|
26
|
+
const [pack, version] = inner.split(':');
|
|
27
|
+
return { pack, version };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function contentHasMarker(content, packName) {
|
|
31
|
+
const firstLine = content.split('\n')[0];
|
|
32
|
+
const parsed = parseMarker(firstLine);
|
|
33
|
+
return parsed && parsed.pack === packName;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function detect(projectDir) {
|
|
37
|
+
return fileExists(path.join(projectDir, '.windsurf'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function install(manifest, projectDir) {
|
|
41
|
+
const installed = [];
|
|
42
|
+
const skillsDir = getSkillsDir();
|
|
43
|
+
const marker = makeMarker(manifest.name, manifest.version);
|
|
44
|
+
|
|
45
|
+
// Rules -> .windsurf/rules/<pack>-<name>.md
|
|
46
|
+
for (const rulePath of manifest.rules) {
|
|
47
|
+
const srcPath = path.join(skillsDir, manifest.name, rulePath);
|
|
48
|
+
const content = await readFileContent(srcPath);
|
|
49
|
+
const basename = path.basename(rulePath, path.extname(rulePath));
|
|
50
|
+
const rulesDir = path.join(projectDir, '.windsurf', 'rules');
|
|
51
|
+
await ensureDir(rulesDir);
|
|
52
|
+
const destPath = path.join(rulesDir, `${manifest.name}-${basename}.md`);
|
|
53
|
+
await writeFileAtomic(destPath, `${marker}\n\n${content}`);
|
|
54
|
+
installed.push(destPath);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Skills -> .windsurf/skills/<pack>-<name>/
|
|
58
|
+
for (const skillPath of manifest.skills) {
|
|
59
|
+
const srcPath = path.join(skillsDir, manifest.name, skillPath);
|
|
60
|
+
const basename = path.basename(skillPath, path.extname(skillPath));
|
|
61
|
+
const destDir = path.join(projectDir, '.windsurf', 'skills', `${manifest.name}-${basename}`);
|
|
62
|
+
|
|
63
|
+
if (await isDirectory(srcPath)) {
|
|
64
|
+
await copyDir(srcPath, destDir);
|
|
65
|
+
await writeFileAtomic(path.join(destDir, MARKER_FILE), marker);
|
|
66
|
+
installed.push(destDir);
|
|
67
|
+
} else {
|
|
68
|
+
await ensureDir(destDir);
|
|
69
|
+
const content = await readFileContent(srcPath);
|
|
70
|
+
await writeFileAtomic(path.join(destDir, 'SKILL.md'), `${marker}\n\n${content}`);
|
|
71
|
+
await writeFileAtomic(path.join(destDir, MARKER_FILE), marker);
|
|
72
|
+
installed.push(destDir);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Agents -> .windsurf/rules/<pack>-agent-<name>.md (Windsurf uses rules for agents)
|
|
77
|
+
for (const agentPath of manifest.agents) {
|
|
78
|
+
const srcPath = path.join(skillsDir, manifest.name, agentPath);
|
|
79
|
+
const content = await readFileContent(srcPath);
|
|
80
|
+
const basename = path.basename(agentPath, path.extname(agentPath));
|
|
81
|
+
const rulesDir = path.join(projectDir, '.windsurf', 'rules');
|
|
82
|
+
await ensureDir(rulesDir);
|
|
83
|
+
const destPath = path.join(rulesDir, `${manifest.name}-agent-${basename}.md`);
|
|
84
|
+
await writeFileAtomic(destPath, `${marker}\n\n${content}`);
|
|
85
|
+
installed.push(destPath);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return installed;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function remove(packName, projectDir) {
|
|
92
|
+
const removed = [];
|
|
93
|
+
|
|
94
|
+
// Remove rules + agent files
|
|
95
|
+
await removeMarkedFiles(path.join(projectDir, '.windsurf', 'rules'), packName, removed);
|
|
96
|
+
|
|
97
|
+
// Remove skills
|
|
98
|
+
const skillsBase = path.join(projectDir, '.windsurf', 'skills');
|
|
99
|
+
const skillDirs = await listDir(skillsBase);
|
|
100
|
+
for (const dir of skillDirs) {
|
|
101
|
+
const markerPath = path.join(skillsBase, dir, MARKER_FILE);
|
|
102
|
+
if (await fileExists(markerPath)) {
|
|
103
|
+
const content = await readFileContent(markerPath);
|
|
104
|
+
if (contentHasMarker(content, packName)) {
|
|
105
|
+
await removeDir(path.join(skillsBase, dir));
|
|
106
|
+
removed.push(path.join(skillsBase, dir));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return removed;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function removeMarkedFiles(dirPath, packName, removed) {
|
|
115
|
+
const files = await listDir(dirPath);
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
const filePath = path.join(dirPath, file);
|
|
118
|
+
if (await isDirectory(filePath)) continue;
|
|
119
|
+
const content = await readFileContent(filePath);
|
|
120
|
+
if (contentHasMarker(content, packName)) {
|
|
121
|
+
await removeFile(filePath);
|
|
122
|
+
removed.push(filePath);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function list(projectDir) {
|
|
128
|
+
const results = [];
|
|
129
|
+
|
|
130
|
+
// List rules + agents
|
|
131
|
+
const rulesDir = path.join(projectDir, '.windsurf', 'rules');
|
|
132
|
+
const ruleFiles = await listDir(rulesDir);
|
|
133
|
+
for (const file of ruleFiles) {
|
|
134
|
+
const filePath = path.join(rulesDir, file);
|
|
135
|
+
if (await isDirectory(filePath)) continue;
|
|
136
|
+
const content = await readFileContent(filePath);
|
|
137
|
+
const firstLine = content.split('\n')[0];
|
|
138
|
+
const parsed = parseMarker(firstLine);
|
|
139
|
+
if (parsed) {
|
|
140
|
+
const type = file.includes('-agent-') ? 'agent' : 'rule';
|
|
141
|
+
results.push({ ...parsed, file, type });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// List skills
|
|
146
|
+
const skillsBase = path.join(projectDir, '.windsurf', 'skills');
|
|
147
|
+
const skillDirs = await listDir(skillsBase);
|
|
148
|
+
for (const dir of skillDirs) {
|
|
149
|
+
const markerPath = path.join(skillsBase, dir, MARKER_FILE);
|
|
150
|
+
if (await fileExists(markerPath)) {
|
|
151
|
+
const content = await readFileContent(markerPath);
|
|
152
|
+
const parsed = parseMarker(content);
|
|
153
|
+
if (parsed) results.push({ ...parsed, file: dir, type: 'skill' });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return results;
|
|
158
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import { cloneRepo, isGitAvailable } from '../utils/git.js';
|
|
6
|
+
import { fileExists, readFileContent, copyDir, removeDir, ensureDir } from '../utils/fs.js';
|
|
7
|
+
import {
|
|
8
|
+
loadTracker,
|
|
9
|
+
saveTracker,
|
|
10
|
+
getInstalledVersion,
|
|
11
|
+
recordInstall,
|
|
12
|
+
} from '../core/tracker.js';
|
|
13
|
+
import { getAdapter, detectTargets } from '../adapters/index.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse a GitHub URL into { url, branch, repoName }.
|
|
17
|
+
* Accepts:
|
|
18
|
+
* https://github.com/user/repo
|
|
19
|
+
* github.com/user/repo
|
|
20
|
+
* https://github.com/user/repo#branch
|
|
21
|
+
* https://github.com/user/repo.git
|
|
22
|
+
*
|
|
23
|
+
* @param {string} raw
|
|
24
|
+
* @returns {{ url: string, branch: string | null, repoName: string }}
|
|
25
|
+
*/
|
|
26
|
+
function parseGithubUrl(raw) {
|
|
27
|
+
let input = raw.trim();
|
|
28
|
+
|
|
29
|
+
// Split off #branch fragment
|
|
30
|
+
let branch = null;
|
|
31
|
+
const hashIndex = input.indexOf('#');
|
|
32
|
+
if (hashIndex !== -1) {
|
|
33
|
+
branch = input.slice(hashIndex + 1);
|
|
34
|
+
input = input.slice(0, hashIndex);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Normalise: add https:// if missing
|
|
38
|
+
if (!input.startsWith('http://') && !input.startsWith('https://')) {
|
|
39
|
+
input = `https://${input}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = new URL(input);
|
|
45
|
+
} catch {
|
|
46
|
+
throw new Error(`Invalid URL: "${raw}"`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!parsed.hostname.includes('github.com')) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Only GitHub URLs are supported. Got: "${parsed.hostname}"`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// pathname looks like /user/repo or /user/repo.git
|
|
56
|
+
const segments = parsed.pathname.replace(/\.git$/, '').split('/').filter(Boolean);
|
|
57
|
+
|
|
58
|
+
if (segments.length < 2) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Cannot extract owner/repo from URL: "${raw}". Expected format: github.com/owner/repo`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const repoName = segments[1];
|
|
65
|
+
|
|
66
|
+
return { url: input, branch, repoName };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Locate manifest.yaml in a cloned directory.
|
|
71
|
+
* Checks root first, then skills/ subdirectory.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} cloneDir
|
|
74
|
+
* @returns {Promise<{ manifestPath: string, packRoot: string } | null>}
|
|
75
|
+
*/
|
|
76
|
+
async function findManifest(cloneDir) {
|
|
77
|
+
// Check root
|
|
78
|
+
const rootManifest = path.join(cloneDir, 'manifest.yaml');
|
|
79
|
+
if (await fileExists(rootManifest)) {
|
|
80
|
+
return { manifestPath: rootManifest, packRoot: cloneDir };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check skills/ subdirectory
|
|
84
|
+
const skillsManifest = path.join(cloneDir, 'skills', 'manifest.yaml');
|
|
85
|
+
if (await fileExists(skillsManifest)) {
|
|
86
|
+
return { manifestPath: skillsManifest, packRoot: path.join(cloneDir, 'skills') };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Load and validate a manifest from a file path.
|
|
94
|
+
* @param {string} manifestPath
|
|
95
|
+
* @returns {Promise<object>}
|
|
96
|
+
*/
|
|
97
|
+
async function loadManifestFromPath(manifestPath) {
|
|
98
|
+
const content = await readFileContent(manifestPath);
|
|
99
|
+
const manifest = yaml.load(content);
|
|
100
|
+
|
|
101
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
102
|
+
throw new Error('manifest.yaml is empty or invalid');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!manifest.name) {
|
|
106
|
+
throw new Error('manifest.yaml is missing required field: name');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const hasContent =
|
|
110
|
+
(manifest.rules && manifest.rules.length > 0) ||
|
|
111
|
+
(manifest.skills && manifest.skills.length > 0) ||
|
|
112
|
+
(manifest.agents && manifest.agents.length > 0);
|
|
113
|
+
|
|
114
|
+
if (!hasContent) {
|
|
115
|
+
throw new Error('manifest.yaml must have at least one of: rules, skills, agents');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
name: manifest.name,
|
|
120
|
+
version: manifest.version || '0.0.0',
|
|
121
|
+
description: manifest.description || '',
|
|
122
|
+
depends: manifest.depends || [],
|
|
123
|
+
tags: manifest.tags || [],
|
|
124
|
+
rules: manifest.rules || [],
|
|
125
|
+
skills: manifest.skills || [],
|
|
126
|
+
agents: manifest.agents || [],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolve target adapters (same logic as install command).
|
|
132
|
+
* @param {string} projectDir
|
|
133
|
+
* @param {string | undefined} targetOption
|
|
134
|
+
* @returns {Promise<string[]>}
|
|
135
|
+
*/
|
|
136
|
+
async function resolveTargets(projectDir, targetOption) {
|
|
137
|
+
if (targetOption) {
|
|
138
|
+
const names = targetOption.split(',').map((t) => t.trim());
|
|
139
|
+
names.forEach((n) => getAdapter(n));
|
|
140
|
+
return names;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const detected = await detectTargets(projectDir);
|
|
144
|
+
if (detected.length > 0) return detected;
|
|
145
|
+
|
|
146
|
+
return ['claude'];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Install a skill pack from a GitHub repository URL.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} rawUrl - GitHub repository URL, optionally with #branch
|
|
153
|
+
* @param {object} [options]
|
|
154
|
+
* @param {string} [options.target] - Comma-separated target names
|
|
155
|
+
*/
|
|
156
|
+
export async function addCommand(rawUrl, options = {}) {
|
|
157
|
+
// 1. Verify git is available
|
|
158
|
+
if (!(await isGitAvailable())) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
'git is not installed or not in PATH. Install git and try again.'
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 2. Parse URL
|
|
165
|
+
const { url, branch, repoName } = parseGithubUrl(rawUrl);
|
|
166
|
+
|
|
167
|
+
console.log(
|
|
168
|
+
chalk.gray(`Repository: ${url}${branch ? ` (branch: ${branch})` : ''}`)
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// 3. Clone into temp directory
|
|
172
|
+
const tmpDir = path.join(
|
|
173
|
+
os.tmpdir(),
|
|
174
|
+
`sp-clone-${repoName}-${Date.now()}`
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
console.log(chalk.gray('Cloning repository...'));
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await cloneRepo(url, tmpDir, branch);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Failed to clone repository: ${err.message}`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
// 4. Find manifest.yaml
|
|
189
|
+
const found = await findManifest(tmpDir);
|
|
190
|
+
|
|
191
|
+
if (!found) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`No manifest.yaml found in repository root or skills/ subdirectory`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const { manifestPath, packRoot } = found;
|
|
198
|
+
|
|
199
|
+
// 5. Load and validate manifest
|
|
200
|
+
const manifest = await loadManifestFromPath(manifestPath);
|
|
201
|
+
|
|
202
|
+
console.log(
|
|
203
|
+
chalk.gray(`Found pack: ${manifest.name}@${manifest.version}`)
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// 6. Copy pack to local cache
|
|
207
|
+
const projectDir = process.cwd();
|
|
208
|
+
const cacheDir = path.join(projectDir, '.skill-pool-cache', '_external', repoName);
|
|
209
|
+
|
|
210
|
+
await ensureDir(path.dirname(cacheDir));
|
|
211
|
+
// Remove existing cache for this pack if present
|
|
212
|
+
await removeDir(cacheDir);
|
|
213
|
+
await copyDir(packRoot, cacheDir);
|
|
214
|
+
|
|
215
|
+
console.log(chalk.gray(`Cached to .skill-pool-cache/_external/${repoName}/`));
|
|
216
|
+
|
|
217
|
+
// 7. Resolve targets
|
|
218
|
+
const targets = await resolveTargets(projectDir, options.target);
|
|
219
|
+
console.log(chalk.gray(`Targets: ${targets.join(', ')}\n`));
|
|
220
|
+
|
|
221
|
+
// 8. Check if already installed at same version
|
|
222
|
+
let tracker = await loadTracker(projectDir);
|
|
223
|
+
const currentVersion = getInstalledVersion(tracker, manifest.name);
|
|
224
|
+
|
|
225
|
+
if (currentVersion === manifest.version) {
|
|
226
|
+
console.log(
|
|
227
|
+
chalk.yellow(`${manifest.name}@${manifest.version} already installed, skipping`)
|
|
228
|
+
);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 9. Run install via adapters
|
|
233
|
+
let totalFiles = 0;
|
|
234
|
+
for (const targetName of targets) {
|
|
235
|
+
const adapter = getAdapter(targetName);
|
|
236
|
+
const files = await adapter.install(manifest, projectDir);
|
|
237
|
+
totalFiles += files.length;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 10. Update tracker with github source
|
|
241
|
+
tracker = recordInstall(tracker, manifest.name, manifest.version, targets);
|
|
242
|
+
|
|
243
|
+
// Add source metadata
|
|
244
|
+
tracker = {
|
|
245
|
+
...tracker,
|
|
246
|
+
installed: {
|
|
247
|
+
...tracker.installed,
|
|
248
|
+
[manifest.name]: {
|
|
249
|
+
...tracker.installed[manifest.name],
|
|
250
|
+
source: `github:${rawUrl}`,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
await saveTracker(projectDir, tracker);
|
|
256
|
+
|
|
257
|
+
console.log(
|
|
258
|
+
chalk.green(` + ${manifest.name}@${manifest.version}`) +
|
|
259
|
+
chalk.gray(` (${totalFiles} rules, source: github)`)
|
|
260
|
+
);
|
|
261
|
+
console.log(chalk.green(`\nInstalled 1 pack for ${targets.join(', ')}.`));
|
|
262
|
+
} finally {
|
|
263
|
+
// 11. Cleanup temp directory
|
|
264
|
+
await removeDir(tmpDir);
|
|
265
|
+
}
|
|
266
|
+
}
|