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.
- package/package.json +1 -1
- package/src/commands/init.mjs +146 -29
package/package.json
CHANGED
package/src/commands/init.mjs
CHANGED
|
@@ -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 —
|
|
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
|
-
|
|
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) {
|