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.
Files changed (2) hide show
  1. package/bin/cli.js +66 -27
  2. 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 pickFolder(projectSlug) {
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}${projectSlug}/${RESET}`, action: 'here' });
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}[6/6]${RESET} ${BOLD}Folder name:${RESET} ${inputBuffer}\x1b[K`;
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}[6/6]${RESET} ${BOLD}Where do you want to install it?${RESET}\n`;
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 = projectSlug;
177
+ inputBuffer = shortName;
160
178
  process.stdout.write(`\x1b[H\x1b[2J`);
161
179
  process.stdout.write(`\n`);
162
- process.stdout.write(`${DIM}[6/6]${RESET} ${BOLD}Folder name:${RESET} ${inputBuffer}`);
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, projectSlug));
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}[6/6]${RESET} ${BOLD}Folder name:${RESET} ${inputBuffer}`);
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}[6/6]${RESET} ${BOLD}Location:${RESET} ${shortPath(dir)}`);
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 ask(`${DIM}[1/6]${RESET} ${BOLD}What's your project in one sentence?${RESET}\n> `);
302
- if (!projectName) { console.log('Project name is required.'); process.exit(1); }
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 2: Time horizon
345
+ // Question 3: Time horizon
306
346
  const horizon = await ask(
307
- `${DIM}[2/6]${RESET} ${BOLD}What's your time horizon?${RESET} ${DIM}(e.g., "3 weeks to demo day", "6 months to launch", "ongoing")${RESET}\n> `
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 3: Metrics
312
- const metricsRaw = await ask(
313
- `${DIM}[3/6]${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> `
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 4: Domains
359
+ // Question 5: Domains
320
360
  const suggested = suggestDomains(projectName);
321
- const domainsRaw = await ask(
322
- `${DIM}[4/6]${RESET} ${BOLD}What domains does your work cover?${RESET} ${DIM}(comma-separated)\nSuggested based on your project: ${suggested}${RESET}\n> `
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 5: People
368
+ // Question 6: People
329
369
  const peopleRaw = await ask(
330
- `${DIM}[5/6]${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> `
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 6: Interactive folder picker
341
- const projectSlug = slugify(projectName) || 'my-battle-plan';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-battle-plan",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "description": "Scaffold a Battle Plan project — a markdown-based context system for LLM-powered project management",
5
5
  "bin": {
6
6
  "create-battle-plan": "./bin/cli.js"