agentport 0.1.0

A unified TUI installer for AI agent skills and plugins
Documentation
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AgentKind {
    Codex,
    Claude,
    Copilot,
}

impl AgentKind {
    pub const ALL: [Self; 3] = [Self::Codex, Self::Claude, Self::Copilot];

    pub fn label(self) -> &'static str {
        match self {
            Self::Codex => "Codex",
            Self::Claude => "Claude Code",
            Self::Copilot => "GitHub Copilot",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum InstallScope {
    Global,
    Project,
}

impl InstallScope {
    pub fn label(self) -> &'static str {
        match self {
            Self::Global => "global",
            Self::Project => "project",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ComponentKind {
    Plugin,
    Skill,
    Command,
    Agent,
    Hook,
    Mcp,
}

#[derive(Debug, Clone)]
pub struct Component {
    pub name: String,
    pub kind: ComponentKind,
    pub source: PathBuf,
    pub active: bool,
}

#[derive(Debug, Clone)]
pub struct Artifact {
    pub id: String,
    pub name: String,
    pub root: PathBuf,
    pub components: Vec<Component>,
    pub codex_plugin: Option<CodexPlugin>,
}

#[derive(Debug, Clone)]
pub struct CodexPlugin {
    pub name: String,
    pub marketplace: Option<String>,
    pub marketplace_root: Option<PathBuf>,
    pub has_hooks: bool,
}

impl Artifact {
    pub fn summary(&self) -> String {
        if self.codex_plugin.is_some() {
            let hooks = self
                .codex_plugin
                .as_ref()
                .is_some_and(|plugin| plugin.has_hooks);
            return if hooks {
                "Codex plugin (includes hooks)".into()
            } else {
                "Codex plugin".into()
            };
        }
        if self
            .components
            .iter()
            .any(|component| component.kind == ComponentKind::Hook)
        {
            return "standalone hooks package".into();
        }
        let skills = self
            .components
            .iter()
            .filter(|component| {
                matches!(
                    component.kind,
                    ComponentKind::Skill | ComponentKind::Command
                )
            })
            .count();
        let extras = self.components.len().saturating_sub(skills);
        format!("{skills} skill(s), {extras} extra component(s)")
    }
}

#[derive(Debug, Clone)]
pub struct DetectedAgent {
    pub kind: AgentKind,
    pub evidence: Vec<String>,
    pub home: PathBuf,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstalledFile {
    pub path: PathBuf,
    pub sha256: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstalledTarget {
    pub agent: AgentKind,
    pub scope: InstallScope,
    pub files: Vec<InstalledFile>,
    #[serde(default)]
    pub native_plugins: Vec<InstalledPlugin>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstalledPlugin {
    pub selector: String,
    pub marketplace: String,
    pub marketplace_source: String,
    pub plugin_owned: bool,
    pub marketplace_owned: bool,
    pub snapshot: Option<PathBuf>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManagedInstallation {
    pub id: String,
    pub package: String,
    pub source: String,
    pub revision: Option<String>,
    pub installed_at_unix: u64,
    pub active_content_approved: bool,
    pub targets: Vec<InstalledTarget>,
}

#[derive(Debug, Clone)]
pub enum PlannedOperation {
    CopyDirectory {
        from: PathBuf,
        to: PathBuf,
    },
    CopyFile {
        from: PathBuf,
        to: PathBuf,
    },
    InstallCodexPlugin {
        plugin: String,
        marketplace: String,
        marketplace_source: String,
        snapshot_from: Option<PathBuf>,
        snapshot_to: Option<PathBuf>,
        standalone_hook: bool,
        revision: Option<String>,
    },
}

impl PlannedOperation {
    pub fn display(&self) -> String {
        match self {
            Self::CopyDirectory { to, .. } | Self::CopyFile { to, .. } => to.display().to_string(),
            Self::InstallCodexPlugin {
                plugin,
                marketplace,
                marketplace_source,
                ..
            } => {
                format!(
                    "codex plugin add {plugin}@{marketplace} (marketplace: {marketplace_source})"
                )
            }
        }
    }
}

#[derive(Debug, Clone)]
pub struct TargetPlan {
    pub agent: AgentKind,
    pub scope: InstallScope,
    pub operations: Vec<PlannedOperation>,
    pub skipped: Vec<String>,
}

#[derive(Debug, Clone)]
pub struct InstallPlan {
    pub package: String,
    pub source: String,
    pub revision: Option<String>,
    pub active_content_approved: bool,
    pub targets: Vec<TargetPlan>,
    pub warnings: Vec<String>,
}