viepilot 2.23.0 → 2.45.2
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/CHANGELOG.md +288 -0
- package/README.md +6 -6
- package/bin/viepilot.cjs +140 -1
- package/bin/vp-tools.cjs +204 -0
- package/docs/brainstorm/session-2026-04-20.md +261 -0
- package/docs/brainstorm/session-2026-04-24.md +131 -0
- package/docs/brainstorm/session-2026-04-25.md +109 -0
- package/docs/skills-reference.md +22 -0
- package/docs/user/features/adapters.md +2 -2
- package/docs/user/features/scaffold-first.md +62 -0
- package/docs/user/features/skill-registry.md +125 -0
- package/lib/adapters/antigravity.cjs +5 -4
- package/lib/domain-packs/ai-product.json +33 -0
- package/lib/domain-packs/data-science.json +33 -0
- package/lib/domain-packs/devops.json +33 -0
- package/lib/domain-packs/mobile.json +33 -0
- package/lib/domain-packs/web-saas.json +33 -0
- package/lib/skill-installer.cjs +274 -0
- package/lib/skill-registry.cjs +212 -0
- package/lib/viepilot-calibrate.cjs +279 -0
- package/lib/viepilot-persona.cjs +446 -0
- package/lib/viepilot-update.cjs +113 -0
- package/package.json +1 -1
- package/skills/vp-audit/SKILL.md +67 -9
- package/skills/vp-auto/SKILL.md +54 -0
- package/skills/vp-brainstorm/SKILL.md +124 -2
- package/skills/vp-crystallize/SKILL.md +82 -0
- package/skills/vp-debug/SKILL.md +37 -0
- package/skills/vp-design/SKILL.md +219 -0
- package/skills/vp-docs/SKILL.md +37 -0
- package/skills/vp-evolve/SKILL.md +69 -6
- package/skills/vp-info/SKILL.md +37 -0
- package/skills/vp-pause/SKILL.md +37 -0
- package/skills/vp-persona/SKILL.md +207 -0
- package/skills/vp-proposal/SKILL.md +37 -0
- package/skills/vp-request/SKILL.md +62 -6
- package/skills/vp-resume/SKILL.md +37 -0
- package/skills/vp-rollback/SKILL.md +61 -1
- package/skills/vp-skills/SKILL.md +311 -0
- package/skills/vp-status/SKILL.md +37 -0
- package/skills/vp-task/SKILL.md +37 -0
- package/skills/vp-ui-components/SKILL.md +37 -0
- package/skills/vp-update/SKILL.md +37 -0
- package/templates/phase/TASK.md +7 -0
- package/templates/project/PROJECT-CONTEXT.md +76 -0
- package/workflows/audit.md +131 -0
- package/workflows/autonomous.md +199 -0
- package/workflows/brainstorm.md +1172 -9
- package/workflows/crystallize.md +639 -3
- package/workflows/design.md +601 -0
- package/workflows/evolve.md +9 -0
- package/workflows/rollback.md +79 -10
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ViePilot Skill Installer — multi-channel skill installation (FEAT-020 Phase 91).
|
|
3
|
+
*
|
|
4
|
+
* Channels supported:
|
|
5
|
+
* npm — "npm:@scope/pkg" or bare "@scope/pkg" / "pkg"
|
|
6
|
+
* github — "github:org/repo"
|
|
7
|
+
* local — "./path" or "/absolute/path"
|
|
8
|
+
*
|
|
9
|
+
* After any install/uninstall/update: calls scanSkills() to refresh registry.
|
|
10
|
+
* Records skill-meta.json in skill dir for updateSkill() re-install support.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const { execSync } = require('child_process');
|
|
19
|
+
const https = require('https');
|
|
20
|
+
const http = require('http');
|
|
21
|
+
|
|
22
|
+
const { scanSkills } = require('./skill-registry.cjs');
|
|
23
|
+
const { listAdapters } = require('./adapters/index.cjs');
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Channel detection
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
function detectChannel(source) {
|
|
30
|
+
if (source.startsWith('github:')) return 'github';
|
|
31
|
+
if (source.startsWith('./') || source.startsWith('/') || source.startsWith('../')) return 'local';
|
|
32
|
+
return 'npm';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Adapter skill dirs
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
function getAdapterSkillDirs(homeDir) {
|
|
40
|
+
const adapters = listAdapters(homeDir);
|
|
41
|
+
return adapters
|
|
42
|
+
.map(a => (typeof a.skillsDir === 'function' ? a.skillsDir(homeDir) : null))
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// npm channel
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
function installFromNpm(pkgName, skillId, skillDirs, tempBase) {
|
|
51
|
+
const tempDir = fs.mkdtempSync(path.join(tempBase, 'vp-npm-'));
|
|
52
|
+
try {
|
|
53
|
+
// Pack the package into tempDir
|
|
54
|
+
execSync(`npm pack ${pkgName} --pack-destination "${tempDir}"`, {
|
|
55
|
+
stdio: 'pipe',
|
|
56
|
+
timeout: 60000,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const tarballs = fs.readdirSync(tempDir).filter(f => f.endsWith('.tgz'));
|
|
60
|
+
if (tarballs.length === 0) throw new Error(`npm pack produced no tarball for ${pkgName}`);
|
|
61
|
+
|
|
62
|
+
const tarball = path.join(tempDir, tarballs[0]);
|
|
63
|
+
const extractDir = path.join(tempDir, 'extracted');
|
|
64
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
65
|
+
|
|
66
|
+
// Extract synchronously
|
|
67
|
+
execSync(`tar -xzf "${tarball}" -C "${extractDir}"`, { stdio: 'pipe' });
|
|
68
|
+
|
|
69
|
+
// npm pack wraps in package/
|
|
70
|
+
const packageDir = path.join(extractDir, 'package');
|
|
71
|
+
const skillMdPath = path.join(packageDir, 'SKILL.md');
|
|
72
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
73
|
+
throw new Error(`SKILL.md not found in npm package ${pkgName}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const installedPaths = [];
|
|
77
|
+
for (const skillsDir of skillDirs) {
|
|
78
|
+
const targetDir = path.join(skillsDir, skillId);
|
|
79
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
80
|
+
fs.cpSync(packageDir, targetDir, { recursive: true });
|
|
81
|
+
installedPaths.push(targetDir);
|
|
82
|
+
}
|
|
83
|
+
return installedPaths;
|
|
84
|
+
} finally {
|
|
85
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// github channel
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
function downloadFile(url, destPath) {
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
const proto = url.startsWith('https:') ? https : http;
|
|
96
|
+
const file = fs.createWriteStream(destPath);
|
|
97
|
+
proto.get(url, res => {
|
|
98
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
99
|
+
file.close();
|
|
100
|
+
fs.unlinkSync(destPath);
|
|
101
|
+
return downloadFile(res.headers.location, destPath).then(resolve).catch(reject);
|
|
102
|
+
}
|
|
103
|
+
if (res.statusCode !== 200) {
|
|
104
|
+
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
res.pipe(file);
|
|
108
|
+
file.on('finish', () => file.close(resolve));
|
|
109
|
+
}).on('error', reject);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function installFromGithub(orgRepo, skillId, skillDirs, tempBase) {
|
|
114
|
+
const [org, repo] = orgRepo.split('/');
|
|
115
|
+
if (!org || !repo) throw new Error(`Invalid github source "github:${orgRepo}" — expected "github:org/repo"`);
|
|
116
|
+
|
|
117
|
+
const tarballUrl = `https://github.com/${org}/${repo}/archive/refs/heads/main.tar.gz`;
|
|
118
|
+
const tempDir = fs.mkdtempSync(path.join(tempBase, 'vp-gh-'));
|
|
119
|
+
try {
|
|
120
|
+
const tarball = path.join(tempDir, 'repo.tar.gz');
|
|
121
|
+
await downloadFile(tarballUrl, tarball);
|
|
122
|
+
|
|
123
|
+
const extractDir = path.join(tempDir, 'extracted');
|
|
124
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
125
|
+
execSync(`tar -xzf "${tarball}" -C "${extractDir}"`, { stdio: 'pipe' });
|
|
126
|
+
|
|
127
|
+
// GitHub archives create org-repo-main/ directory
|
|
128
|
+
const entries = fs.readdirSync(extractDir);
|
|
129
|
+
const repoDir = path.join(extractDir, entries[0]);
|
|
130
|
+
|
|
131
|
+
const skillMdPath = path.join(repoDir, 'SKILL.md');
|
|
132
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
133
|
+
throw new Error(`SKILL.md not found in GitHub repo ${org}/${repo}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const installedPaths = [];
|
|
137
|
+
for (const skillsDir of skillDirs) {
|
|
138
|
+
const targetDir = path.join(skillsDir, skillId);
|
|
139
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
140
|
+
fs.cpSync(repoDir, targetDir, { recursive: true });
|
|
141
|
+
installedPaths.push(targetDir);
|
|
142
|
+
}
|
|
143
|
+
return installedPaths;
|
|
144
|
+
} finally {
|
|
145
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// local channel
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
function installFromLocal(srcPath, skillId, skillDirs) {
|
|
154
|
+
const resolved = path.resolve(srcPath);
|
|
155
|
+
if (!fs.existsSync(resolved)) {
|
|
156
|
+
throw new Error(`Local path not found: ${resolved}`);
|
|
157
|
+
}
|
|
158
|
+
const skillMdPath = path.join(resolved, 'SKILL.md');
|
|
159
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
160
|
+
throw new Error(`SKILL.md not found at ${resolved}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const installedPaths = [];
|
|
164
|
+
for (const skillsDir of skillDirs) {
|
|
165
|
+
const targetDir = path.join(skillsDir, skillId);
|
|
166
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
167
|
+
fs.cpSync(resolved, targetDir, { recursive: true });
|
|
168
|
+
installedPaths.push(targetDir);
|
|
169
|
+
}
|
|
170
|
+
return installedPaths;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// skill-meta.json
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
function writeSkillMeta(installedPaths, id, source) {
|
|
178
|
+
const meta = { id, source, installed_at: new Date().toISOString() };
|
|
179
|
+
for (const p of installedPaths) {
|
|
180
|
+
fs.writeFileSync(path.join(p, 'skill-meta.json'), JSON.stringify(meta, null, 2));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function readSkillMeta(skillId, homeDir) {
|
|
185
|
+
const skillDirs = getAdapterSkillDirs(homeDir);
|
|
186
|
+
for (const skillsDir of skillDirs) {
|
|
187
|
+
const metaPath = path.join(skillsDir, skillId, 'skill-meta.json');
|
|
188
|
+
if (fs.existsSync(metaPath)) {
|
|
189
|
+
try { return JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch { /* skip */ }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Public API
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Install a skill from source.
|
|
201
|
+
* Returns { ok: true, id, installedPaths[] } or { ok: false, error }
|
|
202
|
+
*/
|
|
203
|
+
async function installSkill(source, home = os.homedir()) {
|
|
204
|
+
try {
|
|
205
|
+
const channel = detectChannel(source);
|
|
206
|
+
const skillDirs = getAdapterSkillDirs(home);
|
|
207
|
+
if (skillDirs.length === 0) {
|
|
208
|
+
return { ok: false, error: 'No adapter skill directories found' };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const tempBase = os.tmpdir();
|
|
212
|
+
let skillId;
|
|
213
|
+
let installedPaths;
|
|
214
|
+
|
|
215
|
+
if (channel === 'npm') {
|
|
216
|
+
const pkgName = source.replace(/^npm:/, '');
|
|
217
|
+
skillId = pkgName.split('/').pop().replace(/^vp-skills-/, '');
|
|
218
|
+
installedPaths = installFromNpm(pkgName, skillId, skillDirs, tempBase);
|
|
219
|
+
} else if (channel === 'github') {
|
|
220
|
+
const orgRepo = source.replace('github:', '');
|
|
221
|
+
skillId = orgRepo.split('/').pop();
|
|
222
|
+
installedPaths = await installFromGithub(orgRepo, skillId, skillDirs, tempBase);
|
|
223
|
+
} else {
|
|
224
|
+
// local
|
|
225
|
+
skillId = path.basename(source.replace(/[/\\]+$/, ''));
|
|
226
|
+
installedPaths = installFromLocal(source, skillId, skillDirs);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
writeSkillMeta(installedPaths, skillId, source);
|
|
230
|
+
scanSkills(home);
|
|
231
|
+
|
|
232
|
+
return { ok: true, id: skillId, installedPaths };
|
|
233
|
+
} catch (err) {
|
|
234
|
+
return { ok: false, error: err.message };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Uninstall a skill by id from all adapter dirs.
|
|
240
|
+
* Returns { ok: true, removedPaths[] } or { ok: false, error }
|
|
241
|
+
*/
|
|
242
|
+
function uninstallSkill(id, home = os.homedir()) {
|
|
243
|
+
try {
|
|
244
|
+
const skillDirs = getAdapterSkillDirs(home);
|
|
245
|
+
const removedPaths = [];
|
|
246
|
+
|
|
247
|
+
for (const skillsDir of skillDirs) {
|
|
248
|
+
const targetDir = path.join(skillsDir, id);
|
|
249
|
+
if (fs.existsSync(targetDir)) {
|
|
250
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
251
|
+
removedPaths.push(targetDir);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
scanSkills(home);
|
|
256
|
+
return { ok: true, removedPaths };
|
|
257
|
+
} catch (err) {
|
|
258
|
+
return { ok: false, error: err.message };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Update a skill by re-installing from its recorded source.
|
|
264
|
+
* Returns installSkill result, or { ok: false, error } if source not found.
|
|
265
|
+
*/
|
|
266
|
+
async function updateSkill(id, home = os.homedir()) {
|
|
267
|
+
const meta = readSkillMeta(id, home);
|
|
268
|
+
if (!meta || !meta.source) {
|
|
269
|
+
return { ok: false, error: `No skill-meta.json found for skill "${id}" — cannot update` };
|
|
270
|
+
}
|
|
271
|
+
return installSkill(meta.source, home);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = { installSkill, uninstallSkill, updateSkill, detectChannel };
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ViePilot Skill Registry — scanner + registry builder (FEAT-020 Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* Registry file: ~/.viepilot/skill-registry.json
|
|
5
|
+
* Schema:
|
|
6
|
+
* version — "1.0"
|
|
7
|
+
* last_scan — ISO datetime
|
|
8
|
+
* scan_paths — adapter skillsDir paths scanned
|
|
9
|
+
* skills[] — indexed skill entries (deduplicated by id)
|
|
10
|
+
*
|
|
11
|
+
* Extended SKILL.md format (optional sections):
|
|
12
|
+
* ## Capabilities — one capability per line (- prefix)
|
|
13
|
+
* ## Tags — comma-separated or one per line
|
|
14
|
+
* ## Best Practices — one practice per line (- prefix)
|
|
15
|
+
*
|
|
16
|
+
* Legacy SKILL.md (no extended sections) → fields default to []
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
|
|
25
|
+
const REGISTRY_REL = path.join('.viepilot', 'skill-registry.json');
|
|
26
|
+
const REGISTRY_VERSION = '1.0';
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Section parsers
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse a SKILL.md string and extract extended metadata.
|
|
34
|
+
* Falls back gracefully when sections are absent.
|
|
35
|
+
* @param {string} content
|
|
36
|
+
* @returns {{ description: string, capabilities: string[], tags: string[], best_practices: string[] }}
|
|
37
|
+
*/
|
|
38
|
+
function parseSkillMd(content) {
|
|
39
|
+
const lines = content.split('\n');
|
|
40
|
+
|
|
41
|
+
// Extract first non-empty non-heading paragraph as description
|
|
42
|
+
let description = '';
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
const t = line.trim();
|
|
45
|
+
if (t && !t.startsWith('#')) { description = t; break; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
description,
|
|
50
|
+
capabilities: extractListSection(content, 'Capabilities'),
|
|
51
|
+
tags: extractTagsSection(content, 'Tags'),
|
|
52
|
+
best_practices: extractListSection(content, 'Best Practices'),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extract a `- item` list under `## {sectionName}`.
|
|
58
|
+
* @param {string} content
|
|
59
|
+
* @param {string} sectionName
|
|
60
|
+
* @returns {string[]}
|
|
61
|
+
*/
|
|
62
|
+
function extractListSection(content, sectionName) {
|
|
63
|
+
const re = new RegExp(`^##\\s+${sectionName}\\s*$`, 'im');
|
|
64
|
+
const match = re.exec(content);
|
|
65
|
+
if (!match) return [];
|
|
66
|
+
|
|
67
|
+
const afterHeader = content.slice(match.index + match[0].length);
|
|
68
|
+
const items = [];
|
|
69
|
+
for (const line of afterHeader.split('\n')) {
|
|
70
|
+
const t = line.trim();
|
|
71
|
+
if (t.startsWith('## ')) break; // next section — stop
|
|
72
|
+
const itemMatch = t.match(/^-\s+(.+)/);
|
|
73
|
+
if (itemMatch) items.push(itemMatch[1].trim());
|
|
74
|
+
}
|
|
75
|
+
return items;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Extract comma-separated or line-per-item tags under `## Tags`.
|
|
80
|
+
* @param {string} content
|
|
81
|
+
* @param {string} sectionName
|
|
82
|
+
* @returns {string[]}
|
|
83
|
+
*/
|
|
84
|
+
function extractTagsSection(content, sectionName) {
|
|
85
|
+
const re = new RegExp(`^##\\s+${sectionName}\\s*$`, 'im');
|
|
86
|
+
const match = re.exec(content);
|
|
87
|
+
if (!match) return [];
|
|
88
|
+
|
|
89
|
+
const afterHeader = content.slice(match.index + match[0].length);
|
|
90
|
+
const raw = [];
|
|
91
|
+
for (const line of afterHeader.split('\n')) {
|
|
92
|
+
const t = line.trim();
|
|
93
|
+
if (t.startsWith('## ')) break;
|
|
94
|
+
if (t.startsWith('- ')) { raw.push(t.slice(2).trim()); continue; }
|
|
95
|
+
if (t) {
|
|
96
|
+
// Comma-separated on one line
|
|
97
|
+
raw.push(...t.split(',').map(s => s.trim()).filter(Boolean));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return raw.filter(Boolean);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Scanner
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Scan all adapter skillsDirs, parse SKILL.md files, build registry.
|
|
109
|
+
* Writes ~/.viepilot/skill-registry.json and returns the registry object.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} [home] Override home directory (defaults to os.homedir())
|
|
112
|
+
* @returns {{ version: string, last_scan: string, scan_paths: string[], skills: object[] }}
|
|
113
|
+
*/
|
|
114
|
+
function scanSkills(home) {
|
|
115
|
+
const homeDir = home || os.homedir();
|
|
116
|
+
const { listAdapters } = require('./adapters/index.cjs');
|
|
117
|
+
const adapters = listAdapters();
|
|
118
|
+
|
|
119
|
+
/** @type {Map<string, object>} */
|
|
120
|
+
const skillMap = new Map();
|
|
121
|
+
const scanPaths = [];
|
|
122
|
+
|
|
123
|
+
for (const adapter of adapters) {
|
|
124
|
+
const skillsDir = adapter.skillsDir(homeDir);
|
|
125
|
+
scanPaths.push(skillsDir);
|
|
126
|
+
|
|
127
|
+
if (!fs.existsSync(skillsDir)) continue;
|
|
128
|
+
|
|
129
|
+
let entries;
|
|
130
|
+
try { entries = fs.readdirSync(skillsDir, { withFileTypes: true }); }
|
|
131
|
+
catch (_) { continue; }
|
|
132
|
+
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (!entry.isDirectory()) continue;
|
|
135
|
+
const skillDir = path.join(skillsDir, entry.name);
|
|
136
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
137
|
+
|
|
138
|
+
if (!fs.existsSync(skillMdPath)) continue;
|
|
139
|
+
|
|
140
|
+
let mdContent = '';
|
|
141
|
+
try { mdContent = fs.readFileSync(skillMdPath, 'utf8'); }
|
|
142
|
+
catch (_) { /* skip unreadable */ }
|
|
143
|
+
|
|
144
|
+
const parsed = parseSkillMd(mdContent);
|
|
145
|
+
const skillId = entry.name;
|
|
146
|
+
|
|
147
|
+
if (skillMap.has(skillId)) {
|
|
148
|
+
// Merge — same skill found in multiple adapter dirs
|
|
149
|
+
const existing = skillMap.get(skillId);
|
|
150
|
+
if (!existing.adapters.includes(adapter.id)) {
|
|
151
|
+
existing.adapters.push(adapter.id);
|
|
152
|
+
}
|
|
153
|
+
existing.installed_paths[adapter.id] = skillDir;
|
|
154
|
+
} else {
|
|
155
|
+
skillMap.set(skillId, {
|
|
156
|
+
id: skillId,
|
|
157
|
+
name: skillId, // display name = dir name by default
|
|
158
|
+
source: null,
|
|
159
|
+
version: null,
|
|
160
|
+
description: parsed.description,
|
|
161
|
+
capabilities: parsed.capabilities,
|
|
162
|
+
tags: parsed.tags,
|
|
163
|
+
best_practices: parsed.best_practices,
|
|
164
|
+
adapters: [adapter.id],
|
|
165
|
+
installed_paths: { [adapter.id]: skillDir },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const registry = {
|
|
172
|
+
version: REGISTRY_VERSION,
|
|
173
|
+
last_scan: new Date().toISOString(),
|
|
174
|
+
scan_paths: scanPaths,
|
|
175
|
+
skills: [...skillMap.values()],
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Write registry to ~/.viepilot/skill-registry.json
|
|
179
|
+
const registryDir = path.join(homeDir, '.viepilot');
|
|
180
|
+
if (!fs.existsSync(registryDir)) fs.mkdirSync(registryDir, { recursive: true });
|
|
181
|
+
fs.writeFileSync(
|
|
182
|
+
path.join(registryDir, 'skill-registry.json'),
|
|
183
|
+
JSON.stringify(registry, null, 2),
|
|
184
|
+
'utf8'
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return registry;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Registry reader
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Load the current skill registry from disk.
|
|
196
|
+
* Returns null if the registry has not been written yet.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} [home] Override home directory (defaults to os.homedir())
|
|
199
|
+
* @returns {object|null}
|
|
200
|
+
*/
|
|
201
|
+
function loadRegistry(home) {
|
|
202
|
+
const homeDir = home || os.homedir();
|
|
203
|
+
const registryPath = path.join(homeDir, '.viepilot', 'skill-registry.json');
|
|
204
|
+
if (!fs.existsSync(registryPath)) return null;
|
|
205
|
+
try {
|
|
206
|
+
return JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
207
|
+
} catch (_) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = { scanSkills, loadRegistry, parseSkillMd };
|