cli-five 0.2.1 → 0.2.3
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/package.json +1 -1
- package/src/cli.mjs +3 -1
- package/src/commands/init.mjs +145 -3
- package/src/steps/interview.mjs +22 -10
- package/src/steps/scaffold.mjs +7 -0
- package/src/steps/skills.mjs +177 -171
- package/templates/PROJECT.md.tmpl +1 -1
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -24,6 +24,7 @@ ${kleur.bold('Flags')}
|
|
|
24
24
|
--force Overwrite without confirmation. Dangerous. Use with --yes.
|
|
25
25
|
--dry-run Print actions without writing files
|
|
26
26
|
--no-skills Skip the skills.sh discovery step
|
|
27
|
+
--doc <file> Read project docs to pre-fill interview (repeatable)
|
|
27
28
|
--cost-mode <m> Override cost mode (premium, cheap, mixed) — skips interview question
|
|
28
29
|
--cwd <path> Run against a directory other than the current one
|
|
29
30
|
--version, -v Print version and exit
|
|
@@ -59,13 +60,14 @@ export async function run(argv) {
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
function parse(argv) {
|
|
62
|
-
const out = { _: [], yes: false, force: false, dryRun: false, skills: true, costMode: null, cwd: process.cwd() };
|
|
63
|
+
const out = { _: [], yes: false, force: false, dryRun: false, skills: true, docs: [], costMode: null, cwd: process.cwd() };
|
|
63
64
|
for (let i = 0; i < argv.length; i++) {
|
|
64
65
|
const a = argv[i];
|
|
65
66
|
if (a === '--yes' || a === '-y') out.yes = true;
|
|
66
67
|
else if (a === '--force') out.force = true;
|
|
67
68
|
else if (a === '--dry-run') out.dryRun = true;
|
|
68
69
|
else if (a === '--no-skills') out.skills = false;
|
|
70
|
+
else if (a === '--doc') out.docs.push(argv[++i]);
|
|
69
71
|
else if (a === '--cost-mode') out.costMode = argv[++i];
|
|
70
72
|
else if (a === '--cwd') out.cwd = argv[++i];
|
|
71
73
|
else if (a === '--help' || a === '-h') out._.push('help');
|
package/src/commands/init.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
2
|
import prompts from 'prompts';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { resolve, basename } from 'node:path';
|
|
3
5
|
import { log } from '../util/log.mjs';
|
|
4
6
|
import { detect } from '../steps/detect.mjs';
|
|
5
7
|
import { confirmOverwriteIfNeeded } from '../steps/confirm.mjs';
|
|
@@ -40,9 +42,51 @@ export async function init(args) {
|
|
|
40
42
|
}
|
|
41
43
|
if (!detected.hasAgents && !detected.hasCopilotInstructions) log.dim('No collisions.');
|
|
42
44
|
|
|
43
|
-
// 4.
|
|
44
|
-
log.step('3/6
|
|
45
|
-
|
|
45
|
+
// 4. Input mode — docs or manual interview
|
|
46
|
+
log.step('3/6 Project info');
|
|
47
|
+
let docHints;
|
|
48
|
+
|
|
49
|
+
if (args.docs.length > 0) {
|
|
50
|
+
// --doc was passed on the CLI — use those files directly
|
|
51
|
+
docHints = loadDocs(args.docs, cwd);
|
|
52
|
+
} else if (args.yes) {
|
|
53
|
+
// --yes skips the choice — go straight to defaults
|
|
54
|
+
docHints = loadDocs([], cwd);
|
|
55
|
+
} else {
|
|
56
|
+
const { mode } = await prompts({
|
|
57
|
+
type: 'select',
|
|
58
|
+
name: 'mode',
|
|
59
|
+
message: 'How would you like to describe your project?',
|
|
60
|
+
choices: [
|
|
61
|
+
{ title: 'Provide document(s)', value: 'docs', description: 'Feed existing files (README, PRD, etc.) — we extract what we can' },
|
|
62
|
+
{ title: 'Answer questions', value: 'manual', description: 'Short interactive interview' },
|
|
63
|
+
],
|
|
64
|
+
initial: 0,
|
|
65
|
+
});
|
|
66
|
+
if (mode === undefined) { log.warn('Cancelled.'); return; }
|
|
67
|
+
|
|
68
|
+
if (mode === 'docs') {
|
|
69
|
+
const { paths } = await prompts({
|
|
70
|
+
type: 'list',
|
|
71
|
+
name: 'paths',
|
|
72
|
+
message: 'File paths (comma-separated, relative to project root)',
|
|
73
|
+
separator: ',',
|
|
74
|
+
});
|
|
75
|
+
if (!paths || paths.length === 0) { log.warn('No files provided. Falling back to interview.'); }
|
|
76
|
+
docHints = loadDocs((paths || []).map(p => p.trim()).filter(Boolean), cwd);
|
|
77
|
+
} else {
|
|
78
|
+
docHints = loadDocs([], cwd);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (docHints.files.length > 0) {
|
|
83
|
+
log.info(`Loaded ${docHints.files.length} doc${docHints.files.length > 1 ? 's' : ''}: ${docHints.files.join(', ')}`);
|
|
84
|
+
if (docHints.projectName) log.dim(` → project name: ${docHints.projectName}`);
|
|
85
|
+
if (docHints.oneLiner) log.dim(` → description: ${docHints.oneLiner}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 5. Interview (pre-filled from docs if available, otherwise manual)
|
|
89
|
+
const answers = await interview(detected, args, docHints);
|
|
46
90
|
|
|
47
91
|
// CLI --cost-mode override
|
|
48
92
|
if (args.costMode && ['premium', 'cheap', 'mixed'].includes(args.costMode)) {
|
|
@@ -106,3 +150,101 @@ function printNextSteps(answers) {
|
|
|
106
150
|
log.raw(kleur.dim('Edit cost mode anytime by changing `model:` in .github/agents/*.agent.md.'));
|
|
107
151
|
log.raw('');
|
|
108
152
|
}
|
|
153
|
+
|
|
154
|
+
// ── --doc file loading + extraction ───────────────────────────────────
|
|
155
|
+
|
|
156
|
+
function loadDocs(docPaths, cwd) {
|
|
157
|
+
const empty = { files: [], projectName: '', oneLiner: '', goals: '', constraints: '', raw: '' };
|
|
158
|
+
if (!docPaths || docPaths.length === 0) return empty;
|
|
159
|
+
|
|
160
|
+
const sections = [];
|
|
161
|
+
const files = [];
|
|
162
|
+
let projectName = '';
|
|
163
|
+
let oneLiner = '';
|
|
164
|
+
let goals = '';
|
|
165
|
+
let constraints = '';
|
|
166
|
+
|
|
167
|
+
for (const rawPath of docPaths) {
|
|
168
|
+
const abs = resolve(cwd, rawPath);
|
|
169
|
+
if (!existsSync(abs)) {
|
|
170
|
+
log.warn(`--doc: file not found: ${rawPath}`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let content;
|
|
175
|
+
try {
|
|
176
|
+
content = readFileSync(abs, 'utf8');
|
|
177
|
+
} catch (err) {
|
|
178
|
+
log.warn(`--doc: cannot read ${rawPath}: ${err.message}`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const name = basename(abs);
|
|
183
|
+
files.push(name);
|
|
184
|
+
sections.push(`### ${name}\n\n${content.trim()}`);
|
|
185
|
+
|
|
186
|
+
// ── Heuristic extraction ──────────────────────────────────────
|
|
187
|
+
// Try package.json first (structured data)
|
|
188
|
+
if (name === 'package.json') {
|
|
189
|
+
try {
|
|
190
|
+
const pkg = JSON.parse(content);
|
|
191
|
+
if (pkg.name && !projectName) projectName = pkg.name;
|
|
192
|
+
if (pkg.description && !oneLiner) oneLiner = pkg.description;
|
|
193
|
+
} catch { /* ignore malformed JSON */ }
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// For markdown/text files — extract from headings and first paragraph
|
|
198
|
+
const lines = content.split('\n');
|
|
199
|
+
for (let i = 0; i < lines.length; i++) {
|
|
200
|
+
const line = lines[i].trim();
|
|
201
|
+
|
|
202
|
+
// First H1 → project name hint
|
|
203
|
+
if (!projectName && /^#\s+/.test(line)) {
|
|
204
|
+
projectName = line.replace(/^#+\s*/, '').trim();
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// First non-empty paragraph after H1 → one-liner hint
|
|
209
|
+
if (projectName && !oneLiner && line && !line.startsWith('#') && !line.startsWith('```') && !line.startsWith('- ') && !line.startsWith('|')) {
|
|
210
|
+
oneLiner = line.length > 120 ? line.slice(0, 117) + '...' : line;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ## Goal / ## Purpose / ## Overview → goals hint
|
|
215
|
+
if (!goals && /^##\s+(goal|purpose|overview|objective|vision)/i.test(line)) {
|
|
216
|
+
const body = collectSection(lines, i + 1);
|
|
217
|
+
if (body) goals = body;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ## Constraints → constraints hint
|
|
222
|
+
if (!constraints && /^##\s+(constraint|requirement|limit|scope)/i.test(line)) {
|
|
223
|
+
const body = collectSection(lines, i + 1);
|
|
224
|
+
if (body) constraints = body;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
files,
|
|
232
|
+
projectName,
|
|
233
|
+
oneLiner,
|
|
234
|
+
goals,
|
|
235
|
+
constraints,
|
|
236
|
+
raw: sections.join('\n\n---\n\n'),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Collect text from startIdx until the next heading or EOF. */
|
|
241
|
+
function collectSection(lines, startIdx) {
|
|
242
|
+
const out = [];
|
|
243
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
244
|
+
if (/^##?\s+/.test(lines[i]) && out.length > 0) break;
|
|
245
|
+
const trimmed = lines[i].trim();
|
|
246
|
+
if (trimmed) out.push(trimmed);
|
|
247
|
+
if (out.length >= 3) break; // Keep it short — just the first few lines
|
|
248
|
+
}
|
|
249
|
+
return out.join(' ');
|
|
250
|
+
}
|
package/src/steps/interview.mjs
CHANGED
|
@@ -6,6 +6,14 @@ import prompts from 'prompts';
|
|
|
6
6
|
// Research via Context7: Next.js, Vite+React, Express, Hono all score
|
|
7
7
|
// 80+ benchmark with high snippet counts and well-indexed docs.
|
|
8
8
|
const STACK_PRESETS = [
|
|
9
|
+
{
|
|
10
|
+
title: 'Next.js + Supabase PWA (TypeScript)',
|
|
11
|
+
value: 'nextjs-supabase',
|
|
12
|
+
description: 'Next.js 15 static export, React 19, Tailwind v4, Supabase, SWR, Vercel hosting, PWA.',
|
|
13
|
+
stack: ['Node + TypeScript'],
|
|
14
|
+
frameworks: ['Next.js', 'React', 'Tailwind CSS', 'Supabase', 'SWR'],
|
|
15
|
+
quickstart: 'npx create-next-app@latest . --yes && npm i @supabase/supabase-js swr date-fns clsx && npm run dev',
|
|
16
|
+
},
|
|
9
17
|
{
|
|
10
18
|
title: 'Next.js (TypeScript)',
|
|
11
19
|
value: 'nextjs',
|
|
@@ -70,8 +78,8 @@ const COST_MODES = [
|
|
|
70
78
|
{ title: 'Mixed', value: 'mixed', description: 'Premium Coder + Reviewer, cheap everything else.' },
|
|
71
79
|
];
|
|
72
80
|
|
|
73
|
-
export async function interview(detected, args) {
|
|
74
|
-
if (args.yes) return defaults(detected);
|
|
81
|
+
export async function interview(detected, args, docHints = {}) {
|
|
82
|
+
if (args.yes) return defaults(detected, docHints);
|
|
75
83
|
|
|
76
84
|
const onCancel = () => {
|
|
77
85
|
throw new Error('Interview cancelled. Nothing was written.');
|
|
@@ -84,13 +92,13 @@ export async function interview(detected, args) {
|
|
|
84
92
|
type: 'text',
|
|
85
93
|
name: 'projectName',
|
|
86
94
|
message: 'Project name',
|
|
87
|
-
initial: detected.projectName,
|
|
95
|
+
initial: docHints.projectName || detected.projectName,
|
|
88
96
|
},
|
|
89
97
|
{
|
|
90
98
|
type: 'text',
|
|
91
99
|
name: 'oneLiner',
|
|
92
100
|
message: 'One-line description (becomes PROJECT.md vision)',
|
|
93
|
-
initial: '',
|
|
101
|
+
initial: docHints.oneLiner || '',
|
|
94
102
|
},
|
|
95
103
|
],
|
|
96
104
|
{ onCancel },
|
|
@@ -151,11 +159,13 @@ export async function interview(detected, args) {
|
|
|
151
159
|
type: 'text',
|
|
152
160
|
name: 'goals',
|
|
153
161
|
message: 'Primary goal of this project (one sentence)',
|
|
162
|
+
initial: docHints.goals || '',
|
|
154
163
|
},
|
|
155
164
|
{
|
|
156
165
|
type: 'text',
|
|
157
166
|
name: 'constraints',
|
|
158
167
|
message: 'Hard constraints (perf, deps, deploy, compliance — one sentence, optional)',
|
|
168
|
+
initial: docHints.constraints || '',
|
|
159
169
|
},
|
|
160
170
|
{
|
|
161
171
|
type: 'select',
|
|
@@ -175,30 +185,32 @@ export async function interview(detected, args) {
|
|
|
175
185
|
);
|
|
176
186
|
|
|
177
187
|
return normalize({
|
|
178
|
-
...defaults(detected),
|
|
188
|
+
...defaults(detected, docHints),
|
|
179
189
|
...basic,
|
|
180
190
|
...stackAnswers,
|
|
181
191
|
...rest,
|
|
182
192
|
presetId: preset,
|
|
183
193
|
quickstart: chosenPreset?.quickstart || '',
|
|
194
|
+
docs: docHints.raw || '',
|
|
184
195
|
});
|
|
185
196
|
}
|
|
186
197
|
|
|
187
198
|
/** Default stack is Next.js + TypeScript when nothing detected and --yes. */
|
|
188
|
-
function defaults(detected) {
|
|
199
|
+
function defaults(detected, docHints = {}) {
|
|
189
200
|
const hasDetected = detected.stacks.length > 0;
|
|
190
201
|
const fallback = STACK_PRESETS[0]; // Next.js (TypeScript)
|
|
191
202
|
return {
|
|
192
|
-
projectName: detected.projectName,
|
|
193
|
-
oneLiner: '',
|
|
203
|
+
projectName: docHints.projectName || detected.projectName,
|
|
204
|
+
oneLiner: docHints.oneLiner || '',
|
|
194
205
|
stack: hasDetected ? detected.stacks.map((s) => s.label) : fallback.stack,
|
|
195
206
|
frameworks: hasDetected ? [] : fallback.frameworks,
|
|
196
|
-
goals: '',
|
|
197
|
-
constraints: '',
|
|
207
|
+
goals: docHints.goals || '',
|
|
208
|
+
constraints: docHints.constraints || '',
|
|
198
209
|
costMode: 'premium',
|
|
199
210
|
snark: true,
|
|
200
211
|
presetId: hasDetected ? 'custom' : fallback.value,
|
|
201
212
|
quickstart: hasDetected ? '' : fallback.quickstart,
|
|
213
|
+
docs: docHints.raw || '',
|
|
202
214
|
};
|
|
203
215
|
}
|
|
204
216
|
|
package/src/steps/scaffold.mjs
CHANGED
|
@@ -96,6 +96,13 @@ function buildVars(a) {
|
|
|
96
96
|
CONSTRAINTS: a.constraints || 'None declared.',
|
|
97
97
|
QUICKSTART: a.quickstart || 'TODO — add install + run commands here.',
|
|
98
98
|
COST_MODE: a.costMode,
|
|
99
|
+
DOCS_SECTION: a.docs ? `
|
|
100
|
+
## Source Documents
|
|
101
|
+
|
|
102
|
+
The following documents were provided via \`--doc\` at project init time.
|
|
103
|
+
|
|
104
|
+
${a.docs}
|
|
105
|
+
` : '',
|
|
99
106
|
DATE: new Date().toISOString().slice(0, 10),
|
|
100
107
|
PERSONA_BLOCK: a.snark ? PERSONA_BLOCK : '',
|
|
101
108
|
};
|
package/src/steps/skills.mjs
CHANGED
|
@@ -6,36 +6,9 @@ import { join, dirname } from 'node:path';
|
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
7
|
import { log } from '../util/log.mjs';
|
|
8
8
|
|
|
9
|
-
// ──
|
|
10
|
-
//
|
|
11
|
-
const
|
|
12
|
-
// Universal
|
|
13
|
-
{ terms: ['*'], name: 'frontend-design', desc: 'Production-grade UI design', type: 'skill', source: 'awesome-copilot' },
|
|
14
|
-
{ terms: ['*'], name: 'conventional-commit', desc: 'Commit message standards', type: 'skill', source: 'awesome-copilot' },
|
|
15
|
-
{ terms: ['*'], name: 'documentation-writer', desc: 'Generate project docs', type: 'skill', source: 'awesome-copilot' },
|
|
16
|
-
{ terms: ['*'], name: 'mermaid-diagrams', desc: 'Create software diagrams', type: 'skill', source: 'awesome-copilot' },
|
|
17
|
-
// JavaScript / TypeScript / React / Next
|
|
18
|
-
{ terms: ['node', 'typescript', 'javascript'], name: 'typescript', desc: 'TypeScript best practices', type: 'instruction', source: 'awesome-copilot' },
|
|
19
|
-
{ terms: ['react', 'next'], name: 'reactjs', desc: 'React patterns & conventions', type: 'instruction', source: 'awesome-copilot' },
|
|
20
|
-
{ terms: ['next'], name: 'nextjs', desc: 'Next.js patterns', type: 'instruction', source: 'awesome-copilot' },
|
|
21
|
-
{ terms: ['node', 'typescript', 'react', 'next'], name: 'playwright-tester', desc: 'E2E testing with Playwright', type: 'skill', source: 'awesome-copilot' },
|
|
22
|
-
// Python
|
|
23
|
-
{ terms: ['python', 'django', 'flask', 'fastapi'], name: 'python', desc: 'Python best practices', type: 'instruction', source: 'awesome-copilot' },
|
|
24
|
-
{ terms: ['django'], name: 'django', desc: 'Django conventions', type: 'instruction', source: 'awesome-copilot' },
|
|
25
|
-
{ terms: ['fastapi'], name: 'fastapi', desc: 'FastAPI patterns', type: 'instruction', source: 'awesome-copilot' },
|
|
26
|
-
// .NET
|
|
27
|
-
{ terms: ['dotnet', '.net', 'csharp', 'c#'], name: 'dotnet', desc: '.NET conventions', type: 'instruction', source: 'awesome-copilot' },
|
|
28
|
-
// Rust
|
|
29
|
-
{ terms: ['rust'], name: 'rust', desc: 'Rust best practices', type: 'instruction', source: 'awesome-copilot' },
|
|
30
|
-
// Go
|
|
31
|
-
{ terms: ['go', 'golang'], name: 'go', desc: 'Go conventions', type: 'instruction', source: 'awesome-copilot' },
|
|
32
|
-
// Mobile
|
|
33
|
-
{ terms: ['swift', 'ios'], name: 'swift', desc: 'Swift/iOS patterns', type: 'instruction', source: 'awesome-copilot' },
|
|
34
|
-
{ terms: ['kotlin', 'android'], name: 'kotlin', desc: 'Kotlin/Android patterns', type: 'instruction', source: 'awesome-copilot' },
|
|
35
|
-
// DevOps / Infra
|
|
36
|
-
{ terms: ['docker', 'kubernetes', 'devops'], name: 'docker', desc: 'Docker best practices', type: 'instruction', source: 'awesome-copilot' },
|
|
37
|
-
{ terms: ['terraform'], name: 'terraform', desc: 'Terraform conventions', type: 'instruction', source: 'awesome-copilot' },
|
|
38
|
-
];
|
|
9
|
+
// ── Known-good skill repos (verified working) ────────────────────────
|
|
10
|
+
// awesome-copilot skills are discovered dynamically via `skills find`.
|
|
11
|
+
const AWESOME_COPILOT_REPO = 'github/awesome-copilot';
|
|
39
12
|
|
|
40
13
|
// Well-known skill repos matched by stack term → repo + suggested skill names (skills.sh)
|
|
41
14
|
const SKILL_CATALOG = [
|
|
@@ -141,8 +114,7 @@ export async function skillDiscovery({ cwd, answers, args }) {
|
|
|
141
114
|
if (!env) {
|
|
142
115
|
log.warn('Cannot run skills CLI: no bundled binary, pnpm, or npx found.');
|
|
143
116
|
log.warn('Install Node.js (includes npx) or pnpm, then re-run.');
|
|
144
|
-
|
|
145
|
-
await showAwesomeCopilotOnly(answers);
|
|
117
|
+
printBreadcrumbs();
|
|
146
118
|
return;
|
|
147
119
|
}
|
|
148
120
|
|
|
@@ -165,77 +137,86 @@ export async function skillDiscovery({ cwd, answers, args }) {
|
|
|
165
137
|
}
|
|
166
138
|
|
|
167
139
|
log.ok(`Running skills via ${env.label}`);
|
|
168
|
-
log.raw('');
|
|
169
|
-
|
|
170
|
-
// ── Build recommendations from BOTH sources ─────────────────────────
|
|
171
|
-
const awesomeRecs = buildAwesomeRecommendations(answers);
|
|
172
|
-
const skillsShRecs = buildRecommendations(answers);
|
|
173
|
-
|
|
174
|
-
// ── Display hero section ────────────────────────────────────────────
|
|
175
|
-
displayHeroRecommendations(awesomeRecs, skillsShRecs, answers);
|
|
176
140
|
|
|
177
|
-
// ──
|
|
141
|
+
// ── 1. Auto-search all stack terms silently ─────────────────────────
|
|
178
142
|
const terms = suggestSearches(answers);
|
|
143
|
+
const searchResults = [];
|
|
179
144
|
|
|
180
|
-
// Search skills.sh for additional results
|
|
181
|
-
const allFound = [];
|
|
182
145
|
if (terms.length > 0) {
|
|
183
|
-
log.dim(`Suggested searches: ${terms.map((t) => `"${t}"`).join(', ')}`);
|
|
184
146
|
log.raw('');
|
|
185
|
-
|
|
147
|
+
log.info(`Searching skills.sh for: ${terms.map((t) => kleur.bold(t)).join(', ')} ...`);
|
|
186
148
|
for (const term of terms) {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const output = await runSkillsCapture(env, ['find', term], cwd);
|
|
195
|
-
allFound.push(...parseSkillRefs(output));
|
|
196
|
-
}
|
|
149
|
+
const output = await runSkillsSilent(env, ['find', term], cwd);
|
|
150
|
+
searchResults.push(...parseSkillRefs(output));
|
|
151
|
+
}
|
|
152
|
+
if (searchResults.length > 0) {
|
|
153
|
+
log.ok(`Found ${searchResults.length} skill${searchResults.length > 1 ? 's' : ''} from search.`);
|
|
154
|
+
} else {
|
|
155
|
+
log.dim('No additional skills found via search.');
|
|
197
156
|
}
|
|
198
157
|
}
|
|
199
158
|
|
|
200
|
-
//
|
|
159
|
+
// ── 2. Build static catalog recommendations ─────────────────────────
|
|
160
|
+
const catalogRecs = buildRecommendations(answers);
|
|
161
|
+
|
|
162
|
+
// ── 3. Categorize search results by source ──────────────────────────
|
|
163
|
+
const awesomeResults = [];
|
|
164
|
+
const otherResults = [];
|
|
165
|
+
for (const r of searchResults) {
|
|
166
|
+
const parsed = parseRef(r.ref);
|
|
167
|
+
if (parsed && parsed.repo === AWESOME_COPILOT_REPO) {
|
|
168
|
+
awesomeResults.push({ ...r, ...parsed });
|
|
169
|
+
} else {
|
|
170
|
+
otherResults.push(r);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── 4. Display hero section ─────────────────────────────────────────
|
|
175
|
+
displayHeroRecommendations(awesomeResults, catalogRecs, answers);
|
|
176
|
+
|
|
177
|
+
// ── 5. Build unified picker ─────────────────────────────────────────
|
|
201
178
|
const seen = new Set();
|
|
202
179
|
const choices = [];
|
|
203
180
|
|
|
204
|
-
//
|
|
205
|
-
for (const r of
|
|
206
|
-
const key =
|
|
181
|
+
// awesome-copilot results from search (dynamic, real names)
|
|
182
|
+
for (const r of awesomeResults) {
|
|
183
|
+
const key = r.ref;
|
|
207
184
|
if (seen.has(key)) continue;
|
|
208
185
|
seen.add(key);
|
|
209
186
|
choices.push({
|
|
210
|
-
title: `${kleur.cyan('⬡')} ${r.
|
|
211
|
-
value: { source: 'awesome',
|
|
187
|
+
title: `${kleur.cyan('⬡')} ${r.skill} ${kleur.dim(`(${r.installs})`)} ${kleur.cyan('← awesome-copilot')}`,
|
|
188
|
+
value: { source: 'awesome', repo: r.repo, skill: r.skill, ref: r.ref },
|
|
212
189
|
selected: true,
|
|
213
190
|
});
|
|
214
191
|
}
|
|
215
192
|
|
|
216
|
-
//
|
|
217
|
-
for (const r of
|
|
193
|
+
// Static catalog recs (verified repos)
|
|
194
|
+
for (const r of catalogRecs) {
|
|
218
195
|
const ref = `${r.repo}@${r.skill}`;
|
|
219
196
|
if (seen.has(ref)) continue;
|
|
220
197
|
seen.add(ref);
|
|
221
198
|
choices.push({
|
|
222
199
|
title: `${kleur.yellow('◆')} ${r.skill} ${kleur.dim(`(${r.repo})`)} ${kleur.yellow('← skills.sh')}`,
|
|
223
|
-
value: { source: '
|
|
200
|
+
value: { source: 'catalog', ref, repo: r.repo, skill: r.skill },
|
|
224
201
|
selected: true,
|
|
225
202
|
});
|
|
226
203
|
}
|
|
227
204
|
|
|
228
|
-
//
|
|
229
|
-
for (const r of
|
|
205
|
+
// Other search results (non-awesome-copilot)
|
|
206
|
+
for (const r of otherResults) {
|
|
230
207
|
if (seen.has(r.ref)) continue;
|
|
231
208
|
seen.add(r.ref);
|
|
209
|
+
const parsed = parseRef(r.ref);
|
|
232
210
|
choices.push({
|
|
233
|
-
title: `${kleur.yellow('◆')} ${r.ref} ${kleur.dim(
|
|
234
|
-
value: { source: '
|
|
211
|
+
title: `${kleur.yellow('◆')} ${parsed ? parsed.skill : r.ref} ${kleur.dim(`(${parsed ? parsed.repo : ''} — ${r.installs})`)} ${kleur.yellow('← skills.sh')}`,
|
|
212
|
+
value: { source: 'search', ref: r.ref, repo: parsed?.repo, skill: parsed?.skill },
|
|
235
213
|
selected: false,
|
|
236
214
|
});
|
|
237
215
|
}
|
|
238
216
|
|
|
217
|
+
// ── 6. Prompt user to select skills ─────────────────────────────────
|
|
218
|
+
const installResults = [];
|
|
219
|
+
|
|
239
220
|
if (choices.length > 0) {
|
|
240
221
|
log.raw('');
|
|
241
222
|
const { toInstall } = await prompts({
|
|
@@ -247,51 +228,47 @@ export async function skillDiscovery({ cwd, answers, args }) {
|
|
|
247
228
|
});
|
|
248
229
|
|
|
249
230
|
if (toInstall && toInstall.length > 0) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
231
|
+
log.raw('');
|
|
232
|
+
|
|
233
|
+
// Group by repo for batch install
|
|
234
|
+
const byRepo = new Map();
|
|
235
|
+
const standalone = [];
|
|
236
|
+
|
|
237
|
+
for (const item of toInstall) {
|
|
238
|
+
if (item.repo && item.skill) {
|
|
239
|
+
if (!byRepo.has(item.repo)) byRepo.set(item.repo, []);
|
|
240
|
+
byRepo.get(item.repo).push(item.skill);
|
|
241
|
+
} else if (item.ref) {
|
|
242
|
+
standalone.push(item.ref);
|
|
262
243
|
}
|
|
263
244
|
}
|
|
264
245
|
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
for (const
|
|
271
|
-
|
|
272
|
-
if (!byRepo.has(item.repo)) byRepo.set(item.repo, []);
|
|
273
|
-
byRepo.get(item.repo).push(item.skill);
|
|
274
|
-
} else {
|
|
275
|
-
standalone.push(item.ref);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
for (const [repo, skills] of byRepo) {
|
|
280
|
-
const skillArgs = skills.flatMap((s) => ['--skill', s]);
|
|
281
|
-
log.info(`Installing from ${repo}: ${skills.join(', ')}`);
|
|
282
|
-
await runSkills(env, ['add', repo, ...skillArgs, '-a', 'github-copilot'], cwd);
|
|
246
|
+
// Batch install per repo
|
|
247
|
+
for (const [repo, skills] of byRepo) {
|
|
248
|
+
const skillArgs = skills.flatMap((s) => ['--skill', s]);
|
|
249
|
+
log.info(`Installing from ${kleur.bold(repo)}: ${skills.join(', ')}`);
|
|
250
|
+
const result = await runSkillsTracked(env, ['add', repo, ...skillArgs, '-a', 'github-copilot'], cwd);
|
|
251
|
+
for (const s of skills) {
|
|
252
|
+
installResults.push({ name: `${repo}@${s}`, ...result });
|
|
283
253
|
}
|
|
254
|
+
}
|
|
284
255
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
256
|
+
// Standalone refs
|
|
257
|
+
for (const ref of standalone) {
|
|
258
|
+
log.info(`Installing ${ref}...`);
|
|
259
|
+
const result = await runSkillsTracked(env, ['add', ref, '-a', 'github-copilot'], cwd);
|
|
260
|
+
installResults.push({ name: ref, ...result });
|
|
289
261
|
}
|
|
290
262
|
|
|
291
263
|
await relocateSkills(cwd);
|
|
292
264
|
}
|
|
293
265
|
}
|
|
294
266
|
|
|
267
|
+
// ── 7. Install summary ──────────────────────────────────────────────
|
|
268
|
+
if (installResults.length > 0) {
|
|
269
|
+
printInstallSummary(installResults);
|
|
270
|
+
}
|
|
271
|
+
|
|
295
272
|
// Offer freeform catch-all
|
|
296
273
|
const { freeform } = await prompts({
|
|
297
274
|
type: 'confirm',
|
|
@@ -310,7 +287,7 @@ export async function skillDiscovery({ cwd, answers, args }) {
|
|
|
310
287
|
|
|
311
288
|
// ── Hero display ──────────────────────────────────────────────────────
|
|
312
289
|
|
|
313
|
-
function displayHeroRecommendations(
|
|
290
|
+
function displayHeroRecommendations(awesomeResults, catalogRecs, answers) {
|
|
314
291
|
const stackLabel = [...(answers.stack || []), ...(answers.frameworks || [])].join(', ') || 'general';
|
|
315
292
|
|
|
316
293
|
log.raw('');
|
|
@@ -318,42 +295,50 @@ function displayHeroRecommendations(awesomeRecs, skillsShRecs, answers) {
|
|
|
318
295
|
log.raw(kleur.bold().cyan(' ║') + kleur.bold(' 📦 RECOMMENDED FOR YOUR STACK: ') + kleur.bold().white(stackLabel) + pad('', Math.max(0, 30 - stackLabel.length)) + kleur.bold().cyan(' ║'));
|
|
319
296
|
log.raw(kleur.bold().cyan(' ╚══════════════════════════════════════════════════════════════════╝'));
|
|
320
297
|
|
|
321
|
-
if (
|
|
298
|
+
if (awesomeResults.length > 0) {
|
|
322
299
|
log.raw('');
|
|
323
300
|
log.raw(kleur.cyan(' ⬡ Source: awesome-copilot') + kleur.dim(' (github/awesome-copilot · 30k+ ★)'));
|
|
324
301
|
log.raw(kleur.dim(' ─────────────────────────────────────────────────────────'));
|
|
325
|
-
for (const r of
|
|
326
|
-
|
|
327
|
-
log.raw(` ${kleur.green('✓')} ${pad(r.name, 28)} ${pad(r.desc, 32)} ${typeTag}`);
|
|
302
|
+
for (const r of awesomeResults) {
|
|
303
|
+
log.raw(` ${kleur.green('✓')} ${pad(r.skill, 38)} ${kleur.dim(r.installs)}`);
|
|
328
304
|
}
|
|
329
305
|
}
|
|
330
306
|
|
|
331
|
-
if (
|
|
307
|
+
if (catalogRecs.length > 0) {
|
|
332
308
|
log.raw('');
|
|
333
|
-
log.raw(kleur.yellow(' ◆ Source: skills.sh') + kleur.dim(' (
|
|
309
|
+
log.raw(kleur.yellow(' ◆ Source: skills.sh') + kleur.dim(' (verified repos)'));
|
|
334
310
|
log.raw(kleur.dim(' ─────────────────────────────────────────────────────────'));
|
|
335
|
-
for (const r of
|
|
336
|
-
log.raw(` ${kleur.green('✓')} ${pad(r.skill,
|
|
311
|
+
for (const r of catalogRecs) {
|
|
312
|
+
log.raw(` ${kleur.green('✓')} ${pad(r.skill, 38)} ${kleur.dim(r.repo)}`);
|
|
337
313
|
}
|
|
338
314
|
}
|
|
339
315
|
|
|
340
316
|
log.raw('');
|
|
341
317
|
}
|
|
342
318
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
319
|
+
// ── Install summary ───────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
function printInstallSummary(results) {
|
|
322
|
+
const succeeded = results.filter((r) => r.ok);
|
|
323
|
+
const failed = results.filter((r) => !r.ok);
|
|
347
324
|
|
|
348
325
|
log.raw('');
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
326
|
+
if (succeeded.length > 0) {
|
|
327
|
+
log.ok(`Installed ${succeeded.length} skill${succeeded.length > 1 ? 's' : ''} successfully.`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (failed.length > 0) {
|
|
331
|
+
log.raw('');
|
|
332
|
+
log.warn(`${failed.length} skill${failed.length > 1 ? 's' : ''} failed to install:`);
|
|
333
|
+
for (const f of failed) {
|
|
334
|
+
log.raw(` ${kleur.red('✗')} ${f.name}`);
|
|
335
|
+
if (f.reason) {
|
|
336
|
+
log.raw(` ${kleur.dim(f.reason)}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
log.raw('');
|
|
340
|
+
log.dim('Install failed skills manually: npx skills add <owner/repo> --skill <name>');
|
|
354
341
|
}
|
|
355
|
-
log.raw('');
|
|
356
|
-
printBreadcrumbs();
|
|
357
342
|
}
|
|
358
343
|
|
|
359
344
|
// ── Post-install breadcrumbs ──────────────────────────────────────────
|
|
@@ -364,9 +349,6 @@ function printBreadcrumbs() {
|
|
|
364
349
|
log.raw(kleur.bold().green(' │') + kleur.bold(' 🎯 KEEP DISCOVERING — paste into VS Code / Copilot Chat: ') + kleur.bold().green('│'));
|
|
365
350
|
log.raw(kleur.bold().green(' ├──────────────────────────────────────────────────────────────────┤'));
|
|
366
351
|
log.raw(kleur.bold().green(' │') + ' ' + kleur.bold().green('│'));
|
|
367
|
-
log.raw(kleur.bold().green(' │') + ' Install the awesome-copilot suggestion skill: ' + kleur.bold().green('│'));
|
|
368
|
-
log.raw(kleur.bold().green(' │') + kleur.white(' copilot plugin install awesome-copilot@suggest ') + kleur.bold().green('│'));
|
|
369
|
-
log.raw(kleur.bold().green(' │') + ' ' + kleur.bold().green('│'));
|
|
370
352
|
log.raw(kleur.bold().green(' │') + ' Add awesome-copilot MCP for ongoing search: ' + kleur.bold().green('│'));
|
|
371
353
|
log.raw(kleur.bold().green(' │') + kleur.white(' Add to .vscode/mcp.json: ') + kleur.bold().green('│'));
|
|
372
354
|
log.raw(kleur.bold().green(' │') + kleur.dim(' "awesome-copilot": { ') + kleur.bold().green('│'));
|
|
@@ -381,26 +363,61 @@ function printBreadcrumbs() {
|
|
|
381
363
|
log.raw('');
|
|
382
364
|
}
|
|
383
365
|
|
|
384
|
-
// ──
|
|
366
|
+
// ── Runner ─────────────────────────────────────────────────────────────
|
|
385
367
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const out = [];
|
|
368
|
+
/** Run skills CLI with stdio: inherit (visible to user). */
|
|
369
|
+
function runSkills(env, skillsArgs, cwd) {
|
|
370
|
+
const { cmd, args } = env.runner(skillsArgs);
|
|
371
|
+
return runInteractive(cmd, args, cwd);
|
|
372
|
+
}
|
|
392
373
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
374
|
+
/** Run skills CLI, capture output, track success/failure. Returns { ok, reason }. */
|
|
375
|
+
function runSkillsTracked(env, skillsArgs, cwd) {
|
|
376
|
+
const { cmd, args } = env.runner(skillsArgs);
|
|
377
|
+
return new Promise((resolve) => {
|
|
378
|
+
let output = '';
|
|
379
|
+
const proc = spawn(cmd, args, { cwd, stdio: ['inherit', 'pipe', 'pipe'] });
|
|
380
|
+
proc.stdout.on('data', (chunk) => {
|
|
381
|
+
const text = chunk.toString();
|
|
382
|
+
process.stdout.write(text);
|
|
383
|
+
output += text;
|
|
384
|
+
});
|
|
385
|
+
proc.stderr.on('data', (chunk) => {
|
|
386
|
+
const text = chunk.toString();
|
|
387
|
+
process.stderr.write(text);
|
|
388
|
+
output += text;
|
|
389
|
+
});
|
|
390
|
+
proc.on('exit', (code) => {
|
|
391
|
+
const clean = stripAnsi(output);
|
|
392
|
+
if (code !== 0 || /failed to clone|installation failed|canceled/i.test(clean)) {
|
|
393
|
+
const reason = extractFailureReason(clean);
|
|
394
|
+
resolve({ ok: false, reason });
|
|
395
|
+
} else {
|
|
396
|
+
resolve({ ok: true, reason: null });
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
proc.on('error', (err) => {
|
|
400
|
+
resolve({ ok: false, reason: err.message });
|
|
401
|
+
});
|
|
402
|
+
});
|
|
401
403
|
}
|
|
402
404
|
|
|
403
|
-
|
|
405
|
+
/** Run skills CLI silently — capture stdout/stderr without echoing. */
|
|
406
|
+
function runSkillsSilent(env, skillsArgs, cwd) {
|
|
407
|
+
const { cmd, args } = env.runner(skillsArgs);
|
|
408
|
+
return new Promise((resolve) => {
|
|
409
|
+
let output = '';
|
|
410
|
+
const proc = spawn(cmd, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
411
|
+
proc.stdout.on('data', (chunk) => {
|
|
412
|
+
output += chunk.toString();
|
|
413
|
+
});
|
|
414
|
+
proc.stderr.on('data', (chunk) => {
|
|
415
|
+
output += chunk.toString();
|
|
416
|
+
});
|
|
417
|
+
proc.on('exit', () => resolve(output));
|
|
418
|
+
proc.on('error', () => resolve(''));
|
|
419
|
+
});
|
|
420
|
+
}
|
|
404
421
|
|
|
405
422
|
async function relocateSkills(cwd) {
|
|
406
423
|
const src = join(cwd, '.agents', 'skills');
|
|
@@ -458,12 +475,7 @@ async function relocateSkills(cwd) {
|
|
|
458
475
|
}
|
|
459
476
|
}
|
|
460
477
|
|
|
461
|
-
// ──
|
|
462
|
-
|
|
463
|
-
function runSkills(env, skillsArgs, cwd) {
|
|
464
|
-
const { cmd, args } = env.runner(skillsArgs);
|
|
465
|
-
return runInteractive(cmd, args, cwd);
|
|
466
|
-
}
|
|
478
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
467
479
|
|
|
468
480
|
function buildRecommendations(a) {
|
|
469
481
|
const stackLower = (a.stack || []).map((s) => s.toLowerCase());
|
|
@@ -503,6 +515,24 @@ function suggestSearches(a) {
|
|
|
503
515
|
return [...out];
|
|
504
516
|
}
|
|
505
517
|
|
|
518
|
+
/** Parse owner/repo@skill ref into { repo, skill }. */
|
|
519
|
+
function parseRef(ref) {
|
|
520
|
+
const match = /^(.+?\/.+?)@(.+)$/.exec(ref);
|
|
521
|
+
if (!match) return null;
|
|
522
|
+
return { repo: match[1], skill: match[2] };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** Extract a concise failure reason from skills CLI output. */
|
|
526
|
+
function extractFailureReason(output) {
|
|
527
|
+
const cloneMatch = /Failed to clone[^:]*:\s*(.+)/i.exec(output);
|
|
528
|
+
if (cloneMatch) return cloneMatch[1].trim();
|
|
529
|
+
|
|
530
|
+
const errorMatch = /(?:error|failed)[:\s]+(.+)/im.exec(output);
|
|
531
|
+
if (errorMatch) return errorMatch[1].trim();
|
|
532
|
+
|
|
533
|
+
return 'Unknown error (check output above)';
|
|
534
|
+
}
|
|
535
|
+
|
|
506
536
|
function pad(s, n) {
|
|
507
537
|
return (s || '').padEnd(n);
|
|
508
538
|
}
|
|
@@ -519,30 +549,6 @@ function runInteractive(cmd, args, cwd) {
|
|
|
519
549
|
});
|
|
520
550
|
}
|
|
521
551
|
|
|
522
|
-
/** Run skills CLI, capture stdout/stderr while echoing to the terminal. */
|
|
523
|
-
function runSkillsCapture(env, skillsArgs, cwd) {
|
|
524
|
-
const { cmd, args } = env.runner(skillsArgs);
|
|
525
|
-
return new Promise((resolve) => {
|
|
526
|
-
let output = '';
|
|
527
|
-
const proc = spawn(cmd, args, { cwd, stdio: ['inherit', 'pipe', 'pipe'] });
|
|
528
|
-
proc.stdout.on('data', (chunk) => {
|
|
529
|
-
const text = chunk.toString();
|
|
530
|
-
process.stdout.write(text);
|
|
531
|
-
output += text;
|
|
532
|
-
});
|
|
533
|
-
proc.stderr.on('data', (chunk) => {
|
|
534
|
-
const text = chunk.toString();
|
|
535
|
-
process.stderr.write(text);
|
|
536
|
-
output += text;
|
|
537
|
-
});
|
|
538
|
-
proc.on('exit', () => resolve(output));
|
|
539
|
-
proc.on('error', (err) => {
|
|
540
|
-
log.warn(`Could not launch \`${cmd} ${args.join(' ')}\`: ${err.message}`);
|
|
541
|
-
resolve('');
|
|
542
|
-
});
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
|
|
546
552
|
function stripAnsi(str) {
|
|
547
553
|
// eslint-disable-next-line no-control-regex
|
|
548
554
|
return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
|
|
@@ -27,6 +27,6 @@ _(populate as you discover things this project will NOT do)_
|
|
|
27
27
|
_(populate with measurable outcomes — what does "done" look like?)_
|
|
28
28
|
|
|
29
29
|
---
|
|
30
|
-
|
|
30
|
+
{{DOCS_SECTION}}
|
|
31
31
|
_This file is the durable vision. It changes rarely. Day-to-day status lives in `STATE.md`._
|
|
32
32
|
_Generated by `npx cli-five` on {{DATE}}._
|