feat: add CLI status command, rewrite setup wizard, and modernize table renderer
Three interconnected CLI improvements: New status command (cmd/status.go): - Fetches live claude.ai subscription data using the claudeai client - Renders rate-limit windows (5-hour, 7-day all/Opus/Sonnet) with color-coded progress bars: green <50%, orange 50-80%, red >80% - Shows overage spend limits and usage percentage - Handles partial data gracefully (renders what's available) - Clear onboarding guidance when no session key is configured Setup wizard rewrite (cmd/setup.go): - Replace raw bufio.Reader prompts with charmbracelet/huh multi-step form - Three form groups: welcome screen, credentials (session key + admin key with password echo mode), and preferences (time range + theme select) - Pre-populates from existing config, preserves existing keys on empty input - Dracula theme for the form UI - Graceful Ctrl+C handling via huh.ErrUserAborted Table renderer modernization (internal/cli/render.go): - Replace 120-line manual box-drawing table renderer with lipgloss/table - Automatic column width calculation, rounded borders, right-aligned numeric columns (all except first) - Filter out "---" separator sentinels (not supported by lipgloss/table) - Remove unused style variables (valueStyle, costStyle, tokenStyle, warnStyle) and Table.Widths field Config display update (cmd/config_cmd.go): - Show claude.ai session key and org ID in config output Dependencies (go.mod): - Add charmbracelet/huh v0.8.0 for form-based TUI wizards - Upgrade golang.org/x/text v0.3.8 -> v0.23.0 - Add transitive deps: catppuccin/go, harmonica, hashstructure, etc.
This commit is contained in:
161
cmd/setup.go
161
cmd/setup.go
@@ -1,14 +1,15 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"cburn/internal/config"
|
||||
"cburn/internal/source"
|
||||
"cburn/internal/tui/theme"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -23,89 +24,119 @@ func init() {
|
||||
}
|
||||
|
||||
func runSetup(_ *cobra.Command, _ []string) error {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
// Load existing config or defaults
|
||||
cfg, _ := config.Load()
|
||||
|
||||
// Count sessions
|
||||
files, _ := source.ScanDir(flagDataDir)
|
||||
projectCount := source.CountProjects(files)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println(" Welcome to cburn!")
|
||||
fmt.Println()
|
||||
// Pre-populate from existing config
|
||||
var sessionKey, adminKey string
|
||||
days := cfg.General.DefaultDays
|
||||
if days == 0 {
|
||||
days = 30
|
||||
}
|
||||
themeName := cfg.Appearance.Theme
|
||||
if themeName == "" {
|
||||
themeName = "flexoki-dark"
|
||||
}
|
||||
|
||||
// Build welcome description
|
||||
welcomeDesc := "Let's configure your dashboard."
|
||||
if len(files) > 0 {
|
||||
fmt.Printf(" Found %s sessions in %s (%d projects)\n\n",
|
||||
formatNumber(int64(len(files))), flagDataDir, projectCount)
|
||||
welcomeDesc = fmt.Sprintf("Found %d sessions across %d projects in %s.",
|
||||
len(files), projectCount, flagDataDir)
|
||||
}
|
||||
|
||||
// 1. API key
|
||||
fmt.Println(" 1. Anthropic Admin API key")
|
||||
fmt.Println(" For real cost data from the billing API.")
|
||||
existing := config.GetAdminAPIKey(cfg)
|
||||
if existing != "" {
|
||||
fmt.Printf(" Current: %s\n", maskAPIKey(existing))
|
||||
// Build placeholder text showing masked existing values
|
||||
sessionPlaceholder := "sk-ant-sid... (Enter to skip)"
|
||||
if key := config.GetSessionKey(cfg); key != "" {
|
||||
sessionPlaceholder = maskAPIKey(key) + " (Enter to keep)"
|
||||
}
|
||||
fmt.Print(" > ")
|
||||
apiKey, _ := reader.ReadString('\n')
|
||||
apiKey = strings.TrimSpace(apiKey)
|
||||
if apiKey != "" {
|
||||
cfg.AdminAPI.APIKey = apiKey
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// 2. Default time range
|
||||
fmt.Println(" 2. Default time range")
|
||||
fmt.Println(" (1) 7 days")
|
||||
fmt.Println(" (2) 30 days [default]")
|
||||
fmt.Println(" (3) 90 days")
|
||||
fmt.Print(" > ")
|
||||
choice, _ := reader.ReadString('\n')
|
||||
choice = strings.TrimSpace(choice)
|
||||
switch choice {
|
||||
case "1":
|
||||
cfg.General.DefaultDays = 7
|
||||
case "3":
|
||||
cfg.General.DefaultDays = 90
|
||||
default:
|
||||
cfg.General.DefaultDays = 30
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// 3. Theme
|
||||
fmt.Println(" 3. Color theme")
|
||||
fmt.Println(" (1) Flexoki Dark [default]")
|
||||
fmt.Println(" (2) Catppuccin Mocha")
|
||||
fmt.Println(" (3) Tokyo Night")
|
||||
fmt.Println(" (4) Terminal (ANSI 16)")
|
||||
fmt.Print(" > ")
|
||||
themeChoice, _ := reader.ReadString('\n')
|
||||
themeChoice = strings.TrimSpace(themeChoice)
|
||||
switch themeChoice {
|
||||
case "2":
|
||||
cfg.Appearance.Theme = "catppuccin-mocha"
|
||||
case "3":
|
||||
cfg.Appearance.Theme = "tokyo-night"
|
||||
case "4":
|
||||
cfg.Appearance.Theme = "terminal"
|
||||
default:
|
||||
cfg.Appearance.Theme = "flexoki-dark"
|
||||
adminPlaceholder := "sk-ant-admin-... (Enter to skip)"
|
||||
if key := config.GetAdminAPIKey(cfg); key != "" {
|
||||
adminPlaceholder = maskAPIKey(key) + " (Enter to keep)"
|
||||
}
|
||||
|
||||
// Save
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewNote().
|
||||
Title("Welcome to cburn").
|
||||
Description(welcomeDesc).
|
||||
Next(true).
|
||||
NextLabel("Start"),
|
||||
),
|
||||
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Claude.ai session key").
|
||||
Description("For rate-limit and subscription data.\nclaude.ai > DevTools > Application > Cookies > sessionKey").
|
||||
Placeholder(sessionPlaceholder).
|
||||
EchoMode(huh.EchoModePassword).
|
||||
Value(&sessionKey),
|
||||
|
||||
huh.NewInput().
|
||||
Title("Anthropic Admin API key").
|
||||
Description("For real cost data from the billing API.").
|
||||
Placeholder(adminPlaceholder).
|
||||
EchoMode(huh.EchoModePassword).
|
||||
Value(&adminKey),
|
||||
),
|
||||
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[int]().
|
||||
Title("Default time range").
|
||||
Options(
|
||||
huh.NewOption("7 days", 7),
|
||||
huh.NewOption("30 days", 30),
|
||||
huh.NewOption("90 days", 90),
|
||||
).
|
||||
Value(&days),
|
||||
|
||||
huh.NewSelect[string]().
|
||||
Title("Color theme").
|
||||
Options(themeOpts()...).
|
||||
Value(&themeName),
|
||||
),
|
||||
).WithTheme(huh.ThemeDracula())
|
||||
|
||||
if err := form.Run(); err != nil {
|
||||
if errors.Is(err, huh.ErrUserAborted) {
|
||||
fmt.Println("\n Setup cancelled.")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("setup form: %w", err)
|
||||
}
|
||||
|
||||
// Only overwrite keys if the user typed new ones
|
||||
sessionKey = strings.TrimSpace(sessionKey)
|
||||
if sessionKey != "" {
|
||||
cfg.ClaudeAI.SessionKey = sessionKey
|
||||
}
|
||||
adminKey = strings.TrimSpace(adminKey)
|
||||
if adminKey != "" {
|
||||
cfg.AdminAPI.APIKey = adminKey
|
||||
}
|
||||
cfg.General.DefaultDays = days
|
||||
cfg.Appearance.Theme = themeName
|
||||
|
||||
if err := config.Save(cfg); err != nil {
|
||||
return fmt.Errorf("saving config: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf(" Saved to %s\n", config.ConfigPath())
|
||||
fmt.Printf("\n Saved to %s\n", config.Path())
|
||||
fmt.Println(" Run `cburn setup` anytime to reconfigure.")
|
||||
fmt.Println()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func themeOpts() []huh.Option[string] {
|
||||
opts := make([]huh.Option[string], len(theme.All))
|
||||
for i, t := range theme.All {
|
||||
opts[i] = huh.NewOption(t.Name, t.Name)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func maskAPIKey(key string) string {
|
||||
if len(key) > 16 {
|
||||
return key[:8] + "..." + key[len(key)-4:]
|
||||
|
||||
Reference in New Issue
Block a user