artifactize 0.21.1

The AI-native collaboration layer for one-of-a-kind teammates and their agents: split a project into Artifacts with their own evals, and reuse every review while it still holds.
//! Readiness, maintenance and account command workflows.

use super::{Context, cancellation_listener, print_json};
use artifactize_core::config::{Backend, Provider};
use serde_json::json;
use std::{
    io::{self, Write},
    path::PathBuf,
};

pub(super) async fn tools(
    context: Context,
    options: artifactize_core::diagnostics::ToolCheckOptions,
) -> Result<u8, String> {
    let (cancellation, listener) = cancellation_listener()?;
    let result = artifactize_core::diagnostics::check_tools(
        &context.repo.unwrap_or_else(|| PathBuf::from(".")),
        context.state_dir.as_deref(),
        &options,
        cancellation,
    )
    .await;
    listener.abort();
    let report = result?;
    print_json(&report)?;
    Ok(u8::from(!report.ok))
}

pub(super) async fn login(context: Context, provider: Provider) -> Result<u8, String> {
    let (state, repo) = (context.state_dir.as_deref(), context.repo.as_deref());
    if provider != Provider::Codex {
        let key = super::secret::read(&format!("{provider} API key"))?.parse()?;
        artifactize_core::auth::providers::save_key(state, repo, provider, key).await?;
        if context.json {
            print_json(&json!({ "provider": provider, "saved": true }))?;
        } else {
            writeln!(io::stdout().lock(), "Saved the {provider} API key.")
                .map_err(|e| e.to_string())?;
        }
        return Ok(0);
    }
    artifactize_core::auth::codex::login(state, repo).await?;
    if context.json {
        print_json(&json!({ "provider": "codex", "signed_in": true }))?;
    } else {
        writeln!(io::stdout().lock(), "Signed in with Codex.").map_err(|e| e.to_string())?;
    }
    Ok(0)
}

pub(super) async fn logout(context: Context, provider: Provider) -> Result<u8, String> {
    if provider != Provider::Codex {
        artifactize_core::auth::providers::remove_key(
            context.state_dir.as_deref(),
            context.repo.as_deref(),
            provider,
        )
        .await?;
        if context.json {
            print_json(&json!({ "provider": provider, "saved": false }))?;
        } else {
            writeln!(io::stdout().lock(), "Forgot the saved {provider} API key.")
                .map_err(|e| e.to_string())?;
        }
        return Ok(0);
    }
    let revoked = artifactize_core::auth::codex::logout(
        context.state_dir.as_deref(),
        context.repo.as_deref(),
    )
    .await?;
    if !revoked {
        writeln!(
            io::stderr().lock(),
            "Local tokens removed; their revocation was not confirmed."
        )
        .map_err(|e| e.to_string())?;
    }
    if context.json {
        print_json(&json!({ "provider": "codex", "signed_in": false, "revoked": revoked }))?;
    } else {
        writeln!(io::stdout().lock(), "Signed out of Codex.").map_err(|e| e.to_string())?;
    }
    Ok(0)
}

pub(super) async fn doctor(context: Context) -> Result<u8, String> {
    let report = artifactize_core::diagnostics::doctor(
        context.state_dir.as_deref(),
        context.repo.as_deref(),
    )
    .await?;
    if context.json {
        print_json(&report)?;
    } else {
        let mut out = io::stdout().lock();
        writeln!(
            out,
            "State directory: {}",
            artifactize_core::platform::path_text(&report.state_dir)
        )
        .map_err(|e| e.to_string())?;
        for check in &report.checks {
            writeln!(out, "{} {}: {}", check.status, check.name, check.message)
                .map_err(|e| e.to_string())?;
        }
    }
    Ok(u8::from(!report.ok))
}

pub(super) async fn prune(
    context: Context,
    older_than: Option<std::time::Duration>,
    dry_run: bool,
) -> Result<u8, String> {
    let state = match context.state_dir {
        Some(state) => state,
        None => artifactize_core::store::state_home().map_err(|e| e.to_string())?,
    };
    let report = artifactize_core::store::prune::prune(
        &state,
        context.repo.as_deref(),
        older_than,
        dry_run,
    )?;
    if context.json {
        print_json(&report)?;
    } else {
        let mut out = io::stdout().lock();
        for path in &report.removed {
            writeln!(
                out,
                "Removed {}",
                artifactize_core::platform::path_text(path)
            )
            .map_err(|e| e.to_string())?;
        }
        for path in &report.would_remove {
            writeln!(
                out,
                "Would remove {}",
                artifactize_core::platform::path_text(path)
            )
            .map_err(|e| e.to_string())?;
        }
        for id in &report.skipped_runs {
            writeln!(out, "Skipped Run {id}").map_err(|e| e.to_string())?;
        }
        for id in &report.removed_sessions {
            writeln!(out, "Removed Agent session {id}").map_err(|e| e.to_string())?;
        }
        for id in &report.would_remove_sessions {
            writeln!(out, "Would remove Agent session {id}").map_err(|e| e.to_string())?;
        }
        writeln!(out, "Database audit and repository files were preserved.")
            .map_err(|e| e.to_string())?;
    }
    Ok(0)
}

pub(super) fn limits(
    context: Context,
    signoff_idle_grace: Option<std::time::Duration>,
) -> Result<u8, String> {
    use artifactize_core::limits::Limits;
    let state = artifactize_core::store::state_dir(context.state_dir.as_deref())?;
    let limits = match signoff_idle_grace {
        Some(grace) => Limits::set_signoff_idle_grace(&state, grace)?,
        None => Limits::read(&state)?,
    };
    if context.json {
        print_json(&limits)?;
    } else {
        let mut out = io::stdout().lock();
        writeln!(
            out,
            "Sign-off idle grace: {} seconds",
            limits.signoff_idle_grace().as_secs()
        )
        .map_err(|e| e.to_string())?;
        for (backend, slots) in limits.backends() {
            writeln!(out, "Backend {backend}: {slots} reviews at once")
                .map_err(|e| e.to_string())?;
        }
        let sessions = limits.agent_sessions();
        if sessions.enabled {
            writeln!(
                out,
                "Agent sessions: saved; past {} bytes, collected down to {} bytes",
                sessions.max_bytes, sessions.target_bytes
            )
        } else {
            writeln!(out, "Agent sessions: not saved")
        }
        .map_err(|e| e.to_string())?;
    }
    Ok(0)
}

pub(super) async fn models(context: Context, provider: Backend) -> Result<u8, String> {
    let listing = artifactize_core::llm::models::list(
        provider,
        context.state_dir.as_deref(),
        context.repo.as_deref(),
    )
    .await?;
    if context.json {
        print_json(&listing)?;
    } else {
        let mut out = io::stdout().lock();
        for model in listing.models {
            writeln!(out, "{}\t{}", model.slug, model.display_name).map_err(|e| e.to_string())?;
        }
    }
    Ok(0)
}