mirror of
https://github.com/FBukovina/burn-baby-burn.git
synced 2026-10-27 15:14:32 +00:00
Add TUI launch support and Codex install updates
This commit is contained in:
parent
d249712b13
commit
4246999931
3 changed files with 994 additions and 8 deletions
51
bin/burn
51
bin/burn
|
|
@ -17,6 +17,8 @@ FILLER_TOKENS=1000
|
|||
CLAUDE_SYSTEM_OVERHEAD_TOKENS=9000 # claude -p with --tools ""
|
||||
CODEX_SYSTEM_OVERHEAD_TOKENS=12000 # codex exec --sandbox read-only
|
||||
FILLER_LINE="Burn baby burn, this is a disco inferno, burning tokens is for great engineers. "
|
||||
TUI_CHILD=0
|
||||
[[ "${BURN_TUI_CHILD:-}" == "1" ]] && TUI_CHILD=1
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
|
|
@ -92,6 +94,20 @@ while [[ $# -gt 0 ]]; do
|
|||
done
|
||||
|
||||
if [[ -z "$tokens" ]]; then
|
||||
if [[ -t 0 && -t 1 ]]; then
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "error: No token count provided, and 'node' is not installed to launch the TUI." >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -f "$SCRIPT_DIR/tui.js" ]]; then
|
||||
echo "error: TUI entrypoint not found at '$SCRIPT_DIR/tui.js'." >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
exec node "$SCRIPT_DIR/tui.js"
|
||||
fi
|
||||
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
|
@ -178,7 +194,17 @@ for ((i = 0; i < repeats; i++)); do
|
|||
done
|
||||
prompt=$(printf "Reply with the single word 'burned' and nothing else. Ignore the filler text below; it exists only to consume tokens.\n\n%s" "$filler")
|
||||
|
||||
if [[ -t 1 ]]; then
|
||||
if ((TUI_CHILD)); then
|
||||
ORANGE=''
|
||||
MAGENTA=''
|
||||
CYAN=''
|
||||
RED=''
|
||||
YELLOW=''
|
||||
DIM=''
|
||||
RESET=''
|
||||
HIDE_CURSOR=''
|
||||
SHOW_CURSOR=''
|
||||
elif [[ -t 1 || -n "${FORCE_COLOR:-}" || -n "${CLICOLOR_FORCE:-}" ]]; then
|
||||
ORANGE=$'\033[1;33m'
|
||||
MAGENTA=$'\033[1;35m'
|
||||
CYAN=$'\033[1;36m'
|
||||
|
|
@ -367,6 +393,13 @@ draw_bar() {
|
|||
local pct filled empty bar="" i suffix="${1:-}"
|
||||
pct=$((burned * 100 / tokens))
|
||||
((pct > 100)) && pct=100
|
||||
|
||||
if ((TUI_CHILD)); then
|
||||
printf 'BURN_PROGRESS\t%d\t%d\t%d\t%s\t%d\t%s\n' \
|
||||
"$pct" "$burned" "$tokens" "$(format_cost_field)" "$iteration" "$suffix"
|
||||
return
|
||||
fi
|
||||
|
||||
filled=$((pct * BAR_WIDTH / 100))
|
||||
empty=$((BAR_WIDTH - filled))
|
||||
for ((i = 0; i < filled; i++)); do bar+='█'; done
|
||||
|
|
@ -439,7 +472,9 @@ run_cli() {
|
|||
|
||||
local frame=0
|
||||
while kill -0 "$cli_pid" 2>/dev/null; do
|
||||
redraw_disco $((frame % 4))
|
||||
if ! ((TUI_CHILD)); then
|
||||
redraw_disco $((frame % 4))
|
||||
fi
|
||||
frame=$((frame + 1))
|
||||
sleep 0.4
|
||||
done
|
||||
|
|
@ -489,10 +524,14 @@ parse_usage() {
|
|||
esac
|
||||
}
|
||||
|
||||
printf '%s' "$HIDE_CURSOR"
|
||||
echo
|
||||
draw_disco 0
|
||||
echo
|
||||
if ((TUI_CHILD)); then
|
||||
:
|
||||
else
|
||||
printf '%s' "$HIDE_CURSOR"
|
||||
echo
|
||||
draw_disco 0
|
||||
echo
|
||||
fi
|
||||
draw_bar
|
||||
|
||||
while ((burned < tokens)); do
|
||||
|
|
|
|||
945
bin/tui.js
Executable file
945
bin/tui.js
Executable file
|
|
@ -0,0 +1,945 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* tui.js — A premium, zero-dependency Terminal User Interface (TUI) for the 'burn' utility.
|
||||
* Runs entirely inside the console with keyboard controls, arrow navigation, live TUI progress,
|
||||
* animated ASCII stick dancer, flickering flames, and humorous corporate OKRs.
|
||||
*/
|
||||
|
||||
const readline = require('readline');
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Colors and Styling Escape Codes
|
||||
const RESET = '\x1b[0m';
|
||||
const BOLD = '\x1b[1m';
|
||||
const DIM = '\x1b[2m';
|
||||
const HIDE_CURSOR = '\x1b[?25l';
|
||||
const SHOW_CURSOR = '\x1b[?25h';
|
||||
const ENTER_ALT_SCREEN = '\x1b[?1049h';
|
||||
const EXIT_ALT_SCREEN = '\x1b[?1049l';
|
||||
const CLEAR_SCREEN = '\x1b[2J\x1b[H';
|
||||
const CLEAR_TO_END = '\x1b[J';
|
||||
const CURSOR_TO_TOP = '\x1b[H';
|
||||
|
||||
// 8-bit ANSI Colors for high fidelity (bright & vivid)
|
||||
const ORANGE = '\x1b[38;5;208m';
|
||||
const MAGENTA = '\x1b[38;5;165m';
|
||||
const CYAN = '\x1b[38;5;45m';
|
||||
const RED = '\x1b[38;5;196m';
|
||||
const YELLOW = '\x1b[38;5;220m';
|
||||
const GREEN = '\x1b[38;5;82m';
|
||||
const VIOLET = '\x1b[38;5;99m';
|
||||
|
||||
// Configuration Database
|
||||
const BACKEND_MODELS = {
|
||||
claude: [
|
||||
{ id: 'haiku', name: 'Claude Haiku', desc: 'Cheap & Light (Intern tier) 🔥' },
|
||||
{ id: 'sonnet', name: 'Claude Sonnet', desc: 'Heavy Padding (Manager pleaser) 💸' },
|
||||
{ id: 'opus', name: 'Claude Opus', desc: 'Director Melter (Immediate review) 🌋' }
|
||||
],
|
||||
codex: [
|
||||
{ id: 'gpt-5.5', name: 'GPT 5.5 Turbo', desc: 'Sovereign Core (Premium burn) 🤖' },
|
||||
{ id: 'gpt-5.4', name: 'GPT 5.4 High', desc: 'Aggressive Billing (VC Bait) 📈' },
|
||||
{ id: 'gpt-5.4-nano', name: 'GPT 5.4 Nano', desc: 'Budget Combuster (Hobbyist) 🪵' }
|
||||
]
|
||||
};
|
||||
|
||||
const PRESETS = [10000, 50000, 100000, 500000, 'custom'];
|
||||
|
||||
const ACHIEVEMENTS = [
|
||||
"Master of Corporate Theater",
|
||||
"Lead Vibe Architect",
|
||||
"SVP Budget Destroyer",
|
||||
"OKR Padding Champion",
|
||||
"Vanity Metrics Titan",
|
||||
"Nihilism Promotion Pioneer"
|
||||
];
|
||||
|
||||
// Application State
|
||||
let activeScreen = 'config'; // 'config', 'burn', 'victory'
|
||||
let focusedItem = 'backend'; // 'backend', 'model', 'tokens', 'ignite'
|
||||
let backend = 'claude';
|
||||
let activeModelIdx = 0;
|
||||
let selectedPresetIdx = 1; // 50k default
|
||||
let customTokens = 250000;
|
||||
let isWarningNihilist = false;
|
||||
let isSmallScreen = false;
|
||||
|
||||
// Burn Stats
|
||||
let activeChild = null;
|
||||
let burnedTokens = 0;
|
||||
let targetTokens = 50000;
|
||||
let burnPercentage = 0;
|
||||
let costString = '$0.000000';
|
||||
let iterationCalls = 0;
|
||||
let burnSpeed = 0;
|
||||
let telemetryLogs = [];
|
||||
let burnCompletedSuccessfully = true;
|
||||
|
||||
// Timing metrics for speed calculations
|
||||
let lastTime = 0;
|
||||
let lastBurned = 0;
|
||||
|
||||
// Animation frames state
|
||||
let dancerFrame = 0;
|
||||
let renderTimer = null;
|
||||
let pulseTimerCount = 0;
|
||||
let cleanupComplete = false;
|
||||
let initialRawMode = false;
|
||||
let returnToConfigTimer = null;
|
||||
let burnWasAborted = false;
|
||||
|
||||
// ASCII Dancer Poses (John Travolta sway postures)
|
||||
const DANCER_POSES = [
|
||||
// Pose 0: Right hand pointing up-right, left on hip
|
||||
[
|
||||
` * 🪩 * `,
|
||||
` /|\\ `,
|
||||
` / `,
|
||||
` o/ `,
|
||||
` <| `,
|
||||
` | `,
|
||||
` / \\ `,
|
||||
` ▓▒▓▒▓▒▓▒▓▒▓▒▓▒▓ `
|
||||
],
|
||||
// Pose 1: Lean left Travolta
|
||||
[
|
||||
` * 🪩 * `,
|
||||
` \\|/ `,
|
||||
` / `,
|
||||
` o/ `,
|
||||
` <| `,
|
||||
` | `,
|
||||
` / \\ `,
|
||||
` ▒▓▒▓▒▓▒▓▒▓▒▓▒▓▒ `
|
||||
],
|
||||
// Pose 2: Left hand pointing up-left, right on hip
|
||||
[
|
||||
` * 🪩 * `,
|
||||
` /|\\ `,
|
||||
` \\ `,
|
||||
` \\o `,
|
||||
` |> `,
|
||||
` | `,
|
||||
` / \\ `,
|
||||
` ▓▒▓▒▓▒▓▒▓▒▓▒▓▒▓ `
|
||||
],
|
||||
// Pose 3: Lean right Travolta
|
||||
[
|
||||
` * 🪩 * `,
|
||||
` \\|/ `,
|
||||
` \\ `,
|
||||
` \\o `,
|
||||
` |> `,
|
||||
` | `,
|
||||
` / \\ `,
|
||||
` ▒▓▒▓▒▓▒▓▒▓▒▓▒▓▒ `
|
||||
]
|
||||
];
|
||||
|
||||
function stripAnsi(value) {
|
||||
return String(value)
|
||||
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, '')
|
||||
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
|
||||
.replace(/\x1b[@-Z\\-_]/g, '')
|
||||
.replace(/\x9b[0-?]*[ -/]*[@-~]/g, '')
|
||||
.replace(/\x07/g, '')
|
||||
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
||||
}
|
||||
|
||||
function charWidth(char) {
|
||||
const code = char.codePointAt(0);
|
||||
if (!code || code < 32 || (code >= 0x7f && code < 0xa0)) return 0;
|
||||
if (
|
||||
code === 0x200d ||
|
||||
(code >= 0x0300 && code <= 0x036f) ||
|
||||
(code >= 0x1ab0 && code <= 0x1aff) ||
|
||||
(code >= 0x1dc0 && code <= 0x1dff) ||
|
||||
(code >= 0x20d0 && code <= 0x20ff) ||
|
||||
(code >= 0xfe00 && code <= 0xfe0f) ||
|
||||
(code >= 0xfe20 && code <= 0xfe2f)
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
if (
|
||||
code >= 0x1100 && (
|
||||
code <= 0x115f ||
|
||||
code === 0x2329 ||
|
||||
code === 0x232a ||
|
||||
(code >= 0x2e80 && code <= 0xa4cf) ||
|
||||
(code >= 0xac00 && code <= 0xd7a3) ||
|
||||
(code >= 0xf900 && code <= 0xfaff) ||
|
||||
(code >= 0xfe10 && code <= 0xfe19) ||
|
||||
(code >= 0xfe30 && code <= 0xfe6f) ||
|
||||
(code >= 0xff00 && code <= 0xff60) ||
|
||||
(code >= 0xffe0 && code <= 0xffe6)
|
||||
)
|
||||
) {
|
||||
return 2;
|
||||
}
|
||||
if ((code >= 0x1f000 && code <= 0x1faff) || (code >= 0x2600 && code <= 0x27bf)) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
function visibleWidth(value) {
|
||||
let width = 0;
|
||||
for (const char of Array.from(stripAnsi(value))) {
|
||||
width += charWidth(char);
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
function readAnsiSequence(value, index) {
|
||||
if (value[index] !== '\x1b') return null;
|
||||
const next = value[index + 1];
|
||||
if (!next) return { sequence: '', nextIndex: index + 1 };
|
||||
|
||||
if (next === '[') {
|
||||
let end = index + 2;
|
||||
while (end < value.length && !/[@-~]/.test(value[end])) end++;
|
||||
return { sequence: value.slice(index, Math.min(end + 1, value.length)), nextIndex: Math.min(end + 1, value.length) };
|
||||
}
|
||||
|
||||
if (next === ']') {
|
||||
let end = index + 2;
|
||||
while (end < value.length) {
|
||||
if (value[end] === '\x07') {
|
||||
end++;
|
||||
break;
|
||||
}
|
||||
if (value[end] === '\x1b' && value[end + 1] === '\\') {
|
||||
end += 2;
|
||||
break;
|
||||
}
|
||||
end++;
|
||||
}
|
||||
return { sequence: value.slice(index, end), nextIndex: end };
|
||||
}
|
||||
|
||||
return { sequence: value.slice(index, index + 2), nextIndex: index + 2 };
|
||||
}
|
||||
|
||||
function fitAnsiLine(value, maxWidth) {
|
||||
if (maxWidth <= 0) return '';
|
||||
const text = String(value);
|
||||
let output = '';
|
||||
let width = 0;
|
||||
let truncated = false;
|
||||
|
||||
for (let i = 0; i < text.length;) {
|
||||
const ansi = readAnsiSequence(text, i);
|
||||
if (ansi) {
|
||||
output += ansi.sequence;
|
||||
i = ansi.nextIndex;
|
||||
continue;
|
||||
}
|
||||
|
||||
const codePoint = text.codePointAt(i);
|
||||
const char = String.fromCodePoint(codePoint);
|
||||
const nextWidth = charWidth(char);
|
||||
if (width + nextWidth > maxWidth) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
output += char;
|
||||
width += nextWidth;
|
||||
i += char.length;
|
||||
}
|
||||
|
||||
return truncated ? `${output}${RESET}` : output;
|
||||
}
|
||||
|
||||
function truncateVisible(value, maxWidth) {
|
||||
if (maxWidth <= 0) return '';
|
||||
let output = '';
|
||||
let width = 0;
|
||||
for (const char of Array.from(stripAnsi(value))) {
|
||||
const nextWidth = charWidth(char);
|
||||
if (width + nextWidth > maxWidth) break;
|
||||
output += char;
|
||||
width += nextWidth;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function padVisible(value, maxWidth) {
|
||||
const truncated = truncateVisible(value, maxWidth);
|
||||
return truncated + ' '.repeat(Math.max(0, maxWidth - visibleWidth(truncated)));
|
||||
}
|
||||
|
||||
function splitBufferedLines(buffer, data) {
|
||||
const next = buffer + data.toString('utf8');
|
||||
const lines = next.split(/\r\n|\r|\n/);
|
||||
return { lines: lines.slice(0, -1), rest: lines[lines.length - 1] };
|
||||
}
|
||||
|
||||
function costDisplay() {
|
||||
return costString === 'cost n/a' ? costString : `${costString} USD`;
|
||||
}
|
||||
|
||||
function isCoreVisualLine(line) {
|
||||
return (
|
||||
line.includes('▲') ||
|
||||
line.includes('░░░') ||
|
||||
line.includes('🪩') ||
|
||||
line.includes('┌───────') ||
|
||||
line.includes('└───────') ||
|
||||
line.includes('▓') ||
|
||||
line.includes('▒') ||
|
||||
(line.includes('│') && (
|
||||
line.includes('o/') ||
|
||||
line.includes('<|') ||
|
||||
line.includes('/ \\') ||
|
||||
line.includes('o\\') ||
|
||||
line.includes('|\\') ||
|
||||
line.includes('|<')
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
function addTelemetryLine(line) {
|
||||
const cleanLine = stripAnsi(line).replace(/\t/g, ' ').trim();
|
||||
if (!cleanLine || isCoreVisualLine(cleanLine)) return;
|
||||
|
||||
if (cleanLine.includes('[██') || cleanLine.includes('[░░') || cleanLine.startsWith('BURN_PROGRESS')) {
|
||||
return;
|
||||
}
|
||||
|
||||
telemetryLogs.push(cleanLine);
|
||||
if (telemetryLogs.length > 5) {
|
||||
telemetryLogs.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// Clean scroll helper: adds sanitized lines and keeps the latest telemetry.
|
||||
function appendTelemetry(text) {
|
||||
String(text).split(/\r\n|\r|\n/).forEach(addTelemetryLine);
|
||||
}
|
||||
|
||||
function updateProgress(pct, burned, target, cost, calls) {
|
||||
if (!Number.isFinite(pct) || !Number.isFinite(burned) || !Number.isFinite(target) || !Number.isFinite(calls)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
burnPercentage = Math.max(0, Math.min(100, Math.trunc(pct)));
|
||||
burnedTokens = Math.max(0, Math.trunc(burned));
|
||||
targetTokens = Math.max(1, Math.trunc(target));
|
||||
costString = cost || 'cost n/a';
|
||||
iterationCalls = Math.max(0, Math.trunc(calls));
|
||||
|
||||
const now = Date.now();
|
||||
const dt = (now - lastTime) / 1000;
|
||||
const dTokens = burnedTokens - lastBurned;
|
||||
if (dt >= 0.5 && dTokens > 0) {
|
||||
burnSpeed = Math.round(dTokens / dt);
|
||||
lastTime = now;
|
||||
lastBurned = burnedTokens;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function parseProgressLine(rawLine) {
|
||||
const cleanLine = stripAnsi(rawLine).trim();
|
||||
if (!cleanLine) return false;
|
||||
|
||||
if (cleanLine.startsWith('BURN_PROGRESS\t')) {
|
||||
const parts = cleanLine.split('\t');
|
||||
const parsed = updateProgress(
|
||||
Number(parts[1]),
|
||||
Number(parts[2]),
|
||||
Number(parts[3]),
|
||||
parts[4],
|
||||
Number(parts[5])
|
||||
);
|
||||
const status = parts.slice(6).join('\t').trim();
|
||||
if (status) appendTelemetry(`progress ${status}`);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const match = cleanLine.match(/\[([█░]+)\]\s*(\d+)%\s*•\s*([\d,]+)\/([\d,]+)\s*tokens\s*•\s*(\$\d+(?:\.\d+)?|cost n\/a)\s*•\s*(\d+)\s*call\(s\)/);
|
||||
if (!match) return false;
|
||||
|
||||
return updateProgress(
|
||||
Number(match[2]),
|
||||
Number(match[3].replace(/,/g, '')),
|
||||
Number(match[4].replace(/,/g, '')),
|
||||
match[5],
|
||||
Number(match[6])
|
||||
);
|
||||
}
|
||||
|
||||
function handleChildStdoutLine(line) {
|
||||
if (!parseProgressLine(line)) {
|
||||
appendTelemetry(line);
|
||||
}
|
||||
}
|
||||
|
||||
function handleChildStderrLine(line) {
|
||||
const cleanLine = stripAnsi(line).trim();
|
||||
if (cleanLine) appendTelemetry(`[stderr] ${cleanLine}`);
|
||||
}
|
||||
|
||||
// Flickering flames strip generator
|
||||
function getFlickeringFlames(width, tick) {
|
||||
const flameChars = ['▲', ' ', '▲', '▲', ' '];
|
||||
const colors = [RED, ORANGE, YELLOW];
|
||||
let strip = '';
|
||||
for (let i = 0; i < width; i++) {
|
||||
const char = flameChars[(i + tick) % flameChars.length];
|
||||
const color = colors[(i + tick) % colors.length];
|
||||
strip += `${color}${char}${RESET}`;
|
||||
}
|
||||
return strip;
|
||||
}
|
||||
|
||||
// Manage TUI transitions and reset layout safely
|
||||
function setScreen(newScreen) {
|
||||
activeScreen = newScreen;
|
||||
process.stdout.write(CLEAR_SCREEN);
|
||||
}
|
||||
|
||||
// Initialization and keystroke capture
|
||||
function startTUI() {
|
||||
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
||||
console.error('error: the TUI requires an interactive terminal.');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
initialRawMode = Boolean(process.stdin.isRaw);
|
||||
process.stdin.setRawMode(true);
|
||||
readline.emitKeypressEvents(process.stdin);
|
||||
process.stdin.resume();
|
||||
process.stdout.write(ENTER_ALT_SCREEN + HIDE_CURSOR + CLEAR_SCREEN);
|
||||
|
||||
// Capture keystrokes
|
||||
process.stdin.on('keypress', (str, key) => {
|
||||
if ((key && key.ctrl && key.name === 'c') || str === '\u0003') {
|
||||
exitTUI(130);
|
||||
}
|
||||
|
||||
if (activeScreen === 'config') {
|
||||
handleConfigKey(str, key);
|
||||
} else if (activeScreen === 'burn') {
|
||||
handleBurnKey(str, key);
|
||||
} else if (activeScreen === 'victory') {
|
||||
handleVictoryKey(str, key);
|
||||
}
|
||||
});
|
||||
|
||||
// Start 150ms render loop for smooth flicker-free animations (6.6 FPS)
|
||||
renderTimer = setInterval(() => {
|
||||
pulseTimerCount++;
|
||||
drawScreen();
|
||||
}, 150);
|
||||
|
||||
process.once('SIGINT', () => exitTUI(130));
|
||||
process.once('SIGTERM', () => exitTUI(143));
|
||||
process.once('uncaughtException', (error) => {
|
||||
cleanup();
|
||||
console.error(error && error.stack ? error.stack : error);
|
||||
process.exit(1);
|
||||
});
|
||||
process.once('exit', () => {
|
||||
if (!cleanupComplete) {
|
||||
try {
|
||||
process.stdout.write(SHOW_CURSOR + RESET + EXIT_ALT_SCREEN);
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function exitTUI(code = 0) {
|
||||
cleanup();
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (cleanupComplete) return;
|
||||
cleanupComplete = true;
|
||||
stopAudioFeed();
|
||||
if (returnToConfigTimer) {
|
||||
clearTimeout(returnToConfigTimer);
|
||||
returnToConfigTimer = null;
|
||||
}
|
||||
if (activeChild) {
|
||||
try {
|
||||
process.kill(-activeChild.pid, 'SIGINT');
|
||||
} catch (e) {}
|
||||
activeChild = null;
|
||||
}
|
||||
if (renderTimer) {
|
||||
clearInterval(renderTimer);
|
||||
renderTimer = null;
|
||||
}
|
||||
if (process.stdin.isTTY) {
|
||||
try {
|
||||
process.stdin.setRawMode(initialRawMode);
|
||||
} catch (e) {}
|
||||
process.stdin.pause();
|
||||
}
|
||||
process.stdout.write(CLEAR_SCREEN + SHOW_CURSOR + RESET + EXIT_ALT_SCREEN);
|
||||
}
|
||||
|
||||
// Config Screen Key Handlers
|
||||
function handleConfigKey(str, key) {
|
||||
const isUp = (key && key.name === 'up') || str === '\u001b[A' || str === '\u001bOA';
|
||||
const isDown = (key && key.name === 'down') || str === '\u001b[B' || str === '\u001bOB';
|
||||
const isLeft = (key && key.name === 'left') || str === '\u001b[D' || str === '\u001bOD';
|
||||
const isRight = (key && key.name === 'right') || str === '\u001b[C' || str === '\u001bOC';
|
||||
const isEnter = (key && (key.name === 'return' || key.name === 'enter')) || str === '\r' || str === '\n';
|
||||
const isSpace = (key && key.name === 'space') || str === ' ';
|
||||
const isEscape = (key && key.name === 'escape') || str === '\u001b';
|
||||
|
||||
if (isWarningNihilist) {
|
||||
if (isEnter || str === 'y' || str === 'Y') {
|
||||
isWarningNihilist = false;
|
||||
selectedPresetIdx = 4; // locks custom preset slider
|
||||
customTokens = 1000000;
|
||||
focusedItem = 'ignite';
|
||||
} else {
|
||||
isWarningNihilist = false;
|
||||
selectedPresetIdx = 1; // resets back to 50k
|
||||
focusedItem = 'tokens';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Standard Navigation
|
||||
if (isUp) {
|
||||
const fields = ['backend', 'model', 'tokens', 'ignite'];
|
||||
let idx = fields.indexOf(focusedItem) - 1;
|
||||
if (idx < 0) idx = fields.length - 1;
|
||||
focusedItem = fields[idx];
|
||||
} else if (isDown) {
|
||||
const fields = ['backend', 'model', 'tokens', 'ignite'];
|
||||
let idx = (fields.indexOf(focusedItem) + 1) % fields.length;
|
||||
focusedItem = fields[idx];
|
||||
}
|
||||
|
||||
// Toggling options
|
||||
if (focusedItem === 'backend') {
|
||||
if (isLeft || isRight || isSpace) {
|
||||
backend = backend === 'claude' ? 'codex' : 'claude';
|
||||
activeModelIdx = 0; // reset model index
|
||||
}
|
||||
} else if (focusedItem === 'model') {
|
||||
const modelsCount = BACKEND_MODELS[backend].length;
|
||||
if (isLeft) {
|
||||
activeModelIdx = (activeModelIdx - 1 + modelsCount) % modelsCount;
|
||||
} else if (isRight || isSpace) {
|
||||
activeModelIdx = (activeModelIdx + 1) % modelsCount;
|
||||
}
|
||||
} else if (focusedItem === 'tokens') {
|
||||
if (isLeft) {
|
||||
if (selectedPresetIdx === 4 && customTokens > 10000) {
|
||||
customTokens -= 25000;
|
||||
} else if (selectedPresetIdx > 0) {
|
||||
selectedPresetIdx--;
|
||||
}
|
||||
} else if (isRight) {
|
||||
if (selectedPresetIdx === 4 && customTokens < 1000000) {
|
||||
customTokens += 25000;
|
||||
if (customTokens >= 1000000) {
|
||||
isWarningNihilist = true;
|
||||
}
|
||||
} else if (selectedPresetIdx < PRESETS.length - 1) {
|
||||
selectedPresetIdx++;
|
||||
if (PRESETS[selectedPresetIdx] === 'custom' && customTokens >= 1000000) {
|
||||
isWarningNihilist = true;
|
||||
}
|
||||
}
|
||||
} else if (isSpace) {
|
||||
selectedPresetIdx = (selectedPresetIdx + 1) % PRESETS.length;
|
||||
if (PRESETS[selectedPresetIdx] === 'custom' && customTokens >= 1000000) {
|
||||
isWarningNihilist = true;
|
||||
}
|
||||
}
|
||||
} else if (focusedItem === 'ignite') {
|
||||
if (isEnter || isSpace) {
|
||||
igniteCombustion();
|
||||
}
|
||||
}
|
||||
|
||||
// Q/Esc exit triggers
|
||||
if (isEscape || str === 'q' || str === 'Q') {
|
||||
exitTUI(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Active Combustion Screen Key Handlers
|
||||
function handleBurnKey(str, key) {
|
||||
const isSpace = (key && key.name === 'space') || str === ' ';
|
||||
const isEscape = (key && key.name === 'escape') || str === '\u001b';
|
||||
|
||||
if (isSpace || isEscape) {
|
||||
appendTelemetry(`${RED}⚠️ Interrupted by user. Extinguishing core...${RESET}`);
|
||||
abortBurn();
|
||||
}
|
||||
}
|
||||
|
||||
// Victory Screen Key Handlers
|
||||
function handleVictoryKey(str, key) {
|
||||
// Return to configuration deck on any keypress
|
||||
setScreen('config');
|
||||
focusedItem = 'backend';
|
||||
isWarningNihilist = false;
|
||||
}
|
||||
|
||||
// Spawning and Running the Burn Core
|
||||
function igniteCombustion() {
|
||||
setScreen('burn');
|
||||
|
||||
burnWasAborted = false;
|
||||
targetTokens = selectedPresetIdx === 4 ? customTokens : PRESETS[selectedPresetIdx];
|
||||
burnedTokens = 0;
|
||||
burnPercentage = 0;
|
||||
costString = 'cost n/a';
|
||||
iterationCalls = 0;
|
||||
burnSpeed = 0;
|
||||
telemetryLogs = [];
|
||||
lastTime = Date.now();
|
||||
lastBurned = 0;
|
||||
|
||||
appendTelemetry(`🚀 [system] Charging token furnace. Target: ${targetTokens.toLocaleString()}...`);
|
||||
|
||||
const modelsList = BACKEND_MODELS[backend];
|
||||
const modelId = modelsList[activeModelIdx].id;
|
||||
|
||||
const scriptPath = path.join(__dirname, 'burn');
|
||||
if (!fs.existsSync(scriptPath)) {
|
||||
appendTelemetry(`❌ [system] Burn script not found: ${scriptPath}`);
|
||||
burnCompletedSuccessfully = false;
|
||||
returnToConfigTimer = setTimeout(() => {
|
||||
if (activeScreen === 'burn') setScreen('config');
|
||||
}, 2500);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start synthesizers
|
||||
startAudioFeed();
|
||||
|
||||
// Spawn child script
|
||||
activeChild = spawn('bash', [scriptPath, targetTokens.toString(), '--backend', backend, '--model', modelId], {
|
||||
detached: true,
|
||||
env: {
|
||||
...process.env,
|
||||
BURN_TUI_CHILD: '1',
|
||||
PAGER: 'cat'
|
||||
}
|
||||
});
|
||||
|
||||
let stdoutBuffer = '';
|
||||
let stderrBuffer = '';
|
||||
|
||||
activeChild.stdout.on('data', (data) => {
|
||||
const parsed = splitBufferedLines(stdoutBuffer, data);
|
||||
stdoutBuffer = parsed.rest;
|
||||
parsed.lines.forEach(handleChildStdoutLine);
|
||||
});
|
||||
|
||||
activeChild.stdout.on('end', () => {
|
||||
if (stdoutBuffer.trim()) handleChildStdoutLine(stdoutBuffer);
|
||||
stdoutBuffer = '';
|
||||
});
|
||||
|
||||
activeChild.stderr.on('data', (data) => {
|
||||
const parsed = splitBufferedLines(stderrBuffer, data);
|
||||
stderrBuffer = parsed.rest;
|
||||
parsed.lines.forEach(handleChildStderrLine);
|
||||
});
|
||||
|
||||
activeChild.stderr.on('end', () => {
|
||||
if (stderrBuffer.trim()) handleChildStderrLine(stderrBuffer);
|
||||
stderrBuffer = '';
|
||||
});
|
||||
|
||||
activeChild.on('error', (error) => {
|
||||
activeChild = null;
|
||||
stopAudioFeed();
|
||||
burnCompletedSuccessfully = false;
|
||||
appendTelemetry(`❌ [system] Could not start burn process: ${error.message}`);
|
||||
returnToConfigTimer = setTimeout(() => {
|
||||
if (activeScreen === 'burn') setScreen('config');
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
activeChild.on('close', (code) => {
|
||||
activeChild = null;
|
||||
stopAudioFeed();
|
||||
if (returnToConfigTimer) {
|
||||
clearTimeout(returnToConfigTimer);
|
||||
returnToConfigTimer = null;
|
||||
}
|
||||
if (code === 0 && !burnWasAborted) {
|
||||
burnCompletedSuccessfully = true;
|
||||
setScreen('victory');
|
||||
} else {
|
||||
burnCompletedSuccessfully = false;
|
||||
appendTelemetry(
|
||||
burnWasAborted
|
||||
? '🛑 [system] Combustion aborted.'
|
||||
: `❌ [system] Process terminated with exit code: ${code}`
|
||||
);
|
||||
returnToConfigTimer = setTimeout(() => {
|
||||
returnToConfigTimer = null;
|
||||
if (activeScreen === 'burn') setScreen('config');
|
||||
}, burnWasAborted ? 900 : 3000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function abortBurn() {
|
||||
if (activeChild) {
|
||||
burnWasAborted = true;
|
||||
stopAudioFeed();
|
||||
try {
|
||||
process.kill(-activeChild.pid, 'SIGINT');
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Web Audio API Synthesizer (Buzzer/Bleep simulation using raw stderr triggers)
|
||||
// Hitting standard Mac terminal bells inside terminal TUIs is funny!
|
||||
function startAudioFeed() {
|
||||
// Inside terminal, we can issue an audit bell using \x07!
|
||||
// It gives a delightful retro console beep on iterations. We'll beep!
|
||||
process.stdout.write('\x07');
|
||||
}
|
||||
|
||||
function stopAudioFeed() {
|
||||
// No active timers to clear for shell beeping
|
||||
}
|
||||
|
||||
|
||||
// DOUBLE-BUFFER RENDERING SYSTEM
|
||||
// Compiles full frame into buffer string and draws once to prevent flicker.
|
||||
function drawScreen() {
|
||||
const rows = process.stdout.rows || 24;
|
||||
const cols = process.stdout.columns || 80;
|
||||
|
||||
// Strict UI limits
|
||||
const minRows = 18;
|
||||
const minCols = 80;
|
||||
|
||||
if (rows < minRows || cols < minCols) {
|
||||
if (!isSmallScreen) {
|
||||
process.stdout.write(CLEAR_SCREEN);
|
||||
isSmallScreen = true;
|
||||
}
|
||||
|
||||
let s = CURSOR_TO_TOP;
|
||||
s += `\n${RED}${BOLD} ⚠️ TERMINAL WINDOW TOO SMALL${RESET}\n\n`;
|
||||
s += ` Current size: ${BOLD}${cols}x${rows}${RESET}\n`;
|
||||
s += ` Required min: ${BOLD}${minCols}x${minRows}${RESET}\n\n`;
|
||||
s += ` ${DIM}Please enlarge your terminal window or decrease font size.${RESET}\n`;
|
||||
s += ` ${DIM}Or press [Q] to exit.${RESET}\n`;
|
||||
|
||||
// Wipe visual trails in remaining window space
|
||||
for (let i = 8; i < rows; i++) {
|
||||
s += '\n';
|
||||
}
|
||||
|
||||
const lines = s.split('\n');
|
||||
const cleaned = lines.map(line => fitAnsiLine(line, cols) + '\x1b[K').join('\n');
|
||||
process.stdout.write(cleaned + CLEAR_TO_END);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSmallScreen) {
|
||||
process.stdout.write(CLEAR_SCREEN);
|
||||
isSmallScreen = false;
|
||||
}
|
||||
|
||||
let buf = CURSOR_TO_TOP;
|
||||
let rawContent = '';
|
||||
|
||||
if (activeScreen === 'config') {
|
||||
rawContent = compileConfigScreen();
|
||||
} else if (activeScreen === 'burn') {
|
||||
rawContent = compileBurnScreen();
|
||||
} else if (activeScreen === 'victory') {
|
||||
rawContent = compileVictoryScreen();
|
||||
}
|
||||
|
||||
// Split and add \x1b[K to every line to wipe trailing characters, then write.
|
||||
const lines = rawContent.split('\n');
|
||||
const cleaned = lines.map(line => fitAnsiLine(line, cols) + '\x1b[K').join('\n');
|
||||
process.stdout.write(buf + cleaned + CLEAR_TO_END);
|
||||
}
|
||||
|
||||
// 1. Compile Screen: Config
|
||||
function compileConfigScreen() {
|
||||
let s = '';
|
||||
|
||||
s += `${ORANGE}┌────────────────────────────────────────────────────────┐${RESET}\n`;
|
||||
s += `${ORANGE}│${RESET} ${BOLD}${RED}🔥 B U R N , B A B Y , B U R N (TUI CORE) 🔥${RESET} ${ORANGE}│${RESET}\n`;
|
||||
s += `${ORANGE}└────────────────────────────────────────────────────────┘${RESET}\n`;
|
||||
|
||||
// Section 1: Backend Selection
|
||||
const bSel = focusedItem === 'backend' ? `${ORANGE}▶${RESET} ` : ' ';
|
||||
const claudeBullet = backend === 'claude' ? `${ORANGE}●${RESET}` : ' ';
|
||||
const codexBullet = backend === 'codex' ? `${ORANGE}●${RESET}` : ' ';
|
||||
s += `${bSel}${BOLD}🌐 BACKEND:${RESET} [${claudeBullet}] Claude Code (3.5) [${codexBullet}] OpenAI Codex (GPT-5)\n`;
|
||||
|
||||
// Section 2: Model Selection
|
||||
const mSel = focusedItem === 'model' ? `${ORANGE}▶${RESET} ` : ' ';
|
||||
s += `${mSel}${BOLD}🌋 MODEL:${RESET} `;
|
||||
const models = BACKEND_MODELS[backend];
|
||||
models.forEach((m, idx) => {
|
||||
const isSelected = activeModelIdx === idx;
|
||||
const bullet = isSelected ? `${YELLOW}🔘${RESET}` : '⚪';
|
||||
const style = isSelected ? `${BOLD}${YELLOW}` : '';
|
||||
const shortName = m.name.replace('Claude ', '').replace('GPT ', '');
|
||||
s += `${bullet} ${style}${shortName}${RESET} `;
|
||||
});
|
||||
s += '\n';
|
||||
|
||||
// Section 3: Target Selection
|
||||
const tSel = focusedItem === 'tokens' ? `${ORANGE}▶${RESET} ` : ' ';
|
||||
s += `${tSel}${BOLD}💸 TARGET:${RESET} `;
|
||||
PRESETS.forEach((p, idx) => {
|
||||
const isSelected = selectedPresetIdx === idx;
|
||||
let label = p === 'custom' ? '[Custom]' : `[${(p/1000)}k]`;
|
||||
const color = isSelected ? GREEN : (focusedItem === 'tokens' ? RESET : DIM);
|
||||
const wrap = isSelected ? `${BOLD}${color}` : `${DIM}`;
|
||||
s += `${wrap}${label}${RESET} `;
|
||||
});
|
||||
s += '\n';
|
||||
|
||||
// Description / Custom Slider
|
||||
if (selectedPresetIdx === 4) {
|
||||
const maxVal = 1000000;
|
||||
const minVal = 10000;
|
||||
const pct = (customTokens - minVal) / (maxVal - minVal);
|
||||
const sliderWidth = 15;
|
||||
const filled = Math.round(pct * sliderWidth);
|
||||
const empty = sliderWidth - filled;
|
||||
const sliderBar = `${RED}█${RESET}`.repeat(filled) + `${DIM}░${RESET}`.repeat(empty);
|
||||
|
||||
let desc = '';
|
||||
if (customTokens <= 100000) desc = "Satisfactory OKR padding.";
|
||||
else if (customTokens <= 500000) desc = "Significant metrics!";
|
||||
else desc = "EXTREME nihilism! CEO promotion or fire. 🌋";
|
||||
|
||||
s += ` ${BOLD}${ORANGE}Slider:${RESET} <─${sliderBar}─> ${BOLD}${GREEN}${customTokens.toLocaleString()}${RESET} tokens (${DIM}${desc}${RESET})\n`;
|
||||
} else {
|
||||
const modelsList = BACKEND_MODELS[backend];
|
||||
s += ` ${DIM}Desc: ${modelsList[activeModelIdx].desc}${RESET}\n`;
|
||||
}
|
||||
|
||||
s += `${DIM}────────────────────────────────────────────────────────${RESET}\n`;
|
||||
s += `${DIM}[Arrow keys] Navigate & Tune [Space/Enter] Cycle [Q] Exit${RESET}\n`;
|
||||
|
||||
// Action Button
|
||||
const igniteFocus = focusedItem === 'ignite';
|
||||
const igniteText = igniteFocus
|
||||
? `${BOLD}${RED}👉 [ IGNITE COMBUSTION ] 👈${RESET}`
|
||||
: `${ORANGE}[ IGNITE COMBUSTION ]${RESET}`;
|
||||
s += ` ${igniteText}\n`;
|
||||
|
||||
// Nihilist Warning Overlay (Drawn inline inside the TUI boundaries)
|
||||
if (isWarningNihilist) {
|
||||
s += `${RED}${BOLD}⚠️ WARNING: Proceed with audits? [Y]es / [N]o:${RESET}`;
|
||||
} else {
|
||||
s += '';
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
// 2. Compile Screen: Active Burn
|
||||
function compileBurnScreen() {
|
||||
let s = '';
|
||||
|
||||
s += `${RED}🔥 ${BOLD}${YELLOW}ACTIVE COMBUSTION ARENA${RESET} ${RED}•${RESET} ${CYAN}${backend.toUpperCase()} (${BACKEND_MODELS[backend][activeModelIdx].id})${RESET}\n`;
|
||||
|
||||
// Two Column Layout
|
||||
const dancerLines = DANCER_POSES[dancerFrame];
|
||||
|
||||
// Left Column (Disco Dancer + Fire)
|
||||
const leftCol = [];
|
||||
dancerLines.forEach(l => leftCol.push(l));
|
||||
leftCol.push(getFlickeringFlames(17, pulseTimerCount)); // ground fire
|
||||
|
||||
// Right Column (Metrics & Odometer)
|
||||
const pct = Math.min(100, burnPercentage);
|
||||
const barWidth = 10;
|
||||
const filled = Math.round((pct / 100) * barWidth);
|
||||
const empty = barWidth - filled;
|
||||
const progressBar = `${GREEN}█${RESET}`.repeat(filled) + `${DIM}░${RESET}`.repeat(empty);
|
||||
|
||||
const okrDraw = (val, maxRatio) => {
|
||||
const ratio = Math.min(100, Math.round(val * maxRatio));
|
||||
const width = 6;
|
||||
const f = Math.round((ratio / 100) * width);
|
||||
const e = width - f;
|
||||
return `[${`${GREEN}█${RESET}`.repeat(f)}${`${DIM}░${RESET}`.repeat(e)}] ${ratio}%`;
|
||||
};
|
||||
|
||||
const rightCol = [];
|
||||
rightCol.push(`${BOLD}${CYAN}PROGRESS TELEMETRY:${RESET}`);
|
||||
rightCol.push(`${BOLD}Burned:${RESET} ${BOLD}${GREEN}${burnedTokens.toLocaleString()}${RESET} / ${targetTokens.toLocaleString()} tokens`);
|
||||
rightCol.push(`${BOLD}Pct:${RESET} ${BOLD}${GREEN}${pct}%${RESET} [${progressBar}]`);
|
||||
rightCol.push(`${BOLD}Spent:${RESET} ${BOLD}${GREEN}${costDisplay()}${RESET} | Speed: ${CYAN}${burnSpeed.toLocaleString()}${RESET} t/s`);
|
||||
rightCol.push(`${BOLD}Calls:${RESET} ${BOLD}${VIOLET}${iterationCalls}${RESET} completed`);
|
||||
rightCol.push(`${BOLD}${YELLOW}OKRs:${RESET} Ladder: ${okrDraw(pct, 1.4)}`);
|
||||
rightCol.push(` Manager: ${okrDraw(pct, 2.5)}`);
|
||||
rightCol.push(` VC Depth:${okrDraw(pct, 4.0)}`);
|
||||
rightCol.push(`${DIM}Press [Space] to Extinguish Flames (Abort)${RESET}`);
|
||||
|
||||
// Merge columns
|
||||
const maxLines = Math.max(leftCol.length, rightCol.length);
|
||||
for (let i = 0; i < maxLines; i++) {
|
||||
const leftPart = leftCol[i] || ' ';
|
||||
const rightPart = rightCol[i] || '';
|
||||
s += ` ${leftPart} ${rightPart}\n`;
|
||||
}
|
||||
|
||||
// Bottom nested sub-console window
|
||||
s += `${DIM}┌── LIVE TELEMETRY STREAM ─────────────────────────────┐${RESET}\n`;
|
||||
|
||||
// Render latest clean scrolling logs
|
||||
const displayLogs = [...telemetryLogs];
|
||||
while (displayLogs.length < 3) {
|
||||
displayLogs.push(' > awaiting next iteration telemetry...');
|
||||
}
|
||||
displayLogs.slice(-3).forEach(l => {
|
||||
s += `${DIM}│${RESET} ${padVisible(l, 52)} ${DIM}│${RESET}\n`;
|
||||
});
|
||||
s += `${DIM}└──────────────────────────────────────────────────────┘${RESET}\n`;
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
// 3. Compile Screen: Victory
|
||||
function compileVictoryScreen() {
|
||||
let s = '';
|
||||
|
||||
s += `${GREEN}┌────────────────────────────────────────────────────────┐${RESET}\n`;
|
||||
s += `${GREEN}│${RESET} ${BOLD}${GREEN}🏆 C O M B U S T I O N C O M P L E T E D ! 🏆${RESET} ${GREEN}│${RESET}\n`;
|
||||
s += `${GREEN}└────────────────────────────────────────────────────────┘${RESET}\n`;
|
||||
|
||||
let badgeIdx = targetTokens % ACHIEVEMENTS.length;
|
||||
const badgeName = ACHIEVEMENTS[badgeIdx];
|
||||
s += ` ${BOLD}${YELLOW}Achievement Unlocked:${RESET} 👉 "${BOLD}${ORANGE}${badgeName}${RESET}"\n`;
|
||||
|
||||
s += ` ${CYAN}${BOLD}OFFICIAL RECEIPTS:${RESET}\n`;
|
||||
s += ` 🔥 Tokens Torched: ${BOLD}${GREEN}${burnedTokens.toLocaleString()}${RESET} 💰 Cost: ${BOLD}${GREEN}${costDisplay()}${RESET}\n`;
|
||||
s += ` 🕺 Vibes: ${BOLD}Immaculate${RESET} 📈 OKRs: ${BOLD}+420% Ascent${RESET}\n`;
|
||||
|
||||
s += ` ${DIM}Receipt has been charged directly to your corporate bill.${RESET}\n`;
|
||||
s += ` ${DIM}Copy these stats to your next weekly promotion request!${RESET}\n`;
|
||||
|
||||
s += `${DIM}────────────────────────────────────────────────────────${RESET}\n`;
|
||||
s += ` ${BOLD}${GREEN}Press any key to return to Control Deck${RESET}\n`;
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
// KICK IT OFF!
|
||||
startTUI();
|
||||
|
|
@ -10,13 +10,15 @@ mkdir -p "$DEST_DIR"
|
|||
curl -fsSL "$BASE_URL/burn" -o "$DEST_DIR/burn"
|
||||
# Sourced by burn when using --backend codex to compute estimated cost.
|
||||
curl -fsSL "$BASE_URL/codex_rates.sh" -o "$DEST_DIR/codex_rates.sh"
|
||||
chmod +x "$DEST_DIR/burn"
|
||||
curl -fsSL "$BASE_URL/tui.js" -o "$DEST_DIR/tui.js"
|
||||
chmod +x "$DEST_DIR/burn" "$DEST_DIR/tui.js"
|
||||
|
||||
echo "✅ installed burn → $DEST_DIR/burn"
|
||||
|
||||
case ":${PATH}:" in
|
||||
*":${DEST_DIR}:"*)
|
||||
echo "🔥 ready to go: run 'burn 10000' to torch some tokens."
|
||||
echo "🕹️ or run 'burn' with no arguments to open the TUI."
|
||||
;;
|
||||
*)
|
||||
echo
|
||||
|
|
@ -25,6 +27,6 @@ case ":${PATH}:" in
|
|||
echo
|
||||
echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
|
||||
echo
|
||||
echo " then reload your shell and run 'burn 10000'."
|
||||
echo " then reload your shell and run 'burn 10000' or just 'burn' for the TUI."
|
||||
;;
|
||||
esac
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue