beth-copilot 1.0.10 → 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
  }
@@ -226,6 +582,19 @@ async function promptForInput(question) {
226
582
  });
227
583
  }
228
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
+ */
229
598
  async function installBacklogCli() {
230
599
  const isWindows = process.platform === 'win32';
231
600
  const isMac = process.platform === 'darwin';
@@ -233,6 +602,8 @@ async function installBacklogCli() {
233
602
  log('\nInstalling backlog.md CLI via npm...', COLORS.cyan);
234
603
  logInfo('npm install -g backlog.md');
235
604
 
605
+ // SECURITY: shell:true is required for cross-platform npm execution.
606
+ // All arguments are hardcoded constants - no user input reaches the shell.
236
607
  return new Promise((resolve) => {
237
608
  const child = spawn('npm', ['install', '-g', 'backlog.md'], {
238
609
  stdio: 'inherit',
@@ -284,6 +655,19 @@ function showBacklogAlternatives(isMac) {
284
655
  logInfo('Learn more: https://github.com/MrLesk/Backlog.md');
285
656
  }
286
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
+ */
287
671
  async function installBeads() {
288
672
  const isWindows = process.platform === 'win32';
289
673
  const isMac = process.platform === 'darwin';
@@ -291,6 +675,8 @@ async function installBeads() {
291
675
  log('\nInstalling beads CLI via npm...', COLORS.cyan);
292
676
  logInfo('npm install -g @beads/bd');
293
677
 
678
+ // SECURITY: shell:true is required for cross-platform npm execution.
679
+ // All arguments are hardcoded constants - no user input reaches the shell.
294
680
  return new Promise((resolve) => {
295
681
  const child = spawn('npm', ['install', '-g', '@beads/bd'], {
296
682
  stdio: 'inherit',
@@ -349,6 +735,23 @@ function showBeadsAlternatives(isWindows, isMac) {
349
735
  logInfo('Learn more: https://github.com/steveyegge/beads');
350
736
  }
351
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
+ */
352
755
  async function initializeBeads(cwd) {
353
756
  log('\nInitializing beads in project...', COLORS.cyan);
354
757
 
@@ -358,6 +761,8 @@ async function initializeBeads(cwd) {
358
761
  return false;
359
762
  }
360
763
 
764
+ // SECURITY: bdPath is validated by getBeadsPath() (existsSync check).
765
+ // Only 'init' argument is passed - no user input reaches the shell.
361
766
  return new Promise((resolve) => {
362
767
  const child = spawn(bdPath, ['init'], {
363
768
  stdio: 'inherit',
@@ -383,8 +788,8 @@ async function initializeBeads(cwd) {
383
788
  }
384
789
 
385
790
  function showHelp() {
386
- console.log(`
387
- ${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
388
793
 
389
794
  ${COLORS.bright}Usage:${COLORS.reset}
390
795
  npx beth-copilot init [options] Initialize Beth in current directory
@@ -464,11 +869,14 @@ ${COLORS.yellow}╔════════════════════
464
869
  `);
465
870
  }
466
871
 
467
- console.log(`
468
- ${COLORS.bright}🤠 Beth is moving in.${COLORS.reset}
469
- ${COLORS.cyan}"I don't do excuses. I do results."${COLORS.reset}
470
- ${COLORS.yellow}Tip: Run with --verbose for detailed diagnostics if you hit issues.${COLORS.reset}
471
- `);
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}`);
472
880
 
473
881
  // Check if templates exist
474
882
  if (!existsSync(TEMPLATES_DIR)) {
@@ -600,11 +1008,14 @@ ${COLORS.yellow}Tip: Run with --verbose for detailed diagnostics if you hit issu
600
1008
 
601
1009
  // Allow manual path entry
602
1010
  const customPath = await promptForInput('Enter full path to bd binary (or press Enter to retry installation):');
603
- if (customPath && existsSync(customPath)) {
604
- bdPath = customPath;
605
- logSuccess(`Found beads at: ${bdPath}`);
606
- } else if (customPath) {
607
- logError(`File not found: ${customPath}`);
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
+ }
608
1019
  }
609
1020
  }
610
1021
  } else {
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Beth Animation - Startup splash for the AI Agent Orchestrator
4
+ * "I don't speak dipshit. I speak in consequences."
5
+ */
6
+
7
+ import { readFileSync, existsSync } from 'fs';
8
+ import { dirname, join } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+
13
+ // ANSI escape codes
14
+ const RESET = '\x1b[0m';
15
+ const BOLD = '\x1b[1m';
16
+ const DIM = '\x1b[2m';
17
+ const AMBER = '\x1b[38;2;218;165;32m';
18
+ const GOLD = '\x1b[38;2;255;215;0m';
19
+ const WHITE = '\x1b[38;2;255;255;255m';
20
+
21
+ // Beth's signature quotes
22
+ const QUOTES = [
23
+ "I don't speak dipshit. I speak in consequences.",
24
+ "They broke my wings and forgot I had claws.",
25
+ "I believe in lovin' with your whole soul and destroying anything that wants to kill what you love.",
26
+ "I'm the trailer park. I'm the tornado.",
27
+ "Where's the fun in breaking one thing? When I fix something, I fix it for generations.",
28
+ "I made two decisions based on fear and they cost me everything. I'll never make another.",
29
+ "You want my opinion? You're getting it either way.",
30
+ ];
31
+
32
+ function sleep(ms) {
33
+ return new Promise(resolve => setTimeout(resolve, ms));
34
+ }
35
+
36
+ function clearScreen() {
37
+ process.stdout.write('\x1b[2J\x1b[H');
38
+ }
39
+
40
+ function hideCursor() {
41
+ process.stdout.write('\x1b[?25l');
42
+ }
43
+
44
+ function showCursor() {
45
+ process.stdout.write('\x1b[?25h');
46
+ }
47
+
48
+ function centerText(text, width = process.stdout.columns || 80) {
49
+ const padding = Math.max(0, Math.floor((width - text.length) / 2));
50
+ return ' '.repeat(padding) + text;
51
+ }
52
+
53
+ async function typewriter(text, delay = 30) {
54
+ for (const char of text) {
55
+ process.stdout.write(char);
56
+ await sleep(delay);
57
+ }
58
+ console.log();
59
+ }
60
+
61
+ function getRandomQuote() {
62
+ return QUOTES[Math.floor(Math.random() * QUOTES.length)];
63
+ }
64
+
65
+ async function glitchEffect(iterations = 5) {
66
+ const glitchChars = '░▒▓█│┃┆┇┊┋╳╱╲';
67
+ const cols = process.stdout.columns || 80;
68
+ const rows = process.stdout.rows || 24;
69
+
70
+ for (let i = 0; i < iterations; i++) {
71
+ clearScreen();
72
+ for (let j = 0; j < 10; j++) {
73
+ const col = Math.floor(Math.random() * (cols - 20));
74
+ const row = Math.floor(Math.random() * (rows - 5) + 2);
75
+ const r = Math.floor(Math.random() * 150 + 100);
76
+ const g = Math.floor(Math.random() * 100 + 50);
77
+ const b = Math.floor(Math.random() * 50);
78
+ const char = glitchChars[Math.floor(Math.random() * glitchChars.length)];
79
+ process.stdout.write(`\x1b[${row};${col}H\x1b[38;2;${r};${g};${b}m${char.repeat(4)}`);
80
+ }
81
+ await sleep(80);
82
+ }
83
+ }
84
+
85
+ async function displayPortrait() {
86
+ const artPath = join(__dirname, '..', '..', 'assets', 'beth-portrait.txt');
87
+
88
+ if (!existsSync(artPath)) {
89
+ console.log(`${DIM}Portrait file not found at: ${artPath}${RESET}`);
90
+ return false;
91
+ }
92
+
93
+ const art = readFileSync(artPath, 'utf-8');
94
+ console.log(art);
95
+ return true;
96
+ }
97
+
98
+ function displayBanner() {
99
+ console.log();
100
+ console.log(`${GOLD}${BOLD}${centerText('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}${RESET}`);
101
+ console.log(`${AMBER}${BOLD}${centerText('B E T H')}${RESET}`);
102
+ console.log(`${DIM}${WHITE}${centerText('AI Agent Orchestrator')}${RESET}`);
103
+ console.log(`${GOLD}${BOLD}${centerText('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}${RESET}`);
104
+ }
105
+
106
+ async function displayQuote() {
107
+ const quote = getRandomQuote();
108
+ console.log();
109
+ process.stdout.write(`${AMBER} `);
110
+ await typewriter(`"${quote}"`, 40);
111
+ console.log(RESET);
112
+ }
113
+
114
+ /**
115
+ * Quick banner - no portrait, just text
116
+ */
117
+ export async function quickBanner() {
118
+ console.log();
119
+ console.log(`${GOLD}${BOLD}━━━ ${AMBER}BETH${GOLD} ━━━${RESET}`);
120
+ console.log(`${DIM}${getRandomQuote()}${RESET}`);
121
+ console.log();
122
+ }
123
+
124
+ /**
125
+ * Full animation with portrait
126
+ */
127
+ export async function fullAnimation() {
128
+ hideCursor();
129
+
130
+ try {
131
+ clearScreen();
132
+ await sleep(500);
133
+
134
+ // Glitch intro
135
+ await glitchEffect(5);
136
+
137
+ // Show portrait
138
+ clearScreen();
139
+ const hasPortrait = await displayPortrait();
140
+
141
+ if (hasPortrait) {
142
+ await sleep(1000);
143
+ }
144
+
145
+ // Banner and quote
146
+ displayBanner();
147
+ await sleep(500);
148
+ await displayQuote();
149
+
150
+ console.log();
151
+ console.log(`${DIM}${centerText('Press any key to continue...')}${RESET}`);
152
+
153
+ // Wait for keypress
154
+ if (process.stdin.isTTY) {
155
+ process.stdin.setRawMode(true);
156
+ process.stdin.resume();
157
+ await new Promise(resolve => {
158
+ process.stdin.once('data', resolve);
159
+ });
160
+ process.stdin.setRawMode(false);
161
+ }
162
+ } finally {
163
+ showCursor();
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Minimal startup text
169
+ */
170
+ export function minimalBanner() {
171
+ console.log(`${AMBER}${BOLD}Beth${RESET} ${DIM}| The bigger bear.${RESET}`);
172
+ }
173
+
174
+ // Run if executed directly
175
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
176
+ const mode = process.argv[2] || 'full';
177
+
178
+ switch (mode) {
179
+ case 'quick':
180
+ quickBanner();
181
+ break;
182
+ case 'minimal':
183
+ minimalBanner();
184
+ break;
185
+ case 'full':
186
+ default:
187
+ fullAnimation().catch(console.error);
188
+ }
189
+ }