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.
- package/bin/cli.js +251 -81
- 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
|
-
|
|
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,13 +586,13 @@ ${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
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