1use std::path::Path;
6
7use speccoder_core::extensions::{Extension, ExtensionsReport};
8use speccoder_core::hooks::TrustState;
9
10use super::{Loaded, trust};
11
12#[must_use]
14pub fn entry(e: &Loaded, dir: &Path) -> Extension {
15 let state = if e.usable() {
16 trust::state(dir, e)
17 } else {
18 TrustState::Untrusted
19 };
20 Extension {
21 name: e.name.clone(),
22 source: e.source,
23 version: e.version.clone(),
24 description: e.description.clone(),
25 folder: e.rel.clone(),
26 origin: e.origin.clone(),
27 digest: e.digest.clone(),
28 files: e.files.clone(),
29 contributions: e.contributions(),
30 trust: state,
31 changes: (state == TrustState::Changed)
32 .then(|| trust::changes(dir, e))
33 .flatten(),
34 trust_flag: format!("--trust-extension {}={}", e.name, e.digest),
35 problems: e.problems.clone(),
36 }
37}
38
39#[must_use]
43pub fn list(root: &Path, state: &Path) -> ExtensionsReport {
44 let dir = crate::hooks::dir(state, root);
45 let all = super::load(root);
46 ExtensionsReport {
47 extensions: all.iter().map(|e| entry(e, &dir)).collect(),
48 ..ExtensionsReport::default()
49 }
50}
51
52pub fn trust_shown(
58 root: &Path,
59 state: &Path,
60 name: &str,
61 digest: &str,
62 via: &str,
63) -> Result<(), String> {
64 let all = super::load(root);
65 let Some(e) = all.iter().find(|e| e.name == name) else {
66 return Err(format!("there is no extension `{name}`"));
67 };
68 if !e.usable() {
69 return Err(format!(
70 "{name} cannot be loaded: {}",
71 e.problems.join("; ")
72 ));
73 }
74 if e.digest != digest {
75 return Err(format!(
76 "{name} changed since it was shown ({} now): review it again",
77 e.digest
78 ));
79 }
80 trust::record(
81 &crate::hooks::dir(state, root),
82 e,
83 &crate::approvals::who_in(root),
84 via,
85 );
86 Ok(())
87}
88
89#[must_use]
91pub fn waiting(root: &Path, state: &Path) -> Vec<String> {
92 let dir = crate::hooks::dir(state, root);
93 super::load(root)
94 .iter()
95 .filter(|e| e.usable() && trust::state(&dir, e) != TrustState::Trusted)
96 .map(|e| e.name.clone())
97 .collect()
98}
99
100#[must_use]
102pub fn run_notice(root: &Path, state: &Path) -> Option<String> {
103 let names = waiting(root, state);
104 (!names.is_empty()).then(|| {
105 format!(
106 "{} {} for your trust: nothing of {} runs (speccoder extensions trust {})",
107 names.join(", "),
108 if names.len() == 1 { "waits" } else { "wait" },
109 if names.len() == 1 { "it" } else { "them" },
110 names[0]
111 )
112 })
113}
114
115#[must_use]
117pub fn doctor_lines(root: &Path, state: &Path) -> Vec<(bool, String)> {
118 let dir = crate::hooks::dir(state, root);
119 super::load(root)
120 .iter()
121 .filter_map(|e| {
122 if !e.usable() {
123 return Some((
124 false,
125 format!(
126 "extension {} cannot be loaded: {}",
127 e.name,
128 e.problems.join("; ")
129 ),
130 ));
131 }
132 match trust::state(&dir, e) {
133 TrustState::Trusted => None,
134 TrustState::Untrusted => Some((
135 false,
136 format!(
137 "extension {} waits for your trust: speccoder extensions trust {}",
138 e.name, e.name
139 ),
140 )),
141 TrustState::Changed => Some((
142 false,
143 format!(
144 "extension {} changed since you trusted it: speccoder extensions trust {}",
145 e.name, e.name
146 ),
147 )),
148 }
149 })
150 .collect()
151}
152
153use speccoder_core::{CommandData, CommandResult, CommandStatus};
156
157fn result(message: &str, report: ExtensionsReport, status: CommandStatus) -> CommandResult {
158 CommandResult::new("extensions", status, message)
159 .with_data(CommandData::Extensions(Box::new(report)))
160}
161
162fn summary(r: &ExtensionsReport) -> String {
163 let waiting = r
164 .extensions
165 .iter()
166 .filter(|e| e.problems.is_empty() && e.trust != TrustState::Trusted)
167 .count();
168 let broken = r
169 .extensions
170 .iter()
171 .filter(|e| !e.problems.is_empty())
172 .count();
173 let mut s = match (r.extensions.len(), waiting) {
174 (0, _) => {
175 return "no extensions: `speccoder extensions add <folder | URL@commit>` (docs: extensions)"
176 .to_owned();
177 }
178 (1, 0) => "1 extension, trusted".to_owned(),
179 (n, 0) => format!("{n} extensions, all trusted"),
180 (n, w) => format!(
181 "{n} extension{} — {w} wait{} for your trust",
182 if n == 1 { "" } else { "s" },
183 if w == 1 { "s" } else { "" }
184 ),
185 };
186 if broken > 0 {
187 s.push_str(&format!(", {broken} cannot be loaded"));
188 }
189 s
190}
191
192#[must_use]
194pub fn list_cmd(root: &Path, state: &Path) -> CommandResult {
195 let report = list(root, state);
196 let message = summary(&report);
197 result(&message, report, CommandStatus::Ok)
198}
199
200#[must_use]
202pub fn add_cmd(root: &Path, state: &Path, source: &str) -> CommandResult {
203 match super::add::add(root, source) {
204 Ok(e) => {
205 let mut report = list(root, state);
206 report.added = Some(e.name.clone());
207 result(
208 &format!(
209 "added {} to {}: commit it, then trust it (speccoder extensions trust {}) — nothing of it runs until then",
210 e.name, e.rel, e.name
211 ),
212 report,
213 CommandStatus::Ok,
214 )
215 }
216 Err(err) => {
217 let status = if err.contains("cannot be loaded") || err.contains("plugin.json") {
218 CommandStatus::ConfigError
219 } else {
220 CommandStatus::UsageError
221 };
222 let r = result(&err, list(root, state), status);
223 if status == CommandStatus::ConfigError {
224 r.with_code("EXTENSION-UNLOADABLE")
225 } else {
226 r
227 }
228 }
229 }
230}
231
232#[must_use]
234pub fn remove_cmd(root: &Path, state: &Path, name: &str) -> CommandResult {
235 match super::add::remove(root, state, name) {
236 Ok(()) => {
237 let mut report = list(root, state);
238 report.removed = Some(name.to_owned());
239 result(
240 &format!("removed {name}: commit the change to remove it for everyone"),
241 report,
242 CommandStatus::Ok,
243 )
244 }
245 Err(err) => result(&err, list(root, state), CommandStatus::UsageError),
246 }
247}
248
249pub fn trust_cmd(
252 root: &Path,
253 state: &Path,
254 name: &str,
255 via: &str,
256 confirm: &mut dyn FnMut(&Extension) -> bool,
257) -> CommandResult {
258 let dir = crate::hooks::dir(state, root);
259 let all = super::load(root);
260 let Some(e) = all.iter().find(|e| e.name == name) else {
261 return result(
262 &format!("there is no extension `{name}`"),
263 list(root, state),
264 CommandStatus::NotFound,
265 );
266 };
267 let shown = entry(e, &dir);
268 if !e.usable() {
269 return result(
270 &format!("{name} cannot be loaded: {}", e.problems.join("; ")),
271 list(root, state),
272 CommandStatus::ConfigError,
273 )
274 .with_code("EXTENSION-UNLOADABLE");
275 }
276 if shown.trust == TrustState::Trusted {
277 return result(
278 &format!("{name} is trusted as it is"),
279 list(root, state),
280 CommandStatus::Ok,
281 );
282 }
283 if !confirm(&shown) {
284 return result(
285 &format!("{name} not trusted: nothing of it runs"),
286 list(root, state),
287 CommandStatus::NeedsHuman,
288 )
289 .with_code("EXTENSION-UNTRUSTED");
290 }
291 trust::record(&dir, e, &crate::approvals::who_in(root), via);
292 after_trust(root, &dir, e);
293 let mut report = list(root, state);
294 report.trusted = vec![name.to_owned()];
295 result(
296 &format!("trusted {name}: it runs from now on"),
297 report,
298 CommandStatus::Ok,
299 )
300}
301
302fn after_trust(root: &Path, dir: &Path, e: &Loaded) {
305 for h in e.hooks.iter().filter(|h| h.at().is_none()) {
306 if !crate::hooks::deliver::has_cursor(dir, &h.name) {
307 crate::hooks::deliver::start_here(dir, root, &h.name);
308 }
309 }
310}
311
312pub fn trust_from_sheet(root: &Path, state: &Path, name: &str, digest: &str) -> Result<(), String> {
318 trust_shown(root, state, name, digest, "web")?;
319 let dir = crate::hooks::dir(state, root);
320 if let Some(e) = super::load(root).iter().find(|e| e.name == name) {
321 after_trust(root, &dir, e);
322 }
323 Ok(())
324}
325
326pub fn apply_flags(root: &Path, state: &Path, flags: &[String]) -> Result<(), Box<CommandResult>> {
332 if flags.is_empty() {
333 return Ok(());
334 }
335 let parsed = flags
336 .iter()
337 .map(|f| trust::parse_flag(f))
338 .collect::<Result<Vec<_>, _>>()
339 .map_err(|e| {
340 Box::new(
341 CommandResult::new("extensions", CommandStatus::UsageError, &e).with_code("USAGE"),
342 )
343 })?;
344 let dir = crate::hooks::dir(state, root);
345 let all = super::load(root);
346 trust::apply_flags(&dir, &all, &parsed, &crate::approvals::who_in(root)).map_err(|e| {
347 Box::new(
348 CommandResult::new("extensions", CommandStatus::NeedsHuman, &e)
349 .with_code("EXTENSION-UNTRUSTED"),
350 )
351 })?;
352 for (name, _) in &parsed {
353 if let Some(e) = all.iter().find(|e| &e.name == name) {
354 after_trust(root, &dir, e);
355 }
356 }
357 Ok(())
358}