UNPKG

@chongdashu/cc-statusline

Version:

Interactive CLI tool for generating custom Claude Code statuslines

877 lines (844 loc) 33.5 kB
#!/usr/bin/env node var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // node_modules/tsup/assets/esm_shims.js import path from "path"; import { fileURLToPath } from "url"; var init_esm_shims = __esm({ "node_modules/tsup/assets/esm_shims.js"() { "use strict"; } }); // src/utils/tester.ts var tester_exports = {}; __export(tester_exports, { analyzeTestResult: () => analyzeTestResult, generateMockCcusageOutput: () => generateMockCcusageOutput, generateMockClaudeInput: () => generateMockClaudeInput, testStatuslineScript: () => testStatuslineScript }); import { spawn } from "child_process"; import { promises as fs2 } from "fs"; import path3 from "path"; async function testStatuslineScript(script, mockData) { const startTime = Date.now(); try { const tempDir = "/tmp"; const scriptPath = path3.join(tempDir, `statusline-test-${Date.now()}.sh`); await fs2.writeFile(scriptPath, script, { mode: 493 }); const input = mockData || generateMockClaudeInput(); const result = await executeScript(scriptPath, JSON.stringify(input)); await fs2.unlink(scriptPath).catch(() => { }); const executionTime = Date.now() - startTime; return { success: result.success, output: result.output, error: result.error, executionTime }; } catch (error) { return { success: false, output: "", error: error instanceof Error ? error.message : String(error), executionTime: Date.now() - startTime }; } } function generateMockClaudeInput(config) { return { session_id: "test-session-123", transcript_path: "/home/user/.claude/conversations/test.jsonl", cwd: "/home/user/projects/my-project", workspace: { current_dir: "/home/user/projects/my-project" }, model: { id: "claude-opus-4-1-20250805", display_name: "Opus 4.1", version: "20250805" } }; } function generateMockCcusageOutput() { return { blocks: [ { id: "2025-08-13T08:00:00.000Z", startTime: "2025-08-13T08:00:00.000Z", endTime: "2025-08-13T13:00:00.000Z", usageLimitResetTime: "2025-08-13T13:00:00.000Z", actualEndTime: "2025-08-13T09:30:34.698Z", isActive: true, isGap: false, entries: 12, tokenCounts: { inputTokens: 1250, outputTokens: 2830, cacheCreationInputTokens: 15e3, cacheReadInputTokens: 45e3 }, totalTokens: 64080, costUSD: 3.42, models: ["claude-opus-4-1-20250805"], burnRate: { tokensPerMinute: 850.5, tokensPerMinuteForIndicator: 850, costPerHour: 12.45 }, projection: { totalTokens: 128e3, totalCost: 6.84, remainingMinutes: 210 } } ] }; } async function executeScript(scriptPath, input) { return new Promise((resolve) => { const process2 = spawn("bash", [scriptPath], { stdio: ["pipe", "pipe", "pipe"] }); let stdout = ""; let stderr = ""; process2.stdout.on("data", (data) => { stdout += data.toString(); }); process2.stderr.on("data", (data) => { stderr += data.toString(); }); process2.on("close", (code) => { resolve({ success: code === 0, output: stdout.trim(), error: stderr.trim() || void 0 }); }); process2.on("error", (err) => { resolve({ success: false, output: "", error: err.message }); }); process2.stdin.write(input); process2.stdin.end(); setTimeout(() => { process2.kill(); resolve({ success: false, output: stdout, error: "Script execution timed out (5s)" }); }, 5e3); }); } function analyzeTestResult(result, config) { const issues = []; const suggestions = []; let performance; if (result.executionTime > 1e3) { performance = "timeout"; issues.push("Script execution is very slow (>1s)"); } else if (result.executionTime > 500) { performance = "slow"; issues.push("Script execution is slow (>500ms)"); } else if (result.executionTime > 100) { performance = "good"; } else { performance = "excellent"; } let hasRequiredFeatures = true; if (config.features.includes("directory") && !result.output.includes("projects")) { hasRequiredFeatures = false; issues.push("Directory feature not working properly"); } if (config.features.includes("model") && !result.output.includes("Opus")) { hasRequiredFeatures = false; issues.push("Model feature not working properly"); } if (config.features.includes("git") && config.ccusageIntegration && !result.output.includes("git")) { suggestions.push("Git integration may require actual git repository"); } if (result.error) { issues.push(`Script errors: ${result.error}`); } if (!result.success) { issues.push("Script failed to execute successfully"); } if (config.features.length > 6) { suggestions.push("Consider reducing number of features for better performance"); } if (config.ccusageIntegration && result.executionTime > 200) { suggestions.push("ccusage integration may slow down statusline - consider caching"); } return { performance, hasRequiredFeatures, issues, suggestions }; } var init_tester = __esm({ "src/utils/tester.ts"() { "use strict"; init_esm_shims(); } }); // src/cli/preview.ts var preview_exports = {}; __export(preview_exports, { previewCommand: () => previewCommand }); import { promises as fs3 } from "fs"; import chalk2 from "chalk"; import ora2 from "ora"; async function previewCommand(scriptPath) { console.log(chalk2.cyan("\u{1F50D} Statusline Preview Mode\n")); let script; try { const spinner = ora2(`Loading statusline script from ${scriptPath}...`).start(); script = await fs3.readFile(scriptPath, "utf-8"); spinner.succeed("Script loaded!"); const headerMatch = script.match(/# Theme: (\w+) \| Colors: (\w+) \| Features: ([^\n]+)/i); if (headerMatch) { console.log(chalk2.yellow("Detected Configuration:")); console.log(` Theme: ${headerMatch[1]}`); console.log(` Colors: ${headerMatch[2]}`); console.log(` Features: ${headerMatch[3]} `); } const generationMatch = script.match(/# Generated by cc-statusline.*\n# Custom Claude Code statusline - Created: ([^\n]+)/i); if (generationMatch) { console.log(chalk2.gray(`Generated: ${generationMatch[1]} `)); } } catch (error) { console.error(chalk2.red(`\u274C Failed to load script: ${error instanceof Error ? error.message : String(error)}`)); return; } const testSpinner = ora2("Testing statusline with mock data...").start(); const mockInput = generateMockClaudeInput(); console.log(chalk2.gray("\nMock Claude Code Input:")); console.log(chalk2.gray(JSON.stringify(mockInput, null, 2))); const testResult = await testStatuslineScript(script, mockInput); if (testResult.success) { testSpinner.succeed(`Test completed in ${testResult.executionTime}ms`); console.log(chalk2.green("\n\u2705 Statusline Output:")); console.log(chalk2.white("\u2501".repeat(60))); console.log(testResult.output); console.log(chalk2.white("\u2501".repeat(60))); console.log(chalk2.cyan(` \u{1F4CA} Performance: ${getPerformanceEmoji(getPerformanceLevel(testResult.executionTime))} ${getPerformanceLevel(testResult.executionTime)} (${testResult.executionTime}ms)`)); if (testResult.output.includes("\u{1F4C1}") || testResult.output.includes("\u{1F33F}") || testResult.output.includes("\u{1F916}")) { console.log(chalk2.green("\u2705 Statusline features appear to be working")); } else { console.log(chalk2.yellow("\u26A0\uFE0F Basic features may not be displaying correctly")); } } else { testSpinner.fail("Test failed"); console.error(chalk2.red(` \u274C Error: ${testResult.error}`)); if (testResult.output) { console.log(chalk2.gray("\nPartial output:")); console.log(testResult.output); } } console.log(chalk2.green("\n\u2728 Preview complete! Use `cc-statusline init` to generate a new statusline.")); } function getPerformanceEmoji(performance) { switch (performance) { case "excellent": return "\u{1F680}"; case "good": return "\u2705"; case "slow": return "\u26A0\uFE0F"; case "timeout": return "\u{1F40C}"; default: return "\u2753"; } } function getPerformanceLevel(executionTime) { if (executionTime > 1e3) return "timeout"; if (executionTime > 500) return "slow"; if (executionTime > 100) return "good"; return "excellent"; } var init_preview = __esm({ "src/cli/preview.ts"() { "use strict"; init_esm_shims(); init_tester(); } }); // src/index.ts init_esm_shims(); import { Command } from "commander"; // src/cli/commands.ts init_esm_shims(); // src/cli/prompts.ts init_esm_shims(); import inquirer from "inquirer"; async function collectConfiguration() { console.log("\u{1F680} Welcome to cc-statusline! Let's create your custom Claude Code statusline.\n"); console.log("\u2728 All features are enabled by default. Use \u2191/\u2193 arrows to navigate, SPACE to toggle, ENTER to continue.\n"); const config = await inquirer.prompt([ { type: "checkbox", name: "features", message: "Select statusline features (scroll down for more options):", choices: [ { name: "\u{1F4C1} Working Directory", value: "directory", checked: true }, { name: "\u{1F33F} Git Branch", value: "git", checked: true }, { name: "\u{1F916} Model Name & Version", value: "model", checked: true }, { name: "\u{1F9E0} Context Remaining", value: "context", checked: true }, { name: "\u{1F4B5} Usage & Cost", value: "usage", checked: true }, { name: "\u231B Session Time Remaining", value: "session", checked: true }, { name: "\u{1F4CA} Token Statistics", value: "tokens", checked: true }, { name: "\u26A1 Burn Rate (tokens/min)", value: "burnrate", checked: true } ], validate: (answer) => { if (answer.length < 1) { return "You must choose at least one feature."; } return true; }, pageSize: 10 }, { type: "confirm", name: "colors", message: "\n\u{1F3A8} Enable modern color scheme and emojis?", default: true }, { type: "confirm", name: "logging", message: "\n\u{1F4DD} Enable debug logging to .claude/statusline.log?", default: false } ]); return { features: config.features, runtime: "bash", colors: config.colors, theme: "detailed", ccusageIntegration: true, // Always enabled since npx works logging: config.logging, customEmojis: false }; } // src/generators/bash-generator.ts init_esm_shims(); // src/features/colors.ts init_esm_shims(); function generateColorBashCode(config) { if (!config.enabled) { return ` # ---- color helpers (disabled) ---- use_color=0 C() { :; } RST() { :; } `; } return ` # ---- color helpers (force colors for Claude Code) ---- use_color=1 [ -n "$NO_COLOR" ] && use_color=0 C() { if [ "$use_color" -eq 1 ]; then printf '\\033[%sm' "$1"; fi; } RST() { if [ "$use_color" -eq 1 ]; then printf '\\033[0m'; fi; } `; } function generateBasicColors() { return ` # ---- modern sleek colors ---- dir_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;117m'; fi; } # sky blue model_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;147m'; fi; } # light purple version_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;180m'; fi; } # soft yellow cc_version_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;249m'; fi; } # light gray style_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;245m'; fi; } # gray rst() { if [ "$use_color" -eq 1 ]; then printf '\\033[0m'; fi; } `; } // src/features/git.ts init_esm_shims(); function generateGitBashCode(config, colors) { if (!config.enabled) return ""; const colorCode = colors ? ` # ---- git colors ---- git_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;150m'; fi; } # soft green rst() { if [ "$use_color" -eq 1 ]; then printf '\\033[0m'; fi; } ` : ` git_color() { :; } rst() { :; } `; return `${colorCode} # ---- git ---- git_branch="" if git rev-parse --git-dir >/dev/null 2>&1; then git_branch=$(git branch --show-current 2>/dev/null || git rev-parse --short HEAD 2>/dev/null) fi`; } function generateGitUtilities() { return ` # git utilities num_or_zero() { v="$1"; [[ "$v" =~ ^[0-9]+$ ]] && echo "$v" || echo 0; }`; } // src/features/usage.ts init_esm_shims(); function generateUsageBashCode(config, colors) { if (!config.enabled) return ""; const colorCode = colors ? ` # ---- usage colors ---- usage_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;189m'; fi; } # lavender cost_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;222m'; fi; } # light gold burn_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;220m'; fi; } # bright gold session_color() { rem_pct=$(( 100 - session_pct )) if (( rem_pct <= 10 )); then SCLR='38;5;210' # light pink elif (( rem_pct <= 25 )); then SCLR='38;5;228' # light yellow else SCLR='38;5;194'; fi # light green if [ "$use_color" -eq 1 ]; then printf '\\033[%sm' "$SCLR"; fi } ` : ` usage_color() { :; } cost_color() { :; } burn_color() { :; } session_color() { :; } `; return `${colorCode} # ---- ccusage integration ---- session_txt=""; session_pct=0; session_bar="" cost_usd=""; cost_per_hour=""; tpm=""; tot_tokens="" if command -v jq >/dev/null 2>&1; then blocks_output=$(npx ccusage@latest blocks --json 2>/dev/null || ccusage blocks --json 2>/dev/null) if [ -n "$blocks_output" ]; then active_block=$(echo "$blocks_output" | jq -c '.blocks[] | select(.isActive == true)' 2>/dev/null | head -n1) if [ -n "$active_block" ]; then${config.showCost ? ` cost_usd=$(echo "$active_block" | jq -r '.costUSD // empty') cost_per_hour=$(echo "$active_block" | jq -r '.burnRate.costPerHour // empty')` : ""}${config.showTokens ? ` tot_tokens=$(echo "$active_block" | jq -r '.totalTokens // empty')` : ""}${config.showBurnRate ? ` tpm=$(echo "$active_block" | jq -r '.burnRate.tokensPerMinute // empty')` : ""}${config.showSession || config.showProgressBar ? ` # Session time calculation reset_time_str=$(echo "$active_block" | jq -r '.usageLimitResetTime // .endTime // empty') start_time_str=$(echo "$active_block" | jq -r '.startTime // empty') if [ -n "$reset_time_str" ] && [ -n "$start_time_str" ]; then start_sec=$(to_epoch "$start_time_str"); end_sec=$(to_epoch "$reset_time_str"); now_sec=$(date +%s) total=$(( end_sec - start_sec )); (( total<1 )) && total=1 elapsed=$(( now_sec - start_sec )); (( elapsed<0 ))&&elapsed=0; (( elapsed>total ))&&elapsed=$total session_pct=$(( elapsed * 100 / total )) remaining=$(( end_sec - now_sec )); (( remaining<0 )) && remaining=0 rh=$(( remaining / 3600 )); rm=$(( (remaining % 3600) / 60 )) end_hm=$(fmt_time_hm "$end_sec")${config.showSession ? ` session_txt="$(printf '%dh %dm until reset at %s (%d%%)' "$rh" "$rm" "$end_hm" "$session_pct")"` : ""}${config.showProgressBar ? ` session_bar=$(progress_bar "$session_pct" 10)` : ""} fi` : ""} fi fi fi`; } function generateUsageUtilities() { return ` # ---- time helpers ---- to_epoch() { ts="$1" if command -v gdate >/dev/null 2>&1; then gdate -d "$ts" +%s 2>/dev/null && return; fi date -u -j -f "%Y-%m-%dT%H:%M:%S%z" "\${ts/Z/+0000}" +%s 2>/dev/null && return python3 - "$ts" <<'PY' 2>/dev/null import sys, datetime s=sys.argv[1].replace('Z','+00:00') print(int(datetime.datetime.fromisoformat(s).timestamp())) PY } fmt_time_hm() { epoch="$1" if date -r 0 +%s >/dev/null 2>&1; then date -r "$epoch" +"%H:%M"; else date -d "@$epoch" +"%H:%M"; fi } progress_bar() { pct="\${1:-0}"; width="\${2:-10}" [[ "$pct" =~ ^[0-9]+$ ]] || pct=0; ((pct<0))&&pct=0; ((pct>100))&&pct=100 filled=$(( pct * width / 100 )); empty=$(( width - filled )) printf '%*s' "$filled" '' | tr ' ' '=' printf '%*s' "$empty" '' | tr ' ' '-' }`; } // src/generators/bash-generator.ts function generateBashStatusline(config) { const hasGit = config.features.includes("git"); const hasUsage = config.features.some((f) => ["usage", "session", "tokens", "burnrate"].includes(f)); const hasDirectory = config.features.includes("directory"); const hasModel = config.features.includes("model"); const hasContext = config.features.includes("context"); const usageConfig = { enabled: hasUsage && config.ccusageIntegration, showCost: config.features.includes("usage"), showTokens: config.features.includes("tokens"), showBurnRate: config.features.includes("burnrate"), showSession: config.features.includes("session"), showProgressBar: config.theme !== "minimal" && config.features.includes("session") }; const gitConfig = { enabled: hasGit, showBranch: hasGit, showChanges: false, // Removed delta changes per user request compactMode: config.theme === "compact" }; const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const script = `#!/bin/bash # Generated by cc-statusline (https://www.npmjs.com/package/@chongdashu/cc-statusline) # Custom Claude Code statusline - Created: ${timestamp} # Theme: ${config.theme} | Colors: ${config.colors} | Features: ${config.features.join(", ")} ${config.logging ? generateLoggingCode() : ""} input=$(cat) ${generateColorBashCode({ enabled: config.colors, theme: config.theme })} ${config.colors ? generateBasicColors() : ""} ${hasUsage ? generateUsageUtilities() : ""} ${hasGit ? generateGitUtilities() : ""} ${generateBasicDataExtraction(hasDirectory, hasModel, hasContext)} ${hasGit ? generateGitBashCode(gitConfig, config.colors) : ""} ${hasContext ? generateContextBashCode(config.colors) : ""} ${hasUsage ? generateUsageBashCode(usageConfig, config.colors) : ""} ${config.logging ? generateLoggingOutput() : ""} ${generateDisplaySection(config, gitConfig, usageConfig)} `; return script.replace(/\n\n\n+/g, "\n\n").trim() + "\n"; } function generateLoggingCode() { return ` LOG_FILE="\${HOME}/.claude/statusline.log" TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') # ---- logging ---- { echo "[$TIMESTAMP] Status line triggered with input:" (echo "$input" | jq . 2>/dev/null) || echo "$input" echo "---" } >> "$LOG_FILE" 2>/dev/null `; } function generateBasicDataExtraction(hasDirectory, hasModel, hasContext) { return ` # ---- basics ---- if command -v jq >/dev/null 2>&1; then${hasDirectory ? ` current_dir=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // "unknown"' 2>/dev/null | sed "s|^$HOME|~|g")` : ""}${hasModel ? ` model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"' 2>/dev/null) model_version=$(echo "$input" | jq -r '.model.version // ""' 2>/dev/null)` : ""}${hasContext ? ` session_id=$(echo "$input" | jq -r '.session_id // ""' 2>/dev/null)` : ""} cc_version=$(echo "$input" | jq -r '.version // ""' 2>/dev/null) output_style=$(echo "$input" | jq -r '.output_style.name // ""' 2>/dev/null) else${hasDirectory ? ` current_dir="unknown"` : ""}${hasModel ? ` model_name="Claude"; model_version=""` : ""}${hasContext ? ` session_id=""` : ""} cc_version="" output_style="" fi `; } function generateContextBashCode(colors) { return ` # ---- context window calculation ---- context_pct="" context_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[1;37m'; fi; } # default white # Determine max context based on model get_max_context() { local model_name="$1" case "$model_name" in *"Opus 4"*|*"opus 4"*|*"Opus"*|*"opus"*) echo "200000" # 200K for all Opus versions ;; *"Sonnet 4"*|*"sonnet 4"*|*"Sonnet 3.5"*|*"sonnet 3.5"*|*"Sonnet"*|*"sonnet"*) echo "200000" # 200K for Sonnet 3.5+ and 4.x ;; *"Haiku 3.5"*|*"haiku 3.5"*|*"Haiku 4"*|*"haiku 4"*|*"Haiku"*|*"haiku"*) echo "200000" # 200K for modern Haiku ;; *"Claude 3 Haiku"*|*"claude 3 haiku"*) echo "100000" # 100K for original Claude 3 Haiku ;; *) echo "200000" # Default to 200K ;; esac } if [ -n "$session_id" ] && command -v jq >/dev/null 2>&1; then MAX_CONTEXT=$(get_max_context "$model_name") # Convert current dir to session file path project_dir=$(echo "$current_dir" | sed "s|~|$HOME|g" | sed 's|/|-|g' | sed 's|^-||') session_file="$HOME/.claude/projects/-\${project_dir}/\${session_id}.jsonl" if [ -f "$session_file" ]; then # Get the latest input token count from the session file latest_tokens=$(tail -20 "$session_file" | jq -r 'select(.message.usage) | .message.usage | ((.input_tokens // 0) + (.cache_read_input_tokens // 0))' 2>/dev/null | tail -1) if [ -n "$latest_tokens" ] && [ "$latest_tokens" -gt 0 ]; then context_used_pct=$(( latest_tokens * 100 / MAX_CONTEXT )) context_remaining_pct=$(( 100 - context_used_pct )) # Set color based on remaining percentage if [ "$context_remaining_pct" -le 20 ]; then context_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;203m'; fi; } # coral red elif [ "$context_remaining_pct" -le 40 ]; then context_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;215m'; fi; } # peach else context_color() { if [ "$use_color" -eq 1 ]; then printf '\\033[38;5;158m'; fi; } # mint green fi context_pct="\${context_remaining_pct}%" fi fi fi `; } function generateLoggingOutput() { return ` # ---- log extracted data ---- { echo "[$TIMESTAMP] Extracted: dir=\${current_dir:-}, model=\${model_name:-}, version=\${model_version:-}, git=\${git_branch:-}, context=\${context_pct:-}, cost=\${cost_usd:-}, cost_ph=\${cost_per_hour:-}, tokens=\${tot_tokens:-}, tpm=\${tpm:-}, session_pct=\${session_pct:-}" } >> "$LOG_FILE" 2>/dev/null `; } function generateDisplaySection(config, gitConfig, usageConfig) { const emojis = config.colors && !config.customEmojis; return ` # ---- render statusline ---- # Line 1: Core info (directory, git, model, claude code version, output style) ${config.features.includes("directory") ? `printf '\u{1F4C1} %s%s%s' "$(dir_color)" "$current_dir" "$(rst)"` : ""}${gitConfig.enabled ? ` if [ -n "$git_branch" ]; then printf ' \u{1F33F} %s%s%s' "$(git_color)" "$git_branch" "$(rst)" fi` : ""}${config.features.includes("model") ? ` printf ' \u{1F916} %s%s%s' "$(model_color)" "$model_name" "$(rst)" if [ -n "$model_version" ] && [ "$model_version" != "null" ]; then printf ' \u{1F3F7}\uFE0F %s%s%s' "$(version_color)" "$model_version" "$(rst)" fi` : ""} if [ -n "$cc_version" ] && [ "$cc_version" != "null" ]; then printf ' \u{1F4DF} %sv%s%s' "$(cc_version_color)" "$cc_version" "$(rst)" fi if [ -n "$output_style" ] && [ "$output_style" != "null" ]; then printf ' \u{1F3A8} %s%s%s' "$(style_color)" "$output_style" "$(rst)" fi # Line 2: Context and session time line2=""${config.features.includes("context") ? ` if [ -n "$context_pct" ]; then context_bar=$(progress_bar "$context_remaining_pct" 10) line2="\u{1F9E0} $(context_color)Context Remaining: \${context_pct} [\${context_bar}]$(rst)" fi` : ""}${usageConfig.showSession ? ` if [ -n "$session_txt" ]; then if [ -n "$line2" ]; then line2="$line2 \u231B $(session_color)\${session_txt}$(rst) $(session_color)[\${session_bar}]$(rst)" else line2="\u231B $(session_color)\${session_txt}$(rst) $(session_color)[\${session_bar}]$(rst)" fi fi` : ""}${config.features.includes("context") ? ` if [ -z "$line2" ] && [ -z "$context_pct" ]; then line2="\u{1F9E0} $(context_color)Context Remaining: TBD$(rst)" fi` : ""} # Line 3: Cost and usage analytics line3=""${usageConfig.showCost ? ` if [ -n "$cost_usd" ] && [[ "$cost_usd" =~ ^[0-9.]+$ ]]; then${usageConfig.showBurnRate ? ` if [ -n "$cost_per_hour" ] && [[ "$cost_per_hour" =~ ^[0-9.]+$ ]]; then cost_per_hour_formatted=$(printf '%.2f' "$cost_per_hour") line3="\u{1F4B0} $(cost_color)\\$$(printf '%.2f' \\"$cost_usd\\")$(rst) ($(burn_color)\\$\${cost_per_hour_formatted}/h$(rst))" else line3="\u{1F4B0} $(cost_color)\\$$(printf '%.2f' \\"$cost_usd\\")$(rst)" fi` : ` line3="\u{1F4B0} $(cost_color)\\$$(printf '%.2f' \\"$cost_usd\\")$(rst)"`} fi` : ""}${usageConfig.showTokens ? ` if [ -n "$tot_tokens" ] && [[ "$tot_tokens" =~ ^[0-9]+$ ]]; then${usageConfig.showBurnRate ? ` if [ -n "$tpm" ] && [[ "$tpm" =~ ^[0-9.]+$ ]]; then tpm_formatted=$(printf '%.0f' "$tpm") if [ -n "$line3" ]; then line3="$line3 \u{1F4CA} $(usage_color)\${tot_tokens} tok (\${tpm_formatted} tpm)$(rst)" else line3="\u{1F4CA} $(usage_color)\${tot_tokens} tok (\${tpm_formatted} tpm)$(rst)" fi else if [ -n "$line3" ]; then line3="$line3 \u{1F4CA} $(usage_color)\${tot_tokens} tok$(rst)" else line3="\u{1F4CA} $(usage_color)\${tot_tokens} tok$(rst)" fi fi` : ` if [ -n "$line3" ]; then line3="$line3 \u{1F4CA} $(usage_color)\${tot_tokens} tok$(rst)" else line3="\u{1F4CA} $(usage_color)\${tot_tokens} tok$(rst)" fi`} fi` : ""} # Print lines if [ -n "$line2" ]; then printf '\\n%s' "$line2" fi if [ -n "$line3" ]; then printf '\\n%s' "$line3" fi printf '\\n'`; } // src/utils/validator.ts init_esm_shims(); function validateConfig(config) { const errors = []; const warnings = []; if (!config.features || config.features.length === 0) { errors.push("At least one display feature must be selected"); } if (!["bash", "python", "node"].includes(config.runtime)) { errors.push(`Invalid runtime: ${config.runtime}`); } if (!["minimal", "detailed", "compact"].includes(config.theme)) { errors.push(`Invalid theme: ${config.theme}`); } const usageFeatures = ["usage", "session", "tokens", "burnrate"]; const hasUsageFeatures = config.features.some((f) => usageFeatures.includes(f)); if (hasUsageFeatures && !config.ccusageIntegration) { warnings.push("Usage features selected but ccusage integration is disabled. Some features may not work properly."); } if (config.features.length > 5) { warnings.push("Many features selected. This may impact statusline performance."); } if (config.customEmojis && !config.colors) { warnings.push("Custom emojis enabled but colors disabled. Visual distinction may be limited."); } return { isValid: errors.length === 0, errors, warnings }; } // src/utils/installer.ts init_esm_shims(); import { promises as fs } from "fs"; import path2 from "path"; async function installStatusline(script, outputPath, config) { try { const dir = path2.dirname(outputPath); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(outputPath, script, { mode: 493 }); await updateSettingsJson(dir, path2.basename(outputPath)); } catch (error) { throw new Error(`Failed to install statusline: ${error instanceof Error ? error.message : String(error)}`); } } async function updateSettingsJson(claudeDir, scriptName) { const settingsPath = path2.join(claudeDir, "settings.json"); try { let settings = {}; try { const settingsContent = await fs.readFile(settingsPath, "utf-8"); settings = JSON.parse(settingsContent); } catch { } settings.statusLine = { type: "command", command: `.claude/${scriptName}`, padding: 0 }; await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2)); } catch (error) { console.warn(`Warning: Could not update settings.json: ${error instanceof Error ? error.message : String(error)}`); throw new Error("SETTINGS_UPDATE_FAILED"); } } // src/cli/commands.ts import chalk from "chalk"; import ora from "ora"; import path4 from "path"; async function initCommand(options) { try { const spinner = ora("Initializing statusline generator...").start(); await new Promise((resolve) => setTimeout(resolve, 500)); spinner.stop(); const config = await collectConfiguration(); const validation = validateConfig(config); if (!validation.isValid) { console.error(chalk.red("\u274C Configuration validation failed:")); validation.errors.forEach((error) => console.error(chalk.red(` \u2022 ${error}`))); process.exit(1); } const generationSpinner = ora("Generating statusline script...").start(); const script = generateBashStatusline(config); const filename = "statusline.sh"; generationSpinner.succeed("Statusline script generated!"); console.log(chalk.cyan("\n\u2728 Your statusline will look like:")); console.log(chalk.white("\u2501".repeat(60))); const { testStatuslineScript: testStatuslineScript2, generateMockClaudeInput: generateMockClaudeInput2 } = await Promise.resolve().then(() => (init_tester(), tester_exports)); const mockInput = generateMockClaudeInput2(); const testResult = await testStatuslineScript2(script, mockInput); if (testResult.success) { console.log(testResult.output); } else { console.log(chalk.gray("\u{1F4C1} ~/projects/my-app \u{1F33F} main \u{1F916} Claude \u{1F4B5} $2.48 ($12.50/h)")); console.log(chalk.gray("(Preview unavailable - will work when Claude Code runs it)")); } console.log(chalk.white("\u2501".repeat(60))); const outputPath = options.output || `./.claude/${filename}`; const resolvedPath = path4.resolve(outputPath); if (options.install !== false) { const installSpinner = ora("Installing statusline...").start(); try { await installStatusline(script, resolvedPath, config); installSpinner.succeed("\u2705 Statusline installed!"); console.log(chalk.green("\n\u{1F389} Success! Your custom statusline is ready!")); console.log(chalk.cyan(` \u{1F4C1} Generated file: ${chalk.white(resolvedPath)}`)); console.log(chalk.cyan("\nNext steps:")); console.log(chalk.white(" 1. Restart Claude Code to see your new statusline")); console.log(chalk.white(" 2. Usage statistics work via: npx ccusage@latest")); } catch (error) { installSpinner.fail("Failed to install statusline"); if (error instanceof Error && error.message === "SETTINGS_UPDATE_FAILED") { console.log(chalk.yellow("\n\u26A0\uFE0F Settings.json could not be updated automatically.")); console.log(chalk.cyan("\nManual Configuration Required:")); console.log(chalk.white("Add this to your .claude/settings.json file:")); console.log(chalk.gray("\n{")); console.log(chalk.gray(' "statusLine": {')); console.log(chalk.gray(' "type": "command",')); console.log(chalk.gray(` "command": ".claude/statusline.sh",`)); console.log(chalk.gray(' "padding": 0')); console.log(chalk.gray(" }")); console.log(chalk.gray("}")); console.log(chalk.cyan(` \u{1F4C1} Statusline script saved to: ${chalk.white(resolvedPath)}`)); } else { console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`)); console.log(chalk.cyan(` \u{1F4C1} You can manually save the script to: ${chalk.white(resolvedPath)}`)); } } } else { console.log(chalk.green("\n\u2705 Statusline generated successfully!")); console.log(chalk.cyan(` \u{1F4C1} Save this script to: ${chalk.white(resolvedPath)}`)); console.log(chalk.cyan("\nThen restart Claude Code to see your new statusline.")); } } catch (error) { console.error(chalk.red("\u274C An error occurred:")); console.error(chalk.red(error instanceof Error ? error.message : String(error))); process.exit(1); } } // src/index.ts import chalk3 from "chalk"; var program = new Command(); program.name("cc-statusline").description("Interactive CLI tool for generating custom Claude Code statuslines").version("1.0.0"); program.command("init").description("Create a custom statusline with interactive prompts").option("-o, --output <path>", "Output path for statusline.sh", "./.claude/statusline.sh").option("--no-install", "Don't automatically install to .claude/statusline.sh").action(initCommand); program.command("preview").description("Preview existing statusline.sh with mock data").argument("<script-path>", "Path to statusline.sh file to preview").action(async (scriptPath) => { const { previewCommand: previewCommand2 } = await Promise.resolve().then(() => (init_preview(), preview_exports)); await previewCommand2(scriptPath); }); program.command("test").description("Test statusline with real Claude Code JSON input").option("-c, --config <path>", "Configuration file to test").action(() => { console.log(chalk3.yellow("Test command coming soon!")); }); if (!process.argv.slice(2).length) { program.outputHelp(); } program.parse(process.argv); //# sourceMappingURL=index.js.map