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.
Files changed (52) hide show
  1. package/CHANGELOG.md +288 -0
  2. package/README.md +6 -6
  3. package/bin/viepilot.cjs +140 -1
  4. package/bin/vp-tools.cjs +204 -0
  5. package/docs/brainstorm/session-2026-04-20.md +261 -0
  6. package/docs/brainstorm/session-2026-04-24.md +131 -0
  7. package/docs/brainstorm/session-2026-04-25.md +109 -0
  8. package/docs/skills-reference.md +22 -0
  9. package/docs/user/features/adapters.md +2 -2
  10. package/docs/user/features/scaffold-first.md +62 -0
  11. package/docs/user/features/skill-registry.md +125 -0
  12. package/lib/adapters/antigravity.cjs +5 -4
  13. package/lib/domain-packs/ai-product.json +33 -0
  14. package/lib/domain-packs/data-science.json +33 -0
  15. package/lib/domain-packs/devops.json +33 -0
  16. package/lib/domain-packs/mobile.json +33 -0
  17. package/lib/domain-packs/web-saas.json +33 -0
  18. package/lib/skill-installer.cjs +274 -0
  19. package/lib/skill-registry.cjs +212 -0
  20. package/lib/viepilot-calibrate.cjs +279 -0
  21. package/lib/viepilot-persona.cjs +446 -0
  22. package/lib/viepilot-update.cjs +113 -0
  23. package/package.json +1 -1
  24. package/skills/vp-audit/SKILL.md +67 -9
  25. package/skills/vp-auto/SKILL.md +54 -0
  26. package/skills/vp-brainstorm/SKILL.md +124 -2
  27. package/skills/vp-crystallize/SKILL.md +82 -0
  28. package/skills/vp-debug/SKILL.md +37 -0
  29. package/skills/vp-design/SKILL.md +219 -0
  30. package/skills/vp-docs/SKILL.md +37 -0
  31. package/skills/vp-evolve/SKILL.md +69 -6
  32. package/skills/vp-info/SKILL.md +37 -0
  33. package/skills/vp-pause/SKILL.md +37 -0
  34. package/skills/vp-persona/SKILL.md +207 -0
  35. package/skills/vp-proposal/SKILL.md +37 -0
  36. package/skills/vp-request/SKILL.md +62 -6
  37. package/skills/vp-resume/SKILL.md +37 -0
  38. package/skills/vp-rollback/SKILL.md +61 -1
  39. package/skills/vp-skills/SKILL.md +311 -0
  40. package/skills/vp-status/SKILL.md +37 -0
  41. package/skills/vp-task/SKILL.md +37 -0
  42. package/skills/vp-ui-components/SKILL.md +37 -0
  43. package/skills/vp-update/SKILL.md +37 -0
  44. package/templates/phase/TASK.md +7 -0
  45. package/templates/project/PROJECT-CONTEXT.md +76 -0
  46. package/workflows/audit.md +131 -0
  47. package/workflows/autonomous.md +199 -0
  48. package/workflows/brainstorm.md +1172 -9
  49. package/workflows/crystallize.md +639 -3
  50. package/workflows/design.md +601 -0
  51. package/workflows/evolve.md +9 -0
  52. 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 };