use std::path::PathBuf;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::config::schema::{
AptMirrorDef, CheckSpec, ConfigDocument, EnvironmentMutation, InstallSpec, OriginMap,
};
use crate::util::valid_config_id;
mod output;
pub use crate::model::output::{
InstallOutcome, InstallPreview, InstallReport, SkillStatus, ToolInstallOutcome,
ToolInstallResult, ToolStatus,
};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum Profile {
Minimal,
#[default]
Standard,
Advanced,
Custom(String),
}
impl Profile {
pub fn parse(value: &str) -> Option<Self> {
match value {
"minimal" => Some(Self::Minimal),
"standard" => Some(Self::Standard),
"advanced" => Some(Self::Advanced),
value if valid_config_id(value) => Some(Self::Custom(value.to_string())),
_ => None,
}
}
pub fn as_str(&self) -> &str {
match self {
Self::Minimal => "minimal",
Self::Standard => "standard",
Self::Advanced => "advanced",
Self::Custom(value) => value,
}
}
}
#[cfg(test)]
mod tests {
use crate::model::Profile;
#[test]
fn cli_profiles_use_the_config_id_contract() {
assert!(Profile::parse("ci-agent-2").is_some());
for invalid in ["CI", "ci_agent", "-ci", "ci-", "ci--agent"] {
assert!(Profile::parse(invalid).is_none(), "accepted {invalid}");
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum Agent {
Claude,
OpenCode,
}
impl Agent {
pub fn as_str(self) -> &'static str {
match self {
Self::Claude => "claude",
Self::OpenCode => "opencode",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum InstallKind {
Tool,
Skill,
}
impl InstallKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Tool => "tool",
Self::Skill => "skill",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum BackendKind {
Apt,
Brew,
Cargo,
Rustup,
Npm,
Pip,
#[serde(rename = "uv-tool")]
UvTool,
Winget,
Archive,
Git,
Shell,
}
impl BackendKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Apt => "apt",
Self::Brew => "brew",
Self::Cargo => "cargo",
Self::Rustup => "rustup",
Self::Npm => "npm",
Self::Pip => "pip",
Self::UvTool => "uv-tool",
Self::Winget => "winget",
Self::Archive => "archive",
Self::Git => "git",
Self::Shell => "shell",
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub(crate) struct ToolDef {
pub name: String,
pub display_name: Option<String>,
pub version: Option<String>,
pub optional: bool,
pub allow_insecure_hosts: Vec<String>,
pub detect: Option<CheckSpec>,
pub install: Option<InstallSpec>,
pub verify: Option<CheckSpec>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ArchiveFormat {
#[default]
File,
TarGz,
TarXz,
Zip,
}
impl ToolDef {
pub(crate) fn backend(&self) -> Option<BackendKind> {
match self.install.as_ref()? {
InstallSpec::Apt(_) => Some(BackendKind::Apt),
InstallSpec::Brew(_) => Some(BackendKind::Brew),
InstallSpec::Cargo(_) => Some(BackendKind::Cargo),
InstallSpec::Rustup(_) => Some(BackendKind::Rustup),
InstallSpec::Npm(_) => Some(BackendKind::Npm),
InstallSpec::Pip(_) => Some(BackendKind::Pip),
InstallSpec::UvTool(_) => Some(BackendKind::UvTool),
InstallSpec::Winget(_) => Some(BackendKind::Winget),
InstallSpec::Archive(_) => Some(BackendKind::Archive),
InstallSpec::Git(_) => Some(BackendKind::Git),
InstallSpec::Shell(_) => Some(BackendKind::Shell),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub(crate) struct SkillDef {
pub name: String,
pub display_name: Option<String>,
pub optional: bool,
pub source: String,
pub agents: Vec<Agent>,
pub revision: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub(crate) struct InstallConfig {
pub environment: EnvironmentDef,
pub apt_mirror: Option<AptMirrorDef>,
pub tools: Vec<ToolDef>,
pub skills: Vec<SkillDef>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub(crate) struct EnvironmentDef {
pub mutations: Vec<EnvironmentMutation>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadedConfig {
pub document: ConfigDocument,
pub origins: OriginMap,
pub path: Option<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RegistryEntry {
pub name: String,
pub kind: InstallKind,
pub source: String,
pub profile: String,
pub targets: Vec<RegistryTarget>,
pub installed_at: u64,
#[serde(deserialize_with = "deserialize_explicit_option")]
#[schemars(schema_with = "nullable_string_schema", required)]
pub artifact_id: Option<String>,
#[serde(deserialize_with = "deserialize_explicit_option")]
#[schemars(schema_with = "nullable_string_schema", required)]
pub previous_artifact_id: Option<String>,
#[serde(deserialize_with = "deserialize_explicit_option")]
#[schemars(schema_with = "nullable_string_schema", required)]
pub config_hash: Option<String>,
#[serde(deserialize_with = "deserialize_explicit_option")]
#[schemars(schema_with = "nullable_string_schema", required)]
pub plan_hash: Option<String>,
#[serde(deserialize_with = "deserialize_explicit_option")]
#[schemars(schema_with = "nullable_string_schema", required)]
pub source_revision: Option<String>,
#[serde(deserialize_with = "deserialize_explicit_option")]
#[schemars(schema_with = "nullable_backend_schema", required)]
pub backend: Option<BackendKind>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RegistryTarget {
pub path: PathBuf,
#[serde(deserialize_with = "deserialize_explicit_option")]
#[schemars(schema_with = "nullable_string_schema", required)]
pub binary: Option<String>,
}
fn nullable_string_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
generator.subschema_for::<Option<String>>()
}
fn nullable_backend_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
generator.subschema_for::<Option<BackendKind>>()
}
fn deserialize_explicit_option<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de>,
{
Option::<T>::deserialize(deserializer)
}
impl RegistryEntry {
pub fn stable_id(&self) -> String {
format!("{}:{}", self.kind.as_str(), self.name)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstallOptions {
pub profile: Profile,
pub config_path: Option<PathBuf>,
pub overlay_paths: Vec<PathBuf>,
pub status_bar: bool,
pub yes: bool,
pub only: Vec<String>,
pub exclude: Vec<String>,
}
impl Default for InstallOptions {
fn default() -> Self {
Self {
profile: Profile::Standard,
config_path: None,
overlay_paths: Vec::new(),
status_bar: true,
yes: false,
only: Vec::new(),
exclude: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct DiagnosticCheck {
pub(crate) id: String,
pub(crate) severity: String,
pub(crate) summary: String,
pub(crate) suggestion: Option<String>,
pub(crate) duration_ms: u128,
}