11package list
22
33import (
4- "context"
54 "encoding/json"
65 "fmt"
7- "sort"
86 "strings"
97 "text/tabwriter"
108
119 "github.com/codefly-dev/cli/cmd/common"
12- "github.com/codefly-dev/cli/pkg/environments"
1310 runnablespkg "github.com/codefly-dev/cli/pkg/runnables"
1411 "github.com/codefly-dev/core/resources"
1512 "github.com/spf13/cobra"
@@ -18,8 +15,6 @@ import (
1815var (
1916 listRunnablesModule string
2017 listRunnablesJSON bool
21- listRunnablesEnv string
22- listRunnablesTools bool
2318)
2419
2520// RunnablesCmd lists the runnables declared in the current workspace.
@@ -31,64 +26,16 @@ var RunnablesCmd = &cobra.Command{
3126A runnable is identified by module/name @ version: a version is an immutable
3227release and several versions of one name coexist.
3328
34- Every Runnable a module derives from its published contracts is listed beside
35- the ones it declares. With --tools or --env, each row also carries the tool its
36- owner exposed it as — the model-facing name and the explicit effect. An
37- operation with no exposure is not a tool and is shown without one: absent means
38- not exposed. --tools keeps only the exposed ones.
39-
40- With --env, each derived operation also carries the configuration key its
41- authority is answered under (` + "`runnable-scope-selections`" + ` in
42- workspace.codefly.yaml) and what that environment has answered, judged by the
43- rules ` + "`generate runnable-bindings`" + ` judges it by:
44-
45- selected its answer resolves every slot the owner declared, or it declares none
46- declined answered [] — installed for no resource, granted no authority
47- needs-scopes its owner asks for scope slots nobody has answered
48- invalid generation refuses this answer, and the row says why
49-
50- An answer naming no operation at all is reported beside the rows rather than
51- dropped, because a mistyped key answers no question and leaves its operation
52- reading as unanswered.
53-
5429Examples:
5530 codefly list runnables
5631 codefly list runnables --module=backend
57- codefly list runnables --tools --env=staging
5832 codefly list runnables --json` ,
5933 Args : cobra .NoArgs ,
6034 RunE : func (cmd * cobra.Command , _ []string ) error {
6135 return listRunnables (cmd )
6236 },
6337}
6438
65- // runnableRow is one listed runnable. The tool and the grant live here rather
66- // than on the shared projection because they are this listing's question: a
67- // field on runnablespkg.Identity would also appear in `show runnable`, whose
68- // report already states the operation's own exposure.
69- type runnableRow struct {
70- runnablespkg.Identity
71- // Tool is the exposure the owner declared, nil when the operation is no
72- // tool. Absent means not exposed: that is the owner's fail-closed default
73- // and this listing repeats it rather than inventing one.
74- Tool * runnablespkg.Tool `json:"tool,omitempty"`
75- // Grant is how the environment answers this operation, set only when a
76- // listing was asked about one.
77- Grant * runnablespkg.Grant `json:"grant,omitempty"`
78- }
79-
80- // runnableListing is what a listing asked about an environment answers with:
81- // the rows, and the answers that match no operation. Without --env there is no
82- // environment to have answered anything, and the listing stays the array it
83- // has always been.
84- type runnableListing struct {
85- Runnables []runnableRow `json:"runnables"`
86- // UnansweredKeys are the runnable-scope-selections keys of this
87- // environment that no listed operation binds. Generation refuses each by
88- // name; a listing that only walked the catalogue would never show them.
89- UnansweredKeys []string `json:"unanswered_keys,omitempty"`
90- }
91-
9239func listRunnables (cmd * cobra.Command ) error {
9340 ctx , done := common .NewContext ()
9441 defer done ()
@@ -110,149 +57,51 @@ func listRunnables(cmd *cobra.Command) error {
11057 return fmt .Errorf ("cannot load modules: %w" , err )
11158 }
11259
113- // The environment is resolved once, before any module is read: a listing
114- // asked about an environment the workspace does not declare answers with
115- // that, rather than with rows whose grant column is silently absent.
116- var env * environments.Environment
117- if listRunnablesEnv != "" {
118- if env , err = environments .Select (workspace , listRunnablesEnv ); err != nil {
119- return fmt .Errorf ("cannot list the grants of environment %q: %w" , listRunnablesEnv , err )
120- }
121- }
122-
123- listing , err := runnableListingOf (ctx , modules , env )
124- if err != nil {
125- return err
126- }
127-
128- if listRunnablesJSON {
129- if env == nil {
130- return json .NewEncoder (cmd .OutOrStdout ()).Encode (listing .Runnables )
131- }
132- return json .NewEncoder (cmd .OutOrStdout ()).Encode (listing )
133- }
134- if len (listing .Runnables ) == 0 && len (listing .UnansweredKeys ) == 0 {
135- fmt .Fprintf (cmd .OutOrStdout (), "No runnables in workspace <%s>\n " , workspace .Name )
136- return nil
137- }
138- return writeRunnables (cmd , listing , env )
139- }
140-
141- // runnableListingOf is every runnable the modules hold — the ones they declare
142- // and the operations they derive — and, when an environment was named, how it
143- // answers each derived operation and which of its answers match nothing.
144- func runnableListingOf (ctx context.Context , modules []* resources.Module , env * environments.Environment ) (* runnableListing , error ) {
145- listing := & runnableListing {Runnables : make ([]runnableRow , 0 )}
146- bound := map [string ]bool {}
60+ entries := make ([]runnablespkg.Identity , 0 )
14761 for _ , mod := range modules {
148- runnables , err := mod .LoadRunnables (ctx )
149- if err != nil {
150- return nil , fmt .Errorf ("cannot load runnables: %w" , err )
62+ runnables , loadErr := mod .LoadRunnables (ctx )
63+ if loadErr != nil {
64+ return fmt .Errorf ("cannot load runnables: %w" , loadErr )
15165 }
15266 for _ , r := range runnables {
153- listing . Runnables = append (listing . Runnables , runnableRow { Identity : runnablespkg .NewIdentity (r )} )
67+ entries = append (entries , runnablespkg .NewIdentity (r ))
15468 }
15569 // A module's derived operations are runnables it can be asked to
15670 // perform just as much as the ones it declares, so one listing
15771 // answers the question without the caller knowing which is which.
158- derived , err := runnablespkg .LoadDerivedOperations (mod .Dir ())
159- if err != nil {
160- return nil , fmt .Errorf ("cannot load the derived runnables of module %s: %w" , mod .Name , err )
72+ derived , derivedErr := runnablespkg .LoadDerivedOperations (mod .Dir ())
73+ if derivedErr != nil {
74+ return fmt .Errorf ("cannot load the derived runnables of module %s: %w" , mod .Name , derivedErr )
16175 }
16276 for i := range derived {
163- row := runnableRow {Identity : derived [i ].Identity (), Tool : derived [i ].ToolExposure ()}
164- if env != nil {
165- // ScopeSelections is core's own ScopeSelection messages: the
166- // environment decoded them, so the rules are applied to what
167- // an installation would carry, not to a re-parse of the YAML.
168- selections , answered := env .RunnableScopeSelections [runnablespkg .BindingKey (mod .Name , derived [i ].Entry .Name )]
169- row .Grant = runnablespkg .GrantOf (mod .Name , & derived [i ], selections , answered )
170- bound [row .Grant .Key ] = true
171- }
172- listing .Runnables = append (listing .Runnables , row )
77+ entries = append (entries , derived [i ].Identity ())
17378 }
17479 }
175- if env != nil {
176- answers := make ([]string , 0 , len (env .RunnableScopeSelections ))
177- for key := range env .RunnableScopeSelections {
178- answers = append (answers , key )
179- }
180- sort .Strings (answers )
181- listing .UnansweredKeys = runnablespkg .UnansweredKeys (answers , bound )
182- }
183- if ! listRunnablesTools {
184- return listing , nil
80+
81+ if listRunnablesJSON {
82+ return json .NewEncoder (cmd .OutOrStdout ()).Encode (entries )
18583 }
186- tools := make ([]runnableRow , 0 , len (listing .Runnables ))
187- for i := range listing .Runnables {
188- if listing .Runnables [i ].Tool != nil {
189- tools = append (tools , listing .Runnables [i ])
190- }
84+
85+ if len (entries ) == 0 {
86+ fmt .Fprintf (cmd .OutOrStdout (), "No runnables in workspace <%s>\n " , workspace .Name )
87+ return nil
19188 }
192- listing .Runnables = tools
193- return listing , nil
194- }
19589
196- func writeRunnables (cmd * cobra.Command , listing * runnableListing , env * environments.Environment ) error {
19790 w := tabwriter .NewWriter (cmd .OutOrStdout (), 0 , 0 , 2 , ' ' , 0 )
198- // The tool columns appear only when a caller asked about tools or about an
199- // environment: the default table is the one every existing reader parses.
200- tools := listRunnablesTools || env != nil
201- header := "MODULE\t NAME\t VERSION\t AGENT\t FACILITIES\t TIMEOUT\t SOURCE"
202- if tools {
203- header += "\t TOOL\t EFFECT"
204- }
205- if env != nil {
206- header += "\t KEY\t GRANT"
207- }
208- fmt .Fprintln (w , header )
209- for index := range listing .Runnables {
210- e := & listing .Runnables [index ]
91+ fmt .Fprintln (w , "MODULE\t NAME\t VERSION\t AGENT\t FACILITIES\t TIMEOUT\t SOURCE" )
92+ for index := range entries {
93+ e := & entries [index ]
21194 source := e .Source
21295 if source == "" {
21396 source = "declared"
21497 }
215- row := fmt .Sprintf ( "%s\t %s\t %s\t %s\t %s\t %s\t %s" ,
98+ fmt .Fprintf ( w , "%s\t %s\t %s\t %s\t %s\t %s\t %s\n " ,
21699 e .Module , e .Name , e .Version , e .Agent , strings .Join (e .Execution .Facilities , "," ), e .Execution .Timeout , source )
217- if tools {
218- name , effect := "" , ""
219- if e .Tool != nil {
220- name , effect = e .Tool .Name , e .Tool .Effect
221- }
222- row += "\t " + name + "\t " + effect
223- }
224- if env != nil {
225- key , state := "" , ""
226- if e .Grant != nil {
227- key , state = e .Grant .Key , e .Grant .State
228- }
229- row += "\t " + key + "\t " + state
230- }
231- fmt .Fprintln (w , row )
232- }
233- if err := w .Flush (); err != nil {
234- return err
235- }
236- // A refused answer is printed under the table with its reason, because the
237- // state alone ("invalid") does not say what to change.
238- for index := range listing .Runnables {
239- grant := listing .Runnables [index ].Grant
240- if grant != nil && grant .Reason != "" {
241- fmt .Fprintf (cmd .OutOrStdout (), "\n %s: %s" , grant .Key , grant .Reason )
242- }
243- }
244- if len (listing .UnansweredKeys ) > 0 {
245- fmt .Fprintf (cmd .OutOrStdout (), "\n \n Environment %s answers keys no operation binds; generation refuses each by name:\n " , env .Name )
246- for _ , key := range listing .UnansweredKeys {
247- fmt .Fprintf (cmd .OutOrStdout (), " %s\n " , key )
248- }
249100 }
250- return nil
101+ return w . Flush ()
251102}
252103
253104func init () {
254105 RunnablesCmd .Flags ().StringVar (& listRunnablesModule , "module" , "" , "Module to list runnables from" )
255106 RunnablesCmd .Flags ().BoolVar (& listRunnablesJSON , "json" , false , "Emit machine-readable JSON" )
256- RunnablesCmd .Flags ().StringVar (& listRunnablesEnv , "env" , "" , "Environment whose runnable-scope-selections to report as each operation's grant" )
257- RunnablesCmd .Flags ().BoolVar (& listRunnablesTools , "tools" , false , "List only the operations their owner exposes as a tool" )
258107}
0 commit comments