create-battle-plan 1.4.1 → 1.4.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/bin/cli.js +66 -27
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -71,6 +71,14 @@ function ask(question) {
|
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
async function askRequired(question, errorMsg) {
|
|
75
|
+
while (true) {
|
|
76
|
+
const answer = await ask(question);
|
|
77
|
+
if (answer) return answer;
|
|
78
|
+
console.log(`${YELLOW} ${errorMsg}${RESET}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
// ── Interactive folder picker (raw mode) ─────────────────
|
|
75
83
|
|
|
76
84
|
function getDirs(dir) {
|
|
@@ -84,7 +92,11 @@ function getDirs(dir) {
|
|
|
84
92
|
}
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
function
|
|
95
|
+
function isDirEmpty(dir) {
|
|
96
|
+
try { return fs.readdirSync(dir).length === 0; } catch { return false; }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function pickFolder(shortName) {
|
|
88
100
|
return new Promise((resolve) => {
|
|
89
101
|
// Pause readline so we can use raw mode
|
|
90
102
|
closeReadline();
|
|
@@ -97,8 +109,14 @@ function pickFolder(projectSlug) {
|
|
|
97
109
|
function getOptions() {
|
|
98
110
|
const dirs = getDirs(cwd);
|
|
99
111
|
const options = [];
|
|
112
|
+
if (isDirEmpty(cwd)) {
|
|
113
|
+
options.push({
|
|
114
|
+
label: `${GREEN}● Install in this folder ${BOLD}(${path.basename(cwd)}/)${RESET}${GREEN} — no subfolder${RESET}`,
|
|
115
|
+
action: 'here_no_sub',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
100
118
|
options.push({ label: `${GREEN}+ Create new folder here${RESET}`, action: 'create' });
|
|
101
|
-
options.push({ label: `${CYAN}» Install here as ${BOLD}${
|
|
119
|
+
options.push({ label: `${CYAN}» Install here as ${BOLD}${shortName}/${RESET}`, action: 'here' });
|
|
102
120
|
if (path.dirname(cwd) !== cwd) {
|
|
103
121
|
options.push({ label: `${DIM}../${RESET} ${DIM}(up)${RESET}`, action: 'up' });
|
|
104
122
|
}
|
|
@@ -116,14 +134,14 @@ function pickFolder(projectSlug) {
|
|
|
116
134
|
let output = '';
|
|
117
135
|
|
|
118
136
|
if (mode === 'input') {
|
|
119
|
-
output += `${CLEAR_LINE}\r${DIM}[
|
|
137
|
+
output += `${CLEAR_LINE}\r${DIM}[7/7]${RESET} ${BOLD}Folder name:${RESET} ${inputBuffer}\x1b[K`;
|
|
120
138
|
process.stdout.write(output);
|
|
121
139
|
return;
|
|
122
140
|
}
|
|
123
141
|
|
|
124
142
|
output += `\x1b[H\x1b[2J`; // clear screen
|
|
125
143
|
output += `\n`;
|
|
126
|
-
output += `${DIM}[
|
|
144
|
+
output += `${DIM}[7/7]${RESET} ${BOLD}Where do you want to install it?${RESET}\n`;
|
|
127
145
|
output += `${DIM} ${display}${RESET}\n`;
|
|
128
146
|
output += `\n`;
|
|
129
147
|
output += `${DIM} ↑↓ navigate · enter select · q cancel${RESET}\n`;
|
|
@@ -156,12 +174,14 @@ function pickFolder(projectSlug) {
|
|
|
156
174
|
const opt = options[selected];
|
|
157
175
|
if (opt.action === 'create') {
|
|
158
176
|
mode = 'input';
|
|
159
|
-
inputBuffer =
|
|
177
|
+
inputBuffer = shortName;
|
|
160
178
|
process.stdout.write(`\x1b[H\x1b[2J`);
|
|
161
179
|
process.stdout.write(`\n`);
|
|
162
|
-
process.stdout.write(`${DIM}[
|
|
180
|
+
process.stdout.write(`${DIM}[7/7]${RESET} ${BOLD}Folder name:${RESET} ${inputBuffer}`);
|
|
163
181
|
} else if (opt.action === 'here') {
|
|
164
|
-
finish(path.join(cwd,
|
|
182
|
+
finish(path.join(cwd, shortName));
|
|
183
|
+
} else if (opt.action === 'here_no_sub') {
|
|
184
|
+
finish(cwd);
|
|
165
185
|
} else if (opt.action === 'up') {
|
|
166
186
|
cwd = path.dirname(cwd);
|
|
167
187
|
selected = 0;
|
|
@@ -189,7 +209,7 @@ function pickFolder(projectSlug) {
|
|
|
189
209
|
// Backspace
|
|
190
210
|
inputBuffer = inputBuffer.slice(0, -1);
|
|
191
211
|
process.stdout.write(`\r${CLEAR_LINE}`);
|
|
192
|
-
process.stdout.write(`${DIM}[
|
|
212
|
+
process.stdout.write(`${DIM}[7/7]${RESET} ${BOLD}Folder name:${RESET} ${inputBuffer}`);
|
|
193
213
|
} else if (key === '\x1b' || key === '\x03') {
|
|
194
214
|
// Escape or ctrl-c → back to browse
|
|
195
215
|
mode = 'browse';
|
|
@@ -210,7 +230,7 @@ function pickFolder(projectSlug) {
|
|
|
210
230
|
cleanup();
|
|
211
231
|
process.stdout.write(`\x1b[H\x1b[2J`);
|
|
212
232
|
console.log('');
|
|
213
|
-
console.log(`${DIM}[
|
|
233
|
+
console.log(`${DIM}[7/7]${RESET} ${BOLD}Location:${RESET} ${shortPath(dir)}`);
|
|
214
234
|
console.log('');
|
|
215
235
|
resolve(dir);
|
|
216
236
|
}
|
|
@@ -297,37 +317,57 @@ async function main() {
|
|
|
297
317
|
|
|
298
318
|
initReadline();
|
|
299
319
|
|
|
300
|
-
// Question 1: Project name
|
|
301
|
-
const projectName = await
|
|
302
|
-
|
|
320
|
+
// Question 1: Project description (one sentence — used for context, not the folder name)
|
|
321
|
+
const projectName = await askRequired(
|
|
322
|
+
`${DIM}[1/7]${RESET} ${BOLD}What's your project in one sentence?${RESET}\n> `,
|
|
323
|
+
'A one-sentence description is required — even rough is fine. Try again:'
|
|
324
|
+
);
|
|
325
|
+
console.log('');
|
|
326
|
+
|
|
327
|
+
// Question 2: Short name (the actual folder slug). Default = cwd basename when sensible.
|
|
328
|
+
const cwdBasename = path.basename(process.cwd());
|
|
329
|
+
const cwdSlug = slugify(cwdBasename);
|
|
330
|
+
const cwdIsEmpty = (() => {
|
|
331
|
+
try { return fs.readdirSync(process.cwd()).length === 0; } catch { return false; }
|
|
332
|
+
})();
|
|
333
|
+
const sentenceSlug = slugify(projectName);
|
|
334
|
+
const truncatedSentenceSlug = sentenceSlug.split('-').slice(0, 3).join('-');
|
|
335
|
+
const genericNames = new Set(['projects', 'code', 'src', 'work', 'dev', 'repos', 'workspace', 'documents', 'desktop']);
|
|
336
|
+
const defaultShortName = (cwdIsEmpty && cwdSlug && !genericNames.has(cwdSlug))
|
|
337
|
+
? cwdSlug
|
|
338
|
+
: (truncatedSentenceSlug || 'my-battle-plan');
|
|
339
|
+
const shortNameRaw = await ask(
|
|
340
|
+
`${DIM}[2/7]${RESET} ${BOLD}Short name for the folder?${RESET} ${DIM}(default: ${defaultShortName})${RESET}\n> `
|
|
341
|
+
);
|
|
342
|
+
const shortName = slugify(shortNameRaw) || defaultShortName;
|
|
303
343
|
console.log('');
|
|
304
344
|
|
|
305
|
-
// Question
|
|
345
|
+
// Question 3: Time horizon
|
|
306
346
|
const horizon = await ask(
|
|
307
|
-
`${DIM}[
|
|
347
|
+
`${DIM}[3/7]${RESET} ${BOLD}What's your time horizon?${RESET} ${DIM}(e.g., "3 weeks to demo day", "6 months to launch", "ongoing")${RESET}\n> `
|
|
308
348
|
);
|
|
309
349
|
console.log('');
|
|
310
350
|
|
|
311
|
-
// Question
|
|
312
|
-
const metricsRaw = await
|
|
313
|
-
`${DIM}[
|
|
351
|
+
// Question 4: Metrics
|
|
352
|
+
const metricsRaw = await askRequired(
|
|
353
|
+
`${DIM}[4/7]${RESET} ${BOLD}What are the 3-5 key metrics you want to track?${RESET} ${DIM}(comma-separated, e.g., "outreach sent, calls booked, LOIs signed")${RESET}\n> `,
|
|
354
|
+
'At least one metric is required — pick anything quantifiable you care about. You can rename or add more later in metrics.yml. Try again:'
|
|
314
355
|
);
|
|
315
|
-
if (!metricsRaw) { console.log('At least one metric is required.'); process.exit(1); }
|
|
316
356
|
const metrics = metricsRaw.split(',').map((m) => m.trim()).filter(Boolean);
|
|
317
357
|
console.log('');
|
|
318
358
|
|
|
319
|
-
// Question
|
|
359
|
+
// Question 5: Domains
|
|
320
360
|
const suggested = suggestDomains(projectName);
|
|
321
|
-
const domainsRaw = await
|
|
322
|
-
`${DIM}[
|
|
361
|
+
const domainsRaw = await askRequired(
|
|
362
|
+
`${DIM}[5/7]${RESET} ${BOLD}What domains does your work cover?${RESET} ${DIM}(comma-separated)\nSuggested based on your project: ${suggested}${RESET}\n> `,
|
|
363
|
+
`At least one domain is required — try the suggestions (${suggested}) or any topic area. Try again:`
|
|
323
364
|
);
|
|
324
|
-
if (!domainsRaw) { console.log('At least one domain is required.'); process.exit(1); }
|
|
325
365
|
const domains = domainsRaw.split(',').map((d) => d.trim().toLowerCase()).filter(Boolean);
|
|
326
366
|
console.log('');
|
|
327
367
|
|
|
328
|
-
// Question
|
|
368
|
+
// Question 6: People
|
|
329
369
|
const peopleRaw = await ask(
|
|
330
|
-
`${DIM}[
|
|
370
|
+
`${DIM}[6/7]${RESET} ${BOLD}Who are the key people you'll be working with?${RESET} ${DIM}(format: "Name:Role, Name:Role" — or press enter to skip)${RESET}\n> `
|
|
331
371
|
);
|
|
332
372
|
const people = peopleRaw
|
|
333
373
|
? peopleRaw.split(',').map((p) => {
|
|
@@ -337,9 +377,8 @@ async function main() {
|
|
|
337
377
|
: [];
|
|
338
378
|
console.log('');
|
|
339
379
|
|
|
340
|
-
// Question
|
|
341
|
-
const
|
|
342
|
-
const targetDir = await pickFolder(projectSlug);
|
|
380
|
+
// Question 7: Interactive folder picker
|
|
381
|
+
const targetDir = await pickFolder(shortName);
|
|
343
382
|
|
|
344
383
|
// Re-init readline for any future questions
|
|
345
384
|
initReadline();
|
package/package.json
CHANGED