1use std::io::{self, Write};
12use std::path::Path;
13
14use clap::Subcommand;
15
16use par_term_scripting::manifest::discover_plugins;
17use par_term_scripting::plugin_git;
18
19#[derive(Subcommand)]
21pub enum PluginCommands {
22 Add {
24 url: String,
26
27 #[arg(short = 'y', long)]
29 yes: bool,
30 },
31
32 Update {
34 id: Option<String>,
36
37 #[arg(short = 'y', long)]
39 yes: bool,
40 },
41
42 Remove {
44 id: String,
46
47 #[arg(short = 'y', long)]
49 yes: bool,
50 },
51}
52
53pub fn plugin_add_cli(url: &str, yes: bool) -> anyhow::Result<()> {
55 let root = crate::config::Config::config_dir().join("plugins");
56 println!("Plugins directory: {}", root.display());
57 println!();
58 println!("About to clone a plugin from:");
59 println!(" {url}");
60 println!();
61 println!("Plugins run as supervised subprocesses of par-term. This one is");
62 println!("installed DISABLED — read its code before enabling it in");
63 println!("Settings > Automation > Plugins.");
64 if !yes && !confirm("Clone and install? [y/N] ")? {
65 println!("Cancelled.");
66 return Ok(());
67 }
68 println!();
69 match plugin_git::add(&root, url) {
70 Ok(plugin) => {
71 let manifest = &plugin.manifest;
72 println!(
73 "Installed {} {} ({})",
74 manifest.name, manifest.version, manifest.id
75 );
76 println!(" {}", root.join(&manifest.id).display());
77 println!();
78 println!("It is DISABLED. Read the code, then enable it in");
79 println!("Settings > Automation > Plugins.");
80 Ok(())
81 }
82 Err(reason) => {
83 eprintln!("Error: {reason}");
84 Err(anyhow::anyhow!(reason))
85 }
86 }
87}
88
89pub fn plugin_update_cli(id: Option<&str>, yes: bool) -> anyhow::Result<()> {
91 let root = crate::config::Config::config_dir().join("plugins");
92 let targets: Vec<String> = match id {
93 Some(one) => vec![one.to_string()],
94 None => {
95 let (discovered, _) = discover_plugins(&root);
96 let mut ids: Vec<String> = discovered
97 .into_iter()
98 .map(|plugin| plugin.manifest.id)
99 .filter(|pid| plugin_git::is_git_installed(&root.join(pid)))
100 .collect();
101 ids.sort();
102 if ids.is_empty() {
103 println!("No git-installed plugins under {}.", root.display());
104 println!("Plugins installed by copying a directory by hand are updated");
105 println!("the same way — `plugin update` only manages what it installed.");
106 return Ok(());
107 }
108 ids
109 }
110 };
111
112 let mut failures = 0usize;
113 for pid in targets {
114 println!("=== {pid} ===");
115 if let Err(reason) = update_one(&root, &pid, yes) {
116 eprintln!("Error: {reason}");
117 failures += 1;
118 }
119 println!();
120 }
121 if failures > 0 {
122 return Err(anyhow::anyhow!("{failures} plugin update(s) failed"));
123 }
124 Ok(())
125}
126
127fn update_one(root: &Path, pid: &str, yes: bool) -> anyhow::Result<()> {
128 let dir = root.join(pid);
129 let preview = plugin_git::update_fetch(&dir).map_err(|reason| anyhow::anyhow!(reason))?;
130 if preview.up_to_date {
131 println!("Already up to date ({}).", preview.current);
132 return Ok(());
133 }
134 println!("{} -> {}", preview.current, preview.incoming);
135 println!();
136 println!("{}", preview.diff);
137 println!();
138 if !yes && !confirm("Apply this update? [y/N] ")? {
139 println!("Skipped.");
140 return Ok(());
141 }
142 let new_head = plugin_git::update_apply(&dir).map_err(|reason| anyhow::anyhow!(reason))?;
143 println!("Updated to {new_head}.");
144 Ok(())
145}
146
147pub fn plugin_remove_cli(id: &str, yes: bool) -> anyhow::Result<()> {
149 let root = crate::config::Config::config_dir().join("plugins");
150 println!("About to remove:");
151 println!(" {}", root.join(id).display());
152 println!();
153 if !yes && !confirm("Remove this plugin? [y/N] ")? {
154 println!("Cancelled.");
155 return Ok(());
156 }
157 match plugin_git::remove(&root, id) {
158 Ok(()) => {
159 println!("Removed {id}.");
160 Ok(())
161 }
162 Err(reason) => {
163 eprintln!("Error: {reason}");
164 Err(anyhow::anyhow!(reason))
165 }
166 }
167}
168
169fn confirm(prompt: &str) -> anyhow::Result<bool> {
170 print!("{prompt}");
171 io::stdout().flush()?;
172 let mut response = String::new();
173 io::stdin().read_line(&mut response)?;
174 let response = response.trim().to_lowercase();
175 Ok(response == "y" || response == "yes")
176}