beth-copilot 1.0.10 → 1.0.12

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.
@@ -0,0 +1,155 @@
1
+ #!/bin/bash
2
+ # Beth Animation - "I don't speak dipshit. I speak in consequences."
3
+
4
+ # Colors
5
+ RESET="\033[0m"
6
+ BOLD="\033[1m"
7
+ DIM="\033[2m"
8
+ AMBER="\033[38;2;218;165;32m"
9
+ GOLD="\033[38;2;255;215;0m"
10
+ WHITE="\033[38;2;255;255;255m"
11
+
12
+ # Clear screen and hide cursor
13
+ clear
14
+ tput civis
15
+
16
+ # Trap to restore cursor on exit
17
+ trap 'tput cnorm; echo -e "${RESET}"' EXIT
18
+
19
+ # Get terminal dimensions
20
+ COLS=$(tput cols)
21
+ LINES=$(tput lines)
22
+
23
+ # Path to ASCII art
24
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+ ART_FILE="${SCRIPT_DIR}/../assets/beth-portrait.txt"
26
+
27
+ # Beth's quotes
28
+ QUOTES=(
29
+ "I don't speak dipshit. I speak in consequences."
30
+ "They broke my wings and forgot I had claws."
31
+ "I believe in lovin' with your whole soul and destroying anything that wants to kill what you love."
32
+ "I'm the trailer park. I'm the tornado."
33
+ "Where's the fun in breaking one thing? When I fix something, I fix it for generations."
34
+ "I made two decisions based on fear and they cost me everything. I'll never make another."
35
+ "You want my opinion? You're getting it either way."
36
+ )
37
+
38
+ # Function to center text
39
+ center_text() {
40
+ local text="$1"
41
+ local width=${#text}
42
+ local padding=$(( (COLS - width) / 2 ))
43
+ printf "%${padding}s%s\n" "" "$text"
44
+ }
45
+
46
+ # Function for typewriter effect
47
+ typewriter() {
48
+ local text="$1"
49
+ local delay="${2:-0.03}"
50
+ for (( i=0; i<${#text}; i++ )); do
51
+ printf "%s" "${text:$i:1}"
52
+ sleep "$delay"
53
+ done
54
+ echo
55
+ }
56
+
57
+ # Function to display glitch frame
58
+ glitch_frame() {
59
+ local lines_to_glitch=$((RANDOM % 3 + 1))
60
+ for _ in $(seq 1 $lines_to_glitch); do
61
+ local line=$((RANDOM % 20 + 5))
62
+ tput cup $line 0
63
+ echo -e "\033[38;2;$((RANDOM % 100));$((RANDOM % 50));$((RANDOM % 50))m$(head -c $((RANDOM % 40 + 10)) /dev/urandom | tr -dc '░▒▓█│┃┆┇┊┋')"
64
+ done
65
+ sleep 0.05
66
+ }
67
+
68
+ # Fade in effect - gradually reveal the image
69
+ fade_in() {
70
+ if [[ ! -f "$ART_FILE" ]]; then
71
+ echo "Art file not found: $ART_FILE"
72
+ return 1
73
+ fi
74
+
75
+ local total_lines=$(wc -l < "$ART_FILE")
76
+
77
+ # First pass: dim reveal
78
+ echo -e "${DIM}"
79
+ clear
80
+ cat "$ART_FILE"
81
+ sleep 0.3
82
+
83
+ # Second pass: normal brightness with slight delay
84
+ clear
85
+ cat "$ART_FILE"
86
+ }
87
+
88
+ # Main animation sequence
89
+ main() {
90
+ # Initial blackout with quote tease
91
+ clear
92
+ sleep 0.5
93
+
94
+ # Glitch introduction
95
+ for _ in {1..5}; do
96
+ clear
97
+ for _ in {1..10}; do
98
+ local col=$((RANDOM % (COLS - 20)))
99
+ local row=$((RANDOM % (LINES - 5) + 2))
100
+ tput cup $row $col
101
+ echo -e "\033[38;2;$((RANDOM % 150 + 100));$((RANDOM % 100 + 50));$((RANDOM % 50))m░▒▓█"
102
+ done
103
+ sleep 0.08
104
+ done
105
+
106
+ # Display the portrait
107
+ clear
108
+ if [[ -f "$ART_FILE" ]]; then
109
+ cat "$ART_FILE"
110
+ else
111
+ echo "Portrait not found. Run from beth repository root."
112
+ fi
113
+
114
+ sleep 1
115
+
116
+ # Add name below portrait
117
+ echo
118
+ echo -e "${GOLD}${BOLD}"
119
+ center_text "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
120
+ echo -e "${RESET}"
121
+
122
+ echo -e "${AMBER}${BOLD}"
123
+ center_text "B E T H"
124
+ echo -e "${RESET}"
125
+
126
+ echo -e "${DIM}${WHITE}"
127
+ center_text "AI Agent Orchestrator"
128
+ echo -e "${RESET}"
129
+
130
+ echo -e "${GOLD}${BOLD}"
131
+ center_text "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
132
+ echo -e "${RESET}"
133
+
134
+ sleep 0.5
135
+
136
+ # Random quote with typewriter effect
137
+ local quote="${QUOTES[$((RANDOM % ${#QUOTES[@]}))]}"
138
+ echo
139
+ echo -e "${AMBER}"
140
+ printf " "
141
+ typewriter "\"${quote}\"" 0.04
142
+ echo -e "${RESET}"
143
+
144
+ echo
145
+ echo -e "${DIM}"
146
+ center_text "Press any key to continue..."
147
+ echo -e "${RESET}"
148
+
149
+ read -n 1 -s
150
+ }
151
+
152
+ # Run if executed directly
153
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
154
+ main
155
+ fi
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 {