a3s 0.9.6

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
Documentation
use std::path::PathBuf;

use anyhow::{bail, Context};

use super::{parse_kind, required_value, InstallScope, ReleaseChannel};
use crate::components::catalog::ComponentKind;
use crate::components::id::ComponentId;
use crate::components::lifecycle::InstallSource;

#[derive(Debug, Default)]
pub(super) struct ListOptions {
    pub(super) installed: bool,
    pub(super) available: bool,
    pub(super) check_updates: bool,
    pub(super) json: bool,
    pub(super) kind: Option<ComponentKind>,
}

impl ListOptions {
    pub(super) fn parse(args: &[String]) -> anyhow::Result<Self> {
        let mut options = Self::default();
        for argument in args {
            match argument.as_str() {
                "--installed" => options.installed = true,
                "--available" => options.available = true,
                "--updates" => options.check_updates = true,
                "--json" => options.json = true,
                "--kind" => bail!("--kind requires a value"),
                value if value.starts_with("--kind=") => {
                    options.kind = Some(parse_kind(value.trim_start_matches("--kind="))?);
                }
                other => bail!("unknown list option '{other}'"),
            }
        }
        if options.installed && options.available {
            bail!("--installed and --available are mutually exclusive");
        }
        Ok(options)
    }
}

#[derive(Debug, Default)]
pub(super) struct InstallOptions {
    pub(super) components: Vec<ComponentId>,
    pub(super) version: Option<String>,
    pub(super) source: InstallSource,
    pub(super) channel: ReleaseChannel,
    pub(super) scope: InstallScope,
    pub(super) package: Option<PathBuf>,
    pub(super) force: bool,
    pub(super) migrate: bool,
    pub(super) allow_unsigned: bool,
    pub(super) json: bool,
    pub(super) dry_run: bool,
}

impl InstallOptions {
    pub(super) fn parse(args: &[String]) -> anyhow::Result<Self> {
        let mut options = Self {
            source: InstallSource::Auto,
            channel: ReleaseChannel::Stable,
            scope: InstallScope::User,
            ..Self::default()
        };
        let mut index = 0;
        while index < args.len() {
            match args[index].as_str() {
                "--version" => {
                    index += 1;
                    options.version = Some(required_value(args, index, "--version")?.to_string());
                }
                "--source" => {
                    index += 1;
                    options.source = match required_value(args, index, "--source")? {
                        "auto" => InstallSource::Auto,
                        "homebrew" => InstallSource::Homebrew,
                        "release" => InstallSource::Release,
                        value => bail!("unsupported install source '{value}'"),
                    };
                }
                "--from" => {
                    index += 1;
                    options.package = Some(PathBuf::from(required_value(args, index, "--from")?));
                }
                "--channel" => {
                    index += 1;
                    options.channel = match required_value(args, index, "--channel")? {
                        "stable" => ReleaseChannel::Stable,
                        "beta" => ReleaseChannel::Beta,
                        "nightly" => ReleaseChannel::Nightly,
                        value => bail!("unsupported release channel '{value}'"),
                    };
                }
                "--scope" => {
                    index += 1;
                    options.scope = match required_value(args, index, "--scope")? {
                        "user" => InstallScope::User,
                        "system" => InstallScope::System,
                        value => bail!("unsupported install scope '{value}'"),
                    };
                }
                "--force" => options.force = true,
                "--migrate" => options.migrate = true,
                "--allow-unsigned" => options.allow_unsigned = true,
                "--yes" => {}
                "--dry-run" => options.dry_run = true,
                "--json" => options.json = true,
                value if value.starts_with('-') => bail!("unknown install option '{value}'"),
                value => options.components.push(ComponentId::parse(value)?),
            }
            index += 1;
        }
        Ok(options)
    }
}

#[derive(Debug, Default)]
pub(super) struct UninstallOptions {
    pub(super) components: Vec<ComponentId>,
    pub(super) cascade: bool,
    pub(super) purge: bool,
    pub(super) json: bool,
    pub(super) dry_run: bool,
}

impl UninstallOptions {
    pub(super) fn parse(args: &[String]) -> anyhow::Result<Self> {
        let mut options = Self::default();
        for argument in args {
            match argument.as_str() {
                "--cascade" => options.cascade = true,
                "--purge" => options.purge = true,
                "--yes" => {}
                "--json" => options.json = true,
                "--dry-run" => options.dry_run = true,
                value if value.starts_with('-') => {
                    bail!("unknown uninstall option '{value}'")
                }
                value => options.components.push(ComponentId::parse(value)?),
            }
        }
        Ok(options)
    }
}

#[derive(Debug, Default)]
pub(super) struct UpdateOptions {
    pub(super) components: Vec<ComponentId>,
    pub(super) all: bool,
    pub(super) json: bool,
    pub(super) dry_run: bool,
}

impl UpdateOptions {
    pub(super) fn parse(args: &[String]) -> anyhow::Result<Self> {
        let mut options = Self::default();
        for argument in args {
            match argument.as_str() {
                "--all" => options.all = true,
                "--yes" => {}
                "--json" => options.json = true,
                "--dry-run" => options.dry_run = true,
                value if value.starts_with('-') => bail!("unknown update option '{value}'"),
                value => options.components.push(ComponentId::parse(value)?),
            }
        }
        if options.all && !options.components.is_empty() {
            bail!("--all cannot be combined with component IDs");
        }
        Ok(options)
    }
}

#[derive(Debug)]
pub(super) struct InfoOptions {
    pub(super) component: String,
    pub(super) versions: bool,
    pub(super) sources: bool,
    pub(super) json: bool,
}

impl InfoOptions {
    pub(super) fn parse(args: &[String]) -> anyhow::Result<Self> {
        let mut component = None;
        let mut versions = false;
        let mut sources = false;
        let mut json = false;
        for value in args {
            match value.as_str() {
                "--versions" => versions = true,
                "--sources" => sources = true,
                "--json" => json = true,
                value if value.starts_with('-') => bail!("unknown info option '{value}'"),
                value if component.is_none() => component = Some(value.to_string()),
                _ => bail!("a3s info accepts exactly one component"),
            }
        }
        Ok(Self {
            component: component.context("a3s info requires a component")?,
            versions,
            sources,
            json,
        })
    }
}

#[derive(Debug, Default)]
pub(super) struct DoctorOptions {
    pub(super) component: Option<String>,
    pub(super) json: bool,
}

impl DoctorOptions {
    pub(super) fn parse(args: &[String]) -> anyhow::Result<Self> {
        let mut options = Self::default();
        for value in args {
            match value.as_str() {
                "--json" => options.json = true,
                value if value.starts_with('-') => bail!("unknown doctor option '{value}'"),
                value if options.component.is_none() => options.component = Some(value.to_string()),
                _ => bail!("a3s doctor accepts at most one component"),
            }
        }
        Ok(options)
    }
}