use std::path::Path;
use speccoder_core::extensions::{Extension, ExtensionsReport};
use speccoder_core::hooks::TrustState;
use super::{Loaded, trust};
#[must_use]
pub fn entry(e: &Loaded, dir: &Path) -> Extension {
let state = if e.usable() {
trust::state(dir, e)
} else {
TrustState::Untrusted
};
Extension {
name: e.name.clone(),
source: e.source,
version: e.version.clone(),
description: e.description.clone(),
folder: e.rel.clone(),
origin: e.origin.clone(),
digest: e.digest.clone(),
files: e.files.clone(),
contributions: e.contributions(),
trust: state,
changes: (state == TrustState::Changed)
.then(|| trust::changes(dir, e))
.flatten(),
trust_flag: format!("--trust-extension {}={}", e.name, e.digest),
problems: e.problems.clone(),
}
}
#[must_use]
pub fn list(root: &Path, state: &Path) -> ExtensionsReport {
let dir = crate::hooks::dir(state, root);
let all = super::load(root);
ExtensionsReport {
extensions: all.iter().map(|e| entry(e, &dir)).collect(),
..ExtensionsReport::default()
}
}
pub fn trust_shown(
root: &Path,
state: &Path,
name: &str,
digest: &str,
via: &str,
) -> Result<(), String> {
let all = super::load(root);
let Some(e) = all.iter().find(|e| e.name == name) else {
return Err(format!("there is no extension `{name}`"));
};
if !e.usable() {
return Err(format!(
"{name} cannot be loaded: {}",
e.problems.join("; ")
));
}
if e.digest != digest {
return Err(format!(
"{name} changed since it was shown ({} now): review it again",
e.digest
));
}
trust::record(
&crate::hooks::dir(state, root),
e,
&crate::approvals::who_in(root),
via,
);
Ok(())
}
#[must_use]
pub fn waiting(root: &Path, state: &Path) -> Vec<String> {
let dir = crate::hooks::dir(state, root);
super::load(root)
.iter()
.filter(|e| e.usable() && trust::state(&dir, e) != TrustState::Trusted)
.map(|e| e.name.clone())
.collect()
}
#[must_use]
pub fn run_notice(root: &Path, state: &Path) -> Option<String> {
let names = waiting(root, state);
(!names.is_empty()).then(|| {
format!(
"{} {} for your trust: nothing of {} runs (speccoder extensions trust {})",
names.join(", "),
if names.len() == 1 { "waits" } else { "wait" },
if names.len() == 1 { "it" } else { "them" },
names[0]
)
})
}
#[must_use]
pub fn doctor_lines(root: &Path, state: &Path) -> Vec<(bool, String)> {
let dir = crate::hooks::dir(state, root);
super::load(root)
.iter()
.filter_map(|e| {
if !e.usable() {
return Some((
false,
format!(
"extension {} cannot be loaded: {}",
e.name,
e.problems.join("; ")
),
));
}
match trust::state(&dir, e) {
TrustState::Trusted => None,
TrustState::Untrusted => Some((
false,
format!(
"extension {} waits for your trust: speccoder extensions trust {}",
e.name, e.name
),
)),
TrustState::Changed => Some((
false,
format!(
"extension {} changed since you trusted it: speccoder extensions trust {}",
e.name, e.name
),
)),
}
})
.collect()
}
use speccoder_core::{CommandData, CommandResult, CommandStatus};
fn result(message: &str, report: ExtensionsReport, status: CommandStatus) -> CommandResult {
CommandResult::new("extensions", status, message)
.with_data(CommandData::Extensions(Box::new(report)))
}
fn summary(r: &ExtensionsReport) -> String {
let waiting = r
.extensions
.iter()
.filter(|e| e.problems.is_empty() && e.trust != TrustState::Trusted)
.count();
let broken = r
.extensions
.iter()
.filter(|e| !e.problems.is_empty())
.count();
let mut s = match (r.extensions.len(), waiting) {
(0, _) => {
return "no extensions: `speccoder extensions add <folder | URL@commit>` (docs: extensions)"
.to_owned();
}
(1, 0) => "1 extension, trusted".to_owned(),
(n, 0) => format!("{n} extensions, all trusted"),
(n, w) => format!(
"{n} extension{} — {w} wait{} for your trust",
if n == 1 { "" } else { "s" },
if w == 1 { "s" } else { "" }
),
};
if broken > 0 {
s.push_str(&format!(", {broken} cannot be loaded"));
}
s
}
#[must_use]
pub fn list_cmd(root: &Path, state: &Path) -> CommandResult {
let report = list(root, state);
let message = summary(&report);
result(&message, report, CommandStatus::Ok)
}
#[must_use]
pub fn add_cmd(root: &Path, state: &Path, source: &str) -> CommandResult {
match super::add::add(root, source) {
Ok(e) => {
let mut report = list(root, state);
report.added = Some(e.name.clone());
result(
&format!(
"added {} to {}: commit it, then trust it (speccoder extensions trust {}) — nothing of it runs until then",
e.name, e.rel, e.name
),
report,
CommandStatus::Ok,
)
}
Err(err) => {
let status = if err.contains("cannot be loaded") || err.contains("plugin.json") {
CommandStatus::ConfigError
} else {
CommandStatus::UsageError
};
let r = result(&err, list(root, state), status);
if status == CommandStatus::ConfigError {
r.with_code("EXTENSION-UNLOADABLE")
} else {
r
}
}
}
}
#[must_use]
pub fn remove_cmd(root: &Path, state: &Path, name: &str) -> CommandResult {
match super::add::remove(root, state, name) {
Ok(()) => {
let mut report = list(root, state);
report.removed = Some(name.to_owned());
result(
&format!("removed {name}: commit the change to remove it for everyone"),
report,
CommandStatus::Ok,
)
}
Err(err) => result(&err, list(root, state), CommandStatus::UsageError),
}
}
pub fn trust_cmd(
root: &Path,
state: &Path,
name: &str,
via: &str,
confirm: &mut dyn FnMut(&Extension) -> bool,
) -> CommandResult {
let dir = crate::hooks::dir(state, root);
let all = super::load(root);
let Some(e) = all.iter().find(|e| e.name == name) else {
return result(
&format!("there is no extension `{name}`"),
list(root, state),
CommandStatus::NotFound,
);
};
let shown = entry(e, &dir);
if !e.usable() {
return result(
&format!("{name} cannot be loaded: {}", e.problems.join("; ")),
list(root, state),
CommandStatus::ConfigError,
)
.with_code("EXTENSION-UNLOADABLE");
}
if shown.trust == TrustState::Trusted {
return result(
&format!("{name} is trusted as it is"),
list(root, state),
CommandStatus::Ok,
);
}
if !confirm(&shown) {
return result(
&format!("{name} not trusted: nothing of it runs"),
list(root, state),
CommandStatus::NeedsHuman,
)
.with_code("EXTENSION-UNTRUSTED");
}
trust::record(&dir, e, &crate::approvals::who_in(root), via);
after_trust(root, &dir, e);
let mut report = list(root, state);
report.trusted = vec![name.to_owned()];
result(
&format!("trusted {name}: it runs from now on"),
report,
CommandStatus::Ok,
)
}
fn after_trust(root: &Path, dir: &Path, e: &Loaded) {
for h in e.hooks.iter().filter(|h| h.at().is_none()) {
if !crate::hooks::deliver::has_cursor(dir, &h.name) {
crate::hooks::deliver::start_here(dir, root, &h.name);
}
}
}
pub fn trust_from_sheet(root: &Path, state: &Path, name: &str, digest: &str) -> Result<(), String> {
trust_shown(root, state, name, digest, "web")?;
let dir = crate::hooks::dir(state, root);
if let Some(e) = super::load(root).iter().find(|e| e.name == name) {
after_trust(root, &dir, e);
}
Ok(())
}
pub fn apply_flags(root: &Path, state: &Path, flags: &[String]) -> Result<(), Box<CommandResult>> {
if flags.is_empty() {
return Ok(());
}
let parsed = flags
.iter()
.map(|f| trust::parse_flag(f))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| {
Box::new(
CommandResult::new("extensions", CommandStatus::UsageError, &e).with_code("USAGE"),
)
})?;
let dir = crate::hooks::dir(state, root);
let all = super::load(root);
trust::apply_flags(&dir, &all, &parsed, &crate::approvals::who_in(root)).map_err(|e| {
Box::new(
CommandResult::new("extensions", CommandStatus::NeedsHuman, &e)
.with_code("EXTENSION-UNTRUSTED"),
)
})?;
for (name, _) in &parsed {
if let Some(e) = all.iter().find(|e| &e.name == name) {
after_trust(root, &dir, e);
}
}
Ok(())
}