@@ -3,14 +3,17 @@ package main
33
44import (
55 "fmt"
6+ "io"
67 "os"
78 "text/tabwriter"
9+ "time"
810
911 v1 "github.com/altuslabsxyz/devnet-builder/api/proto/gen/v1"
1012 "github.com/altuslabsxyz/devnet-builder/internal/client"
1113 "github.com/altuslabsxyz/devnet-builder/internal/version"
1214 "github.com/fatih/color"
1315 "github.com/spf13/cobra"
16+ "gopkg.in/yaml.v3"
1417)
1518
1619var (
@@ -65,7 +68,7 @@ func main() {
6568 newDiffCmd (),
6669 newDeployCmd (), // deprecated
6770 newListCmd (),
68- newStatusCmd (),
71+ newDescribeCmd (),
6972 newStartCmd (),
7073 newStopCmd (),
7174 newDestroyCmd (), // deprecated
@@ -241,29 +244,6 @@ func newListCmd() *cobra.Command {
241244 }
242245}
243246
244- func newStatusCmd () * cobra.Command {
245- return & cobra.Command {
246- Use : "status [devnet]" ,
247- Short : "Show devnet status" ,
248- Args : cobra .ExactArgs (1 ),
249- RunE : func (cmd * cobra.Command , args []string ) error {
250- name := args [0 ]
251-
252- if daemonClient == nil {
253- return fmt .Errorf ("daemon not running - start with: devnetd" )
254- }
255-
256- devnet , err := daemonClient .GetDevnet (cmd .Context (), name )
257- if err != nil {
258- return err
259- }
260-
261- printDevnetStatus (devnet )
262- return nil
263- },
264- }
265- }
266-
267247func newStartCmd () * cobra.Command {
268248 return & cobra.Command {
269249 Use : "start [devnet]" ,
@@ -353,37 +333,173 @@ func newDestroyCmd() *cobra.Command {
353333 return cmd
354334}
355335
356- func printDevnetStatus (d * v1.Devnet ) {
336+ func newDescribeCmd () * cobra.Command {
337+ var outputFormat string
338+
339+ cmd := & cobra.Command {
340+ Use : "describe <devnet>" ,
341+ Short : "Show detailed devnet information" ,
342+ Long : `Show detailed information about a devnet including status conditions,
343+ recent events, and node details. Similar to kubectl describe.` ,
344+ Args : cobra .ExactArgs (1 ),
345+ RunE : func (cmd * cobra.Command , args []string ) error {
346+ name := args [0 ]
347+
348+ if daemonClient == nil {
349+ return fmt .Errorf ("daemon not running - start with: devnetd" )
350+ }
351+
352+ devnet , err := daemonClient .GetDevnet (cmd .Context (), name )
353+ if err != nil {
354+ return err
355+ }
356+
357+ nodes , err := daemonClient .ListNodes (cmd .Context (), name )
358+ if err != nil {
359+ // Don't fail if nodes can't be listed
360+ nodes = nil
361+ }
362+
363+ if outputFormat == "yaml" {
364+ return printDescribeYAML (devnet , nodes )
365+ }
366+
367+ formatDescribeOutput (os .Stdout , devnet , nodes )
368+ return nil
369+ },
370+ }
371+
372+ cmd .Flags ().StringVarP (& outputFormat , "output" , "o" , "" , "Output format (yaml)" )
373+ return cmd
374+ }
375+
376+ func formatDescribeOutput (w io.Writer , d * v1.Devnet , nodes []* v1.Node ) {
377+ if d == nil {
378+ fmt .Fprintf (w , "No devnet data available\n " )
379+ return
380+ }
381+ if d .Status == nil {
382+ d .Status = & v1.DevnetStatus {}
383+ }
384+ if d .Metadata == nil {
385+ d .Metadata = & v1.DevnetMetadata {}
386+ }
387+ if d .Spec == nil {
388+ d .Spec = & v1.DevnetSpec {}
389+ }
390+
357391 // Phase with color
358392 phase := d .Status .Phase
359393 switch phase {
360394 case "Running" :
361- color .Green ( "● %s" , phase )
395+ color .New ( color . FgGreen ). Fprintf ( w , "● %s\n " , phase )
362396 case "Pending" , "Provisioning" :
363- color .Yellow ( "◐ %s" , phase )
397+ color .New ( color . FgYellow ). Fprintf ( w , "◐ %s\n " , phase )
364398 case "Stopped" :
365- color .White ( "○ %s" , phase )
399+ color .New ( color . FgWhite ). Fprintf ( w , "○ %s\n " , phase )
366400 case "Degraded" :
367- color .Red ( "◑ %s" , phase )
401+ color .New ( color . FgRed ). Fprintf ( w , "◑ %s\n " , phase )
368402 default :
369- fmt .Printf ( "? %s" , phase )
403+ fmt .Fprintf ( w , "? %s\n " , phase )
370404 }
371405
372- fmt .Printf ("\n Name: %s\n " , d .Metadata .Name )
373- fmt .Printf ("Plugin: %s\n " , d .Spec .Plugin )
374- fmt .Printf ("Mode: %s\n " , d .Spec .Mode )
375- fmt .Printf ("Validators: %d\n " , d .Spec .Validators )
406+ // Basic info
407+ fmt .Fprintf (w , "\n Name: %s\n " , d .Metadata .Name )
408+ if d .Metadata .CreatedAt != nil {
409+ age := time .Since (d .Metadata .CreatedAt .AsTime ()).Round (time .Second )
410+ fmt .Fprintf (w , "Age: %s\n " , age )
411+ }
412+ fmt .Fprintf (w , "Plugin: %s\n " , d .Spec .Plugin )
413+ fmt .Fprintf (w , "Mode: %s\n " , d .Spec .Mode )
414+ fmt .Fprintf (w , "Validators: %d\n " , d .Spec .Validators )
376415 if d .Spec .FullNodes > 0 {
377- fmt .Printf ( "Full Nodes: %d\n " , d .Spec .FullNodes )
416+ fmt .Fprintf ( w , "Full Nodes: %d\n " , d .Spec .FullNodes )
378417 }
379- fmt .Printf ("Nodes: %d/%d ready\n " , d .Status .ReadyNodes , d .Status .Nodes )
418+
419+ // Status section
420+ fmt .Fprintf (w , "\n Status:\n " )
421+ fmt .Fprintf (w , " Nodes: %d/%d ready\n " , d .Status .ReadyNodes , d .Status .Nodes )
380422 if d .Status .CurrentHeight > 0 {
381- fmt .Printf ( " Height: %d\n " , d .Status .CurrentHeight )
423+ fmt .Fprintf ( w , " Height: %d\n " , d .Status .CurrentHeight )
382424 }
383425 if d .Status .SdkVersion != "" {
384- fmt .Printf ( "SDK: %s\n " , d .Status .SdkVersion )
426+ fmt .Fprintf ( w , " SDK Version: %s\n " , d .Status .SdkVersion )
385427 }
386428 if d .Status .Message != "" {
387- fmt .Printf ("Message: %s\n " , d .Status .Message )
429+ fmt .Fprintf (w , " Message: %s\n " , d .Status .Message )
430+ }
431+
432+ // Conditions section
433+ if len (d .Status .Conditions ) > 0 {
434+ fmt .Fprintf (w , "\n Conditions:\n " )
435+ fmt .Fprintf (w , " %-20s %-8s %-25s %s\n " , "TYPE" , "STATUS" , "REASON" , "MESSAGE" )
436+ for _ , c := range d .Status .Conditions {
437+ status := c .Status
438+ if c .Status == "True" {
439+ status = color .GreenString ("True" )
440+ } else if c .Status == "False" {
441+ status = color .RedString ("False" )
442+ }
443+ fmt .Fprintf (w , " %-20s %-8s %-25s %s\n " , c .Type , status , c .Reason , c .Message )
444+ }
445+ }
446+
447+ // Nodes section
448+ if len (nodes ) > 0 {
449+ fmt .Fprintf (w , "\n Nodes:\n " )
450+ fmt .Fprintf (w , " %-6s %-10s %-10s %-10s %-8s %s\n " , "INDEX" , "ROLE" , "PHASE" , "HEIGHT" , "RESTARTS" , "MESSAGE" )
451+ for _ , n := range nodes {
452+ phase := n .Status .Phase
453+ switch phase {
454+ case "Running" :
455+ phase = color .GreenString (phase )
456+ case "Pending" , "Starting" :
457+ phase = color .YellowString (phase )
458+ case "Crashed" :
459+ phase = color .RedString (phase )
460+ }
461+ msg := n .Status .Message
462+ if len (msg ) > 30 {
463+ msg = msg [:27 ] + "..."
464+ }
465+ fmt .Fprintf (w , " %-6d %-10s %-10s %-10d %-8d %s\n " ,
466+ n .Metadata .Index ,
467+ n .Spec .Role ,
468+ phase ,
469+ n .Status .BlockHeight ,
470+ n .Status .RestartCount ,
471+ msg ,
472+ )
473+ }
474+ }
475+
476+ // Events section
477+ if len (d .Status .Events ) > 0 {
478+ fmt .Fprintf (w , "\n Events:\n " )
479+ fmt .Fprintf (w , " %-8s %-20s %-20s %s\n " , "TYPE" , "REASON" , "AGE" , "MESSAGE" )
480+ for _ , e := range d .Status .Events {
481+ eventType := e .Type
482+ if e .Type == "Warning" {
483+ eventType = color .YellowString ("Warning" )
484+ }
485+ age := "Unknown"
486+ if e .Timestamp != nil {
487+ age = time .Since (e .Timestamp .AsTime ()).Round (time .Second ).String ()
488+ }
489+ fmt .Fprintf (w , " %-8s %-20s %-20s %s\n " , eventType , e .Reason , age , e .Message )
490+ }
491+ }
492+ }
493+
494+ func printDescribeYAML (d * v1.Devnet , nodes []* v1.Node ) error {
495+ data := map [string ]interface {}{
496+ "devnet" : d ,
497+ "nodes" : nodes ,
498+ }
499+ out , err := yaml .Marshal (data )
500+ if err != nil {
501+ return err
388502 }
503+ fmt .Println (string (out ))
504+ return nil
389505}
0 commit comments