a3s 0.9.5

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

pub(in crate::api) fn find_default_web_dir() -> Option<PathBuf> {
    let mut candidates = Vec::new();
    if let Ok(executable) = std::env::current_exe() {
        candidates.extend(packaged_candidates(&executable));
    }
    if let Ok(cwd) = std::env::current_dir() {
        candidates.extend(upward_candidates(&cwd));
        candidates.push(cwd.join("dist/workspace"));
        candidates.push(cwd.join("dist"));
    }
    candidates.push(Path::new(env!("CARGO_MANIFEST_DIR")).join("../../apps/web/dist/workspace"));
    candidates.push(Path::new(env!("CARGO_MANIFEST_DIR")).join("../../apps/web/dist"));

    find_existing_web_dir(candidates)
}

fn packaged_candidates(executable: &Path) -> Vec<PathBuf> {
    let mut candidates = packaged_layout_candidates(executable);
    if let Ok(canonical) = executable.canonicalize() {
        if canonical != executable {
            candidates.extend(packaged_layout_candidates(&canonical));
        }
    }
    candidates
}

fn packaged_layout_candidates(executable: &Path) -> Vec<PathBuf> {
    let Some(bin_dir) = executable.parent() else {
        return Vec::new();
    };

    let mut candidates = vec![bin_dir.join("web")];
    if let Some(prefix) = bin_dir.parent() {
        candidates.push(prefix.join("share/a3s/web"));
    }
    candidates
}

fn find_existing_web_dir(candidates: impl IntoIterator<Item = PathBuf>) -> Option<PathBuf> {
    candidates
        .into_iter()
        .map(clean_path)
        .find(|candidate| candidate.join("index.html").is_file())
}

fn upward_candidates(start: &Path) -> Vec<PathBuf> {
    let mut candidates = Vec::new();
    let mut current = Some(start);
    while let Some(dir) = current {
        candidates.push(dir.join("apps/web/dist/workspace"));
        candidates.push(dir.join("apps/web/dist"));
        current = dir.parent();
    }
    candidates
}

fn clean_path(path: PathBuf) -> PathBuf {
    path.canonicalize().unwrap_or(path)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn packaged_layouts_cover_archives_and_install_prefixes() {
        let executable = Path::new("prefix").join("bin").join("a3s");

        assert_eq!(
            packaged_layout_candidates(&executable),
            vec![
                Path::new("prefix").join("bin/web"),
                Path::new("prefix").join("share/a3s/web"),
            ]
        );
    }

    #[test]
    fn packaged_layouts_are_checked_before_development_fallbacks() {
        let executable = Path::new("prefix").join("bin").join("a3s");
        let mut candidates = packaged_layout_candidates(&executable);
        candidates.extend(upward_candidates(Path::new("workspace/project")));

        assert_eq!(candidates[0], Path::new("prefix").join("bin/web"));
        assert_eq!(candidates[1], Path::new("prefix").join("share/a3s/web"));
        assert!(candidates[2].ends_with("apps/web/dist/workspace"));
    }

    #[test]
    fn existing_packaged_assets_win_over_development_fallbacks() {
        let root = tempfile::tempdir().expect("temporary asset layouts");
        let packaged = root.path().join("bin/web");
        let development = root.path().join("apps/web/dist/workspace");
        std::fs::create_dir_all(&packaged).expect("packaged Web directory");
        std::fs::create_dir_all(&development).expect("development Web directory");
        std::fs::write(packaged.join("index.html"), "packaged").expect("packaged index");
        std::fs::write(development.join("index.html"), "development").expect("development index");

        let found =
            find_existing_web_dir([packaged.clone(), development]).expect("existing Web directory");

        assert_eq!(found, packaged.canonicalize().expect("canonical package"));
    }

    #[cfg(unix)]
    #[test]
    fn executable_symlink_can_find_assets_in_its_cellar_prefix() {
        use std::os::unix::fs::symlink;

        let root = tempfile::tempdir().expect("temporary Homebrew layout");
        let linked_bin = root.path().join("bin");
        let cellar_bin = root.path().join("Cellar/a3s/0.9.2/bin");
        let cellar_web = root.path().join("Cellar/a3s/0.9.2/share/a3s/web");
        std::fs::create_dir_all(&linked_bin).expect("linked bin directory");
        std::fs::create_dir_all(&cellar_bin).expect("Cellar bin directory");
        std::fs::create_dir_all(&cellar_web).expect("Cellar Web directory");
        std::fs::write(cellar_bin.join("a3s"), "binary").expect("Cellar binary");
        std::fs::write(cellar_web.join("index.html"), "web").expect("Cellar Web index");
        let executable = linked_bin.join("a3s");
        symlink(cellar_bin.join("a3s"), &executable).expect("binary symlink");

        let found = find_existing_web_dir(packaged_candidates(&executable))
            .expect("Web assets beside the canonical executable prefix");

        assert_eq!(
            found,
            cellar_web.canonicalize().expect("canonical Web path")
        );
    }
}