@chongdashu/cc-statusline
Version:
Interactive CLI tool for generating custom Claude Code statuslines
877 lines (844 loc) • 33.5 kB
JavaScript
#!/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