starweaver-cli 0.10.0

Command-line interface for Starweaver
Documentation
use serde_json::json;
use starweaver_oauth::{CodexOAuthClient, OAuthStore, redact_record};

use super::{CliService, render_json_lines};
use crate::{CliError, CliResult, args::AuthCommand};

impl CliService {
    pub(super) fn auth(command: AuthCommand) -> CliResult<String> {
        match command {
            AuthCommand::Login(command) => auth_login(command),
            AuthCommand::Status(command) => auth_status(command),
            AuthCommand::Refresh(command) => auth_refresh(command),
            AuthCommand::Logout(command) => auth_logout(command),
            AuthCommand::Doctor(command) => auth_doctor(command),
        }
    }
}

fn oauth_store(auth_file: Option<String>) -> OAuthStore {
    auth_file.map_or_else(OAuthStore::default_store, OAuthStore::new)
}

pub(super) fn oauth_cli_error(error: impl std::fmt::Display) -> CliError {
    CliError::Config(error.to_string())
}

fn auth_login(command: crate::args::AuthProviderCommand) -> CliResult<String> {
    let store = oauth_store(command.auth_file);
    let provider = command.provider;
    if provider != "codex" {
        return Err(CliError::Config(format!(
            "unknown OAuth provider: {provider}"
        )));
    }
    let client = CodexOAuthClient::with_store(store.clone()).map_err(oauth_cli_error)?;
    let runtime =
        tokio::runtime::Runtime::new().map_err(|error| CliError::Run(error.to_string()))?;
    let (device_code, record) = runtime
        .block_on(async {
            let device_code = client.request_device_code().await?;
            eprintln!("Open this URL in your browser and sign in to ChatGPT:");
            eprintln!("{}", device_code.verification_url);
            eprintln!();
            eprintln!("Enter this one-time code:");
            eprintln!("{}", device_code.user_code);
            eprintln!();
            eprintln!("Waiting for browser authorization...");
            let token_code = client
                .poll_device_token(&device_code, command.timeout_seconds)
                .await?;
            let record = client.exchange_device_code(&token_code).await?;
            Ok::<_, starweaver_oauth::OAuthError>((device_code, record))
        })
        .map_err(oauth_cli_error)?;
    store
        .set_provider("codex", record.clone())
        .map_err(oauth_cli_error)?;
    let identity = record
        .account
        .email
        .clone()
        .or_else(|| record.account.chatgpt_user_id.clone())
        .unwrap_or_else(|| "unknown account".to_string());
    let value = json!({
        "provider": "codex",
        "logged_in": true,
        "identity": identity,
        "auth_path": store.path(),
        "verification_url": device_code.verification_url,
    });
    Ok(format!("{}\n", serde_json::to_string(&value)?))
}

fn auth_status(command: crate::args::AuthStatusCommand) -> CliResult<String> {
    let store = oauth_store(command.auth_file);
    let auth = store.load().map_err(oauth_cli_error)?;
    let provider_names = command.provider.map_or_else(
        || auth.providers.keys().cloned().collect::<Vec<_>>(),
        |provider| vec![provider],
    );
    if provider_names.is_empty() {
        let value = json!({
            "auth_path": store.path(),
            "providers": [],
        });
        return Ok(format!("{}\n", serde_json::to_string(&value)?));
    }
    let rows = provider_names
        .into_iter()
        .map(|provider| {
            let record = auth.providers.get(&provider).cloned();
            json!({
                "provider": provider,
                "logged_in": record.is_some(),
                "auth_path": store.path(),
                "record": record.map(|record| record.status_value()),
            })
        })
        .collect::<Vec<_>>();
    render_json_lines(&rows)
}

fn auth_refresh(command: crate::args::AuthProviderCommand) -> CliResult<String> {
    let store = oauth_store(command.auth_file);
    let provider = command.provider;
    if provider != "codex" {
        return Err(CliError::Config(format!(
            "unknown OAuth provider: {provider}"
        )));
    }
    let client = CodexOAuthClient::with_store(store.clone()).map_err(oauth_cli_error)?;
    let record = store
        .get_provider("codex")
        .map_err(oauth_cli_error)?
        .ok_or_else(|| CliError::Config("OAuth provider is not logged in: codex".to_string()))?;
    let runtime =
        tokio::runtime::Runtime::new().map_err(|error| CliError::Run(error.to_string()))?;
    let refreshed = runtime
        .block_on(client.refresh_record(&record))
        .map_err(oauth_cli_error)?;
    store
        .set_provider("codex", refreshed.clone())
        .map_err(oauth_cli_error)?;
    let value = json!({
        "provider": "codex",
        "refreshed": true,
        "auth_path": store.path(),
        "record": refreshed.status_value(),
    });
    Ok(format!("{}\n", serde_json::to_string(&value)?))
}

fn auth_logout(command: crate::args::AuthLogoutCommand) -> CliResult<String> {
    let store = oauth_store(command.auth_file);
    let provider = command.provider;
    if provider != "codex" {
        return Err(CliError::Config(format!(
            "unknown OAuth provider: {provider}"
        )));
    }
    let record = store.get_provider("codex").map_err(oauth_cli_error)?;
    if let Some(record) = record.as_ref().filter(|_| command.revoke) {
        let client = CodexOAuthClient::with_store(store.clone()).map_err(oauth_cli_error)?;
        let runtime =
            tokio::runtime::Runtime::new().map_err(|error| CliError::Run(error.to_string()))?;
        runtime
            .block_on(client.revoke_record(record))
            .map_err(oauth_cli_error)?;
    }
    let removed = store.remove_provider("codex").map_err(oauth_cli_error)?;
    Ok(format!(
        "provider=codex\nremoved={removed}\nrevoked={}\nauth_path={}\n",
        command.revoke && record.is_some(),
        store.path().display()
    ))
}

fn auth_doctor(command: crate::args::AuthDoctorCommand) -> CliResult<String> {
    let store = oauth_store(command.auth_file);
    let auth = store.load().map_err(oauth_cli_error)?;
    let rows = auth
        .providers
        .iter()
        .map(|(provider, record)| {
            json!({
                "provider": provider,
                "auth_path": store.path(),
                "record": redact_record(record),
            })
        })
        .collect::<Vec<_>>();
    if rows.is_empty() {
        let value = json!({
            "auth_path": store.path(),
            "providers": [],
        });
        Ok(format!("{}\n", serde_json::to_string(&value)?))
    } else {
        render_json_lines(&rows)
    }
}