cli-five 0.2.2 → 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 +14 -10
- package/src/steps/scaffold.mjs +7 -0
- 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
|
@@ -78,8 +78,8 @@ const COST_MODES = [
|
|
|
78
78
|
{ title: 'Mixed', value: 'mixed', description: 'Premium Coder + Reviewer, cheap everything else.' },
|
|
79
79
|
];
|
|
80
80
|
|
|
81
|
-
export async function interview(detected, args) {
|
|
82
|
-
if (args.yes) return defaults(detected);
|
|
81
|
+
export async function interview(detected, args, docHints = {}) {
|
|
82
|
+
if (args.yes) return defaults(detected, docHints);
|
|
83
83
|
|
|
84
84
|
const onCancel = () => {
|
|
85
85
|
throw new Error('Interview cancelled. Nothing was written.');
|
|
@@ -92,13 +92,13 @@ export async function interview(detected, args) {
|
|
|
92
92
|
type: 'text',
|
|
93
93
|
name: 'projectName',
|
|
94
94
|
message: 'Project name',
|
|
95
|
-
initial: detected.projectName,
|
|
95
|
+
initial: docHints.projectName || detected.projectName,
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
type: 'text',
|
|
99
99
|
name: 'oneLiner',
|
|
100
100
|
message: 'One-line description (becomes PROJECT.md vision)',
|
|
101
|
-
initial: '',
|
|
101
|
+
initial: docHints.oneLiner || '',
|
|
102
102
|
},
|
|
103
103
|
],
|
|
104
104
|
{ onCancel },
|
|
@@ -159,11 +159,13 @@ export async function interview(detected, args) {
|
|
|
159
159
|
type: 'text',
|
|
160
160
|
name: 'goals',
|
|
161
161
|
message: 'Primary goal of this project (one sentence)',
|
|
162
|
+
initial: docHints.goals || '',
|
|
162
163
|
},
|
|
163
164
|
{
|
|
164
165
|
type: 'text',
|
|
165
166
|
name: 'constraints',
|
|
166
167
|
message: 'Hard constraints (perf, deps, deploy, compliance — one sentence, optional)',
|
|
168
|
+
initial: docHints.constraints || '',
|
|
167
169
|
},
|
|
168
170
|
{
|
|
169
171
|
type: 'select',
|
|
@@ -183,30 +185,32 @@ export async function interview(detected, args) {
|
|
|
183
185
|
);
|
|
184
186
|
|
|
185
187
|
return normalize({
|
|
186
|
-
...defaults(detected),
|
|
188
|
+
...defaults(detected, docHints),
|
|
187
189
|
...basic,
|
|
188
190
|
...stackAnswers,
|
|
189
191
|
...rest,
|
|
190
192
|
presetId: preset,
|
|
191
193
|
quickstart: chosenPreset?.quickstart || '',
|
|
194
|
+
docs: docHints.raw || '',
|
|
192
195
|
});
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
/** Default stack is Next.js + TypeScript when nothing detected and --yes. */
|
|
196
|
-
function defaults(detected) {
|
|
199
|
+
function defaults(detected, docHints = {}) {
|
|
197
200
|
const hasDetected = detected.stacks.length > 0;
|
|
198
201
|
const fallback = STACK_PRESETS[0]; // Next.js (TypeScript)
|
|
199
202
|
return {
|
|
200
|
-
projectName: detected.projectName,
|
|
201
|
-
oneLiner: '',
|
|
203
|
+
projectName: docHints.projectName || detected.projectName,
|
|
204
|
+
oneLiner: docHints.oneLiner || '',
|
|
202
205
|
stack: hasDetected ? detected.stacks.map((s) => s.label) : fallback.stack,
|
|
203
206
|
frameworks: hasDetected ? [] : fallback.frameworks,
|
|
204
|
-
goals: '',
|
|
205
|
-
constraints: '',
|
|
207
|
+
goals: docHints.goals || '',
|
|
208
|
+
constraints: docHints.constraints || '',
|
|
206
209
|
costMode: 'premium',
|
|
207
210
|
snark: true,
|
|
208
211
|
presetId: hasDetected ? 'custom' : fallback.value,
|
|
209
212
|
quickstart: hasDetected ? '' : fallback.quickstart,
|
|
213
|
+
docs: docHints.raw || '',
|
|
210
214
|
};
|
|
211
215
|
}
|
|
212
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
|
};
|
|
@@ -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}}._
|