11package aitools
22
33import (
4+ "errors"
45 "fmt"
56 "sort"
67 "strings"
@@ -17,6 +18,8 @@ import (
1718var listSkillsFn = defaultListSkills
1819
1920func newListCmd () * cobra.Command {
21+ var projectFlag , globalFlag bool
22+
2023 // --skills is accepted for forward-compat (future component types)
2124 // but currently skills is the only component, so the output is the same.
2225 var showSkills bool
@@ -25,16 +28,27 @@ func newListCmd() *cobra.Command {
2528 Use : "list" ,
2629 Short : "List installed AI tools components" ,
2730 RunE : func (cmd * cobra.Command , args []string ) error {
28- _ = showSkills
29- return listSkillsFn (cmd )
31+ if projectFlag && globalFlag {
32+ return errors .New ("cannot use --global and --project together" )
33+ }
34+ // For list: no flag = show both scopes (empty string).
35+ var scope string
36+ if projectFlag {
37+ scope = installer .ScopeProject
38+ } else if globalFlag {
39+ scope = installer .ScopeGlobal
40+ }
41+ return listSkillsFn (cmd , scope )
3042 },
3143 }
3244
3345 cmd .Flags ().BoolVar (& showSkills , "skills" , false , "Show detailed skills information" )
46+ cmd .Flags ().BoolVar (& projectFlag , "project" , false , "Show only project-scoped skills" )
47+ cmd .Flags ().BoolVar (& globalFlag , "global" , false , "Show only globally-scoped skills" )
3448 return cmd
3549}
3650
37- func defaultListSkills (cmd * cobra.Command ) error {
51+ func defaultListSkills (cmd * cobra.Command , scope string ) error {
3852 ctx := cmd .Context ()
3953
4054 src := & installer.GitHubManifestSource {}
@@ -48,14 +62,28 @@ func defaultListSkills(cmd *cobra.Command) error {
4862 return fmt .Errorf ("failed to fetch manifest: %w" , err )
4963 }
5064
51- globalDir , err := installer .GlobalSkillsDir (ctx )
52- if err != nil {
53- return err
65+ // Load global state.
66+ var globalState * installer.InstallState
67+ if scope != installer .ScopeProject {
68+ globalDir , gErr := installer .GlobalSkillsDir (ctx )
69+ if gErr == nil {
70+ globalState , err = installer .LoadState (globalDir )
71+ if err != nil {
72+ log .Debugf (ctx , "Could not load global install state: %v" , err )
73+ }
74+ }
5475 }
5576
56- state , err := installer .LoadState (globalDir )
57- if err != nil {
58- log .Debugf (ctx , "Could not load install state: %v" , err )
77+ // Load project state.
78+ var projectState * installer.InstallState
79+ if scope != installer .ScopeGlobal {
80+ projectDir , pErr := installer .ProjectSkillsDir (ctx )
81+ if pErr == nil {
82+ projectState , err = installer .LoadState (projectDir )
83+ if err != nil {
84+ log .Debugf (ctx , "Could not load project install state: %v" , err )
85+ }
86+ }
5987 }
6088
6189 // Build sorted list of skill names.
@@ -73,7 +101,10 @@ func defaultListSkills(cmd *cobra.Command) error {
73101 tw := tabwriter .NewWriter (& buf , 0 , 4 , 2 , ' ' , 0 )
74102 fmt .Fprintln (tw , " NAME\t VERSION\t INSTALLED" )
75103
76- installedCount := 0
104+ bothScopes := globalState != nil && projectState != nil
105+
106+ globalCount := 0
107+ projectCount := 0
77108 for _ , name := range names {
78109 meta := manifest .Skills [name ]
79110
@@ -82,15 +113,15 @@ func defaultListSkills(cmd *cobra.Command) error {
82113 tag = " [experimental]"
83114 }
84115
85- installedStr := "not installed"
86- if state != nil {
87- if v , ok := state .Skills [name ]; ok {
88- installedCount ++
89- if v == meta . Version {
90- installedStr = "v" + v + " (up to date)"
91- } else {
92- installedStr = "v" + v + " (update available)"
93- }
116+ installedStr := installedStatus ( name , meta . Version , globalState , projectState , bothScopes )
117+ if globalState != nil {
118+ if _ , ok := globalState .Skills [name ]; ok {
119+ globalCount ++
120+ }
121+ }
122+ if projectState != nil {
123+ if _ , ok := projectState . Skills [ name ]; ok {
124+ projectCount ++
94125 }
95126 }
96127
@@ -99,6 +130,59 @@ func defaultListSkills(cmd *cobra.Command) error {
99130 tw .Flush ()
100131 cmdio .LogString (ctx , buf .String ())
101132
102- cmdio .LogString (ctx , fmt .Sprintf ("%d/%d skills installed (global)" , installedCount , len (names )))
133+ // Summary line.
134+ switch {
135+ case bothScopes :
136+ cmdio .LogString (ctx , fmt .Sprintf ("%d/%d skills installed (global), %d/%d (project)" , globalCount , len (names ), projectCount , len (names )))
137+ case projectState != nil :
138+ cmdio .LogString (ctx , fmt .Sprintf ("%d/%d skills installed (project)" , projectCount , len (names )))
139+ default :
140+ installedCount := globalCount
141+ cmdio .LogString (ctx , fmt .Sprintf ("%d/%d skills installed (global)" , installedCount , len (names )))
142+ }
103143 return nil
104144}
145+
146+ // installedStatus returns the display string for a skill's installation status.
147+ func installedStatus (name , latestVersion string , globalState , projectState * installer.InstallState , bothScopes bool ) string {
148+ globalVer := ""
149+ projectVer := ""
150+
151+ if globalState != nil {
152+ globalVer = globalState .Skills [name ]
153+ }
154+ if projectState != nil {
155+ projectVer = projectState .Skills [name ]
156+ }
157+
158+ if globalVer == "" && projectVer == "" {
159+ return "not installed"
160+ }
161+
162+ // If both scopes have the skill, show the project version (takes precedence).
163+ if bothScopes && globalVer != "" && projectVer != "" {
164+ return versionLabel (projectVer , latestVersion ) + " (project, global)"
165+ }
166+
167+ if projectVer != "" {
168+ label := versionLabel (projectVer , latestVersion )
169+ if bothScopes {
170+ return label + " (project)"
171+ }
172+ return label
173+ }
174+
175+ label := versionLabel (globalVer , latestVersion )
176+ if bothScopes {
177+ return label + " (global)"
178+ }
179+ return label
180+ }
181+
182+ // versionLabel formats version with update status.
183+ func versionLabel (installed , latest string ) string {
184+ if installed == latest {
185+ return "v" + installed + " (up to date)"
186+ }
187+ return "v" + installed + " (update available)"
188+ }
0 commit comments