beth-copilot 1.0.6 → 1.0.11

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
@@ -5,6 +5,7 @@ import { dirname, join, relative } from 'path';
5
5
  import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync } from 'fs';
6
6
  import { createRequire } from 'module';
7
7
  import { execSync, spawn } from 'child_process';
8
+ import { validateBeadsPath, validateBacklogPath, validateBinaryPath } from './lib/pathValidation.js';
8
9
 
9
10
  const require = createRequire(import.meta.url);
10
11
  const __filename = fileURLToPath(import.meta.url);
@@ -18,12 +19,367 @@ const CURRENT_VERSION = packageJson.version;
18
19
  const COLORS = {
19
20
  reset: '\x1b[0m',
20
21
  bright: '\x1b[1m',
22
+ dim: '\x1b[2m',
21
23
  red: '\x1b[31m',
22
24
  green: '\x1b[32m',
23
25
  yellow: '\x1b[33m',
26
+ blue: '\x1b[34m',
27
+ magenta: '\x1b[35m',
24
28
  cyan: '\x1b[36m',
29
+ white: '\x1b[37m',
30
+ bgRed: '\x1b[41m',
31
+ bgYellow: '\x1b[43m',
25
32
  };
26
33
 
34
+ // Beth's dramatic ASCII banner
35
+ const BETH_ASCII = [
36
+ '██████╗ ███████╗████████╗██╗ ██╗',
37
+ '██╔══██╗██╔════╝╚══██╔══╝██║ ██║',
38
+ '██████╔╝█████╗ ██║ ███████║',
39
+ '██╔══██╗██╔══╝ ██║ ██╔══██║',
40
+ '██████╔╝███████╗ ██║ ██║ ██║',
41
+ '╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝',
42
+ ];
43
+
44
+ // Fire characters for animation (from light to intense)
45
+ const FIRE_CHARS = [' ', '.', ':', '*', 's', 'S', '#', '$', '&', '@'];
46
+ const FIRE_CHARS_SIMPLE = [' ', '.', '*', '^', ')', '(', '%', '#'];
47
+
48
+ // Generate a fire line with flickering effect
49
+ function generateFireLine(width, intensity, frame) {
50
+ let line = '';
51
+ for (let i = 0; i < width; i++) {
52
+ // Create wave pattern for fire
53
+ const wave = Math.sin((i + frame) * 0.3) * 0.5 + 0.5;
54
+ const noise = Math.random();
55
+ const heat = (intensity * wave * 0.7 + noise * 0.3);
56
+
57
+ if (heat > 0.85) {
58
+ line += FIRE_CHARS_SIMPLE[7]; // #
59
+ } else if (heat > 0.7) {
60
+ line += FIRE_CHARS_SIMPLE[6]; // %
61
+ } else if (heat > 0.55) {
62
+ line += FIRE_CHARS_SIMPLE[Math.random() > 0.5 ? 4 : 5]; // ) or (
63
+ } else if (heat > 0.4) {
64
+ line += FIRE_CHARS_SIMPLE[3]; // ^
65
+ } else if (heat > 0.25) {
66
+ line += FIRE_CHARS_SIMPLE[2]; // *
67
+ } else if (heat > 0.1) {
68
+ line += FIRE_CHARS_SIMPLE[1]; // .
69
+ } else {
70
+ line += ' ';
71
+ }
72
+ }
73
+ return line;
74
+ }
75
+
76
+ const BETH_TAGLINES = [
77
+ "I don't speak dipshit. I speak in consequences.",
78
+ "They broke my wings and forgot I had claws.",
79
+ "I'm the trailer park AND the tornado.",
80
+ "I don't do excuses. I do results.",
81
+ "You want my opinion? You're getting it either way.",
82
+ "I believe in lovin' with your whole soul and destroyin' anything that wants to kill what you love.",
83
+ "The sting never fades. That's the point.",
84
+ ];
85
+
86
+ function sleep(ms) {
87
+ return new Promise(resolve => setTimeout(resolve, ms));
88
+ }
89
+
90
+ async function animateBethBanner() {
91
+ // Simple, clean fire animation
92
+ const RESET = '\x1b[0m';
93
+ const BRIGHT = '\x1b[1m';
94
+
95
+ // Fire color palette
96
+ const FIRE_COLORS = [
97
+ '\x1b[97m', // white (hottest)
98
+ '\x1b[93m', // bright yellow
99
+ '\x1b[33m', // yellow
100
+ '\x1b[38;5;214m', // gold
101
+ '\x1b[38;5;208m', // orange
102
+ '\x1b[91m', // red
103
+ '\x1b[31m', // dark red
104
+ '\x1b[38;5;52m', // ember
105
+ ];
106
+
107
+ // BETH gradient (red to yellow)
108
+ const BETH_COLORS = [
109
+ '\x1b[38;5;196m', '\x1b[38;5;202m', '\x1b[38;5;208m',
110
+ '\x1b[38;5;214m', '\x1b[38;5;220m', '\x1b[38;5;226m',
111
+ ];
112
+
113
+ // Convert to character arrays
114
+ const bethLines = BETH_ASCII.map(s => [...s]);
115
+ const W = bethLines[0].length;
116
+ const H = bethLines.length;
117
+ const FIRE_H = 4;
118
+ const TOTAL_H = H + FIRE_H;
119
+
120
+ // Helpers
121
+ const pick = arr => arr[Math.floor(Math.random() * arr.length)];
122
+ const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
123
+
124
+ // Hide cursor and make space
125
+ process.stdout.write('\x1b[?25l\n');
126
+ for (let i = 0; i < TOTAL_H; i++) console.log('');
127
+
128
+ for (let frame = 0; frame < 70; frame++) {
129
+ process.stdout.write(`\x1b[${TOTAL_H}A`);
130
+
131
+ // BETH visibility (fades in from frame 15-45)
132
+ const vis = clamp((frame - 15) / 30, 0, 1);
133
+ // Fire dies down at end
134
+ const fireStrength = frame > 55 ? 1 - (frame - 55) / 15 : 1;
135
+
136
+ // Render BETH rows
137
+ for (let r = 0; r < H; r++) {
138
+ let line = '';
139
+ for (let c = 0; c < W; c++) {
140
+ const ch = bethLines[r][c];
141
+ if (ch === ' ') {
142
+ // Gap - show fire through it sometimes
143
+ if (Math.random() < 0.15 * fireStrength) {
144
+ const h = 0.3 + Math.random() * 0.3;
145
+ const ci = clamp(Math.floor((1 - h) * 5), 0, FIRE_COLORS.length - 1);
146
+ line += FIRE_COLORS[ci] + pick(['^', '*', '.']);
147
+ } else {
148
+ line += ' ';
149
+ }
150
+ } else {
151
+ // BETH character
152
+ if (Math.random() < vis) {
153
+ const ci = Math.floor((c / W) * BETH_COLORS.length);
154
+ const col = BETH_COLORS[clamp(ci, 0, BETH_COLORS.length - 1)];
155
+ line += (Math.random() > 0.95 ? '\x1b[97m' : col) + BRIGHT + ch;
156
+ } else {
157
+ // Not visible yet - show fire
158
+ const h = 0.5 + Math.random() * 0.5;
159
+ const ci = clamp(Math.floor((1 - h) * 4), 0, FIRE_COLORS.length - 1);
160
+ line += FIRE_COLORS[ci] + pick(['#', '@', '%', '&']);
161
+ }
162
+ }
163
+ }
164
+ console.log(line + RESET);
165
+ }
166
+
167
+ // Fire rows below
168
+ for (let fr = 0; fr < FIRE_H; fr++) {
169
+ let line = '';
170
+ const baseHeat = (1 - fr / FIRE_H) * fireStrength;
171
+ for (let c = 0; c < W; c++) {
172
+ const wave = Math.sin((c + frame * 2) * 0.15) * 0.15;
173
+ const heat = clamp(baseHeat + wave + (Math.random() - 0.5) * 0.3, 0, 1);
174
+
175
+ let ch;
176
+ if (heat > 0.6) ch = pick(['#', '@', '%']);
177
+ else if (heat > 0.35) ch = pick(['^', '*', '(', ')']);
178
+ else if (heat > 0.15) ch = pick(['.', ':', '*']);
179
+ else ch = ' ';
180
+
181
+ const ci = clamp(Math.floor((1 - heat) * 6), 0, FIRE_COLORS.length - 1);
182
+ line += FIRE_COLORS[ci] + ch;
183
+ }
184
+ console.log(line + RESET);
185
+ }
186
+
187
+ await sleep(frame < 15 ? 80 : frame < 45 ? 50 : 60);
188
+ }
189
+
190
+ // Final clean frame
191
+ process.stdout.write(`\x1b[${TOTAL_H}A`);
192
+ for (let r = 0; r < H; r++) {
193
+ let line = '';
194
+ for (let c = 0; c < W; c++) {
195
+ const ci = Math.floor((c / W) * BETH_COLORS.length);
196
+ line += BETH_COLORS[clamp(ci, 0, BETH_COLORS.length - 1)] + BRIGHT + bethLines[r][c];
197
+ }
198
+ console.log(line + RESET);
199
+ }
200
+ // Clear fire area with spaces
201
+ for (let fr = 0; fr < FIRE_H; fr++) {
202
+ console.log(' '.repeat(W));
203
+ }
204
+
205
+ process.stdout.write('\x1b[?25h');
206
+
207
+ const tagline = BETH_TAGLINES[Math.floor(Math.random() * BETH_TAGLINES.length)];
208
+ console.log('');
209
+ process.stdout.write(COLORS.cyan + COLORS.bright + '"');
210
+ for (const ch of tagline) {
211
+ process.stdout.write(ch);
212
+ await sleep(18);
213
+ }
214
+ console.log('"' + COLORS.reset);
215
+ console.log('');
216
+
217
+ // Show version and quick help
218
+ console.log(`${COLORS.dim}v${CURRENT_VERSION}${COLORS.reset} ${COLORS.dim}AI Orchestrator for GitHub Copilot${COLORS.reset}`);
219
+ console.log('');
220
+ console.log(`${COLORS.bright}Commands:${COLORS.reset}`);
221
+ console.log(` ${COLORS.cyan}npx beth-copilot init${COLORS.reset} Install Beth in your project`);
222
+ console.log(` ${COLORS.cyan}npx beth-copilot help${COLORS.reset} Show full documentation`);
223
+ console.log('');
224
+ console.log(`${COLORS.bright}After install:${COLORS.reset} Open VS Code → Copilot Chat → ${COLORS.cyan}@Beth${COLORS.reset}`);
225
+ console.log('');
226
+ }
227
+
228
+ function showBethBannerStatic({ showQuickHelp = true } = {}) {
229
+ const bethColors = [
230
+ '\x1b[38;5;196m',
231
+ '\x1b[38;5;202m',
232
+ '\x1b[38;5;208m',
233
+ '\x1b[38;5;214m',
234
+ '\x1b[38;5;220m',
235
+ '\x1b[38;5;226m',
236
+ ];
237
+
238
+ const fireColors = [
239
+ '\x1b[93m', // bright yellow
240
+ '\x1b[38;5;208m', // orange
241
+ '\x1b[91m', // red
242
+ '\x1b[38;5;52m', // dark red
243
+ ];
244
+
245
+ console.log('\n');
246
+ const bethChars = BETH_ASCII.map(line => [...line]);
247
+ const bethWidth = bethChars[0].length;
248
+
249
+ // BETH with gradient
250
+ for (let row = 0; row < BETH_ASCII.length; row++) {
251
+ let line = '';
252
+ for (let c = 0; c < bethWidth; c++) {
253
+ const char = bethChars[row][c];
254
+ const colorIndex = Math.floor((c / bethWidth) * bethColors.length);
255
+ line += bethColors[Math.min(colorIndex, bethColors.length - 1)] + COLORS.bright + char;
256
+ }
257
+ console.log(line + COLORS.reset);
258
+ }
259
+
260
+
261
+
262
+ const tagline = BETH_TAGLINES[Math.floor(Math.random() * BETH_TAGLINES.length)];
263
+ console.log('');
264
+ console.log(COLORS.cyan + COLORS.bright + '"' + tagline + '"' + COLORS.reset);
265
+ console.log('');
266
+
267
+ // Show version and quick help (optional)
268
+ if (showQuickHelp) {
269
+ console.log(`${COLORS.dim}v${CURRENT_VERSION}${COLORS.reset} ${COLORS.dim}AI Orchestrator for GitHub Copilot${COLORS.reset}`);
270
+ console.log('');
271
+ console.log(`${COLORS.bright}Commands:${COLORS.reset}`);
272
+ console.log(` ${COLORS.cyan}npx beth-copilot init${COLORS.reset} Install Beth in your project`);
273
+ console.log(` ${COLORS.cyan}npx beth-copilot help${COLORS.reset} Show full documentation`);
274
+ console.log('');
275
+ console.log(`${COLORS.bright}After install:${COLORS.reset} Open VS Code → Copilot Chat → ${COLORS.cyan}@Beth${COLORS.reset}`);
276
+ console.log('');
277
+ }
278
+ }
279
+
280
+ // Compact Beth portrait with colors
281
+ const BETH_PORTRAIT = [
282
+ ' .╭━━━━━━━╮.',
283
+ ' ╭──╯ ▒▓▓▓▓▒ ╰──╮',
284
+ ' ╱ ▓██████████▓ ╲',
285
+ ' ╱ ████▓▓██▓▓████ ╲',
286
+ ' │ ███ ◉ ██ ◉ ███ │',
287
+ ' │ ███▄▄▄▄▄▄███ │',
288
+ ' │ ▀██▄══▄██▀ │',
289
+ ' │ ╰────╯ │',
290
+ ' │ ▓██████████▓ │',
291
+ ' ╲ ████████████ ╱',
292
+ ' ╲ ▀██████████▀ ╱',
293
+ ' ╰───╮ ╭───╯',
294
+ ' ╰──────╯',
295
+ ];
296
+
297
+ // Portrait animation for init command
298
+ async function animatePortrait() {
299
+ const AMBER = '\x1b[38;2;218;165;32m';
300
+ const GOLD = '\x1b[38;2;255;215;0m';
301
+ const SKIN = '\x1b[38;2;235;210;160m';
302
+ const DARK = '\x1b[38;2;139;90;43m';
303
+ const EYE = '\x1b[38;2;70;130;180m';
304
+ const LIP = '\x1b[38;2;180;80;80m';
305
+ const WHITE = '\x1b[38;2;255;255;255m';
306
+ const RESET = '\x1b[0m';
307
+ const BOLD = '\x1b[1m';
308
+ const DIM = '\x1b[2m';
309
+
310
+ // Hide cursor
311
+ process.stdout.write('\x1b[?25l');
312
+
313
+ try {
314
+ // Clear screen
315
+ process.stdout.write('\x1b[2J\x1b[H');
316
+ await sleep(200);
317
+
318
+ // Quick glitch effect
319
+ const glitchChars = '░▒▓█';
320
+ for (let frame = 0; frame < 3; frame++) {
321
+ process.stdout.write('\x1b[H');
322
+ for (let j = 0; j < 5; j++) {
323
+ const col = Math.floor(Math.random() * 20) + 5;
324
+ const row = Math.floor(Math.random() * 10) + 2;
325
+ const r = Math.floor(Math.random() * 150 + 100);
326
+ const g = Math.floor(Math.random() * 100 + 50);
327
+ const b = Math.floor(Math.random() * 50);
328
+ const char = glitchChars[Math.floor(Math.random() * glitchChars.length)];
329
+ process.stdout.write(`\x1b[${row};${col}H\x1b[38;2;${r};${g};${b}m${char.repeat(3)}`);
330
+ }
331
+ await sleep(50);
332
+ }
333
+
334
+ // Display portrait with colors
335
+ process.stdout.write('\x1b[2J\x1b[H');
336
+ console.log();
337
+
338
+ for (let i = 0; i < BETH_PORTRAIT.length; i++) {
339
+ let line = BETH_PORTRAIT[i];
340
+ // Colorize: frame in amber, face content in skin tones
341
+ line = line
342
+ .replace(/[╭╮╯╰│╱╲━.─]/g, `${AMBER}$&${RESET}`)
343
+ .replace(/[▓█▒░▀▄▐▌]/g, `${SKIN}$&${RESET}`)
344
+ .replace(/◉/g, `${EYE}◉${RESET}`)
345
+ .replace(/══/g, `${LIP}══${RESET}`);
346
+ console.log(' ' + line);
347
+ await sleep(40);
348
+ }
349
+
350
+ await sleep(300);
351
+
352
+ // Banner below portrait
353
+ console.log();
354
+ console.log(` ${GOLD}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
355
+ console.log(` ${AMBER}${BOLD} B E T H${RESET}`);
356
+ console.log(` ${DIM}${WHITE} AI Agent Orchestrator${RESET}`);
357
+ console.log(` ${GOLD}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
358
+
359
+ await sleep(300);
360
+
361
+ // Typewriter quote
362
+ const quote = BETH_TAGLINES[Math.floor(Math.random() * BETH_TAGLINES.length)];
363
+ console.log();
364
+ process.stdout.write(` ${AMBER}"`);
365
+ for (const ch of quote) {
366
+ process.stdout.write(ch);
367
+ await sleep(20);
368
+ }
369
+ console.log(`"${RESET}`);
370
+ console.log();
371
+
372
+ } finally {
373
+ // Show cursor
374
+ process.stdout.write('\x1b[?25h');
375
+ }
376
+ }
377
+
378
+ // Detect if we can do animations (TTY and not piped)
379
+ function canAnimate() {
380
+ return process.stdout.isTTY && !process.env.CI && !process.env.NO_COLOR;
381
+ }
382
+
27
383
  function log(message, color = '') {
28
384
  console.log(`${color}${message}${COLORS.reset}`);
29
385
  }
@@ -44,6 +400,32 @@ function logInfo(message) {
44
400
  log(` ${message}`, COLORS.cyan);
45
401
  }
46
402
 
403
+ function logDebug(message) {
404
+ if (globalThis.VERBOSE) {
405
+ log(` [debug] ${message}`, COLORS.yellow);
406
+ }
407
+ }
408
+
409
+ function showPathDiagnostics() {
410
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
411
+ const isWindows = process.platform === 'win32';
412
+
413
+ console.log('');
414
+ log('PATH Diagnostics:', COLORS.bright);
415
+ logInfo(`Platform: ${process.platform}`);
416
+ logInfo(`HOME: ${homeDir}`);
417
+ logInfo(`PATH: ${process.env.PATH}`);
418
+
419
+ if (isWindows) {
420
+ logInfo(`APPDATA: ${process.env.APPDATA || '(not set)'}`);
421
+ logInfo(`npm prefix: Run "npm config get prefix" to check`);
422
+ } else {
423
+ logInfo(`npm prefix: Run "npm config get prefix" to check`);
424
+ logInfo(`Common locations: ~/.local/bin, /usr/local/bin, ~/.npm-global/bin`);
425
+ }
426
+ console.log('');
427
+ }
428
+
47
429
  async function checkForUpdates() {
48
430
  try {
49
431
  const response = await fetch('https://registry.npmjs.org/beth-copilot/latest', {
@@ -75,24 +457,96 @@ async function checkForUpdates() {
75
457
  }
76
458
  }
77
459
 
78
- function isBacklogCliInstalled() {
460
+ function getBacklogPath() {
461
+ // Check if backlog is available in PATH
79
462
  try {
463
+ logDebug('Checking if backlog is in PATH...');
80
464
  execSync('backlog --version', { stdio: 'ignore' });
81
- return true;
465
+ logDebug('Found backlog in PATH');
466
+ return 'backlog';
82
467
  } catch {
83
- return false;
468
+ logDebug('backlog not in PATH, checking common locations...');
469
+ // Check common installation paths
470
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
471
+ const isWindows = process.platform === 'win32';
472
+
473
+ const commonPaths = isWindows ? [
474
+ join(process.env.APPDATA || '', 'npm', 'backlog.cmd'),
475
+ join(homeDir, 'AppData', 'Roaming', 'npm', 'backlog.cmd'),
476
+ join(homeDir, 'AppData', 'Local', 'npm-global', 'backlog.cmd'),
477
+ ] : [
478
+ join(homeDir, '.local', 'bin', 'backlog'),
479
+ join(homeDir, 'bin', 'backlog'),
480
+ '/usr/local/bin/backlog',
481
+ join(homeDir, '.npm-global', 'bin', 'backlog'),
482
+ join(homeDir, '.bun', 'bin', 'backlog'),
483
+ ];
484
+
485
+ for (const backlogPath of commonPaths) {
486
+ logDebug(`Checking: ${backlogPath}`);
487
+ if (existsSync(backlogPath)) {
488
+ logDebug(`Found at: ${backlogPath}`);
489
+ return backlogPath;
490
+ }
491
+ }
492
+
493
+ logDebug('backlog not found in any common location');
494
+ return null;
84
495
  }
85
496
  }
86
497
 
87
- function isBeadsInstalled() {
498
+ function isBacklogCliInstalled() {
499
+ return getBacklogPath() !== null;
500
+ }
501
+
502
+ function getBeadsPath() {
503
+ // Check if bd is available in PATH
88
504
  try {
505
+ logDebug('Checking if bd is in PATH...');
89
506
  execSync('bd --version', { stdio: 'ignore' });
90
- return true;
507
+ logDebug('Found bd in PATH');
508
+ return 'bd';
91
509
  } catch {
92
- return false;
510
+ logDebug('bd not in PATH, checking common locations...');
511
+ // Check common installation paths based on platform
512
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
513
+ const isWindows = process.platform === 'win32';
514
+
515
+ const commonPaths = isWindows ? [
516
+ // Windows: npm global, Go bin, local apps
517
+ join(process.env.APPDATA || '', 'npm', 'bd.cmd'),
518
+ join(homeDir, 'AppData', 'Roaming', 'npm', 'bd.cmd'),
519
+ join(homeDir, 'AppData', 'Local', 'Microsoft', 'WindowsApps', 'bd.exe'),
520
+ join(homeDir, 'go', 'bin', 'bd.exe'),
521
+ join(process.env.GOPATH || join(homeDir, 'go'), 'bin', 'bd.exe'),
522
+ ] : [
523
+ // Unix: homebrew, npm global, go bin, local bin
524
+ '/opt/homebrew/bin/bd',
525
+ '/usr/local/bin/bd',
526
+ join(homeDir, '.local', 'bin', 'bd'),
527
+ join(homeDir, 'bin', 'bd'),
528
+ join(homeDir, '.npm-global', 'bin', 'bd'),
529
+ join(homeDir, 'go', 'bin', 'bd'),
530
+ join(process.env.GOPATH || join(homeDir, 'go'), 'bin', 'bd'),
531
+ ];
532
+
533
+ for (const bdPath of commonPaths) {
534
+ logDebug(`Checking: ${bdPath}`);
535
+ if (existsSync(bdPath)) {
536
+ logDebug(`Found at: ${bdPath}`);
537
+ return bdPath;
538
+ }
539
+ }
540
+
541
+ logDebug('bd not found in any common location');
542
+ return null;
93
543
  }
94
544
  }
95
545
 
546
+ function isBeadsInstalled() {
547
+ return getBeadsPath() !== null;
548
+ }
549
+
96
550
  function isBeadsInitialized(cwd) {
97
551
  // Check if .beads directory exists in the project
98
552
  return existsSync(join(cwd, '.beads'));
@@ -113,9 +567,43 @@ async function promptYesNo(question) {
113
567
  });
114
568
  }
115
569
 
570
+ async function promptForInput(question) {
571
+ const readline = await import('readline');
572
+ const rl = readline.createInterface({
573
+ input: process.stdin,
574
+ output: process.stdout
575
+ });
576
+
577
+ return new Promise((resolve) => {
578
+ rl.question(`${question} `, (answer) => {
579
+ rl.close();
580
+ resolve(answer.trim());
581
+ });
582
+ });
583
+ }
584
+
585
+ /**
586
+ * Installs the backlog.md CLI globally via npm.
587
+ *
588
+ * SECURITY NOTE - shell:true usage:
589
+ * - Required for cross-platform npm execution (npm.cmd on Windows, npm on Unix)
590
+ * - Arguments are HARDCODED - no user input is passed to the shell
591
+ * - Command injection risk: NONE (no dynamic/user-supplied values)
592
+ *
593
+ * Alternative considered: Using platform-specific binary names (npm.cmd vs npm)
594
+ * would eliminate shell:true but adds complexity and edge cases for non-standard installs.
595
+ *
596
+ * @returns {Promise<boolean>} True if installation succeeded and was verified
597
+ */
116
598
  async function installBacklogCli() {
117
- log('\nInstalling backlog.md CLI...', COLORS.cyan);
599
+ const isWindows = process.platform === 'win32';
600
+ const isMac = process.platform === 'darwin';
601
+
602
+ log('\nInstalling backlog.md CLI via npm...', COLORS.cyan);
603
+ logInfo('npm install -g backlog.md');
118
604
 
605
+ // SECURITY: shell:true is required for cross-platform npm execution.
606
+ // All arguments are hardcoded constants - no user input reaches the shell.
119
607
  return new Promise((resolve) => {
120
608
  const child = spawn('npm', ['install', '-g', 'backlog.md'], {
121
609
  stdio: 'inherit',
@@ -124,60 +612,159 @@ async function installBacklogCli() {
124
612
 
125
613
  child.on('close', (code) => {
126
614
  if (code === 0) {
127
- logSuccess('backlog.md CLI installed successfully!');
128
- resolve(true);
615
+ // CRITICAL: Verify installation actually worked before claiming success
616
+ const verifiedPath = getBacklogPath();
617
+ if (verifiedPath) {
618
+ logSuccess('backlog.md CLI installed and verified!');
619
+ resolve(true);
620
+ } else {
621
+ logWarning('npm reported success but backlog CLI not found in PATH.');
622
+ logInfo('This can happen if npm global bin is not in your PATH.');
623
+ if (globalThis.VERBOSE) {
624
+ showPathDiagnostics();
625
+ } else {
626
+ logInfo('Run with --verbose for PATH diagnostics.');
627
+ }
628
+ console.log('');
629
+ showBacklogAlternatives(isMac);
630
+ resolve(false);
631
+ }
129
632
  } else {
130
- logWarning('Failed to install backlog.md CLI. You can install it manually:');
131
- logInfo('npm i -g backlog.md');
132
- logInfo(' or');
133
- logInfo('bun i -g backlog.md');
633
+ logError('npm install failed.');
634
+ console.log('');
635
+ showBacklogAlternatives(isMac);
134
636
  resolve(false);
135
637
  }
136
638
  });
137
639
 
138
640
  child.on('error', () => {
139
- logWarning('Failed to install backlog.md CLI. You can install it manually:');
140
- logInfo('npm i -g backlog.md');
641
+ logError('Failed to run npm.');
642
+ logInfo('Make sure npm is installed and in your PATH.');
141
643
  resolve(false);
142
644
  });
143
645
  });
144
646
  }
145
647
 
648
+ function showBacklogAlternatives(isMac) {
649
+ logInfo('Alternative installation methods:');
650
+ if (isMac) {
651
+ logInfo(' Homebrew: brew install backlog-md');
652
+ }
653
+ logInfo(' Bun: bun install -g backlog.md');
654
+ logInfo('');
655
+ logInfo('Learn more: https://github.com/MrLesk/Backlog.md');
656
+ }
657
+
658
+ /**
659
+ * Installs the beads CLI globally via npm.
660
+ *
661
+ * SECURITY NOTE - shell:true usage:
662
+ * - Required for cross-platform npm execution (npm.cmd on Windows, npm on Unix)
663
+ * - Arguments are HARDCODED - no user input is passed to the shell
664
+ * - Command injection risk: NONE (no dynamic/user-supplied values)
665
+ *
666
+ * Alternative considered: Using platform-specific binary names (npm.cmd vs npm)
667
+ * would eliminate shell:true but adds complexity and edge cases for non-standard installs.
668
+ *
669
+ * @returns {Promise<boolean>} True if installation succeeded and was verified
670
+ */
146
671
  async function installBeads() {
147
- log('\nInstalling beads CLI...', COLORS.cyan);
148
- logInfo('curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash');
672
+ const isWindows = process.platform === 'win32';
673
+ const isMac = process.platform === 'darwin';
674
+
675
+ log('\nInstalling beads CLI via npm...', COLORS.cyan);
676
+ logInfo('npm install -g @beads/bd');
149
677
 
678
+ // SECURITY: shell:true is required for cross-platform npm execution.
679
+ // All arguments are hardcoded constants - no user input reaches the shell.
150
680
  return new Promise((resolve) => {
151
- const child = spawn('bash', ['-c', 'curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash'], {
681
+ const child = spawn('npm', ['install', '-g', '@beads/bd'], {
152
682
  stdio: 'inherit',
153
683
  shell: true
154
684
  });
155
685
 
156
686
  child.on('close', (code) => {
157
687
  if (code === 0) {
158
- logSuccess('beads CLI installed successfully!');
159
- resolve(true);
688
+ // CRITICAL: Verify installation actually worked before claiming success
689
+ // npm can exit 0 even when the package isn't properly installed
690
+ const verifiedPath = getBeadsPath();
691
+ if (verifiedPath) {
692
+ logSuccess('beads CLI installed and verified!');
693
+ resolve(true);
694
+ } else {
695
+ logWarning('npm reported success but beads CLI not found in PATH.');
696
+ logInfo('This can happen if npm global bin is not in your PATH.');
697
+ if (globalThis.VERBOSE) {
698
+ showPathDiagnostics();
699
+ } else {
700
+ logInfo('Run with --verbose for PATH diagnostics.');
701
+ }
702
+ console.log('');
703
+ showBeadsAlternatives(isWindows, isMac);
704
+ resolve(false);
705
+ }
160
706
  } else {
161
- logError('Failed to install beads CLI.');
162
- logInfo('Install manually: curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash');
163
- logInfo('Learn more: https://github.com/steveyegge/beads');
707
+ logError('npm install failed.');
708
+ console.log('');
709
+ showBeadsAlternatives(isWindows, isMac);
164
710
  resolve(false);
165
711
  }
166
712
  });
167
713
 
168
714
  child.on('error', () => {
169
- logError('Failed to install beads CLI.');
170
- logInfo('Install manually: curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash');
715
+ logError('Failed to run npm.');
716
+ logInfo('Make sure npm is installed and in your PATH.');
171
717
  resolve(false);
172
718
  });
173
719
  });
174
720
  }
175
721
 
722
+ function showBeadsAlternatives(isWindows, isMac) {
723
+ logInfo('Alternative installation methods:');
724
+ if (isWindows) {
725
+ logInfo(' PowerShell: irm https://raw.githubusercontent.com/steveyegge/beads/main/install.ps1 | iex');
726
+ logInfo(' Go: go install github.com/steveyegge/beads/cmd/bd@latest');
727
+ } else {
728
+ if (isMac) {
729
+ logInfo(' Homebrew: brew install beads');
730
+ }
731
+ logInfo(' Script: curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash');
732
+ logInfo(' Go: go install github.com/steveyegge/beads/cmd/bd@latest');
733
+ }
734
+ logInfo('');
735
+ logInfo('Learn more: https://github.com/steveyegge/beads');
736
+ }
737
+
738
+ /**
739
+ * Initializes beads in the current project directory.
740
+ *
741
+ * SECURITY NOTE - shell:true usage:
742
+ * - bdPath is validated via getBeadsPath() which only returns paths that:
743
+ * 1. Pass execSync('bd --version') verification, OR
744
+ * 2. Exist on disk (verified via existsSync) from a HARDCODED list of paths
745
+ * - Arguments are HARDCODED ('init') - no user input is passed to the shell
746
+ * - Command injection risk: LOW (bdPath is validated, no user input in args)
747
+ *
748
+ * The shell:true is used for PATH resolution consistency, though it could be
749
+ * eliminated since we have an absolute path. Kept for consistency with other
750
+ * spawn calls and to handle edge cases in shell script wrappers.
751
+ *
752
+ * @param {string} cwd - Current working directory (validated by caller)
753
+ * @returns {Promise<boolean>} True if initialization succeeded
754
+ */
176
755
  async function initializeBeads(cwd) {
177
756
  log('\nInitializing beads in project...', COLORS.cyan);
178
757
 
758
+ const bdPath = getBeadsPath();
759
+ if (!bdPath) {
760
+ logWarning('Failed to initialize beads. Run manually: bd init');
761
+ return false;
762
+ }
763
+
764
+ // SECURITY: bdPath is validated by getBeadsPath() (existsSync check).
765
+ // Only 'init' argument is passed - no user input reaches the shell.
179
766
  return new Promise((resolve) => {
180
- const child = spawn('bd', ['init'], {
767
+ const child = spawn(bdPath, ['init'], {
181
768
  stdio: 'inherit',
182
769
  shell: true,
183
770
  cwd
@@ -201,8 +788,8 @@ async function initializeBeads(cwd) {
201
788
  }
202
789
 
203
790
  function showHelp() {
204
- console.log(`
205
- ${COLORS.bright}Beth${COLORS.reset} - AI Orchestrator for GitHub Copilot
791
+ showBethBannerStatic({ showQuickHelp: false });
792
+ console.log(`${COLORS.bright}Beth${COLORS.reset} - AI Orchestrator for GitHub Copilot
206
793
 
207
794
  ${COLORS.bright}Usage:${COLORS.reset}
208
795
  npx beth-copilot init [options] Initialize Beth in current directory
@@ -213,6 +800,7 @@ ${COLORS.bright}Options:${COLORS.reset}
213
800
  --skip-backlog Don't create Backlog.md
214
801
  --skip-mcp Don't create mcp.json.example
215
802
  --skip-beads Skip beads check (not recommended)
803
+ --verbose Show detailed diagnostics on errors
216
804
 
217
805
  ${COLORS.bright}Examples:${COLORS.reset}
218
806
  npx beth-copilot init Set up Beth in current project
@@ -281,10 +869,14 @@ ${COLORS.yellow}╔════════════════════
281
869
  `);
282
870
  }
283
871
 
284
- console.log(`
285
- ${COLORS.bright}🤠 Beth is moving in.${COLORS.reset}
286
- ${COLORS.cyan}"I don't do excuses. I do results."${COLORS.reset}
287
- `);
872
+ // Show Beth's fire animation
873
+ if (canAnimate()) {
874
+ await animateBethBanner();
875
+ } else {
876
+ showBethBannerStatic({ showQuickHelp: false });
877
+ }
878
+
879
+ log(`${COLORS.yellow}Tip: Run with --verbose for detailed diagnostics if you hit issues.${COLORS.reset}`);
288
880
 
289
881
  // Check if templates exist
290
882
  if (!existsSync(TEMPLATES_DIR)) {
@@ -382,7 +974,10 @@ ${COLORS.cyan}"I don't do excuses. I do results."${COLORS.reset}
382
974
  console.log('');
383
975
  log('Checking beads (required for task tracking)...', COLORS.cyan);
384
976
 
385
- if (!isBeadsInstalled()) {
977
+ let bdPath = getBeadsPath();
978
+
979
+ // Loop until beads is installed
980
+ while (!bdPath) {
386
981
  logWarning('beads CLI is not installed.');
387
982
  logInfo('Beth requires beads for task tracking. Agents use it to coordinate work.');
388
983
  logInfo('Learn more: https://github.com/steveyegge/beads');
@@ -391,16 +986,70 @@ ${COLORS.cyan}"I don't do excuses. I do results."${COLORS.reset}
391
986
  const shouldInstallBeads = await promptYesNo('Install beads CLI now? (required)');
392
987
  if (shouldInstallBeads) {
393
988
  const installed = await installBeads();
394
- if (!installed) {
395
- logError('beads installation failed. Beth requires beads to function.');
396
- logInfo('Install manually and run "beth init" again.');
989
+ if (installed) {
990
+ // Re-check for beads after installation
991
+ bdPath = getBeadsPath();
992
+ if (!bdPath) {
993
+ console.log('');
994
+ logWarning('beads installed but not found in common paths.');
995
+ logInfo('The installer may have placed it in a custom location.');
996
+ console.log('');
997
+ logInfo('Please try one of these options:');
998
+ logInfo(' 1. Open a NEW terminal and run: npx beth-copilot init');
999
+ logInfo(' 2. Add ~/.local/bin to your PATH and retry');
1000
+ logInfo(' 3. Run: source ~/.bashrc (or ~/.zshrc) then retry');
1001
+ console.log('');
1002
+
1003
+ const retryCheck = await promptYesNo('Retry detection? (select No to enter path manually)');
1004
+ if (retryCheck) {
1005
+ bdPath = getBeadsPath();
1006
+ continue;
1007
+ }
1008
+
1009
+ // Allow manual path entry
1010
+ const customPath = await promptForInput('Enter full path to bd binary (or press Enter to retry installation):');
1011
+ if (customPath) {
1012
+ const validation = validateBeadsPath(customPath);
1013
+ if (validation.valid) {
1014
+ bdPath = validation.normalizedPath;
1015
+ logSuccess(`Found beads at: ${bdPath}`);
1016
+ } else {
1017
+ logError(`Invalid path: ${validation.error}`);
1018
+ }
1019
+ }
1020
+ }
1021
+ } else {
1022
+ console.log('');
1023
+ logError('Installation script failed.');
1024
+ logInfo('You can try installing manually:');
1025
+ logInfo(' curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash');
1026
+ console.log('');
1027
+ }
1028
+ } else {
1029
+ console.log('');
1030
+ logError('beads is REQUIRED for Beth to function.');
1031
+ logInfo('Beth agents use beads to track tasks, dependencies, and coordinate work.');
1032
+ logInfo('Without beads, the multi-agent workflow will not work correctly.');
1033
+ console.log('');
1034
+
1035
+ const tryAgain = await promptYesNo('Would you like to try installing beads?');
1036
+ if (!tryAgain) {
1037
+ logError('Cannot continue without beads. Exiting.');
1038
+ logInfo('Install beads manually and run "npx beth-copilot init" again:');
1039
+ logInfo(' npm install -g @beads/bd');
397
1040
  process.exit(1);
398
1041
  }
1042
+ }
1043
+ }
1044
+
1045
+ // Show path info if not in standard PATH
1046
+ if (bdPath && bdPath !== 'bd') {
1047
+ logSuccess(`beads CLI found at: ${bdPath}`);
1048
+ const isWindows = process.platform === 'win32';
1049
+ if (isWindows) {
1050
+ logInfo('Tip: Ensure npm global bin is in your PATH to use "bd" directly.');
399
1051
  } else {
400
- logError('beads is required for Beth to function.');
401
- logInfo('Install beads and run "beth init" again:');
402
- logInfo(' curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash');
403
- process.exit(1);
1052
+ logInfo('Tip: Add ~/.local/bin or npm global bin to your PATH to use "bd" directly.');
404
1053
  }
405
1054
  } else {
406
1055
  logSuccess('beads CLI is installed');
@@ -409,11 +1058,20 @@ ${COLORS.cyan}"I don't do excuses. I do results."${COLORS.reset}
409
1058
  // Initialize beads in the project if not already done
410
1059
  if (!isBeadsInitialized(cwd)) {
411
1060
  logInfo('beads not initialized in this project.');
412
- const shouldInitBeads = await promptYesNo('Initialize beads now?');
413
- if (shouldInitBeads) {
414
- await initializeBeads(cwd);
415
- } else {
416
- logWarning('Remember to run "bd init" before using Beth.');
1061
+ let initialized = false;
1062
+
1063
+ while (!initialized) {
1064
+ const shouldInitBeads = await promptYesNo('Initialize beads now? (required)');
1065
+ if (shouldInitBeads) {
1066
+ initialized = await initializeBeads(cwd);
1067
+ if (!initialized) {
1068
+ logWarning('Initialization failed. Let\'s try again.');
1069
+ }
1070
+ } else {
1071
+ logError('beads must be initialized for Beth to work correctly.');
1072
+ logInfo('The .beads directory stores task tracking data used by all agents.');
1073
+ console.log('');
1074
+ }
417
1075
  }
418
1076
  } else {
419
1077
  logSuccess('beads is initialized in this project');
@@ -422,22 +1080,90 @@ ${COLORS.cyan}"I don't do excuses. I do results."${COLORS.reset}
422
1080
  logWarning('Skipped beads check (--skip-beads). Beth may not function correctly.');
423
1081
  }
424
1082
 
425
- // Check for backlog.md CLI (optional)
426
- if (!skipBacklog && !isBacklogCliInstalled()) {
427
- console.log('');
428
- logWarning('backlog.md CLI is not installed (optional).');
429
- logInfo('The CLI provides TUI boards, web UI, and task management commands.');
430
- logInfo('Learn more: https://github.com/MrLesk/Backlog.md');
1083
+ // Check for backlog.md CLI (REQUIRED for Beth)
1084
+ if (!skipBacklog) {
431
1085
  console.log('');
1086
+ log('Checking backlog.md CLI (required for task management)...', COLORS.cyan);
432
1087
 
433
- const shouldInstall = await promptYesNo('Would you like to install the backlog.md CLI globally?');
434
- if (shouldInstall) {
435
- await installBacklogCli();
436
- } else {
437
- logInfo('Skipped. You can install it later with: npm i -g backlog.md');
1088
+ let backlogPath = getBacklogPath();
1089
+
1090
+ // Loop until backlog.md is installed
1091
+ while (!backlogPath) {
1092
+ logWarning('backlog.md CLI is not installed.');
1093
+ logInfo('Beth requires backlog.md for human-readable task tracking and boards.');
1094
+ logInfo('Learn more: https://github.com/MrLesk/Backlog.md');
1095
+ console.log('');
1096
+
1097
+ const shouldInstall = await promptYesNo('Install backlog.md CLI now? (required)');
1098
+ if (shouldInstall) {
1099
+ const installed = await installBacklogCli();
1100
+ if (installed) {
1101
+ // Re-check for backlog after installation
1102
+ backlogPath = getBacklogPath();
1103
+ if (!backlogPath) {
1104
+ console.log('');
1105
+ logWarning('backlog.md installed but not found in common paths.');
1106
+ logInfo('The installer may have placed it in a custom location.');
1107
+ console.log('');
1108
+ logInfo('Please try one of these options:');
1109
+ logInfo(' 1. Open a NEW terminal and run: npx beth-copilot init');
1110
+ logInfo(' 2. Run: source ~/.bashrc (or ~/.zshrc) then retry');
1111
+ console.log('');
1112
+
1113
+ const retryCheck = await promptYesNo('Retry detection?');
1114
+ if (retryCheck) {
1115
+ backlogPath = getBacklogPath();
1116
+ }
1117
+ }
1118
+ } else {
1119
+ console.log('');
1120
+ logError('Installation failed.');
1121
+ logInfo('You can try installing manually:');
1122
+ logInfo(' npm install -g backlog.md');
1123
+ if (process.platform === 'darwin') {
1124
+ logInfo(' brew install backlog-md');
1125
+ }
1126
+ logInfo(' bun install -g backlog.md');
1127
+ console.log('');
1128
+ }
1129
+ } else {
1130
+ console.log('');
1131
+ logError('backlog.md is REQUIRED for Beth to function.');
1132
+ logInfo('Beth uses Backlog.md to maintain human-readable task history and boards.');
1133
+ logInfo('This complements beads for a complete task management workflow.');
1134
+ console.log('');
1135
+
1136
+ const tryAgain = await promptYesNo('Would you like to try installing backlog.md?');
1137
+ if (!tryAgain) {
1138
+ logError('Cannot continue without backlog.md. Exiting.');
1139
+ logInfo('Install manually and run "npx beth-copilot init" again:');
1140
+ logInfo(' npm install -g backlog.md');
1141
+ process.exit(1);
1142
+ }
1143
+ }
438
1144
  }
439
- } else if (!skipBacklog) {
440
- logSuccess('backlog.md CLI is already installed');
1145
+
1146
+ logSuccess('backlog.md CLI is installed');
1147
+ } else {
1148
+ logWarning('Skipped backlog check (--skip-backlog). Beth may not function correctly.');
1149
+ }
1150
+
1151
+ // Final verification
1152
+ console.log('');
1153
+ log('Verifying installation...', COLORS.cyan);
1154
+
1155
+ const finalBeadsOk = skipBeads || getBeadsPath();
1156
+ const finalBacklogOk = skipBacklog || getBacklogPath();
1157
+ const finalBeadsInit = skipBeads || isBeadsInitialized(cwd);
1158
+
1159
+ if (finalBeadsOk && finalBacklogOk && finalBeadsInit) {
1160
+ logSuccess('All dependencies installed and configured!');
1161
+ } else {
1162
+ if (!finalBeadsOk) logError('beads CLI not found');
1163
+ if (!finalBacklogOk) logError('backlog.md CLI not found');
1164
+ if (!finalBeadsInit) logError('beads not initialized in project');
1165
+ logError('Setup incomplete. Please resolve issues above and run init again.');
1166
+ process.exit(1);
441
1167
  }
442
1168
 
443
1169
  // Next steps
@@ -458,7 +1184,7 @@ ${COLORS.cyan}"They broke my wings and forgot I had claws."${COLORS.reset}
458
1184
 
459
1185
  // Input validation constants
460
1186
  const ALLOWED_COMMANDS = ['init', 'help', '--help', '-h'];
461
- const ALLOWED_FLAGS = ['--force', '--skip-backlog', '--skip-mcp', '--skip-beads'];
1187
+ const ALLOWED_FLAGS = ['--force', '--skip-backlog', '--skip-mcp', '--skip-beads', '--verbose'];
462
1188
  const MAX_ARG_LENGTH = 50;
463
1189
 
464
1190
  // Validate and sanitize input
@@ -488,10 +1214,14 @@ const options = {
488
1214
  skipBacklog: args.includes('--skip-backlog'),
489
1215
  skipMcp: args.includes('--skip-mcp'),
490
1216
  skipBeads: args.includes('--skip-beads'),
1217
+ verbose: args.includes('--verbose'),
491
1218
  };
492
1219
 
493
- // Validate unknown flags
494
- const unknownFlags = args.filter(arg => arg.startsWith('--') && !ALLOWED_FLAGS.includes(arg));
1220
+ // Set global verbose flag for logDebug
1221
+ globalThis.VERBOSE = options.verbose;
1222
+
1223
+ // Validate unknown flags (exclude --help which is handled as a command)
1224
+ const unknownFlags = args.filter(arg => arg.startsWith('--') && !ALLOWED_FLAGS.includes(arg) && arg !== '--help');
495
1225
  if (unknownFlags.length > 0) {
496
1226
  logError(`Unknown flag: ${unknownFlags[0].slice(0, MAX_ARG_LENGTH)}`);
497
1227
  console.log('Run "npx beth-copilot help" for usage information.');