@@ -4,6 +4,7 @@ use skilld_core::{ListedSkill, SkillListing, UpdatePlanV1};
44use skilld_ui:: text:: { grouped_number, is_unsafe_terminal, sanitize, width, wrap} ;
55use skilld_ui:: { Role , paint} ;
66
7+ use crate :: provenance:: { RemoteProvenance , source_status_caution} ;
78use crate :: run:: { FileContent , PulledFile , RunOutcome , SkillOrigin , TransientSkill } ;
89use crate :: { CommandError , CommandErrorKind } ;
910
@@ -115,9 +116,18 @@ pub(crate) struct SearchItem {
115116 pub name : String ,
116117 pub selector : String ,
117118 pub description : Option < String > ,
119+ pub owner : String ,
120+ pub repository : String ,
118121 pub stargazer_count : u64 ,
119122}
120123
124+ impl SearchItem {
125+ /// `owner/repository`, the way GitHub names it.
126+ fn slug ( & self ) -> String {
127+ format ! ( "{}/{}" , self . owner, self . repository)
128+ }
129+ }
130+
121131pub ( crate ) fn render_search (
122132 outcome : & SearchOutcome ,
123133 mode : OutputMode ,
@@ -247,6 +257,8 @@ fn render_plain(outcome: &SearchOutcome) -> String {
247257 output. push ( '\t' ) ;
248258 output. push_str ( & escape_plain ( & item. selector ) ) ;
249259 output. push ( '\t' ) ;
260+ output. push_str ( & escape_plain ( & item. slug ( ) ) ) ;
261+ output. push ( '\t' ) ;
250262 output. push_str ( & item. stargazer_count . to_string ( ) ) ;
251263 output. push ( '\t' ) ;
252264 output. push_str ( & escape_plain (
@@ -296,23 +308,32 @@ fn render_human(
296308 for item in & outcome. items {
297309 output. push ( '\n' ) ;
298310 let name = sanitize ( & item. name ) ;
311+ let slug = sanitize ( & item. slug ( ) ) ;
299312 let stars = format ! ( "{} stars" , grouped_number( item. stargazer_count) ) ;
300- if 2 + width ( & name) + 2 + width ( & stars) <= columns {
301- let gap = columns - 2 - width ( & name) - width ( & stars) ;
313+ let meta = format ! (
314+ "{} · {}" ,
315+ paint( & slug, Role :: Dim , color) ,
316+ paint( & stars, Role :: Warn , color)
317+ ) ;
318+ let meta_width = width ( & slug) + 3 + width ( & stars) ;
319+ if 2 + width ( & name) + 2 + meta_width <= columns {
320+ let gap = columns - 2 - width ( & name) - meta_width;
302321 output. push_str ( " " ) ;
303322 output. push_str ( & paint ( & name, Role :: Emphasis , color) ) ;
304323 output. push_str ( & " " . repeat ( gap) ) ;
305- output. push_str ( & paint ( & stars , Role :: Warn , color ) ) ;
324+ output. push_str ( & meta ) ;
306325 output. push ( '\n' ) ;
307326 } else {
308327 for line in wrap ( & name, columns. saturating_sub ( 2 ) ) {
309328 output. push_str ( " " ) ;
310329 output. push_str ( & paint ( & line, Role :: Emphasis , color) ) ;
311330 output. push ( '\n' ) ;
312331 }
313- output. push_str ( " " ) ;
314- output. push_str ( & paint ( & stars, Role :: Warn , color) ) ;
315- output. push ( '\n' ) ;
332+ for line in wrap ( & format ! ( "{slug} · {stars}" ) , columns. saturating_sub ( 2 ) ) {
333+ output. push_str ( " " ) ;
334+ output. push_str ( & paint ( & line, Role :: Dim , color) ) ;
335+ output. push ( '\n' ) ;
336+ }
316337 }
317338
318339 if let Some ( description) = & item. description {
@@ -588,9 +609,17 @@ fn render_load(skill: &TransientSkill, color: bool, platform: CommandPlatform) -
588609 out. push_str ( "skilld wrote no Skill files.\n " ) ;
589610 out. push_str ( & field ( "Source" , "skilld-maintained Skill" , color) ) ;
590611 }
591- SkillOrigin :: Remote { source, .. } => {
612+ SkillOrigin :: Remote {
613+ source, provenance, ..
614+ } => {
592615 out. push_str ( "skilld retained no Skill files.\n " ) ;
593616 out. push_str ( "It created no lockfile entry, Agent target, or project file.\n " ) ;
617+ out. push_str ( & paint (
618+ & sanitize ( & provenance. headline ( & skill. name ) ) ,
619+ Role :: Emphasis ,
620+ color,
621+ ) ) ;
622+ out. push ( '\n' ) ;
594623 out. push_str ( & field ( "Source" , source, color) ) ;
595624 }
596625 SkillOrigin :: Local { root } => {
@@ -603,6 +632,7 @@ fn render_load(skill: &TransientSkill, color: bool, platform: CommandPlatform) -
603632 }
604633 out. push_str ( & field ( "Source status" , skill. source_status , color) ) ;
605634 out. push_str ( source_status_caution ( skill. source_status ) ) ;
635+ out. push_str ( & read_it_first ( & skill. origin , color) ) ;
606636
607637 out. push ( '\n' ) ;
608638 out. push_str ( & paint ( "--- SKILL.md ---" , Role :: Dim , color) ) ;
@@ -699,6 +729,7 @@ fn render_files(
699729 }
700730 out. push_str ( & field ( "Source status" , source_status, color) ) ;
701731 out. push_str ( source_status_caution ( source_status) ) ;
732+ out. push_str ( & read_it_first ( origin, color) ) ;
702733 out. push ( '\n' ) ;
703734 for file in files {
704735 let path = sanitize ( & file. path ) ;
@@ -849,17 +880,14 @@ fn shell_quote(argument: &str, platform: CommandPlatform) -> String {
849880 }
850881}
851882
852- /// State what the status covers, on every status.
853- ///
854- /// A verified Artifact proves where the bytes came from. It says nothing about
855- /// what the instructions ask an Agent to do, and the output must not imply it.
856- fn source_status_caution ( status : & str ) -> & ' static str {
857- match status {
858- "verified" => {
859- "skilld checked where this Skill came from, not what it asks you to do.\n Read it before you follow it.\n "
883+ /// Point at the exact SKILL.md on GitHub. Local and bundled Skills sit on
884+ /// disk already, so they get no link.
885+ fn read_it_first ( origin : & SkillOrigin , color : bool ) -> String {
886+ match origin {
887+ SkillOrigin :: Remote { provenance, .. } => {
888+ field ( "Read it first" , & provenance. source_url , color)
860889 }
861- "unverified" => "skilld did not check this source. Read this Skill before you follow it.\n " ,
862- _ => "Read this Skill before you follow it.\n " ,
890+ SkillOrigin :: Bundled | SkillOrigin :: Local { .. } => String :: new ( ) ,
863891 }
864892}
865893
@@ -884,19 +912,51 @@ fn safe_terminal_text(value: &str) -> String {
884912
885913#[ derive( Serialize ) ]
886914#[ serde( tag = "_tag" , rename_all = "lowercase" ) ]
915+ #[ serde( rename_all_fields = "camelCase" ) ]
887916enum JsonOrigin {
888- Bundled { source : & ' static str } ,
889- Remote { source : String , direct : bool } ,
890- Local { root : String } ,
917+ Bundled {
918+ source : & ' static str ,
919+ } ,
920+ Remote {
921+ source : String ,
922+ direct : bool ,
923+ owner : String ,
924+ repository : String ,
925+ skill_path : String ,
926+ commit : String ,
927+ source_url : String ,
928+ } ,
929+ Local {
930+ root : String ,
931+ } ,
891932}
892933
893934fn origin_json ( origin : & SkillOrigin ) -> JsonOrigin {
894935 match origin {
895936 SkillOrigin :: Bundled => JsonOrigin :: Bundled { source : "skilld" } ,
896- SkillOrigin :: Remote { source, direct, .. } => JsonOrigin :: Remote {
897- source : source. clone ( ) ,
898- direct : * direct,
899- } ,
937+ SkillOrigin :: Remote {
938+ source,
939+ direct,
940+ provenance,
941+ ..
942+ } => {
943+ let RemoteProvenance {
944+ owner,
945+ repository,
946+ skill_path,
947+ commit_sha,
948+ source_url,
949+ } = provenance. as_ref ( ) ;
950+ JsonOrigin :: Remote {
951+ source : source. clone ( ) ,
952+ direct : * direct,
953+ owner : owner. clone ( ) ,
954+ repository : repository. clone ( ) ,
955+ skill_path : skill_path. clone ( ) ,
956+ commit : commit_sha. clone ( ) ,
957+ source_url : source_url. clone ( ) ,
958+ }
959+ }
900960 SkillOrigin :: Local { root } => JsonOrigin :: Local {
901961 root : root. display ( ) . to_string ( ) ,
902962 } ,
0 commit comments