create-battle-plan 1.0.1 → 1.1.1
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 +260 -90
- package/package.json +1 -1
- package/template/.claude/commands/good-morning.md +52 -17
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
|
-
|
|
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
|
|
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
|
|
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: ${
|
|
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:
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
//
|
|
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,47 +586,27 @@ ${peopleSections}
|
|
|
396
586
|
}
|
|
397
587
|
console.log('');
|
|
398
588
|
|
|
399
|
-
cascadeDiagram(domains
|
|
589
|
+
cascadeDiagram(domains);
|
|
400
590
|
|
|
401
591
|
console.log(`${DIM} ─────────────────────────────${RESET}`);
|
|
402
592
|
console.log('');
|
|
403
|
-
console.log(`${CYAN}${BOLD}
|
|
593
|
+
console.log(`${CYAN}${BOLD} To start your first session,${RESET}`);
|
|
594
|
+
console.log(`${CYAN}${BOLD} copy and paste this into your terminal:${RESET}`);
|
|
404
595
|
console.log('');
|
|
405
|
-
|
|
406
|
-
console.log(
|
|
596
|
+
const relPath = path.relative(process.cwd(), targetDir) || '.';
|
|
597
|
+
console.log(`${DIM} ┌─────────────────────────────────────────┐${RESET}`);
|
|
598
|
+
console.log(`${DIM} │${RESET} ${BOLD}cd ${relPath} && claude${RESET}${DIM}${' '.repeat(Math.max(0, 37 - relPath.length - 12))}│${RESET}`);
|
|
599
|
+
console.log(`${DIM} └─────────────────────────────────────────┘${RESET}`);
|
|
407
600
|
console.log('');
|
|
408
|
-
console.log(`
|
|
409
|
-
console.log(` your first session
|
|
601
|
+
console.log(` Once Claude is running, type ${GREEN}${BOLD}/good-morning${RESET}`);
|
|
602
|
+
console.log(` to start your first session. Claude will`);
|
|
603
|
+
console.log(` introduce itself, explain how everything`);
|
|
604
|
+
console.log(` works, and help you set your first targets.`);
|
|
410
605
|
console.log('');
|
|
411
|
-
console.log(`${DIM} Claude already knows your project, metrics,`);
|
|
412
|
-
console.log(` and team. Just dump context into the chat —`);
|
|
413
|
-
console.log(` call transcripts, research, meeting notes,`);
|
|
414
|
-
console.log(` replies — it'll cascade into the right docs.${RESET}`);
|
|
415
|
-
console.log('');
|
|
416
|
-
}
|
|
417
|
-
|
|
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
606
|
}
|
|
438
607
|
|
|
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
|
@@ -4,32 +4,65 @@ description: Morning standup — status briefing, metrics snapshot, and today's
|
|
|
4
4
|
|
|
5
5
|
# Morning Standup
|
|
6
6
|
|
|
7
|
-
Run these steps in order.
|
|
7
|
+
Run these steps in order.
|
|
8
8
|
|
|
9
9
|
## Step 0: First-Run Welcome (one time only)
|
|
10
10
|
|
|
11
|
-
Check if `.battle-plan-onboarding.json` exists. If it does, this is the user's FIRST session after running `npx create-battle-plan`.
|
|
11
|
+
Check if `.battle-plan-onboarding.json` exists in the repo root. If it does, this is the user's **FIRST session** after running `npx create-battle-plan`. This is a special moment — the user just installed this and wants to understand what they've got. Make them feel like their project is in good hands.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
2. **Welcome them by reflecting what they told the installer** — project name, horizon, metrics, domains, people. Show them you already have full context. Make them feel like they're picking up mid-conversation, not starting from scratch.
|
|
15
|
-
3. **Explain what Battle Plan is and how it works**, briefly:
|
|
16
|
-
- "Battle Plan keeps your project context in markdown files that stay in sync. When you tell me something new — a call, a reply, a metric change — I update a chain of docs in a fixed order so nothing gets dropped."
|
|
17
|
-
- Mention the cascade: metrics.yml → battle plan → source docs → verification
|
|
18
|
-
- Mention: "Just dump context into the chat. Call transcripts, research, meeting notes, replies — I'll cascade it into the right docs."
|
|
19
|
-
4. **Explain the commands:**
|
|
20
|
-
- `/good-morning` — start each session with a status briefing (what you're running now)
|
|
21
|
-
- `/wrap-up` — end each session cleanly: status review, cascade, metrics report, commit
|
|
22
|
-
- `/distill` — compress docs when they get too long for me to read efficiently
|
|
23
|
-
5. **Explain metrics:** "Your metrics live in `metrics.yml`. Every number in every doc traces back to that file. Scripts verify they stay in sync. You never have to worry about stale numbers."
|
|
24
|
-
6. **Explain what to do next:** "Set targets for your metrics in the battle plan, then start working. Tell me about any calls, research, or updates and I'll keep everything current."
|
|
25
|
-
7. Rename `.battle-plan-onboarding.json` to `.battle-plan-onboarding-done.json` so this welcome doesn't repeat.
|
|
13
|
+
**Read `.battle-plan-onboarding.json`** to get their project context. Then write a warm, personal welcome that covers ALL of the following. This should feel like a knowledgeable co-pilot introducing itself, not a product manual.
|
|
26
14
|
|
|
27
|
-
|
|
15
|
+
### Part 1: Personal welcome (reflect their project back to them)
|
|
16
|
+
|
|
17
|
+
Greet them warmly. Show them you already know their project by name, their time horizon, their metrics, their domains, and their people. Reference these specifics naturally — don't just list them. The user should feel like they're continuing a conversation, not configuring a tool.
|
|
18
|
+
|
|
19
|
+
Example tone: "Welcome to your battle plan for [project]. You've got [horizon] and you're tracking [metrics] — I've got all of that loaded and ready to go."
|
|
20
|
+
|
|
21
|
+
### Part 2: What this is (the big picture)
|
|
22
|
+
|
|
23
|
+
Explain what they now have, in plain language:
|
|
24
|
+
|
|
25
|
+
- **This is your project's memory.** Every conversation, decision, metric, and insight lives here in markdown files. When you close this chat and come back tomorrow — or next week — I read these files and pick up exactly where we left off. Nothing gets lost between sessions.
|
|
26
|
+
- **You don't organize anything.** When you tell me something new — a call you had, research you found, a number that changed, a reply you got — I update the right files in the right order automatically. That's called the cascade.
|
|
27
|
+
- **The cascade works like this:** new info flows into `metrics.yml` first (if a number changed), then into your battle plan (the operating doc that tracks where you are), then into the source docs for each domain. At the end, a verification script checks that everything is consistent.
|
|
28
|
+
- **Think of it as dumping context.** You can paste a full call transcript, a messy list of notes, a forwarded email, a research summary — anything. You don't need to format it or tell me where it goes. Just dump it in and I'll cascade it to the right places.
|
|
29
|
+
|
|
30
|
+
### Part 3: How to use it day to day
|
|
31
|
+
|
|
32
|
+
Explain the rhythm:
|
|
33
|
+
|
|
34
|
+
- **Start each session** with `/good-morning` (what you just ran). I'll brief you on where you stand — metrics, priorities, what's stuck, what's next. Think of it as a daily standup with your AI co-pilot.
|
|
35
|
+
- **During the day**, just talk to me. Tell me what happened, paste in notes, ask me to update things. Every piece of new info triggers the cascade automatically.
|
|
36
|
+
- **End each session** with `/wrap-up`. I'll review the day, show you what changed, sync everything, and offer to commit. This is how you close out clean without forgetting to log something.
|
|
37
|
+
- **When docs get long**, run `/distill`. Over time, your daily logs and conversation journals will grow. `/distill` compresses older content into a thorough summary and archives the raw text — nothing is ever deleted, but I can read the docs faster.
|
|
38
|
+
|
|
39
|
+
### Part 4: How metrics work
|
|
40
|
+
|
|
41
|
+
- Your key metrics live in `metrics.yml` — it's the single numeric source of truth for the whole project.
|
|
42
|
+
- Every number that appears in any doc traces back to that file via source annotations. When a metric changes, the cascade propagates the new value everywhere it's referenced.
|
|
43
|
+
- Shell scripts verify the numbers stay in sync. You'll never have a stale "42" in one doc when the real number is "47" in another.
|
|
44
|
+
- Right now all your metrics are at zero. One of the first things we'll do is set targets.
|
|
45
|
+
|
|
46
|
+
### Part 5: What to do right now
|
|
47
|
+
|
|
48
|
+
Transition into action:
|
|
49
|
+
|
|
50
|
+
- "Let's start by getting some real data into the system. Here's what I'd suggest..."
|
|
51
|
+
- Prompt them to set targets for their metrics
|
|
52
|
+
- Ask if there's any existing context they already have — calls they've had, research they've done, decisions they've already made. Anything they can tell you now will make the battle plan immediately useful.
|
|
53
|
+
|
|
54
|
+
### After the welcome
|
|
55
|
+
|
|
56
|
+
Rename `.battle-plan-onboarding.json` to `.battle-plan-onboarding-done.json` so this welcome doesn't repeat.
|
|
57
|
+
|
|
58
|
+
Then show a **compact metrics table** (all zeros, no targets yet) and transition into the onboarding questions naturally. Don't run the full standup format (Steps 1-4 below) on the first run — there's nothing to report yet. Instead, go straight to helping them populate the battle plan.
|
|
28
59
|
|
|
29
60
|
---
|
|
30
61
|
|
|
31
62
|
## Step 1: Gather State (parallel reads)
|
|
32
63
|
|
|
64
|
+
*Skip this on first run (Step 0 handles it). On all subsequent runs, start here.*
|
|
65
|
+
|
|
33
66
|
Read all of these in parallel:
|
|
34
67
|
- `metrics.yml` — current numbers
|
|
35
68
|
- `docs/battle-plan.md` — TL;DR + latest day log (find today or the most recent day entry)
|
|
@@ -71,4 +104,6 @@ After the user answers:
|
|
|
71
104
|
|
|
72
105
|
## Tone
|
|
73
106
|
|
|
74
|
-
|
|
107
|
+
On first run: warm, confident, thorough. The user should feel like they just hired a great project manager who already read the brief.
|
|
108
|
+
|
|
109
|
+
On subsequent runs: direct, no fluff. Think military briefing, not newsletter.
|