@@ -3,6 +3,7 @@ mod local_store;
33mod outdated;
44pub use outdated:: { NoOutdatedProgress , OutdatedProgress , ancestor_roots} ;
55mod output;
6+ mod provenance;
67mod remote;
78mod run;
89
@@ -21,6 +22,8 @@ pub use local_store::{
2122 TargetInstall , TransactionGate ,
2223} ;
2324pub use output:: { CommandPlatform , OutputContext } ;
25+ pub use provenance:: RemoteProvenance ;
26+ use provenance:: source_status_caution;
2427pub use remote:: {
2528 Cancellation , HeaderValue , HttpAdapter , HttpHeader , HttpMethod , HttpRequest , HttpResponse ,
2629 NativeRemoteConfig , NeverCancelled , NoRemoteProgress , NoTokenProvider , PreparedRemoteSkill ,
@@ -34,9 +37,9 @@ pub use run::{
3437use 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} ;
4144use skilld_ui:: text:: is_unsafe_terminal;
4245use 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+
200212pub 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-
10371077fn 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