zenith_cli/commands/plugin/
run.rs1use std::path::Path;
5
6use super::agent::{ALL_AGENTS, Agent, Scope};
7use super::detect::{detect_present, is_installed};
8use super::install::{WriteOutcome, install_agent};
9use super::path_check::zenith_on_path;
10use super::uninstall::{RemoveOutcome, uninstall_agent};
11
12#[derive(Debug, Clone)]
14pub enum Targets {
15 Agents(Vec<Agent>),
17 All,
19 Auto,
21}
22
23pub fn run_install(
25 project_root: &Path,
26 targets: Targets,
27 scope: Scope,
28 force: bool,
29 dry_run: bool,
30) -> u8 {
31 let agents = match resolve(targets, project_root) {
32 Ok(a) => a,
33 Err(code) => return code,
34 };
35
36 let mut any_overwrite = false;
37 let mut any_error = false;
38 let verb = if dry_run {
39 "would install"
40 } else {
41 "installed"
42 };
43
44 for agent in agents {
45 let report = install_agent(agent, scope, project_root, force, dry_run);
46 if let Some(reason) = &report.unsupported {
47 println!("- {}: skipped ({reason})", agent.display());
48 continue;
49 }
50 let root = report
51 .root
52 .as_ref()
53 .map(|p| p.display().to_string())
54 .unwrap_or_default();
55
56 let mut installed = 0usize;
57 let mut current = 0usize;
58 for f in &report.files {
59 match &f.outcome {
60 Ok(WriteOutcome::Installed) => installed += 1,
61 Ok(WriteOutcome::AlreadyCurrent) => current += 1,
62 Ok(WriteOutcome::WouldOverwrite) => {
63 any_overwrite = true;
64 println!(" differs (needs --force): {}", f.path.display());
65 }
66 Err(e) => {
67 any_error = true;
68 println!(" error: {}: {e}", f.path.display());
69 }
70 }
71 }
72 println!(
73 "- {} ({}): {verb} {installed}, current {current} → {root}",
74 agent.display(),
75 scope_label(scope),
76 );
77 }
78
79 if zenith_on_path().is_none() {
80 println!("warning: the installed skill calls `zenith`, but `zenith` is not on your PATH.");
81 println!(
82 " Install it: `cargo install --path zenith-cli`, or `./scripts/install.sh`, then ensure its dir is on PATH."
83 );
84 }
85
86 finish(any_error, any_overwrite, dry_run)
87}
88
89pub fn run_uninstall(project_root: &Path, targets: Targets, scope: Scope, dry_run: bool) -> u8 {
91 let agents = match resolve(targets, project_root) {
92 Ok(a) => a,
93 Err(code) => return code,
94 };
95
96 let mut any_error = false;
97 let verb = if dry_run { "would remove" } else { "removed" };
98
99 for agent in agents {
100 let report = uninstall_agent(agent, scope, project_root, dry_run);
101 if let Some(reason) = &report.unsupported {
102 println!("- {}: skipped ({reason})", agent.display());
103 continue;
104 }
105 let mut removed = 0usize;
106 let mut absent = 0usize;
107 for item in &report.items {
108 match &item.outcome {
109 Ok(RemoveOutcome::Removed) => removed += 1,
110 Ok(RemoveOutcome::Absent) => absent += 1,
111 Err(e) => {
112 any_error = true;
113 println!(" error: {}: {e}", item.path.display());
114 }
115 }
116 }
117 println!(
118 "- {} ({}): {verb} {removed}, absent {absent}",
119 agent.display(),
120 scope_label(scope),
121 );
122 }
123
124 if any_error { 2 } else { 0 }
125}
126
127pub fn run_list(project_root: &Path) -> u8 {
129 println!("Zenith skill install state (agent / project / user):");
130 for agent in ALL_AGENTS {
131 let proj = mark(is_installed(*agent, Scope::Project, project_root));
132 let user = mark(is_installed(*agent, Scope::User, project_root));
133 let present = if detect_present(project_root).contains(agent) {
134 " (detected)"
135 } else {
136 ""
137 };
138 println!(
139 "- {:<14} project:{proj} user:{user}{present}",
140 agent.display()
141 );
142 }
143 match zenith_on_path() {
144 Some(path) => println!("zenith binary: {}", path.display()),
145 None => println!(
146 "zenith binary: NOT on PATH — the skill calls `zenith` by name; \
147 install it (`cargo install --path zenith-cli` or `./scripts/install.sh`) and put its dir on PATH"
148 ),
149 }
150 0
151}
152
153fn resolve(targets: Targets, project_root: &Path) -> Result<Vec<Agent>, u8> {
157 match targets {
158 Targets::Agents(a) if !a.is_empty() => Ok(a),
159 Targets::Agents(_) => {
160 eprintln!(
161 "error: no agent selected — pass --all, an agent flag, or none to auto-detect"
162 );
163 Err(2)
164 }
165 Targets::All => Ok(ALL_AGENTS.to_vec()),
166 Targets::Auto => {
167 let found = detect_present(project_root);
168 if found.is_empty() {
169 eprintln!(
170 "no agents detected in {} or your home directory.",
171 project_root.display()
172 );
173 eprintln!("pass an explicit flag (e.g. --claude) or --all to choose targets.");
174 return Err(1);
175 }
176 let names: Vec<&str> = found.iter().map(|a| a.display()).collect();
177 eprintln!("detected: {}", names.join(", "));
178 Ok(found)
179 }
180 }
181}
182
183fn finish(any_error: bool, any_overwrite: bool, dry_run: bool) -> u8 {
184 if any_error {
185 return 2;
186 }
187 if any_overwrite {
188 if dry_run {
189 eprintln!("some files differ; re-run without --dry-run and with --force to overwrite.");
190 } else {
191 eprintln!("some files were left unchanged; re-run with --force to overwrite them.");
192 }
193 return 2;
194 }
195 0
196}
197
198fn scope_label(scope: Scope) -> &'static str {
199 match scope {
200 Scope::Project => "project",
201 Scope::User => "user",
202 }
203}
204
205fn mark(installed: bool) -> &'static str {
206 if installed { "yes" } else { "—" }
207}