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/assets/beth-portrait-small.txt +13 -0
- package/assets/beth-portrait.txt +60 -0
- package/assets/beth-questioning.png +0 -0
- package/assets/yellowstone-beth.png +0 -0
- package/bin/beth-animation.sh +155 -0
- package/bin/cli.js +423 -12
- package/bin/lib/animation.js +189 -0
- package/bin/lib/pathValidation.js +233 -0
- package/bin/lib/pathValidation.test.js +280 -0
- package/package.json +9 -2
- package/sbom.json +129 -0
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
|
-
|
|
387
|
-
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
+
}
|