zenith_cli/commands/plugin/
install.rs1use std::fs;
4use std::io;
5use std::path::{Path, PathBuf};
6
7use super::agent::{Agent, Scope, SkillFormat};
8use super::assets::{COMMAND_FILES, SKILL_FILES};
9use super::paths::{SkillTarget, command_dir, skill_target};
10use super::render::render_rule;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum WriteOutcome {
15 Installed,
17 AlreadyCurrent,
19 WouldOverwrite,
21}
22
23#[derive(Debug, Clone)]
25pub struct FileResult {
26 pub path: PathBuf,
27 pub outcome: Result<WriteOutcome, String>,
28}
29
30#[derive(Debug, Clone)]
32pub struct AgentInstall {
33 pub agent: Agent,
34 pub root: Option<PathBuf>,
36 pub files: Vec<FileResult>,
37 pub unsupported: Option<String>,
39}
40
41pub fn install_agent(
43 agent: Agent,
44 scope: Scope,
45 project_root: &Path,
46 force: bool,
47 dry_run: bool,
48) -> AgentInstall {
49 let Some(target) = skill_target(agent, scope, project_root) else {
50 return AgentInstall {
51 agent,
52 root: None,
53 files: Vec::new(),
54 unsupported: Some(format!(
55 "{} {} scope has no automatic target",
56 agent.display(),
57 scope_label(scope)
58 )),
59 };
60 };
61
62 let mut files = Vec::new();
63 match (agent.format(), &target) {
64 (SkillFormat::Folder, SkillTarget::Folder(dir)) => {
65 for (rel, body) in SKILL_FILES {
66 let path = dir.join(rel);
67 files.push(write(&path, body, force, dry_run));
68 }
69 if let Some(cmd_dir) = command_dir(agent, scope, project_root) {
70 for (name, body) in COMMAND_FILES {
71 let path = cmd_dir.join(name);
72 files.push(write(&path, body, force, dry_run));
73 }
74 }
75 }
76 (SkillFormat::Rule, SkillTarget::Rule(path)) => {
77 let body = render_rule(agent);
78 files.push(write(path, &body, force, dry_run));
79 }
80 (SkillFormat::Folder, SkillTarget::Rule(_))
82 | (SkillFormat::Rule, SkillTarget::Folder(_)) => {}
83 }
84
85 AgentInstall {
86 agent,
87 root: Some(target.root().to_path_buf()),
88 files,
89 unsupported: None,
90 }
91}
92
93fn write(path: &Path, content: &str, force: bool, dry_run: bool) -> FileResult {
95 let outcome = (|| -> io::Result<WriteOutcome> {
96 let planned = plan(path, content, force)?;
97 if !dry_run && planned == WriteOutcome::Installed {
98 if let Some(parent) = path.parent() {
99 fs::create_dir_all(parent)?;
100 }
101 fs::write(path, content)?;
102 }
103 Ok(planned)
104 })();
105 FileResult {
106 path: path.to_path_buf(),
107 outcome: outcome.map_err(|e| e.to_string()),
108 }
109}
110
111fn plan(path: &Path, content: &str, force: bool) -> io::Result<WriteOutcome> {
113 if path.exists() {
114 let existing = fs::read(path)?;
115 if existing == content.as_bytes() {
116 return Ok(WriteOutcome::AlreadyCurrent);
117 }
118 if !force {
119 return Ok(WriteOutcome::WouldOverwrite);
120 }
121 }
122 Ok(WriteOutcome::Installed)
123}
124
125fn scope_label(scope: Scope) -> &'static str {
126 match scope {
127 Scope::Project => "project",
128 Scope::User => "user",
129 }
130}