@@ -25,7 +25,7 @@ import (
2525// (local/dev deployments where the core isn't behind TLS). Hidden, as
2626// elsewhere in the CLI.
2727func addControlPlaneFlags (cmd * cobra.Command ) {
28- cmd .PersistentFlags ().Bool ("json" , false , "output raw JSON instead of a table" )
28+ cmd .PersistentFlags ().Bool ("json" , false , "Output raw JSON instead of a table" )
2929 cmd .PersistentFlags ().Bool ("insecure-http-auth" , false , "Allow authentication over plain HTTP (insecure, for local development only)" )
3030 if err := cmd .PersistentFlags ().MarkHidden ("insecure-http-auth" ); err != nil {
3131 panic (fmt .Sprintf ("hide insecure-http-auth flag: %v" , err ))
@@ -94,12 +94,12 @@ func runControlPlaneDelete(
9494 // delete call — e.g. a ULID passed straight through, or a concurrent
9595 // delete) is the desired end state, not an error.
9696 if isCoreNotFound (err ) {
97- cmd .Printf ( "%s not found; nothing to delete\n " , label )
97+ fmt . Fprintf ( cmd .OutOrStdout (), "%s not found; nothing to delete\n " , label )
9898 return nil
9999 }
100100 return err
101101 }
102- cmd .Printf ( " Deleted %s\n " , label )
102+ fmt . Fprintf ( cmd .OutOrStdout (), "✓ Deleted %s\n " , label )
103103 return nil
104104 })
105105}
@@ -142,36 +142,42 @@ func confirmControlPlaneDeletion(ctx context.Context, w io.Writer, label string,
142142}
143143
144144// runCoreList fetches a slice via fn and renders it as an aligned table
145- // (default) or the raw wire JSON (--json). headers names the columns; row
146- // maps one item to its cells in the same order. The human view keeps the
147- // output actionable — only the columns a person acts on — while --json
148- // preserves the full model for scripting.
149- func runCoreList [T any ](cmd * cobra.Command , headers []string , row func (T ) []string , fn func (ctx context.Context , c * coreapi.Client ) ([]T , error )) error {
150- return runCore (cmd , renderCoreList (cmd , headers , row , fn ))
145+ // (default) or the raw wire JSON (--json). empty is the full sentence printed
146+ // to stdout in place of the table when there are no items (e.g. "No
147+ // organizations found."). headers names the columns; row maps one item to its
148+ // cells in the same order. The human view keeps the output actionable — only
149+ // the columns a person acts on — while --json preserves the full model for
150+ // scripting.
151+ func runCoreList [T any ](cmd * cobra.Command , empty string , headers []string , row func (T ) []string , fn func (ctx context.Context , c * coreapi.Client ) ([]T , error )) error {
152+ return runCore (cmd , renderCoreList (cmd , empty , headers , row , fn ))
151153}
152154
153155// runCoreListForCluster is runCoreList for a resource-provider command (see
154- // runCoreForCluster): identical table/JSON rendering, but dialing the core that
155- // fronts clusterHost rather than the active context.
156- func runCoreListForCluster [T any ](cmd * cobra.Command , clusterHost string , headers []string , row func (T ) []string , fn func (ctx context.Context , c * coreapi.Client ) ([]T , error )) error {
157- return runCoreForCluster (cmd , clusterHost , renderCoreList (cmd , headers , row , fn ))
156+ // runCoreForCluster): identical table/JSON/empty-state rendering, but dialing
157+ // the core that fronts clusterHost rather than the active context.
158+ func runCoreListForCluster [T any ](cmd * cobra.Command , clusterHost , empty string , headers []string , row func (T ) []string , fn func (ctx context.Context , c * coreapi.Client ) ([]T , error )) error {
159+ return runCoreForCluster (cmd , clusterHost , renderCoreList (cmd , empty , headers , row , fn ))
158160}
159161
160162// renderCoreList builds the run-function shared by runCoreList and
161- // runCoreListForCluster: fetch via fn, then render as a table (default) or raw
162- // JSON (--json). Kept separate from the client-selection so the two list
163- // variants differ only in which core they dial.
164- func renderCoreList [T any ](cmd * cobra.Command , headers []string , row func (T ) []string , fn func (ctx context.Context , c * coreapi.Client ) ([]T , error )) func (context.Context , * coreapi.Client ) error {
163+ // runCoreListForCluster: fetch via fn, then render as a table (default), the
164+ // empty sentence (no items), or raw JSON (--json). Kept separate from the
165+ // client-selection so the two list variants differ only in which core they
166+ // dial.
167+ func renderCoreList [T any ](cmd * cobra.Command , empty string , headers []string , row func (T ) []string , fn func (ctx context.Context , c * coreapi.Client ) ([]T , error )) func (context.Context , * coreapi.Client ) error {
165168 return func (ctx context.Context , c * coreapi.Client ) error {
166169 items , err := fn (ctx , c )
167170 if err != nil {
168171 return err
169172 }
170173 if jsonRequested (cmd ) {
174+ if items == nil {
175+ items = []T {} // a nil slice encodes as null; scripts expect []
176+ }
171177 return printJSON (cmd .OutOrStdout (), items )
172178 }
173179 if len (items ) == 0 {
174- fmt .Fprintln (cmd .ErrOrStderr (), "(none)" )
180+ fmt .Fprintln (cmd .OutOrStdout (), empty )
175181 return nil
176182 }
177183 return printTable (cmd .OutOrStdout (), headers , items , row )
@@ -348,19 +354,26 @@ func writeTableRow(b *strings.Builder, cells []string, widths []int, styleFor fu
348354 b .WriteByte ('\n' )
349355}
350356
351- // runCoreJSON runs fn against an authenticated control-plane client and
352- // prints its result as indented JSON. It owns the preamble every
353- // control-plane command shares: silence usage so input errors don't spam
354- // the usage block, build the client, and map an API error to a
355- // problem-detail SilentError. Commands supply only the call + the value to
356- // render.
357- func runCoreJSON (cmd * cobra.Command , fn func (ctx context.Context , c * coreapi.Client ) (any , error )) error {
357+ // runCoreMutation runs fn against the control plane and renders its outcome
358+ // the way the rest of the CLI renders mutations: prints the caller's
359+ // ✓-prefixed confirmation on stdout by default, or the wire object as JSON
360+ // when --json was passed. fn
361+ // returns both so the human line can name the created resource while --json
362+ // preserves the full wire model (additive-only: synthesized fields like the
363+ // repo remote URL are merged in, nothing is ever omitted). It owns the same
364+ // preamble as the other runCore variants: silence usage, build the client,
365+ // map API errors to problem-detail messages.
366+ func runCoreMutation (cmd * cobra.Command , fn func (ctx context.Context , c * coreapi.Client ) (message string , wire any , err error )) error {
358367 return runCore (cmd , func (ctx context.Context , c * coreapi.Client ) error {
359- out , err := fn (ctx , c )
368+ message , wire , err := fn (ctx , c )
360369 if err != nil {
361370 return err
362371 }
363- return printJSON (cmd .OutOrStdout (), out )
372+ if jsonRequested (cmd ) {
373+ return printJSON (cmd .OutOrStdout (), wire )
374+ }
375+ fmt .Fprintln (cmd .OutOrStdout (), message )
376+ return nil
364377 })
365378}
366379
@@ -370,11 +383,13 @@ func runCoreJSON(cmd *cobra.Command, fn func(ctx context.Context, c *coreapi.Cli
370383// without standing up the auth/context/TLS stack.
371384var activeCoreClient = func (context.Context ) (* coreapi.Client , error ) { return coreapi .New () }
372385
373- // runCore is the variant for commands that don't render JSON (delete,
374- // revoke, remove): it runs the same preamble — silence usage, build
375- // client, map API errors — and leaves any success output to fn. The client
376- // dials the active context's core (coreapi.New); use runCoreForCluster for
377- // commands addressed at a specific cluster.
386+ // runCore is the shared base for every active-context control-plane command:
387+ // it owns the preamble only — silence usage, build the client, map API
388+ // errors — and leaves all rendering to fn. The delete/revoke verbs call it
389+ // directly and render their own output; runCoreList, runCoreObject, and
390+ // runCoreMutation build on it to add their table/JSON/confirmation
391+ // rendering. The client dials the active context's core (coreapi.New); use
392+ // runCoreForCluster for commands addressed at a specific cluster.
378393func runCore (cmd * cobra.Command , fn func (ctx context.Context , c * coreapi.Client ) error ) error {
379394 return runCoreClient (cmd , activeCoreClient , fn )
380395}
@@ -446,7 +461,7 @@ func renderCoreError(err error) error {
446461}
447462
448463// printJSON writes v as indented JSON to w — the --json view for list/get
449- // and the default for create commands that echo the new object .
464+ // and mutations .
450465func printJSON (w io.Writer , v any ) error {
451466 enc := json .NewEncoder (w )
452467 enc .SetIndent ("" , " " )
0 commit comments