use serde::{Deserialize, Serialize};
use super::id::ComponentId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ComponentKind {
BuiltIn,
Product,
Capability,
Extension,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Distribution {
Bundled,
Release(ReleaseSpec),
Delegated { parent: &'static str },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReleaseSpec {
pub binary: &'static str,
pub github_owner: &'static str,
pub github_repo: &'static str,
pub homebrew_formula: Option<&'static str>,
pub install_dir_env: &'static str,
pub asset_family: AssetFamily,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssetFamily {
BoxPackage,
PortableBinary,
BenchPackage,
}
impl AssetFamily {
pub fn target(self) -> Option<&'static str> {
match (self, std::env::consts::OS, std::env::consts::ARCH) {
(Self::BoxPackage, "macos", "aarch64") => Some("macos-arm64"),
(Self::BoxPackage, "linux", "aarch64") => Some("linux-arm64"),
(Self::BoxPackage, "linux", "x86_64") => Some("linux-x86_64"),
(Self::PortableBinary | Self::BenchPackage, "macos", "aarch64") => Some("darwin-arm64"),
(Self::PortableBinary | Self::BenchPackage, "macos", "x86_64") => Some("darwin-x86_64"),
(Self::PortableBinary | Self::BenchPackage, "linux", "aarch64") => Some("linux-arm64"),
(Self::PortableBinary | Self::BenchPackage, "linux", "x86_64") => Some("linux-x86_64"),
(Self::PortableBinary | Self::BenchPackage, "windows", "x86_64") => {
Some("windows-x86_64")
}
_ => None,
}
}
pub fn archive_name(self, binary: &str, version: &str, target: &str) -> String {
match self {
Self::BoxPackage => {
format!("{binary}-v{version}-{target}.tar.gz")
}
Self::PortableBinary | Self::BenchPackage => {
let extension = if target.starts_with("windows-") {
"zip"
} else {
"tar.gz"
};
format!("{binary}-{version}-{target}.{extension}")
}
}
}
pub fn executable_name(self, binary: &str, target: &str) -> String {
if matches!(self, Self::PortableBinary | Self::BenchPackage)
&& target.starts_with("windows-")
{
format!("{binary}.exe")
} else {
binary.to_string()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ComponentSpec {
pub id: &'static str,
pub kind: ComponentKind,
pub description: &'static str,
pub distribution: Distribution,
pub auto_install_on_use: bool,
pub removable: bool,
}
const COMPONENTS: &[ComponentSpec] = &[
ComponentSpec {
id: "code",
kind: ComponentKind::BuiltIn,
description: "Interactive coding agent and asset management",
distribution: Distribution::Bundled,
auto_install_on_use: false,
removable: false,
},
ComponentSpec {
id: "box",
kind: ComponentKind::Product,
description: "Isolated application and command runtime",
distribution: Distribution::Release(ReleaseSpec {
binary: "a3s-box",
github_owner: "A3S-Lab",
github_repo: "Box",
homebrew_formula: Some("a3s-lab/tap/a3s-box"),
install_dir_env: "A3S_BOX_INSTALL_DIR",
asset_family: AssetFamily::BoxPackage,
}),
auto_install_on_use: true,
removable: true,
},
ComponentSpec {
id: "bench",
kind: ComponentKind::Product,
description: "Reproducible agent evaluation",
distribution: Distribution::Release(ReleaseSpec {
binary: "a3s-bench",
github_owner: "A3S-Lab",
github_repo: "Bench",
homebrew_formula: None,
install_dir_env: "A3S_BENCH_INSTALL_DIR",
asset_family: AssetFamily::BenchPackage,
}),
auto_install_on_use: false,
removable: true,
},
ComponentSpec {
id: "search",
kind: ComponentKind::Product,
description: "Embeddable meta search engine",
distribution: Distribution::Release(ReleaseSpec {
binary: "a3s-search",
github_owner: "A3S-Lab",
github_repo: "Search",
homebrew_formula: Some("a3s-lab/tap/a3s-search"),
install_dir_env: "A3S_SEARCH_INSTALL_DIR",
asset_family: AssetFamily::PortableBinary,
}),
auto_install_on_use: false,
removable: true,
},
ComponentSpec {
id: "use",
kind: ComponentKind::Product,
description: "Browser, Office, and external application capabilities",
distribution: Distribution::Release(ReleaseSpec {
binary: "a3s-use",
github_owner: "A3S-Lab",
github_repo: "Use",
homebrew_formula: Some("a3s-lab/tap/a3s-use"),
install_dir_env: "A3S_USE_INSTALL_DIR",
asset_family: AssetFamily::PortableBinary,
}),
auto_install_on_use: true,
removable: true,
},
ComponentSpec {
id: "use/browser",
kind: ComponentKind::Capability,
description: "Browser runtime readiness",
distribution: Distribution::Delegated { parent: "use" },
auto_install_on_use: false,
removable: true,
},
ComponentSpec {
id: "use/office",
kind: ComponentKind::Capability,
description: "OfficeCLI runtime readiness",
distribution: Distribution::Delegated { parent: "use" },
auto_install_on_use: false,
removable: true,
},
];
pub fn all() -> &'static [ComponentSpec] {
COMPONENTS
}
pub fn find(id: &ComponentId) -> Option<&'static ComponentSpec> {
COMPONENTS
.iter()
.find(|component| component.id == id.as_str())
}
pub fn release(spec: &ComponentSpec) -> Option<ReleaseSpec> {
match spec.distribution {
Distribution::Release(release) => Some(release),
Distribution::Bundled | Distribution::Delegated { .. } => None,
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use super::*;
#[test]
fn catalog_ids_are_valid_and_unique() {
let mut ids = BTreeSet::new();
for spec in all() {
ComponentId::parse(spec.id).unwrap();
assert!(ids.insert(spec.id), "duplicate component {}", spec.id);
}
}
#[test]
fn catalog_contains_required_use_hierarchy() {
for id in ["use", "use/browser", "use/office"] {
assert!(find(&ComponentId::parse(id).unwrap()).is_some());
}
}
#[test]
fn archive_names_match_existing_release_conventions() {
assert_eq!(
AssetFamily::BoxPackage.archive_name("a3s-box", "2.5.2", "linux-x86_64"),
"a3s-box-v2.5.2-linux-x86_64.tar.gz"
);
assert_eq!(
AssetFamily::PortableBinary.archive_name("a3s-search", "1.4.1", "darwin-arm64"),
"a3s-search-1.4.1-darwin-arm64.tar.gz"
);
assert_eq!(
AssetFamily::PortableBinary.archive_name("a3s-use", "0.1.0", "windows-x86_64"),
"a3s-use-0.1.0-windows-x86_64.zip"
);
assert_eq!(
AssetFamily::PortableBinary.executable_name("a3s-use", "windows-x86_64"),
"a3s-use.exe"
);
}
}