whetstone-cli 3.1.3

Installer and CLI for Claude Code token optimization (Headroom + RTK + Memory)
mod changelog;
mod cli;
mod config;
mod dashboard;
mod db;
mod doctor;
mod headroom;
mod integrations;
mod memory;
mod migrate;
mod preflight;
mod release;
mod rtk;
mod setup;
mod shell;
mod stats;
mod ui;
mod uninstall;
mod update;
mod version;
mod wizard;
mod wrapper;

use clap::Parser;
use cli::{Cli, Command};

fn show_upgrade_banner(cmd: &Option<Command>) {
    let skip = matches!(
        cmd,
        Some(
            Command::Update { .. }
                | Command::Version
                | Command::Dashboard
                | Command::Stats
                | Command::Migrate { .. }
        )
    );
    if skip {
        return;
    }
    let outdated = update::check_cached_upgrade();
    let v2_project = migrate::detect()
        .map(|d| d.needs_migration())
        .unwrap_or(false);
    ui::upgrade_banner(&outdated, v2_project);
}

fn main() {
    let cli = Cli::parse();

    show_upgrade_banner(&cli.command);

    match cli.command {
        None => {
            wrapper::wrap_claude(&[]);
        }
        Some(cmd) => match cmd {
            Command::Setup {
                full,
                headroom_extras,
            } => {
                if let Err(e) = setup::run(full, &headroom_extras) {
                    ui::fail(&format!("{e:#}"));
                }
            }
            Command::Uninstall => {
                if let Err(e) = uninstall::run() {
                    ui::fail(&format!("{e:#}"));
                }
            }
            Command::Claude { args } | Command::Code { args } => {
                wrapper::wrap_claude(&args);
            }
            Command::Proxy { args } => {
                wrapper::wrap_proxy(&args);
            }
            Command::Rtk { args } => {
                wrapper::wrap_rtk(&args);
            }
            Command::Version => {
                let outdated = update::check_cached_upgrade();
                let is_outdated = |name: &str| outdated.iter().any(|c| c.name == name);

                let entries = vec![
                    ui::VersionEntry {
                        name: "whetstone",
                        version: Some(version::current().to_string()),
                        outdated: is_outdated("whetstone"),
                    },
                    ui::VersionEntry {
                        name: "headroom",
                        version: headroom::installed_version(),
                        outdated: is_outdated("headroom"),
                    },
                    ui::VersionEntry {
                        name: "rtk",
                        version: rtk::installed_version(),
                        outdated: is_outdated("rtk"),
                    },
                ];
                ui::version_report(&entries);
            }
            Command::Update { full } => {
                if let Err(e) = update::run(full) {
                    ui::fail(&format!("{e:#}"));
                }
            }
            Command::Dashboard => {
                if let Err(e) = dashboard::run() {
                    ui::fail(&format!("{e:#}"));
                }
            }
            Command::Doctor => {
                if let Err(e) = doctor::run() {
                    ui::fail(&format!("{e:#}"));
                }
            }
            Command::Release { action } => {
                if let Err(e) = release::run(&action) {
                    ui::fail(&format!("{e:#}"));
                }
            }
            Command::ReleasePublish { action } => {
                if let Err(e) = release::run_publish(&action) {
                    ui::fail(&format!("{e:#}"));
                }
            }
            Command::ChangelogSync {
                input,
                output,
                limit,
            } => {
                if let Err(e) = run_changelog_sync(input, output, limit) {
                    ui::fail(&format!("{e:#}"));
                }
            }
            Command::Stats => {
                if let Err(e) = stats::run() {
                    ui::fail(&format!("{e:#}"));
                }
            }
            Command::Db { action } => {
                if let Err(e) = db::dispatch(action) {
                    ui::fail(&e.to_string());
                }
            }
            Command::Migrate {
                dry_run,
                yes,
                rollback,
            } => {
                let result = match rollback {
                    Some(id) => migrate::rollback(&id),
                    None => migrate::run(migrate::MigrateOptions { dry_run, yes }),
                };
                if let Err(e) = result {
                    ui::fail(&format!("{e:#}"));
                }
            }
        },
    }
}

fn run_changelog_sync(
    input: Option<String>,
    output: Option<String>,
    limit: usize,
) -> anyhow::Result<()> {
    let cwd = std::env::current_dir()?;
    let input_path = input
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|| cwd.join("CHANGELOG.md"));
    let output_path = output
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|| cwd.join("site/src/changelog.js"));

    let entries = changelog::parse_file(&input_path, limit)?;
    let js = changelog::render_js(&entries);

    if let Some(parent) = output_path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(&output_path, js)?;

    ui::ok(&format!(
        "changelog: {} -> {} ({} releases)",
        input_path.display(),
        output_path.display(),
        entries.len()
    ));
    Ok(())
}