create-battle-plan 1.0.1 → 1.1.0

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 +251 -81
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -3,31 +3,16 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const readline = require('readline');
6
+ const os = require('os');
6
7
 
7
- const rl = readline.createInterface({
8
- input: process.stdin,
9
- output: process.stdout,
10
- });
11
-
12
- function ask(question) {
13
- return new Promise((resolve) => {
14
- rl.question(question, (answer) => resolve(answer.trim()));
15
- });
16
- }
8
+ // ── Helpers ──────────────────────────────────────────────
17
9
 
18
10
  function slugify(text) {
19
- return text
20
- .toLowerCase()
21
- .replace(/[^a-z0-9]+/g, '-')
22
- .replace(/^-|-$/g, '');
11
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
23
12
  }
24
13
 
25
14
  function metricKey(text) {
26
- return text
27
- .trim()
28
- .toLowerCase()
29
- .replace(/[^a-z0-9]+/g, '_')
30
- .replace(/^_|_$/g, '');
15
+ return text.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
31
16
  }
32
17
 
33
18
  function capitalize(text) {
@@ -47,13 +32,210 @@ function copyDir(src, dest) {
47
32
  }
48
33
  }
49
34
 
35
+ function shortPath(p) {
36
+ const home = os.homedir();
37
+ if (p === home) return '~';
38
+ if (p.startsWith(home + '/')) return '~/' + p.slice(home.length + 1);
39
+ return p;
40
+ }
41
+
42
+ // ── Colors ───────────────────────────────────────────────
43
+
50
44
  const BOLD = '\x1b[1m';
51
45
  const DIM = '\x1b[2m';
52
46
  const GREEN = '\x1b[32m';
53
47
  const CYAN = '\x1b[36m';
54
48
  const YELLOW = '\x1b[33m';
55
49
  const WHITE = '\x1b[37m';
50
+ const INVERSE = '\x1b[7m';
56
51
  const RESET = '\x1b[0m';
52
+ const CLEAR_LINE = '\x1b[2K';
53
+ const HIDE_CURSOR = '\x1b[?25l';
54
+ const SHOW_CURSOR = '\x1b[?25h';
55
+
56
+ // ── Simple question (readline) ───────────────────────────
57
+
58
+ let rl;
59
+
60
+ function initReadline() {
61
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout });
62
+ }
63
+
64
+ function closeReadline() {
65
+ if (rl) { rl.close(); rl = null; }
66
+ }
67
+
68
+ function ask(question) {
69
+ return new Promise((resolve) => {
70
+ rl.question(question, (answer) => resolve(answer.trim()));
71
+ });
72
+ }
73
+
74
+ // ── Interactive folder picker (raw mode) ─────────────────
75
+
76
+ function getDirs(dir) {
77
+ try {
78
+ return fs.readdirSync(dir, { withFileTypes: true })
79
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.'))
80
+ .map((e) => e.name)
81
+ .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
82
+ } catch {
83
+ return [];
84
+ }
85
+ }
86
+
87
+ function pickFolder(projectSlug) {
88
+ return new Promise((resolve) => {
89
+ // Pause readline so we can use raw mode
90
+ closeReadline();
91
+
92
+ let cwd = process.cwd();
93
+ let selected = 0;
94
+ let mode = 'browse'; // 'browse' or 'input'
95
+ let inputBuffer = '';
96
+
97
+ function getOptions() {
98
+ const dirs = getDirs(cwd);
99
+ const options = [];
100
+ options.push({ label: `${GREEN}+ Create new folder here${RESET}`, action: 'create' });
101
+ options.push({ label: `${CYAN}» Install here as ${BOLD}${projectSlug}/${RESET}`, action: 'here' });
102
+ if (path.dirname(cwd) !== cwd) {
103
+ options.push({ label: `${DIM}../${RESET} ${DIM}(up)${RESET}`, action: 'up' });
104
+ }
105
+ for (const d of dirs) {
106
+ options.push({ label: ` ${d}/`, action: 'enter', dir: d });
107
+ }
108
+ return options;
109
+ }
110
+
111
+ function render() {
112
+ const options = getOptions();
113
+ const display = shortPath(cwd);
114
+
115
+ // Move cursor up to clear previous render
116
+ let output = '';
117
+
118
+ if (mode === 'input') {
119
+ output += `${CLEAR_LINE}\r${DIM}[6/6]${RESET} ${BOLD}Folder name:${RESET} ${inputBuffer}\x1b[K`;
120
+ process.stdout.write(output);
121
+ return;
122
+ }
123
+
124
+ output += `\x1b[H\x1b[2J`; // clear screen
125
+ output += `\n`;
126
+ output += `${DIM}[6/6]${RESET} ${BOLD}Where do you want to install it?${RESET}\n`;
127
+ output += `${DIM} ${display}${RESET}\n`;
128
+ output += `\n`;
129
+ output += `${DIM} ↑↓ navigate · enter select · q cancel${RESET}\n`;
130
+ output += `\n`;
131
+
132
+ for (let i = 0; i < options.length; i++) {
133
+ if (i === selected) {
134
+ output += ` ${INVERSE} › ${options[i].label} ${RESET}\n`;
135
+ } else {
136
+ output += ` ${options[i].label}\n`;
137
+ }
138
+ }
139
+
140
+ process.stdout.write(output);
141
+ }
142
+
143
+ function handleBrowseKey(key) {
144
+ const options = getOptions();
145
+
146
+ if (key === '\x1b[A' || key === 'k') {
147
+ // Up
148
+ selected = Math.max(0, selected - 1);
149
+ render();
150
+ } else if (key === '\x1b[B' || key === 'j') {
151
+ // Down
152
+ selected = Math.min(options.length - 1, selected + 1);
153
+ render();
154
+ } else if (key === '\r' || key === '\n') {
155
+ // Enter
156
+ const opt = options[selected];
157
+ if (opt.action === 'create') {
158
+ mode = 'input';
159
+ inputBuffer = projectSlug;
160
+ process.stdout.write(`\x1b[H\x1b[2J`);
161
+ process.stdout.write(`\n`);
162
+ process.stdout.write(`${DIM}[6/6]${RESET} ${BOLD}Folder name:${RESET} ${inputBuffer}`);
163
+ } else if (opt.action === 'here') {
164
+ finish(path.join(cwd, projectSlug));
165
+ } else if (opt.action === 'up') {
166
+ cwd = path.dirname(cwd);
167
+ selected = 0;
168
+ render();
169
+ } else if (opt.action === 'enter') {
170
+ cwd = path.join(cwd, opt.dir);
171
+ selected = 0;
172
+ render();
173
+ }
174
+ } else if (key === 'q' || key === '\x03') {
175
+ // q or ctrl-c
176
+ cleanup();
177
+ process.stdout.write(SHOW_CURSOR);
178
+ process.exit(0);
179
+ }
180
+ }
181
+
182
+ function handleInputKey(key) {
183
+ if (key === '\r' || key === '\n') {
184
+ // Confirm
185
+ if (inputBuffer.length > 0) {
186
+ finish(path.join(cwd, inputBuffer));
187
+ }
188
+ } else if (key === '\x7f' || key === '\b') {
189
+ // Backspace
190
+ inputBuffer = inputBuffer.slice(0, -1);
191
+ process.stdout.write(`\r${CLEAR_LINE}`);
192
+ process.stdout.write(`${DIM}[6/6]${RESET} ${BOLD}Folder name:${RESET} ${inputBuffer}`);
193
+ } else if (key === '\x1b' || key === '\x03') {
194
+ // Escape or ctrl-c → back to browse
195
+ mode = 'browse';
196
+ render();
197
+ } else if (key.length === 1 && key.charCodeAt(0) >= 32) {
198
+ inputBuffer += key;
199
+ process.stdout.write(key);
200
+ }
201
+ }
202
+
203
+ function cleanup() {
204
+ process.stdin.setRawMode(false);
205
+ process.stdin.removeListener('data', onData);
206
+ process.stdout.write(SHOW_CURSOR);
207
+ }
208
+
209
+ function finish(dir) {
210
+ cleanup();
211
+ process.stdout.write(`\x1b[H\x1b[2J`);
212
+ console.log('');
213
+ console.log(`${DIM}[6/6]${RESET} ${BOLD}Location:${RESET} ${shortPath(dir)}`);
214
+ console.log('');
215
+ resolve(dir);
216
+ }
217
+
218
+ function onData(data) {
219
+ const key = data.toString();
220
+
221
+ // Handle multi-byte escape sequences
222
+ if (mode === 'browse') {
223
+ handleBrowseKey(key);
224
+ } else {
225
+ handleInputKey(key);
226
+ }
227
+ }
228
+
229
+ process.stdout.write(HIDE_CURSOR);
230
+ process.stdin.setRawMode(true);
231
+ process.stdin.resume();
232
+ process.stdin.on('data', onData);
233
+
234
+ render();
235
+ });
236
+ }
237
+
238
+ // ── Banner & Diagram ─────────────────────────────────────
57
239
 
58
240
  function banner() {
59
241
  console.log('');
@@ -72,10 +254,9 @@ function banner() {
72
254
  console.log('');
73
255
  }
74
256
 
75
- function cascadeDiagram(domains, metrics) {
257
+ function cascadeDiagram(domains) {
76
258
  const domainStr = domains.slice(0, 3).join(' ');
77
259
  const dots = domains.length > 3 ? ' ...' : '';
78
- const metricStr = metrics.slice(0, 3).map((m) => metricKey(m)).join(', ');
79
260
 
80
261
  console.log(`${DIM} Your cascade:${RESET}`);
81
262
  console.log('');
@@ -91,15 +272,34 @@ function cascadeDiagram(domains, metrics) {
91
272
  console.log('');
92
273
  }
93
274
 
275
+ // ── Domain suggestions ───────────────────────────────────
276
+
277
+ function suggestDomains(desc) {
278
+ const d = desc.toLowerCase();
279
+ const s = [];
280
+ if (/market|customer|user|audience|segment|icp/.test(d)) s.push('market');
281
+ if (/valid|test|hypothes|experiment|interview/.test(d)) s.push('validation');
282
+ if (/strat|position|compete|pricing|business/.test(d)) s.push('strategy');
283
+ if (/research|learn|study|paper|domain/.test(d)) s.push('research');
284
+ if (/content|write|blog|newsletter|social/.test(d)) s.push('content');
285
+ if (/logist|ops|supply|shipping|fulfil/.test(d)) s.push('logistics');
286
+ if (/product|feature|build|ship|release/.test(d)) s.push('product');
287
+ if (/sales|outreach|pipeline|deal|close/.test(d)) s.push('sales');
288
+ if (/fund|invest|pitch|raise|capital/.test(d)) s.push('fundraising');
289
+ if (s.length === 0) s.push('market', 'validation', 'strategy', 'research');
290
+ return s.join(', ');
291
+ }
292
+
293
+ // ── Main ─────────────────────────────────────────────────
294
+
94
295
  async function main() {
95
296
  banner();
96
297
 
298
+ initReadline();
299
+
97
300
  // Question 1: Project name
98
301
  const projectName = await ask(`${DIM}[1/6]${RESET} ${BOLD}What's your project in one sentence?${RESET}\n> `);
99
- if (!projectName) {
100
- console.log('Project name is required.');
101
- process.exit(1);
102
- }
302
+ if (!projectName) { console.log('Project name is required.'); process.exit(1); }
103
303
  console.log('');
104
304
 
105
305
  // Question 2: Time horizon
@@ -112,22 +312,16 @@ async function main() {
112
312
  const metricsRaw = await ask(
113
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> `
114
314
  );
115
- if (!metricsRaw) {
116
- console.log('At least one metric is required.');
117
- process.exit(1);
118
- }
315
+ if (!metricsRaw) { console.log('At least one metric is required.'); process.exit(1); }
119
316
  const metrics = metricsRaw.split(',').map((m) => m.trim()).filter(Boolean);
120
317
  console.log('');
121
318
 
122
319
  // Question 4: Domains
123
- const suggestedDomains = suggestDomains(projectName);
320
+ const suggested = suggestDomains(projectName);
124
321
  const domainsRaw = await ask(
125
- `${DIM}[4/6]${RESET} ${BOLD}What domains does your work cover?${RESET} ${DIM}(comma-separated)\nSuggested based on your project: ${suggestedDomains}${RESET}\n> `
322
+ `${DIM}[4/6]${RESET} ${BOLD}What domains does your work cover?${RESET} ${DIM}(comma-separated)\nSuggested based on your project: ${suggested}${RESET}\n> `
126
323
  );
127
- if (!domainsRaw) {
128
- console.log('At least one domain is required.');
129
- process.exit(1);
130
- }
324
+ if (!domainsRaw) { console.log('At least one domain is required.'); process.exit(1); }
131
325
  const domains = domainsRaw.split(',').map((d) => d.trim().toLowerCase()).filter(Boolean);
132
326
  console.log('');
133
327
 
@@ -143,24 +337,23 @@ async function main() {
143
337
  : [];
144
338
  console.log('');
145
339
 
146
- // Question 6: Where to install
147
- const defaultDir = `./${slugify(projectName) || 'my-battle-plan'}`;
148
- const dirAnswer = await ask(
149
- `${DIM}[6/6]${RESET} ${BOLD}Where do you want to install it?${RESET} ${DIM}(default: ${defaultDir})${RESET}\n> `
150
- );
151
- const targetDir = path.resolve(dirAnswer || defaultDir);
152
- console.log('');
340
+ // Question 6: Interactive folder picker
341
+ const projectSlug = slugify(projectName) || 'my-battle-plan';
342
+ const targetDir = await pickFolder(projectSlug);
343
+
344
+ // Re-init readline for any future questions
345
+ initReadline();
346
+ closeReadline();
153
347
 
154
- rl.close();
348
+ // ── Scaffold ─────────────────────────────────────────
155
349
 
156
- // --- Scaffold ---
157
350
  console.log(`${DIM} ─────────────────────────────${RESET}`);
158
351
  console.log('');
159
352
  console.log(`${CYAN} Scaffolding...${RESET}`);
160
353
  console.log('');
161
354
 
162
355
  if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
163
- console.log(`${YELLOW}Warning: ${targetDir} already exists and is not empty.${RESET}`);
356
+ console.log(`${YELLOW} Warning: ${shortPath(targetDir)} already exists and is not empty.${RESET}`);
164
357
  process.exit(1);
165
358
  }
166
359
 
@@ -346,16 +539,8 @@ ${peopleSections}
346
539
  fs.writeFileSync(
347
540
  path.join(targetDir, '.battle-plan-onboarding.json'),
348
541
  JSON.stringify(
349
- {
350
- project_name: projectName,
351
- horizon,
352
- metrics,
353
- domains,
354
- people,
355
- installed_at: today,
356
- },
357
- null,
358
- 2
542
+ { project_name: projectName, horizon, metrics, domains, people, installed_at: today },
543
+ null, 2
359
544
  ) + '\n'
360
545
  );
361
546
 
@@ -373,21 +558,26 @@ ${peopleSections}
373
558
  execSync('git commit -m "Initial battle plan scaffold"', {
374
559
  cwd: targetDir,
375
560
  stdio: 'pipe',
376
- env: { ...process.env, GIT_AUTHOR_NAME: 'Battle Plan', GIT_AUTHOR_EMAIL: 'noreply@battleplan.dev', GIT_COMMITTER_NAME: 'Battle Plan', GIT_COMMITTER_EMAIL: 'noreply@battleplan.dev' },
561
+ env: {
562
+ ...process.env,
563
+ GIT_AUTHOR_NAME: 'Battle Plan', GIT_AUTHOR_EMAIL: 'noreply@battleplan.dev',
564
+ GIT_COMMITTER_NAME: 'Battle Plan', GIT_COMMITTER_EMAIL: 'noreply@battleplan.dev',
565
+ },
377
566
  });
378
567
  console.log(`${DIM} + git repo initialized${RESET}`);
379
568
  } catch {
380
569
  // git not available or failed — not critical
381
570
  }
382
571
 
383
- // --- Done ---
572
+ // ── Done ───────────────────────────────────────────
573
+
384
574
  console.log('');
385
575
  console.log(`${DIM} ─────────────────────────────${RESET}`);
386
576
  console.log('');
387
577
  console.log(`${GREEN}${BOLD} Ready.${RESET}`);
388
578
  console.log('');
389
579
  console.log(`${BOLD} Project:${RESET} ${projectName}`);
390
- console.log(`${BOLD} Location:${RESET} ${targetDir}`);
580
+ console.log(`${BOLD} Location:${RESET} ${shortPath(targetDir)}`);
391
581
  console.log(`${BOLD} Horizon:${RESET} ${horizon || 'not set'}`);
392
582
  console.log(`${BOLD} Metrics:${RESET} ${metrics.join(', ')}`);
393
583
  console.log(`${BOLD} Domains:${RESET} ${domains.join(', ')}`);
@@ -396,13 +586,13 @@ ${peopleSections}
396
586
  }
397
587
  console.log('');
398
588
 
399
- cascadeDiagram(domains, metrics);
589
+ cascadeDiagram(domains);
400
590
 
401
591
  console.log(`${DIM} ─────────────────────────────${RESET}`);
402
592
  console.log('');
403
593
  console.log(`${CYAN}${BOLD} Next steps:${RESET}`);
404
594
  console.log('');
405
- console.log(` ${BOLD}cd ${path.relative(process.cwd(), targetDir)}${RESET}`);
595
+ console.log(` ${BOLD}cd ${path.relative(process.cwd(), targetDir) || '.'}${RESET}`);
406
596
  console.log(` ${BOLD}claude${RESET}`);
407
597
  console.log('');
408
598
  console.log(` Then type ${GREEN}${BOLD}/good-morning${RESET} to start`);
@@ -415,28 +605,8 @@ ${peopleSections}
415
605
  console.log('');
416
606
  }
417
607
 
418
- function suggestDomains(projectDescription) {
419
- const desc = projectDescription.toLowerCase();
420
- const suggestions = [];
421
-
422
- if (/market|customer|user|audience|segment|icp/.test(desc)) suggestions.push('market');
423
- if (/valid|test|hypothes|experiment|interview/.test(desc)) suggestions.push('validation');
424
- if (/strat|position|compete|pricing|business/.test(desc)) suggestions.push('strategy');
425
- if (/research|learn|study|paper|domain/.test(desc)) suggestions.push('research');
426
- if (/content|write|blog|newsletter|social/.test(desc)) suggestions.push('content');
427
- if (/logist|ops|supply|shipping|fulfil/.test(desc)) suggestions.push('logistics');
428
- if (/product|feature|build|ship|release/.test(desc)) suggestions.push('product');
429
- if (/sales|outreach|pipeline|deal|close/.test(desc)) suggestions.push('sales');
430
- if (/fund|invest|pitch|raise|capital/.test(desc)) suggestions.push('fundraising');
431
-
432
- if (suggestions.length === 0) {
433
- suggestions.push('market', 'validation', 'strategy', 'research');
434
- }
435
-
436
- return suggestions.join(', ');
437
- }
438
-
439
608
  main().catch((err) => {
609
+ process.stdout.write(SHOW_CURSOR);
440
610
  console.error(err);
441
611
  process.exit(1);
442
612
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-battle-plan",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
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"