cli-five 0.2.3 → 0.2.4

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 +1 -1
  2. package/src/commands/init.mjs +146 -29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-five",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
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": {
@@ -1,7 +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
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
4
+ import { resolve, basename, join, extname } from 'node:path';
5
5
  import { log } from '../util/log.mjs';
6
6
  import { detect } from '../steps/detect.mjs';
7
7
  import { confirmOverwriteIfNeeded } from '../steps/confirm.mjs';
@@ -47,39 +47,18 @@ export async function init(args) {
47
47
  let docHints;
48
48
 
49
49
  if (args.docs.length > 0) {
50
- // --doc was passed on the CLI — use those files directly
50
+ // --doc was passed on the CLI — validate with retry
51
51
  docHints = loadDocs(args.docs, cwd);
52
+ if (docHints.files.length === 0) {
53
+ log.warn('None of the --doc files could be loaded.');
54
+ }
52
55
  } else if (args.yes) {
53
- // --yes skips the choice — go straight to defaults
54
56
  docHints = loadDocs([], cwd);
55
57
  } 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
- }
58
+ docHints = await collectDocFiles(cwd);
80
59
  }
81
60
 
82
- if (docHints.files.length > 0) {
61
+ if (docHints.files.length > 0 && args.docs.length > 0) {
83
62
  log.info(`Loaded ${docHints.files.length} doc${docHints.files.length > 1 ? 's' : ''}: ${docHints.files.join(', ')}`);
84
63
  if (docHints.projectName) log.dim(` → project name: ${docHints.projectName}`);
85
64
  if (docHints.oneLiner) log.dim(` → description: ${docHints.oneLiner}`);
@@ -151,6 +130,144 @@ function printNextSteps(answers) {
151
130
  log.raw('');
152
131
  }
153
132
 
133
+ // ── Interactive doc file collection ───────────────────────────────────
134
+
135
+ const MANUAL_SENTINEL = '__manual__';
136
+ const SKIP_SENTINEL = '__skip__';
137
+
138
+ async function collectDocFiles(cwd) {
139
+ const { mode } = await prompts({
140
+ type: 'select',
141
+ name: 'mode',
142
+ message: 'How would you like to describe your project?',
143
+ choices: [
144
+ { title: 'Provide document(s)', value: 'docs', description: 'Feed existing files (README, PRD, etc.) — we extract what we can' },
145
+ { title: 'Answer questions', value: 'manual', description: 'Short interactive interview' },
146
+ ],
147
+ initial: 0,
148
+ });
149
+ if (mode === undefined || mode === 'manual') return loadDocs([], cwd);
150
+
151
+ // Retry loop — keep asking until we get valid files or user opts out
152
+ while (true) {
153
+ const selectedPaths = await pickFiles(cwd);
154
+
155
+ // User cancelled or chose skip
156
+ if (selectedPaths === null) return loadDocs([], cwd);
157
+
158
+ const hints = loadDocs(selectedPaths, cwd);
159
+ if (hints.files.length > 0) {
160
+ log.info(`Loaded ${hints.files.length} doc${hints.files.length > 1 ? 's' : ''}: ${hints.files.join(', ')}`);
161
+ if (hints.projectName) log.dim(` → project name: ${hints.projectName}`);
162
+ if (hints.oneLiner) log.dim(` → description: ${hints.oneLiner}`);
163
+ return hints;
164
+ }
165
+
166
+ // Nothing loaded — offer retry
167
+ log.warn('No valid files were loaded.');
168
+ const { next } = await prompts({
169
+ type: 'select',
170
+ name: 'next',
171
+ message: 'What would you like to do?',
172
+ choices: [
173
+ { title: 'Try selecting files again', value: 'retry' },
174
+ { title: 'Answer questions manually instead', value: 'manual' },
175
+ ],
176
+ });
177
+ if (next !== 'retry') return loadDocs([], cwd);
178
+ }
179
+ }
180
+
181
+ async function pickFiles(cwd) {
182
+ const candidates = discoverDocCandidates(cwd);
183
+
184
+ if (candidates.length > 0) {
185
+ const choices = [
186
+ ...candidates.map(f => ({ title: f, value: f })),
187
+ { title: kleur.dim('Type path(s) manually'), value: MANUAL_SENTINEL },
188
+ { title: kleur.dim('Skip — answer questions instead'), value: SKIP_SENTINEL },
189
+ ];
190
+
191
+ const { files } = await prompts({
192
+ type: 'autocompleteMultiselect',
193
+ name: 'files',
194
+ message: 'Select project documents',
195
+ choices,
196
+ hint: 'Type to filter, space to select, enter to confirm',
197
+ suggest: (input, choices) =>
198
+ choices.filter(c =>
199
+ c.value === MANUAL_SENTINEL || c.value === SKIP_SENTINEL ||
200
+ c.title.toLowerCase().includes(input.toLowerCase())
201
+ ),
202
+ });
203
+
204
+ if (!files || files.length === 0) return null;
205
+ if (files.includes(SKIP_SENTINEL)) return null;
206
+ if (!files.includes(MANUAL_SENTINEL)) return files;
207
+ // Fall through to manual entry
208
+ }
209
+
210
+ // Manual entry (also reached when no candidates found)
211
+ return await manualPathEntry(cwd);
212
+ }
213
+
214
+ async function manualPathEntry(cwd) {
215
+ const { raw } = await prompts({
216
+ type: 'list',
217
+ name: 'raw',
218
+ message: 'File paths (comma-separated, relative to project root)',
219
+ separator: ',',
220
+ });
221
+
222
+ const paths = (raw || []).map(p => p.trim()).filter(Boolean);
223
+ if (paths.length === 0) return null;
224
+
225
+ // Validate immediately so user sees which ones failed
226
+ const valid = [];
227
+ const invalid = [];
228
+ for (const p of paths) {
229
+ if (existsSync(resolve(cwd, p))) {
230
+ valid.push(p);
231
+ } else {
232
+ invalid.push(p);
233
+ }
234
+ }
235
+
236
+ if (invalid.length > 0) {
237
+ for (const p of invalid) log.warn(`Not found: ${p}`);
238
+ }
239
+
240
+ return valid.length > 0 ? valid : paths; // return all — loadDocs will warn again, triggers retry
241
+ }
242
+
243
+ function discoverDocCandidates(cwd) {
244
+ const IGNORE = new Set(['node_modules', '.git', '.github', 'dist', 'build', '.next', 'coverage', '.turbo', '.vercel']);
245
+ const DOC_EXTS = new Set(['.md', '.txt', '.rst', '.mdx']);
246
+ const DOC_NAMES = new Set(['package.json']);
247
+ const results = [];
248
+
249
+ function walk(dir, prefix) {
250
+ let entries;
251
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
252
+ for (const entry of entries) {
253
+ if (entry.name.startsWith('.') && entry.name !== '.github') continue;
254
+ if (IGNORE.has(entry.name)) continue;
255
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
256
+ if (entry.isDirectory()) {
257
+ if (rel.split('/').length < 3) walk(join(dir, entry.name), rel);
258
+ } else {
259
+ const ext = extname(entry.name).toLowerCase();
260
+ if (DOC_EXTS.has(ext) || DOC_NAMES.has(entry.name.toLowerCase())) {
261
+ results.push(rel);
262
+ }
263
+ }
264
+ }
265
+ }
266
+
267
+ walk(cwd, '');
268
+ return results.sort();
269
+ }
270
+
154
271
  // ── --doc file loading + extraction ───────────────────────────────────
155
272
 
156
273
  function loadDocs(docPaths, cwd) {