Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 53 additions & 19 deletions crates/skilld-command/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,7 @@ use skilld_core::{
UpdatePlanItem, UpdatePlanV1, UpdateRelation, UpdateRetryAfter, VERSION,
classify_update_comparison, select_target_ids,
};
use skilld_ui::{Line, Screen};
use skilld_ui::{Detail, Line, Marker, Screen};

use output::{
OutputMode, SearchItem, SearchOutcome, render_error, render_search, render_update_check,
Expand DownExpand Up@@ -1868,9 +1868,11 @@ impl Host for LocalHost {
let mut failures = BTreeMap::<String, Vec<&outdated::UnmanagedSkill>>::new();
for (skill, result) in unmanaged.iter().zip(results) {
match result {
Ok(Some(candidate)) => {
lines.extend(outdated::render_unmanaged(skill, Some(&candidate)))
}
Ok(Some(candidate)) => lines.extend(outdated::render_unmanaged(
skill,
Some(&candidate),
&self.project_root,
)),
Ok(None) => no_match.push(skill),
Err(error) => failures.entry(error.message).or_default().push(skill),
}
Expand DownExpand Up@@ -2317,18 +2319,35 @@ impl LocalHost {
});
match state {
Ok(RemoteSourceState::Current) => {
vec![Line::success(format!("Current Skill {name}."))]
vec![Line::record(
Marker::Success,
format!("Current Skill {name}."),
name,
Some("current".to_owned()),
Vec::new(),
)]
}
Ok(RemoteSourceState::Stale { .. }) => {
vec![Line::warn(format!(
"Outdated Skill {name}. Run skilld update {name}{global}."
))]
let update = format!("skilld update {name}{global}");
vec![Line::record(
Marker::Warn,
format!("Outdated Skill {name}. Run {update}."),
name,
Some("outdated".to_owned()),
vec![Detail::command("update", update)],
)]
}
Err(error) => {
vec![Line::error(format!(
"Source state unavailable for Skill {name}: {}.",
error.message
))]
vec![Line::record(
Marker::Error,
format!(
"Source state unavailable for Skill {name}: {}.",
error.message
),
name,
Some("source unavailable".to_owned()),
vec![Detail::plain("error", error.message.clone())],
)]
}
}
}
Expand All@@ -2340,14 +2359,29 @@ impl LocalHost {
.map(|locked| locked.agent)
.collect::<Vec<_>>();
let agent_flags = outdated::agent_flags(&agents);
vec![Line::warn(format!(
"Unverified Skill {name}. Run skilld install {source} --direct{global}{agent_flags} to update it."
))]
}
(LockedSource::BundledSkilld, _) => {
vec![Line::plain(format!("skilld-maintained Skill {name}."))]
let install = format!("skilld install {source} --direct{global}{agent_flags}");
vec![Line::record(
Marker::Warn,
format!("Unverified Skill {name}. Run {install} to update it."),
name,
Some("unverified".to_owned()),
vec![Detail::command("install", install)],
)]
}
_ => vec![Line::plain(format!("Local Skill {name}."))],
(LockedSource::BundledSkilld, _) => vec![Line::record(
Marker::Note,
format!("skilld-maintained Skill {name}."),
name,
Some("skilld-maintained".to_owned()),
Vec::new(),
)],
_ => vec![Line::record(
Marker::Note,
format!("Local Skill {name}."),
name,
Some("local".to_owned()),
Vec::new(),
)],
}
}

Expand Down
5 changes: 3 additions & 2 deletions crates/skilld-command/src/local_store.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -794,8 +794,9 @@ impl LocalStore {
));
}
let bytes = fs::read(&path).map_err(fs_error)?;
let document: LockDocument = serde_json::from_slice(&bytes)
.map_err(|error| StoreError::InvalidLockfile(error.to_string()))?;
let document: LockDocument = serde_json::from_slice(&bytes).map_err(|_| {
StoreError::InvalidLockfile("the Skill lockfile is not valid JSON".to_owned())
})?;
if document.version != 1 {
return Err(StoreError::InvalidLockfile(format!(
"unsupported Skill lockfile version: {}",
Expand Down
71 changes: 49 additions & 22 deletions crates/skilld-command/src/outdated.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,8 @@ use std::fs;
use std::path::{Path, PathBuf};

use skilld_core::{AgentTargetId, InstallScope, SkillName};
use skilld_ui::Line;
use skilld_ui::text::{display_path, grouped_number};
use skilld_ui::{Detail, Line, Marker};

use crate::ResolvedTarget;
use crate::local_store::normalize_path;
Expand DownExpand Up@@ -136,11 +137,16 @@ pub(crate) fn render_no_match(skills: &[&UnmanagedSkill]) -> Vec<Line> {
if skills.is_empty() {
return vec![];
}
vec![Line::warn(format!(
"No Repository match for {} ({}).",
skill_count(skills.len()),
name_list(skills)
))]
let count = skill_count(skills.len());
vec![Line::group(
Marker::Warn,
format!("No Repository match for {count} ({}).", name_list(skills)),
format!("No Repository match for {count}"),
skills
.iter()
.map(|skill| (skill.name.clone(), agent_list(skill)))
.collect(),
)]
}

pub(crate) fn render_search_failures(
Expand All@@ -149,11 +155,19 @@ pub(crate) fn render_search_failures(
failures
.iter()
.map(|(message, skills)| {
Line::warn(format!(
"Skill search unavailable for {} ({}): {message}.",
skill_count(skills.len()),
name_list(skills)
))
Line::group(
Marker::Warn,
format!(
"Skill search unavailable for {} ({}): {message}.",
skill_count(skills.len()),
name_list(skills)
),
format!("Skill search unavailable: {message}"),
skills
.iter()
.map(|skill| (skill.name.clone(), agent_list(skill)))
.collect(),
)
})
.collect()
}
Expand All@@ -173,6 +187,7 @@ fn skill_count(count: usize) -> String {
pub(crate) fn render_unmanaged(
skill: &UnmanagedSkill,
candidate: Option<&SkillCandidate>,
display_base: &Path,
) -> Vec<Line> {
let agents = agent_list(skill);
let Some(candidate) = candidate else {
Expand All@@ -184,17 +199,29 @@ pub(crate) fn render_unmanaged(
""
};
let agent_flags = agent_flags(&skill.agents);
vec![
Line::warn(format!(
"Unmanaged Skill {} ({agents}). Candidate source {}, {} stars.",
skill.name, candidate.selector, candidate.stargazer_count
)),
Line::hint(format!(
"Delete {}, then run skilld install {}{global}{agent_flags}.",
skill.path.display(),
candidate.selector
)),
]
let install = format!("skilld install {}{global}{agent_flags}", candidate.selector);
let plain = format!(
"Unmanaged Skill {} ({agents}). Candidate source {}, {} stars.\nDelete {}, then run {install}.",
skill.name,
candidate.selector,
candidate.stargazer_count,
skill.path.display()
);
vec![Line::record(
Marker::Warn,
plain,
skill.name.clone(),
Some(format!("{agents} · unmanaged")),
vec![
Detail::plain("candidate", candidate.selector.clone()),
Detail::plain(
"stars",
format!("★ {}", grouped_number(candidate.stargazer_count)),
),
Detail::command("install", install),
Detail::path("delete", display_path(&skill.path, display_base)),
],
)]
}

pub(crate) fn agent_flags(agents: &[AgentTargetId]) -> String {
Expand Down
16 changes: 13 additions & 3 deletions crates/skilld-command/src/output.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -160,7 +160,17 @@ pub(crate) fn render_error(error: &CommandError, mode: OutputMode) -> Vec<u8> {
})
.unwrap_or_else(|_| b"OUTPUT_RENDER_FAILED: error output could not be encoded\n".to_vec());
}
format!("{error}\n").into_bytes()
match mode {
OutputMode::Human { color, .. } => format!(
"{} {} {}\n",
skilld_ui::paint("✗", skilld_ui::Role::Error, color),
skilld_ui::paint(&error.message, skilld_ui::Role::Emphasis, color),
skilld_ui::paint(&format!("({})", error.code), skilld_ui::Role::Dim, color),
)
.into_bytes(),
OutputMode::Plain => format!("{error}\n").into_bytes(),
OutputMode::JsonV1 => unreachable!("JSON errors return early"),
}
}

fn render_plain(outcome: &SearchOutcome) -> String {
Expand DownExpand Up@@ -240,10 +250,10 @@ fn render_human(outcome: &SearchOutcome, terminal_width: u16, color: bool) -> St
output.push('\n');
}
}
let install = format!("Install: skilld install {}", sanitize(&item.selector));
let install = format!("skilld install {}", sanitize(&item.selector));
for line in wrap(&install, columns.saturating_sub(2)) {
output.push_str(" ");
output.push_str(&paint(&line, Role::Dim, color));
output.push_str(&skilld_ui::paint_command(&line, color));
output.push('\n');
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/skilld-command/tests/output.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -237,7 +237,7 @@ fn human_search_is_polished_and_respects_terminal_width() {
assert!(stdout.contains("1 of 14 Skills"));
assert!(stdout.contains("227,068 stars"));
assert!(stdout.contains("skilld:mattpocock/skills/grill-me"));
assert!(stdout.contains("Install: skilld install"));
assert!(stdout.contains("skilld install"));
assert!(
stdout
.lines()
Expand Down
7 changes: 6 additions & 1 deletion crates/skilld-native/src/main.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,6 +76,7 @@ fn main() -> ExitCode {
host.with_outdated_progress(Arc::new(status::OutdatedProgressLine::for_terminal(
std::io::stderr().is_terminal(),
active_agent_detected(),
!environment_present("NO_COLOR"),
)))
} else {
host
Expand All@@ -88,7 +89,11 @@ fn main() -> ExitCode {
Ok(summary) => {
let exit_code = summary.exit_code();
let mut stdout = std::io::stdout().lock();
if let Err(error) = write_static_summary(&summary, &mut stdout) {
if let Err(error) = write_static_summary(
&summary,
stdout.is_terminal() && !environment_present("NO_COLOR"),
&mut stdout,
) {
eprintln!("{error}");
ExitCode::from(2)
} else if stdout.flush().is_err() {
Expand Down
20 changes: 14 additions & 6 deletions crates/skilld-native/src/status.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -200,15 +200,17 @@ where
/// it so only results remain.
pub struct OutdatedProgressLine {
enabled: bool,
color: bool,
frame: std::sync::atomic::AtomicUsize,
}

impl OutdatedProgressLine {
/// Progress streams only for humans on a terminal. Agents, CI, and pipes
/// get quiet stderr.
pub fn for_terminal(is_terminal: bool, active_agent: bool) -> Self {
pub fn for_terminal(is_terminal: bool, active_agent: bool, color: bool) -> Self {
Self {
enabled: is_terminal && !active_agent,
color,
frame: std::sync::atomic::AtomicUsize::new(0),
}
}
Expand All@@ -229,7 +231,11 @@ impl skilld_command::OutdatedProgress for OutdatedProgressLine {
}
self.erase();
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "• {line}");
let _ = writeln!(
stderr,
"{}",
paint(&format!("• {line}"), Role::Dim, self.color)
);
let _ = stderr.flush();
}

Expand All@@ -238,8 +244,10 @@ impl skilld_command::OutdatedProgress for OutdatedProgressLine {
return;
}
let frame = spinner::frame(self.frame.fetch_add(1, Ordering::Relaxed));
let glyph = paint(frame, Role::Brand, self.color);
let name = paint(name, Role::Emphasis, self.color);
let mut stderr = std::io::stderr().lock();
let _ = write!(stderr, "\r\x1b[2K{frame} Checking {name}…");
let _ = write!(stderr, "\r\x1b[2K{glyph} Checking {name}…");
let _ = stderr.flush();
}

Expand DownExpand Up@@ -379,16 +387,16 @@ mod tests {
fn outdated_progress_stays_quiet_for_agents() {
use skilld_command::OutdatedProgress;

let agent = super::OutdatedProgressLine::for_terminal(true, true);
let agent = super::OutdatedProgressLine::for_terminal(true, true, true);
agent.found("example (project scope)");
agent.checking("example");
agent.finish();
assert!(!agent.enabled);

let human = super::OutdatedProgressLine::for_terminal(true, false);
let human = super::OutdatedProgressLine::for_terminal(true, false, true);
assert!(human.enabled);

let piped = super::OutdatedProgressLine::for_terminal(false, false);
let piped = super::OutdatedProgressLine::for_terminal(false, false, true);
assert!(!piped.enabled);
}

Expand Down
Loading