cli-five 0.2.1 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-five",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Code Like I'm Five — scaffold a 5-agent VS Code Copilot team into any repo.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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',
@@ -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
- // ── Awesome-copilot catalog (github/awesome-copilot) ──────────────────
10
- // Curated subset mapped by stack term → { name, description, type }
11
- const AWESOME_CATALOG = [
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
- // Still show awesome-copilot recs (they don't need skills CLI)
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
- // ── Combined picker ─────────────────────────────────────────────────
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 { go } = await prompts({
188
- type: 'confirm',
189
- name: 'go',
190
- message: `Search skills.sh for "${term}"?`,
191
- initial: true,
192
- });
193
- if (go) {
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
- // Build combined skill picker — awesome recs + catalog recs + search results
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
- // Awesome-copilot recs first (hero placement)
205
- for (const r of awesomeRecs) {
206
- const key = `awesome:${r.name}`;
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.name} ${kleur.dim(`(${r.desc})`)} ${kleur.cyan('← awesome-copilot')}`,
211
- value: { source: 'awesome', name: r.name, type: r.type },
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
- // skills.sh catalog recs
217
- for (const r of skillsShRecs) {
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: 'skillssh', ref, repo: r.repo, skill: r.skill },
200
+ value: { source: 'catalog', ref, repo: r.repo, skill: r.skill },
224
201
  selected: true,
225
202
  });
226
203
  }
227
204
 
228
- // skills.sh search results
229
- for (const r of allFound) {
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(`— ${r.installs}`)} ${kleur.yellow('← skills.sh')}`,
234
- value: { source: 'skillssh', ref: r.ref },
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
- // Separate awesome-copilot entries from skills.sh entries
251
- const awesomeItems = toInstall.filter((i) => i.source === 'awesome');
252
- const skillsShItems = toInstall.filter((i) => i.source === 'skillssh');
253
-
254
- // Install awesome-copilot skills/instructions via npx skills (they're in the registry too)
255
- if (awesomeItems.length > 0) {
256
- log.raw('');
257
- log.info(`${kleur.cyan('awesome-copilot')} resources selected: ${awesomeItems.map((i) => i.name).join(', ')}`);
258
- log.dim('These will be installed via skills CLI from the awesome-copilot registry.');
259
- for (const item of awesomeItems) {
260
- log.info(`Installing ${item.name} (${item.type})...`);
261
- await runSkills(env, ['add', `awesome-copilot@${item.name}`, '-a', 'github-copilot'], cwd);
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
- // Install skills.sh entries
266
- if (skillsShItems.length > 0) {
267
- const byRepo = new Map();
268
- const standalone = [];
269
-
270
- for (const item of skillsShItems) {
271
- if (item.repo) {
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
- for (const ref of standalone) {
286
- log.info(`Installing ${ref}...`);
287
- await runSkills(env, ['add', ref, '-a', 'github-copilot'], cwd);
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(awesomeRecs, skillsShRecs, answers) {
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 (awesomeRecs.length > 0) {
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 awesomeRecs) {
326
- const typeTag = kleur.dim(`[${r.type}]`);
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 (skillsShRecs.length > 0) {
307
+ if (catalogRecs.length > 0) {
332
308
  log.raw('');
333
- log.raw(kleur.yellow(' ◆ Source: skills.sh') + kleur.dim(' (skills.sh registry)'));
309
+ log.raw(kleur.yellow(' ◆ Source: skills.sh') + kleur.dim(' (verified repos)'));
334
310
  log.raw(kleur.dim(' ─────────────────────────────────────────────────────────'));
335
- for (const r of skillsShRecs) {
336
- log.raw(` ${kleur.green('✓')} ${pad(r.skill, 28)} ${kleur.dim(r.repo)}`);
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
- /** Fallback when skills CLI is unavailable — show awesome-copilot recs as copy-paste commands */
344
- async function showAwesomeCopilotOnly(answers) {
345
- const recs = buildAwesomeRecommendations(answers);
346
- if (recs.length === 0) return;
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
- log.info('Cannot install skills automatically, but here are recommendations:');
350
- displayHeroRecommendations(recs, [], answers);
351
- log.raw(kleur.dim(' Install manually in VS Code Chat:'));
352
- for (const r of recs) {
353
- log.raw(` ${kleur.white(`copilot plugin install awesome-copilot@${r.name}`)}`);
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
- // ── Awesome-copilot recommendation builder ────────────────────────────
366
+ // ── Runner ─────────────────────────────────────────────────────────────
385
367
 
386
- function buildAwesomeRecommendations(a) {
387
- const stackLower = (a.stack || []).map((s) => s.toLowerCase());
388
- const fwLower = (a.frameworks || []).map((f) => f.toLowerCase());
389
- const all = [...stackLower, ...fwLower];
390
- const seen = new Set();
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
- for (const entry of AWESOME_CATALOG) {
394
- const matches = entry.terms.includes('*') || entry.terms.some((t) => all.some((s) => s.includes(t)));
395
- if (!matches) continue;
396
- if (seen.has(entry.name)) continue;
397
- seen.add(entry.name);
398
- out.push({ name: entry.name, desc: entry.desc, type: entry.type, source: entry.source });
399
- }
400
- return out;
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
- // ── Relocation ─────────────────────────────────────────────────────────
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
- // ── Runner ─────────────────────────────────────────────────────────────
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, '');