From 22aa1df30a3072425fbdf7a92f320ecd05d581a9 Mon Sep 17 00:00:00 2001 From: Snider Date: Fri, 30 Jan 2026 10:41:35 +0000 Subject: [PATCH] refactor(cli): clean DX with direct function calls - cli.Success(), cli.Error(), etc. now print directly - String-returning versions renamed to cli.FmtSuccess(), etc. - Removes App() from common usage path - Usage: cli.Success("done") instead of fmt.Println(cli.Success("done")) Co-Authored-By: Claude Opus 4.5 --- cmd/doctor/doctor.go | 4 +- pkg/cli/runtime.go | 148 ++++++++++++++++++------------------------- pkg/cli/styles.go | 24 +++---- 3 files changed, 75 insertions(+), 101 deletions(-) diff --git a/cmd/doctor/doctor.go b/cmd/doctor/doctor.go index c253295..a042b2b 100644 --- a/cmd/doctor/doctor.go +++ b/cmd/doctor/doctor.go @@ -95,12 +95,12 @@ func runDoctor(verbose bool) error { // Summary fmt.Println() if failed > 0 { - fmt.Println(cli.Error(i18n.T("cmd.doctor.issues", map[string]interface{}{"Count": failed}))) + cli.Error(i18n.T("cmd.doctor.issues", map[string]interface{}{"Count": failed})) fmt.Printf("\n%s\n", i18n.T("cmd.doctor.install_missing")) printInstallInstructions() return fmt.Errorf("%s", i18n.T("cmd.doctor.issues_error", map[string]interface{}{"Count": failed})) } - fmt.Println(cli.Success(i18n.T("cmd.doctor.ready"))) + cli.Success(i18n.T("cmd.doctor.ready")) return nil } diff --git a/pkg/cli/runtime.go b/pkg/cli/runtime.go index b86e2ba..526ed65 100644 --- a/pkg/cli/runtime.go +++ b/pkg/cli/runtime.go @@ -1,10 +1,16 @@ // Package cli provides the CLI runtime and utilities. // -// The CLI uses the Core framework for its own runtime, providing: -// - Global singleton access via cli.App() -// - Output service for styled terminal printing -// - Signal handling for graceful shutdown -// - Worker bundle spawning for commands +// The CLI uses the Core framework for its own runtime. Usage is simple: +// +// cli.Init(cli.Options{AppName: "core"}) +// defer cli.Shutdown() +// +// cli.Success("Done!") +// cli.Error("Failed") +// if cli.Confirm("Proceed?") { ... } +// +// // When you need the Core instance +// c := cli.Core() package cli import ( @@ -19,39 +25,32 @@ import ( ) var ( - instance *Runtime + instance *runtime once sync.Once ) -// Runtime is the CLI's Core runtime. -type Runtime struct { - Core *framework.Core +// runtime is the CLI's internal Core runtime. +type runtime struct { + core *framework.Core ctx context.Context cancel context.CancelFunc } -// RuntimeOptions configures the CLI runtime. -type RuntimeOptions struct { - // AppName is the CLI application name (used in output) +// Options configures the CLI runtime. +type Options struct { AppName string - // Version is the CLI version string Version string } // Init initialises the global CLI runtime. // Call this once at startup (typically in main.go). -func Init(opts RuntimeOptions) error { +func Init(opts Options) error { var initErr error once.Do(func() { ctx, cancel := context.WithCancel(context.Background()) - core, err := framework.New( - framework.WithService(NewOutputService(OutputServiceOptions{ - AppName: opts.AppName, - })), - framework.WithService(NewSignalService(SignalServiceOptions{ - Cancel: cancel, - })), + c, err := framework.New( + framework.WithName("signal", newSignalService(cancel)), framework.WithServiceLock(), ) if err != nil { @@ -60,14 +59,13 @@ func Init(opts RuntimeOptions) error { return } - instance = &Runtime{ - Core: core, + instance = &runtime{ + core: c, ctx: ctx, cancel: cancel, } - // Start services - if err := core.ServiceStartup(ctx, nil); err != nil { + if err := c.ServiceStartup(ctx, nil); err != nil { initErr = err return } @@ -75,114 +73,91 @@ func Init(opts RuntimeOptions) error { return initErr } -// App returns the global CLI runtime. -// Panics if Init() hasn't been called. -func App() *Runtime { +func mustInit() { if instance == nil { - panic("cli.App() called before cli.Init()") + panic("cli not initialised - call cli.Init() first") } - return instance +} + +// --- Core Access --- + +// Core returns the CLI's framework Core instance. +func Core() *framework.Core { + mustInit() + return instance.core } // Context returns the CLI's root context. -// This context is cancelled on shutdown signals. -func (r *Runtime) Context() context.Context { - return r.ctx +// Cancelled on SIGINT/SIGTERM. +func Context() context.Context { + mustInit() + return instance.ctx } -// Shutdown gracefully shuts down the CLI runtime. -func (r *Runtime) Shutdown() { - r.cancel() - r.Core.ServiceShutdown(r.ctx) -} - -// Output returns the output service for styled printing. -func (r *Runtime) Output() *OutputService { - return framework.MustServiceFor[*OutputService](r.Core, "output") -} - -// --- Output Service --- - -// OutputServiceOptions configures the output service. -type OutputServiceOptions struct { - AppName string -} - -// OutputService provides styled terminal output. -type OutputService struct { - *framework.ServiceRuntime[OutputServiceOptions] -} - -// NewOutputService creates an output service factory. -func NewOutputService(opts OutputServiceOptions) func(*framework.Core) (any, error) { - return func(c *framework.Core) (any, error) { - return &OutputService{ - ServiceRuntime: framework.NewServiceRuntime(c, opts), - }, nil +// Shutdown gracefully shuts down the CLI. +func Shutdown() { + if instance == nil { + return } + instance.cancel() + instance.core.ServiceShutdown(instance.ctx) } +// --- Output Functions --- + // Success prints a success message with checkmark. -func (s *OutputService) Success(msg string) { +func Success(msg string) { fmt.Println(SuccessStyle.Render(SymbolCheck + " " + msg)) } // Error prints an error message with cross. -func (s *OutputService) Error(msg string) { +func Error(msg string) { fmt.Println(ErrorStyle.Render(SymbolCross + " " + msg)) } // Warning prints a warning message. -func (s *OutputService) Warning(msg string) { +func Warning(msg string) { fmt.Println(WarningStyle.Render(SymbolWarning + " " + msg)) } // Info prints an info message. -func (s *OutputService) Info(msg string) { +func Info(msg string) { fmt.Println(InfoStyle.Render(SymbolInfo + " " + msg)) } // Title prints a title/header. -func (s *OutputService) Title(msg string) { +func Title(msg string) { fmt.Println(TitleStyle.Render(msg)) } // Dim prints dimmed/subtle text. -func (s *OutputService) Dim(msg string) { +func Dim(msg string) { fmt.Println(DimStyle.Render(msg)) } -// --- Signal Service --- +// --- Signal Service (internal) --- -// SignalServiceOptions configures the signal service. -type SignalServiceOptions struct { - Cancel context.CancelFunc -} - -// SignalService handles OS signals for graceful shutdown. -type SignalService struct { - *framework.ServiceRuntime[SignalServiceOptions] +type signalService struct { + cancel context.CancelFunc sigChan chan os.Signal } -// NewSignalService creates a signal service factory. -func NewSignalService(opts SignalServiceOptions) func(*framework.Core) (any, error) { +func newSignalService(cancel context.CancelFunc) func(*framework.Core) (any, error) { return func(c *framework.Core) (any, error) { - return &SignalService{ - ServiceRuntime: framework.NewServiceRuntime(c, opts), - sigChan: make(chan os.Signal, 1), + return &signalService{ + cancel: cancel, + sigChan: make(chan os.Signal, 1), }, nil } } -// OnStartup starts listening for signals. -func (s *SignalService) OnStartup(ctx context.Context) error { +func (s *signalService) OnStartup(ctx context.Context) error { signal.Notify(s.sigChan, syscall.SIGINT, syscall.SIGTERM) go func() { select { case <-s.sigChan: - s.Opts().Cancel() + s.cancel() case <-ctx.Done(): } }() @@ -190,8 +165,7 @@ func (s *SignalService) OnStartup(ctx context.Context) error { return nil } -// OnShutdown stops listening for signals. -func (s *SignalService) OnShutdown(ctx context.Context) error { +func (s *signalService) OnShutdown(ctx context.Context) error { signal.Stop(s.sigChan) close(s.sigChan) return nil diff --git a/pkg/cli/styles.go b/pkg/cli/styles.go index b22156d..49d46bc 100644 --- a/pkg/cli/styles.go +++ b/pkg/cli/styles.go @@ -327,23 +327,23 @@ var ( // Helper Functions // ───────────────────────────────────────────────────────────────────────────── -// Success returns a styled success message with checkmark. -func Success(msg string) string { +// FmtSuccess returns a styled success message with checkmark. +func FmtSuccess(msg string) string { return fmt.Sprintf("%s %s", SuccessStyle.Render(SymbolCheck), msg) } -// Error returns a styled error message with cross. -func Error(msg string) string { +// FmtError returns a styled error message with cross. +func FmtError(msg string) string { return fmt.Sprintf("%s %s", ErrorStyle.Render(SymbolCross), msg) } -// Warning returns a styled warning message with warning symbol. -func Warning(msg string) string { +// FmtWarning returns a styled warning message with warning symbol. +func FmtWarning(msg string) string { return fmt.Sprintf("%s %s", WarningStyle.Render(SymbolWarning), msg) } -// Info returns a styled info message with info symbol. -func Info(msg string) string { +// FmtInfo returns a styled info message with info symbol. +func FmtInfo(msg string) string { return fmt.Sprintf("%s %s", InfoStyle.Render(SymbolInfo), msg) } @@ -464,13 +464,13 @@ func Header(title string, withSeparator bool) string { return fmt.Sprintf("\n%s", HeaderStyle.Render(title)) } -// Title returns a styled command/section title. -func Title(text string) string { +// FmtTitle returns a styled command/section title. +func FmtTitle(text string) string { return TitleStyle.Render(text) } -// Dim returns dimmed text. -func Dim(text string) string { +// FmtDim returns dimmed text. +func FmtDim(text string) string { return DimStyle.Render(text) }