a3s 0.8.0

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
use std::net::{SocketAddr, ToSocketAddrs};
use std::path::PathBuf;

const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 29653;

#[derive(Debug)]
pub(super) struct ServeOptions {
    pub addr: SocketAddr,
    pub workspace: PathBuf,
    pub web_dir: Option<PathBuf>,
    pub api_only: bool,
    pub help: bool,
}

impl ServeOptions {
    pub(super) fn parse(args: &[String]) -> anyhow::Result<Self> {
        let mut host = std::env::var("A3S_CODE_WEB_HOST").unwrap_or_else(|_| DEFAULT_HOST.into());
        let mut port = std::env::var("A3S_CODE_WEB_PORT")
            .ok()
            .and_then(|value| value.parse::<u16>().ok())
            .unwrap_or(DEFAULT_PORT);
        let mut workspace = std::env::current_dir()?;
        let mut web_dir = std::env::var_os("A3S_CODE_WEB_DIR").map(PathBuf::from);
        let mut api_only = false;
        let mut help = false;

        let mut index = 0;
        while index < args.len() {
            match args[index].as_str() {
                "-h" | "--help" | "help" => {
                    help = true;
                    index += 1;
                }
                "--host" => {
                    host = take_value(args, &mut index, "--host")?;
                }
                "--port" => {
                    let value = take_value(args, &mut index, "--port")?;
                    port = value
                        .parse::<u16>()
                        .map_err(|_| anyhow::anyhow!("--port must be a number from 0 to 65535"))?;
                }
                "--workspace" | "-w" => {
                    workspace = PathBuf::from(take_value(args, &mut index, "--workspace")?);
                }
                "--web-dir" => {
                    web_dir = Some(PathBuf::from(take_value(args, &mut index, "--web-dir")?));
                }
                "--api-only" => {
                    api_only = true;
                    index += 1;
                }
                other => anyhow::bail!("unknown a3s code serve option `{other}`"),
            }
        }

        let addr = resolve_addr(&host, port)?;
        Ok(Self {
            addr,
            workspace,
            web_dir,
            api_only,
            help,
        })
    }
}

fn take_value(args: &[String], index: &mut usize, flag: &str) -> anyhow::Result<String> {
    let value_index = *index + 1;
    let value = args
        .get(value_index)
        .filter(|value| !value.starts_with('-'))
        .ok_or_else(|| anyhow::anyhow!("{flag} requires a value"))?;
    *index += 2;
    Ok(value.clone())
}

fn resolve_addr(host: &str, port: u16) -> anyhow::Result<SocketAddr> {
    let mut addrs = format!("{host}:{port}")
        .to_socket_addrs()
        .map_err(|e| anyhow::anyhow!("invalid host/port {host}:{port}: {e}"))?;
    addrs
        .next()
        .ok_or_else(|| anyhow::anyhow!("could not resolve {host}:{port}"))
}