Skip to content

Commit ebd0435

Browse files
committed
feat(cli): show author and source after run and install
Every surface that shows a remote Skill now names the GitHub owner and Repository, the commit, and the exact SKILL.md URL at that commit. - `skilld run`: headline `name · owner/repository @ commit`, then `Read it first: <SKILL.md URL>` after the source status caution. JSON origin gains owner, repository, skillPath, commit, sourceUrl. - `skilld install`: the same block plus the source status and its meaning for verified, local, and unverified. Replaces the bare "Review the unverified Skill before use." hint. - `skilld search`: `owner/repository` before the star count in human, plain, and JSON rows. - `skilld view`: the Source link points at the exact SKILL.md. `Host::install` and `Host::install_request` return `InstalledSkill` (name, locked source, source status) instead of names. Claude-Session: https://claude.ai/code/session_018T67Ndp8FAjnHWthXABbJW
1 parent 141a5d2 commit ebd0435

12 files changed

Lines changed: 530 additions & 90 deletions

File tree

crates/skilld-command/src/lib.rs

Lines changed: 138 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod local_store;
33
mod outdated;
44
pub use outdated::{NoOutdatedProgress, OutdatedProgress, ancestor_roots};
55
mod output;
6+
mod provenance;
67
mod remote;
78
mod run;
89

@@ -21,6 +22,8 @@ pub use local_store::{
2122
TargetInstall, TransactionGate,
2223
};
2324
pub use output::{CommandPlatform, OutputContext};
25+
pub use provenance::RemoteProvenance;
26+
use provenance::source_status_caution;
2427
pub use remote::{
2528
Cancellation, HeaderValue, HttpAdapter, HttpHeader, HttpMethod, HttpRequest, HttpResponse,
2629
NativeRemoteConfig, NeverCancelled, NoRemoteProgress, NoTokenProvider, PreparedRemoteSkill,
@@ -34,9 +37,9 @@ pub use run::{
3437
use skilld_core::{
3538
AGENT_TARGETS, AgentTargetId, CommitHistory, CommitSha, DomainError, GlobalTargetPath,
3639
InstallMode, InstallOperation, InstallRequest, InstallScope, InstallSource, LockedSource,
37-
NotTrackedReason, RemoteSelector, SourceRef, UpdateFailure, UpdateLatestCommit,
38-
UpdateModelError, UpdatePlan, UpdatePlanItem, UpdatePlanV1, UpdateRelation, UpdateRetryAfter,
39-
VERSION, classify_update_comparison, select_target_ids,
40+
NotTrackedReason, SourceRef, UpdateFailure, UpdateLatestCommit, UpdateModelError, UpdatePlan,
41+
UpdatePlanItem, UpdatePlanV1, UpdateRelation, UpdateRetryAfter, VERSION,
42+
classify_update_comparison, select_target_ids,
4043
};
4144
use skilld_ui::text::is_unsafe_terminal;
4245
use skilld_ui::{Detail, Line, Marker, Screen};
@@ -197,12 +200,28 @@ enum ConfigCommand {
197200
List,
198201
}
199202

203+
/// One Skill an install wrote, with the source the lockfile now records.
204+
#[derive(Clone, Debug, Eq, PartialEq)]
205+
pub struct InstalledSkill {
206+
pub name: String,
207+
pub source: LockedSource,
208+
/// `verified`, `local`, or `unverified`.
209+
pub source_status: &'static str,
210+
}
211+
200212
pub trait Host {
201213
fn list(&self, scope: InstallScope) -> Result<Vec<String>, CommandError>;
202214

203-
fn install(&self, source: InstallSource, scope: InstallScope) -> Result<String, CommandError>;
215+
fn install(
216+
&self,
217+
source: InstallSource,
218+
scope: InstallScope,
219+
) -> Result<InstalledSkill, CommandError>;
204220

205-
fn install_request(&self, request: InstallRequest) -> Result<Vec<String>, CommandError> {
221+
fn install_request(
222+
&self,
223+
request: InstallRequest,
224+
) -> Result<Vec<InstalledSkill>, CommandError> {
206225
if !request.targets.is_empty() || request.mode.is_some() {
207226
return Err(CommandError::unsupported_host(
208227
"Agent target selection is unavailable on this host",
@@ -213,7 +232,7 @@ pub trait Host {
213232
"lockfile restore is unavailable",
214233
));
215234
};
216-
self.install(source, request.scope).map(|name| vec![name])
235+
self.install(source, request.scope).map(|skill| vec![skill])
217236
}
218237

219238
fn run_skill(
@@ -824,18 +843,15 @@ fn dispatch<H: Host>(
824843
.map(InstallMode::parse)
825844
.transpose()
826845
.map_err(CommandError::domain)?;
827-
let names = host.install_request(InstallRequest {
846+
let installed = host.install_request(InstallRequest {
828847
operation,
829848
scope,
830849
targets,
831850
mode,
832851
})?;
833-
let mut lines = names
834-
.into_iter()
835-
.map(|name| Line::success(format!("Installed Skill {name}.")))
836-
.collect::<Vec<_>>();
837-
if direct {
838-
lines.push(Line::hint("Review the unverified Skill before use."));
852+
let mut lines = Vec::new();
853+
for skill in &installed {
854+
lines.extend(render_installed(skill)?);
839855
}
840856
Ok(CommandOutput::Screen(Screen::new(lines)))
841857
}
@@ -958,6 +974,8 @@ fn dispatch<H: Host>(
958974
name: result.name,
959975
selector: selector.to_string(),
960976
description: result.description,
977+
owner: result.source.owner,
978+
repository: result.source.repository,
961979
stargazer_count: result.stargazer_count,
962980
})
963981
})
@@ -995,15 +1013,48 @@ fn dispatch<H: Host>(
9951013
}
9961014
}
9971015

998-
fn render_view(view: SkillView) -> Result<Vec<Line>, CommandError> {
999-
let source = match view.skill.source {
1000-
LockedSource::Local { path } => Line::field("Source", format!("local {path}")),
1001-
LockedSource::BundledSkilld => Line::field("Source", "skilld-maintained Skill"),
1002-
LockedSource::Remote { source, .. } => {
1003-
let selector = RemoteSelector::parse(&source).map_err(CommandError::remote)?;
1004-
Line::linked_field("Source", selector.canonical(), github_url(&selector)?)
1016+
/// Say who published the Skill, where its bytes came from, and what the
1017+
/// status means. Every status gets its meaning; a remote source gets the
1018+
/// exact SKILL.md on GitHub.
1019+
fn render_installed(skill: &InstalledSkill) -> Result<Vec<Line>, CommandError> {
1020+
let provenance = RemoteProvenance::from_locked(&skill.source)?;
1021+
let mut lines = vec![Line::success(format!("Installed Skill {}.", skill.name))];
1022+
if let Some(provenance) = &provenance {
1023+
lines.push(Line::item(provenance.headline(&skill.name)));
1024+
}
1025+
lines.push(source_line(&skill.source, provenance.as_ref()));
1026+
lines.push(Line::field("Source status", skill.source_status));
1027+
lines.extend(
1028+
source_status_caution(skill.source_status)
1029+
.lines()
1030+
.map(Line::hint),
1031+
);
1032+
if let Some(provenance) = &provenance {
1033+
lines.push(Line::linked_field(
1034+
"Read it first",
1035+
provenance.source_url.clone(),
1036+
provenance.source_url.clone(),
1037+
));
1038+
}
1039+
Ok(lines)
1040+
}
1041+
1042+
/// The `Source` row. A remote source links to its exact SKILL.md when the
1043+
/// terminal supports hyperlinks.
1044+
fn source_line(source: &LockedSource, provenance: Option<&RemoteProvenance>) -> Line {
1045+
match (source, provenance) {
1046+
(LockedSource::Local { path }, _) => Line::field("Source", format!("local {path}")),
1047+
(LockedSource::BundledSkilld, _) => Line::field("Source", "skilld-maintained Skill"),
1048+
(LockedSource::Remote { source, .. }, Some(provenance)) => {
1049+
Line::linked_field("Source", source, provenance.source_url.clone())
10051050
}
1006-
};
1051+
(LockedSource::Remote { source, .. }, None) => Line::field("Source", source),
1052+
}
1053+
}
1054+
1055+
fn render_view(view: SkillView) -> Result<Vec<Line>, CommandError> {
1056+
let provenance = RemoteProvenance::from_locked(&view.skill.source)?;
1057+
let source = source_line(&view.skill.source, provenance.as_ref());
10071058
let targets = if view.skill.targets.is_empty() {
10081059
"none".to_owned()
10091060
} else {
@@ -1023,17 +1074,6 @@ fn render_view(view: SkillView) -> Result<Vec<Line>, CommandError> {
10231074
])
10241075
}
10251076

1026-
/// Build a GitHub repository URL from a parsed remote selector.
1027-
fn github_url(selector: &RemoteSelector) -> Result<String, CommandError> {
1028-
let mut url = url::Url::parse("https://github.com/")
1029-
.map_err(|_| CommandError::service("the GitHub Repository URL could not be built"))?;
1030-
url.path_segments_mut()
1031-
.map_err(|_| CommandError::service("the GitHub Repository URL could not be built"))?
1032-
.push(&selector.source().owner)
1033-
.push(&selector.source().repository);
1034-
Ok(url.into())
1035-
}
1036-
10371077
fn scope(global: bool) -> InstallScope {
10381078
if global {
10391079
InstallScope::Global
@@ -1299,7 +1339,7 @@ impl LocalHost {
12991339
scope: InstallScope,
13001340
targets: &[TargetInstall],
13011341
known: &[ResolvedTarget],
1302-
) -> Result<String, CommandError> {
1342+
) -> Result<InstalledSkill, CommandError> {
13031343
let selector = skilld_core::RemoteSelector::parse(source).map_err(CommandError::remote)?;
13041344
let prepared = self
13051345
.remote_provider()?
@@ -1316,7 +1356,25 @@ impl LocalHost {
13161356
known,
13171357
)
13181358
.map_err(CommandError::store)?;
1319-
Ok(name.to_string())
1359+
self.installed(scope, &name, known)
1360+
}
1361+
1362+
/// Read back what the lockfile recorded for one installed Skill.
1363+
fn installed(
1364+
&self,
1365+
scope: InstallScope,
1366+
name: &skilld_core::SkillName,
1367+
known: &[ResolvedTarget],
1368+
) -> Result<InstalledSkill, CommandError> {
1369+
let view = self
1370+
.store(scope)
1371+
.view(name, known)
1372+
.map_err(CommandError::store)?;
1373+
Ok(InstalledSkill {
1374+
name: view.name,
1375+
source: view.skill.source,
1376+
source_status: view.skill.source_status.as_str(),
1377+
})
13201378
}
13211379

13221380
fn run_remote(
@@ -1381,6 +1439,12 @@ impl LocalHost {
13811439
))
13821440
.map_err(CommandError::remote)?
13831441
.canonical();
1442+
let provenance = RemoteProvenance::new(
1443+
locked_selector.source().owner.as_str(),
1444+
locked_selector.source().repository.as_str(),
1445+
skill_path.as_str(),
1446+
revision.as_str(),
1447+
)?;
13841448
let source_status = prepared.source_status.as_str();
13851449
let (name, _, files) =
13861450
skilld_core::prepare_unverified_files(prepared.files).map_err(CommandError::remote)?;
@@ -1389,6 +1453,7 @@ impl LocalHost {
13891453
source: selector.canonical(),
13901454
exact_source,
13911455
direct,
1456+
provenance: Box::new(provenance),
13921457
};
13931458
if !wanted.is_empty() {
13941459
return Ok(RunOutcome::Files {
@@ -1474,7 +1539,11 @@ impl LocalHost {
14741539
})))
14751540
}
14761541

1477-
fn restore(&self, request: &InstallRequest, direct: bool) -> Result<Vec<String>, CommandError> {
1542+
fn restore(
1543+
&self,
1544+
request: &InstallRequest,
1545+
direct: bool,
1546+
) -> Result<Vec<InstalledSkill>, CommandError> {
14781547
let (targets, known) = if request.targets.is_empty() {
14791548
(None, self.known_targets(request.scope)?)
14801549
} else {
@@ -1596,7 +1665,7 @@ impl LocalHost {
15961665
.map_err(CommandError::store)?;
15971666
}
15981667
}
1599-
restored.push(name);
1668+
restored.push(self.installed(request.scope, &skill_name, &known)?);
16001669
}
16011670
Ok(restored)
16021671
}
@@ -1608,7 +1677,11 @@ impl Host for LocalHost {
16081677
self.store(scope).list(&known).map_err(CommandError::store)
16091678
}
16101679

1611-
fn install(&self, source: InstallSource, scope: InstallScope) -> Result<String, CommandError> {
1680+
fn install(
1681+
&self,
1682+
source: InstallSource,
1683+
scope: InstallScope,
1684+
) -> Result<InstalledSkill, CommandError> {
16121685
self.install_request(InstallRequest {
16131686
operation: InstallOperation::Install(source),
16141687
scope,
@@ -1620,7 +1693,10 @@ impl Host for LocalHost {
16201693
.ok_or_else(|| CommandError::service("Skill install returned no result"))
16211694
}
16221695

1623-
fn install_request(&self, request: InstallRequest) -> Result<Vec<String>, CommandError> {
1696+
fn install_request(
1697+
&self,
1698+
request: InstallRequest,
1699+
) -> Result<Vec<InstalledSkill>, CommandError> {
16241700
let source = match request.operation.clone() {
16251701
InstallOperation::Restore => return self.restore(&request, false),
16261702
InstallOperation::DirectRestore => return self.restore(&request, true),
@@ -1630,17 +1706,17 @@ impl Host for LocalHost {
16301706
match source {
16311707
InstallSource::Remote(source) => self
16321708
.install_remote(&source, false, request.scope, &targets, &known)
1633-
.map(|name| vec![name]),
1709+
.map(|skill| vec![skill]),
16341710
InstallSource::DirectRemote(source) => self
16351711
.install_remote(&source, true, request.scope, &targets, &known)
1636-
.map(|name| vec![name]),
1712+
.map(|skill| vec![skill]),
16371713
source => {
16381714
let (source, locked_source) = self.resolve_source(source)?;
16391715
let name = self
16401716
.store(request.scope)
16411717
.install_from(&source, locked_source, &targets, &known)
16421718
.map_err(CommandError::store)?;
1643-
Ok(vec![name.to_string()])
1719+
Ok(vec![self.installed(request.scope, &name, &known)?])
16441720
}
16451721
}
16461722
}
@@ -3044,20 +3120,31 @@ mod tests {
30443120
&self,
30453121
source: InstallSource,
30463122
scope: InstallScope,
3047-
) -> Result<String, CommandError> {
3123+
) -> Result<InstalledSkill, CommandError> {
30483124
assert_eq!(source, InstallSource::BundledSkilld);
30493125
assert_eq!(scope, InstallScope::Global);
3050-
Ok("skilld".to_owned())
3126+
Ok(bundled_skilld())
30513127
}
30523128

3053-
fn install_request(&self, request: InstallRequest) -> Result<Vec<String>, CommandError> {
3129+
fn install_request(
3130+
&self,
3131+
request: InstallRequest,
3132+
) -> Result<Vec<InstalledSkill>, CommandError> {
30543133
assert_eq!(
30553134
request.operation,
30563135
InstallOperation::Install(InstallSource::BundledSkilld)
30573136
);
30583137
assert_eq!(request.scope, InstallScope::Global);
30593138
assert_eq!(request.targets, [AgentTargetId::Codex]);
3060-
Ok(vec!["skilld".to_owned()])
3139+
Ok(vec![bundled_skilld()])
3140+
}
3141+
}
3142+
3143+
fn bundled_skilld() -> InstalledSkill {
3144+
InstalledSkill {
3145+
name: "skilld".to_owned(),
3146+
source: LockedSource::BundledSkilld,
3147+
source_status: "local",
30613148
}
30623149
}
30633150

@@ -3108,7 +3195,12 @@ mod tests {
31083195
assert_eq!(result.exit_code, 0);
31093196
assert_eq!(
31103197
String::from_utf8(stdout).unwrap(),
3111-
"Installed Skill skilld.\n"
3198+
concat!(
3199+
"Installed Skill skilld.\n",
3200+
"Source: skilld-maintained Skill\n",
3201+
"Source status: local\n",
3202+
"Read this Skill before you follow it.\n",
3203+
)
31123204
);
31133205
assert!(stderr.is_empty());
31143206
}

0 commit comments

Comments
 (0)