cli-five 0.1.4 → 0.1.5

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 (2) hide show
  1. package/package.json +3 -2
  2. package/src/steps/skills.mjs +183 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-five",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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": {
@@ -30,6 +30,7 @@
30
30
  "license": "MIT",
31
31
  "dependencies": {
32
32
  "kleur": "^4.1.5",
33
- "prompts": "^2.4.2"
33
+ "prompts": "^2.4.2",
34
+ "skills": "^1.5.0"
34
35
  }
35
36
  }
@@ -1,5 +1,8 @@
1
1
  import prompts from 'prompts';
2
2
  import { spawn, execFileSync } from 'node:child_process';
3
+ import { existsSync, mkdirSync, readdirSync, renameSync, rmSync } from 'node:fs';
4
+ import { join, dirname } from 'node:path';
5
+ import { createRequire } from 'node:module';
3
6
  import { log } from '../util/log.mjs';
4
7
 
5
8
  // Well-known skill repos matched by stack term → repo + suggested skill names
@@ -13,6 +16,83 @@ const SKILL_CATALOG = [
13
16
  { terms: ['rust'], repo: 'anthropics/skills', skills: ['code-review'] },
14
17
  ];
15
18
 
19
+ // ── Environment detection ─────────────────────────────────────────────
20
+
21
+ function resolveSkillsBin() {
22
+ try {
23
+ const require = createRequire(import.meta.url);
24
+ const pkgPath = require.resolve('skills/package.json');
25
+ const bin = join(dirname(pkgPath), 'bin', 'cli.mjs');
26
+ return existsSync(bin) ? bin : null;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ function whichVersion(cmd) {
33
+ try {
34
+ return execFileSync(cmd, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'] })
35
+ .toString()
36
+ .trim()
37
+ .split('\n')[0];
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /** Detect whether the target workspace itself uses pnpm. */
44
+ function projectUsesPnpm(cwd) {
45
+ return existsSync(join(cwd, 'pnpm-lock.yaml')) || existsSync(join(cwd, 'pnpm-workspace.yaml'));
46
+ }
47
+
48
+ /**
49
+ * Detect the runtime environment.
50
+ * Returns { runner, label } where runner is a function (skillsArgs, cwd) → {cmd, args}.
51
+ */
52
+ function detectEnv(cwd) {
53
+ const bundledBin = resolveSkillsBin();
54
+ const pnpmVer = whichVersion('pnpm');
55
+ const npxVer = whichVersion('npx');
56
+ const projectPnpm = projectUsesPnpm(cwd);
57
+
58
+ // ── Terse status lines ────────────────────────────────────────────
59
+ log.info(`skills CLI (bundled) ${bundledBin ? '✓' : '✗ not resolved'}`);
60
+ log.info(`pnpm ${pnpmVer ? `✓ ${pnpmVer}` : '✗ not found'}${projectPnpm ? ' (project uses pnpm)' : ''}`);
61
+ log.info(`npx ${npxVer ? `✓ ${npxVer}` : '✗ not found'}`);
62
+
63
+ // ── Priority: bundled > pnpm (if project or system) > npx ─────────
64
+ if (bundledBin) {
65
+ return {
66
+ label: 'bundled skills CLI',
67
+ runner: (skillsArgs) => ({ cmd: process.execPath, args: [bundledBin, ...skillsArgs] }),
68
+ pnpmVer,
69
+ npxVer,
70
+ projectPnpm,
71
+ };
72
+ }
73
+ if (pnpmVer) {
74
+ return {
75
+ label: 'pnpm dlx',
76
+ runner: (skillsArgs) => ({ cmd: 'pnpm', args: ['dlx', 'skills', ...skillsArgs] }),
77
+ pnpmVer,
78
+ npxVer,
79
+ projectPnpm,
80
+ };
81
+ }
82
+ if (npxVer) {
83
+ return {
84
+ label: 'npx',
85
+ runner: (skillsArgs) => ({ cmd: 'npx', args: ['-y', 'skills', ...skillsArgs] }),
86
+ pnpmVer,
87
+ npxVer,
88
+ projectPnpm,
89
+ };
90
+ }
91
+ return null;
92
+ }
93
+
94
+ // ── Main entry ────────────────────────────────────────────────────────
95
+
16
96
  export async function skillDiscovery({ cwd, answers, args }) {
17
97
  if (!args.skills) {
18
98
  log.dim('Skipping skills discovery (--no-skills).');
@@ -23,15 +103,43 @@ export async function skillDiscovery({ cwd, answers, args }) {
23
103
  return;
24
104
  }
25
105
 
106
+ // ── Environment checks ──────────────────────────────────────────────
107
+ const env = detectEnv(cwd);
108
+
109
+ if (!env) {
110
+ log.warn('Cannot run skills CLI: no bundled binary, pnpm, or npx found.');
111
+ log.warn('Install Node.js (includes npx) or pnpm, then re-run.');
112
+ return;
113
+ }
114
+
115
+ // Offer pnpm if the user doesn't have it
116
+ if (!env.pnpmVer) {
117
+ const { wantPnpm } = await prompts({
118
+ type: 'confirm',
119
+ name: 'wantPnpm',
120
+ message: 'pnpm not found. Install it? (faster, stricter deps, saves disk)',
121
+ initial: false,
122
+ });
123
+ if (wantPnpm) {
124
+ try {
125
+ execFileSync('npm', ['install', '-g', 'pnpm'], { stdio: 'inherit' });
126
+ log.ok('pnpm installed.');
127
+ } catch (err) {
128
+ log.warn(`pnpm install failed: ${err.message}`);
129
+ }
130
+ }
131
+ }
132
+
133
+ log.ok(`Running skills via ${env.label}`);
26
134
  log.raw('');
27
- log.raw('Skill discovery powered by skills.sh (Vercel)');
135
+
136
+ // ── Skill discovery ─────────────────────────────────────────────────
28
137
 
29
138
  // 1. Build recommendations from catalog based on stack
30
139
  const recs = buildRecommendations(answers);
31
140
  if (recs.length === 0) {
32
141
  log.dim('No stack-specific recommendations found.');
33
142
  } else {
34
- log.raw('');
35
143
  log.raw(` ${pad('Skill', 36)} ${pad('Repo', 36)}`);
36
144
  log.raw(` ${'─'.repeat(36)} ${'─'.repeat(36)}`);
37
145
  for (const r of recs) {
@@ -47,7 +155,6 @@ export async function skillDiscovery({ cwd, answers, args }) {
47
155
  log.raw('');
48
156
  }
49
157
 
50
- // 3. Offer to launch interactive find for each term
51
158
  for (const term of terms) {
52
159
  const { go } = await prompts({
53
160
  type: 'confirm',
@@ -56,11 +163,11 @@ export async function skillDiscovery({ cwd, answers, args }) {
56
163
  initial: true,
57
164
  });
58
165
  if (go) {
59
- await runInteractive('npx', ['-y', 'skills', 'find', term], cwd);
166
+ await runSkills(env, ['find', term], cwd);
60
167
  }
61
168
  }
62
169
 
63
- // 4. Offer to install from well-known repos
170
+ // 3. Offer to install from well-known repos
64
171
  if (recs.length > 0) {
65
172
  const { install } = await prompts({
66
173
  type: 'multiselect',
@@ -75,7 +182,6 @@ export async function skillDiscovery({ cwd, answers, args }) {
75
182
  });
76
183
 
77
184
  if (install && install.length > 0) {
78
- // Group by repo
79
185
  const byRepo = new Map();
80
186
  for (const r of install) {
81
187
  if (!byRepo.has(r.repo)) byRepo.set(r.repo, []);
@@ -84,16 +190,14 @@ export async function skillDiscovery({ cwd, answers, args }) {
84
190
  for (const [repo, skills] of byRepo) {
85
191
  const skillArgs = skills.flatMap((s) => ['--skill', s]);
86
192
  log.info(`Installing from ${repo}: ${skills.join(', ')}`);
87
- await runInteractive(
88
- 'npx',
89
- ['-y', 'skills', 'add', repo, ...skillArgs, '-a', 'github-copilot'],
90
- cwd,
91
- );
193
+ await runSkills(env, ['add', repo, ...skillArgs, '-a', 'github-copilot'], cwd);
92
194
  }
195
+
196
+ await relocateSkills(cwd);
93
197
  }
94
198
  }
95
199
 
96
- // 5. Offer freeform catch-all
200
+ // 4. Offer freeform catch-all
97
201
  const { freeform } = await prompts({
98
202
  type: 'confirm',
99
203
  name: 'freeform',
@@ -101,12 +205,78 @@ export async function skillDiscovery({ cwd, answers, args }) {
101
205
  initial: false,
102
206
  });
103
207
  if (freeform) {
104
- await runInteractive('npx', ['-y', 'skills', 'find'], cwd);
208
+ await runSkills(env, ['find'], cwd);
209
+ await relocateSkills(cwd);
105
210
  }
106
211
 
107
212
  log.dim('Done. Run `npx skills find` anytime to discover more.');
108
213
  }
109
214
 
215
+ // ── Relocation ─────────────────────────────────────────────────────────
216
+
217
+ async function relocateSkills(cwd) {
218
+ const src = join(cwd, '.agents', 'skills');
219
+ const dest = join(cwd, '.github', 'skills');
220
+
221
+ if (!existsSync(src)) return;
222
+
223
+ const candidates = [];
224
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
225
+ if (!entry.isDirectory()) continue;
226
+ if (!existsSync(join(src, entry.name, 'SKILL.md'))) continue;
227
+ if (existsSync(join(dest, entry.name))) continue;
228
+ candidates.push(entry.name);
229
+ }
230
+
231
+ if (candidates.length === 0) return;
232
+
233
+ log.raw('');
234
+ log.info(`${candidates.length} skill${candidates.length > 1 ? 's' : ''} in .agents/skills/: ${candidates.join(', ')}`);
235
+ log.info('cli-five prefers .github/skills/ (co-located with agents & instructions).');
236
+ log.info('Both locations work — Copilot discovers either.');
237
+
238
+ const { move } = await prompts({
239
+ type: 'confirm',
240
+ name: 'move',
241
+ message: 'Move to .github/skills/?',
242
+ initial: true,
243
+ });
244
+
245
+ if (!move) {
246
+ log.dim('Keeping skills in .agents/skills/.');
247
+ return;
248
+ }
249
+
250
+ let moved = 0;
251
+ for (const name of candidates) {
252
+ mkdirSync(dest, { recursive: true });
253
+ renameSync(join(src, name), join(dest, name));
254
+ moved++;
255
+ }
256
+
257
+ log.ok(`Moved ${moved} skill${moved > 1 ? 's' : ''} → .github/skills/`);
258
+
259
+ // Clean up empty dirs
260
+ try {
261
+ if (readdirSync(src).length === 0) {
262
+ rmSync(src, { recursive: true });
263
+ const agentsDir = join(cwd, '.agents');
264
+ if (existsSync(agentsDir) && readdirSync(agentsDir).length === 0) {
265
+ rmSync(agentsDir, { recursive: true });
266
+ }
267
+ }
268
+ } catch {
269
+ /* best-effort */
270
+ }
271
+ }
272
+
273
+ // ── Runner ─────────────────────────────────────────────────────────────
274
+
275
+ function runSkills(env, skillsArgs, cwd) {
276
+ const { cmd, args } = env.runner(skillsArgs);
277
+ return runInteractive(cmd, args, cwd);
278
+ }
279
+
110
280
  function buildRecommendations(a) {
111
281
  const stackLower = (a.stack || []).map((s) => s.toLowerCase());
112
282
  const fwLower = (a.frameworks || []).map((f) => f.toLowerCase());