@@ -2,14 +2,14 @@ package app
22
33import (
44 "context"
5- "flag"
65 "fmt"
76 "os"
87 "os/signal"
98 "syscall"
109 "time"
1110
1211 "github.com/alexandredaubois/ember/internal/fetcher"
12+ "github.com/spf13/cobra"
1313)
1414
1515type config struct {
@@ -23,70 +23,95 @@ type config struct {
2323 daemon bool
2424}
2525
26- func Run ( args [] string , version string ) error {
26+ func newRootCmd ( version string ) * cobra. Command {
2727 var cfg config
2828
29- fs := flag .NewFlagSet ("ember" , flag .ContinueOnError )
30- fs .StringVar (& cfg .addr , "addr" , "http://localhost:2019" , "Caddy admin API address" )
31- fs .DurationVar (& cfg .interval , "interval" , 1 * time .Second , "polling interval" )
32- fs .IntVar (& cfg .slowThreshold , "slow-threshold" , 500 , "slow request threshold (ms)" )
33- fs .BoolVar (& cfg .noColor , "no-color" , false , "disable colors" )
34- fs .BoolVar (& cfg .jsonMode , "json" , false , "JSON output mode" )
35- fs .IntVar (& cfg .pid , "pid" , 0 , "FrankenPHP process PID (auto-detected if not set)" )
36- fs .StringVar (& cfg .expose , "expose" , "" , "expose Prometheus metrics on address (e.g. :9191)" )
37- fs .BoolVar (& cfg .daemon , "daemon" , false , "run in daemon mode (no TUI, requires --expose)" )
38- showVersion := fs .Bool ("version" , false , "show version" )
39- completion := fs .String ("completion" , "" , "generate shell completions (bash, zsh, fish)" )
29+ cmd := & cobra.Command {
30+ Use : "ember [flags]" ,
31+ Short : "Real-time TUI dashboard for Caddy & FrankenPHP" ,
32+ Version : version ,
33+ Long : `Ember - Real-time TUI dashboard for Caddy & FrankenPHP
4034
41- fs .Usage = func () { printUsage (fs .Output (), version ) }
35+ Monitor your Caddy server in real time: per-host traffic, latency
36+ percentiles, status codes, and more. When FrankenPHP is detected,
37+ unlock per-thread introspection, worker management, and memory tracking.
4238
43- if err := fs .Parse (args ); err != nil {
44- return err
45- }
39+ Keybindings:
40+ Tab / 1 / 2 Switch between Caddy and FrankenPHP tabs
41+ Up / Down / j / k Navigate list
42+ Home / End Jump to first / last item
43+ PgUp / PgDn Page navigation
44+ Enter Open detail panel
45+ s / S Cycle sort field
46+ p Pause / resume
47+ r Restart workers (FrankenPHP)
48+ / Filter
49+ g Full-screen graphs
50+ ? Help overlay
51+ q Quit` ,
52+ Example : ` ember # default: localhost:2019
53+ ember --addr http://prod:2019 # custom address
54+ ember --json # pipe-friendly JSON output
55+ ember --expose :9191 # TUI + Prometheus endpoint
56+ ember --expose :9191 --daemon # headless metrics exporter` ,
57+ SilenceUsage : true ,
58+ SilenceErrors : true ,
59+ PreRunE : func (cmd * cobra.Command , args []string ) error {
60+ return validate (& cfg )
61+ },
62+ RunE : func (cmd * cobra.Command , args []string ) error {
63+ ctx , cancel := signal .NotifyContext (context .Background (), os .Interrupt , syscall .SIGTERM )
64+ defer cancel ()
4665
47- if * showVersion {
48- fmt .Printf ("ember %s\n " , version )
49- return nil
50- }
66+ pid := int32 (cfg .pid )
67+ if pid == 0 {
68+ detected , err := fetcher .FindFrankenPHPProcess (ctx )
69+ if err != nil {
70+ detected , err = fetcher .FindCaddyProcess (ctx )
71+ if err != nil && (cfg .jsonMode || cfg .daemon ) {
72+ fmt .Fprintf (os .Stderr , "warning: no frankenphp or caddy process found\n " )
73+ }
74+ }
75+ if err == nil {
76+ pid = detected
77+ }
78+ }
5179
52- if * completion != "" {
53- return printCompletion ( os . Stdout , * completion )
54- }
80+ f := fetcher . NewHTTPFetcher ( cfg . addr , pid )
81+ hasFrankenPHP := f . DetectFrankenPHP ( ctx )
82+ f . FetchServerNames ( ctx )
5583
56- if err := validate (& cfg ); err != nil {
57- return err
84+ switch {
85+ case cfg .jsonMode :
86+ runJSON (ctx , f , cfg .interval )
87+ case cfg .daemon :
88+ return runDaemon (ctx , f , & cfg )
89+ default :
90+ return runTUI (f , & cfg , hasFrankenPHP , version )
91+ }
92+ return nil
93+ },
5894 }
5995
60- ctx , cancel := signal .NotifyContext (context .Background (), os .Interrupt , syscall .SIGTERM )
61- defer cancel ()
96+ f := cmd .Flags ()
97+ f .StringVar (& cfg .addr , "addr" , "http://localhost:2019" , "Caddy admin API address" )
98+ f .DurationVar (& cfg .interval , "interval" , 1 * time .Second , "Polling interval" )
99+ f .IntVar (& cfg .slowThreshold , "slow-threshold" , 500 , "Slow request threshold in ms" )
100+ f .BoolVar (& cfg .noColor , "no-color" , false , "Disable colors" )
101+ f .BoolVar (& cfg .jsonMode , "json" , false , "JSON output mode (streaming JSONL)" )
102+ f .IntVar (& cfg .pid , "pid" , 0 , "FrankenPHP PID (auto-detected if not set)" )
103+ f .StringVar (& cfg .expose , "expose" , "" , "Expose Prometheus metrics (e.g. :9191)" )
104+ f .BoolVar (& cfg .daemon , "daemon" , false , "Headless mode (requires --expose)" )
62105
63- pid := int32 (cfg .pid )
64- if pid == 0 {
65- detected , err := fetcher .FindFrankenPHPProcess (ctx )
66- if err != nil {
67- detected , err = fetcher .FindCaddyProcess (ctx )
68- if err != nil && (cfg .jsonMode || cfg .daemon ) {
69- fmt .Fprintf (os .Stderr , "warning: no frankenphp or caddy process found\n " )
70- }
71- }
72- if err == nil {
73- pid = detected
74- }
75- }
106+ cmd .SetVersionTemplate ("ember {{.Version}}\n " )
76107
77- f := fetcher .NewHTTPFetcher (cfg .addr , pid )
78- hasFrankenPHP := f .DetectFrankenPHP (ctx )
79- f .FetchServerNames (ctx )
108+ return cmd
109+ }
80110
81- switch {
82- case cfg .jsonMode :
83- runJSON (ctx , f , cfg .interval )
84- case cfg .daemon :
85- runDaemon (ctx , f , & cfg )
86- default :
87- return runTUI (ctx , f , & cfg , hasFrankenPHP , version )
88- }
89- return nil
111+ func Run (args []string , version string ) error {
112+ cmd := newRootCmd (version )
113+ cmd .SetArgs (args )
114+ return cmd .Execute ()
90115}
91116
92117func validate (cfg * config ) error {
0 commit comments