create-battle-plan 1.0.0 → 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.
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,30 +337,37 @@ 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
 
167
360
  // Copy template
168
361
  const templateDir = path.join(__dirname, '..', 'template');
169
362
  copyDir(templateDir, targetDir);
363
+
364
+ // npm strips .gitignore from packages, so we ship it as 'gitignore' and rename
365
+ const gitignoreSrc = path.join(targetDir, 'gitignore');
366
+ const gitignoreDest = path.join(targetDir, '.gitignore');
367
+ if (fs.existsSync(gitignoreSrc) && !fs.existsSync(gitignoreDest)) {
368
+ fs.renameSync(gitignoreSrc, gitignoreDest);
369
+ }
370
+
170
371
  console.log(`${DIM} + CLAUDE.md (system prompt)${RESET}`);
171
372
  console.log(`${DIM} + tools/ (verification scripts)${RESET}`);
172
373
  console.log(`${DIM} + .claude/commands/ (slash commands)${RESET}`);
@@ -338,16 +539,8 @@ ${peopleSections}
338
539
  fs.writeFileSync(
339
540
  path.join(targetDir, '.battle-plan-onboarding.json'),
340
541
  JSON.stringify(
341
- {
342
- project_name: projectName,
343
- horizon,
344
- metrics,
345
- domains,
346
- people,
347
- installed_at: today,
348
- },
349
- null,
350
- 2
542
+ { project_name: projectName, horizon, metrics, domains, people, installed_at: today },
543
+ null, 2
351
544
  ) + '\n'
352
545
  );
353
546
 
@@ -365,21 +558,26 @@ ${peopleSections}
365
558
  execSync('git commit -m "Initial battle plan scaffold"', {
366
559
  cwd: targetDir,
367
560
  stdio: 'pipe',
368
- 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
+ },
369
566
  });
370
567
  console.log(`${DIM} + git repo initialized${RESET}`);
371
568
  } catch {
372
569
  // git not available or failed — not critical
373
570
  }
374
571
 
375
- // --- Done ---
572
+ // ── Done ───────────────────────────────────────────
573
+
376
574
  console.log('');
377
575
  console.log(`${DIM} ─────────────────────────────${RESET}`);
378
576
  console.log('');
379
577
  console.log(`${GREEN}${BOLD} Ready.${RESET}`);
380
578
  console.log('');
381
579
  console.log(`${BOLD} Project:${RESET} ${projectName}`);
382
- console.log(`${BOLD} Location:${RESET} ${targetDir}`);
580
+ console.log(`${BOLD} Location:${RESET} ${shortPath(targetDir)}`);
383
581
  console.log(`${BOLD} Horizon:${RESET} ${horizon || 'not set'}`);
384
582
  console.log(`${BOLD} Metrics:${RESET} ${metrics.join(', ')}`);
385
583
  console.log(`${BOLD} Domains:${RESET} ${domains.join(', ')}`);
@@ -388,13 +586,13 @@ ${peopleSections}
388
586
  }
389
587
  console.log('');
390
588
 
391
- cascadeDiagram(domains, metrics);
589
+ cascadeDiagram(domains);
392
590
 
393
591
  console.log(`${DIM} ─────────────────────────────${RESET}`);
394
592
  console.log('');
395
593
  console.log(`${CYAN}${BOLD} Next steps:${RESET}`);
396
594
  console.log('');
397
- console.log(` ${BOLD}cd ${path.relative(process.cwd(), targetDir)}${RESET}`);
595
+ console.log(` ${BOLD}cd ${path.relative(process.cwd(), targetDir) || '.'}${RESET}`);
398
596
  console.log(` ${BOLD}claude${RESET}`);
399
597
  console.log('');
400
598
  console.log(` Then type ${GREEN}${BOLD}/good-morning${RESET} to start`);
@@ -407,28 +605,8 @@ ${peopleSections}
407
605
  console.log('');
408
606
  }
409
607
 
410
- function suggestDomains(projectDescription) {
411
- const desc = projectDescription.toLowerCase();
412
- const suggestions = [];
413
-
414
- if (/market|customer|user|audience|segment|icp/.test(desc)) suggestions.push('market');
415
- if (/valid|test|hypothes|experiment|interview/.test(desc)) suggestions.push('validation');
416
- if (/strat|position|compete|pricing|business/.test(desc)) suggestions.push('strategy');
417
- if (/research|learn|study|paper|domain/.test(desc)) suggestions.push('research');
418
- if (/content|write|blog|newsletter|social/.test(desc)) suggestions.push('content');
419
- if (/logist|ops|supply|shipping|fulfil/.test(desc)) suggestions.push('logistics');
420
- if (/product|feature|build|ship|release/.test(desc)) suggestions.push('product');
421
- if (/sales|outreach|pipeline|deal|close/.test(desc)) suggestions.push('sales');
422
- if (/fund|invest|pitch|raise|capital/.test(desc)) suggestions.push('fundraising');
423
-
424
- if (suggestions.length === 0) {
425
- suggestions.push('market', 'validation', 'strategy', 'research');
426
- }
427
-
428
- return suggestions.join(', ');
429
- }
430
-
431
608
  main().catch((err) => {
609
+ process.stdout.write(SHOW_CURSOR);
432
610
  console.error(err);
433
611
  process.exit(1);
434
612
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-battle-plan",
3
- "version": "1.0.0",
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"
@@ -0,0 +1,4 @@
1
+ .cascaderc
2
+ .battle-plan-onboarding.json
3
+ .battle-plan-onboarding-done.json
4
+ node_modules/