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 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-01-30 10:41:35 +00:00
parent 23d399407c
commit 22aa1df30a
3 changed files with 75 additions and 101 deletions

View file

@ -95,12 +95,12 @@ func runDoctor(verbose bool) error {
// Summary // Summary
fmt.Println() fmt.Println()
if failed > 0 { 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")) fmt.Printf("\n%s\n", i18n.T("cmd.doctor.install_missing"))
printInstallInstructions() printInstallInstructions()
return fmt.Errorf("%s", i18n.T("cmd.doctor.issues_error", map[string]interface{}{"Count": failed})) 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 return nil
} }

View file

@ -1,10 +1,16 @@
// Package cli provides the CLI runtime and utilities. // Package cli provides the CLI runtime and utilities.
// //
// The CLI uses the Core framework for its own runtime, providing: // The CLI uses the Core framework for its own runtime. Usage is simple:
// - Global singleton access via cli.App() //
// - Output service for styled terminal printing // cli.Init(cli.Options{AppName: "core"})
// - Signal handling for graceful shutdown // defer cli.Shutdown()
// - Worker bundle spawning for commands //
// cli.Success("Done!")
// cli.Error("Failed")
// if cli.Confirm("Proceed?") { ... }
//
// // When you need the Core instance
// c := cli.Core()
package cli package cli
import ( import (
@ -19,39 +25,32 @@ import (
) )
var ( var (
instance *Runtime instance *runtime
once sync.Once once sync.Once
) )
// Runtime is the CLI's Core runtime. // runtime is the CLI's internal Core runtime.
type Runtime struct { type runtime struct {
Core *framework.Core core *framework.Core
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
} }
// RuntimeOptions configures the CLI runtime. // Options configures the CLI runtime.
type RuntimeOptions struct { type Options struct {
// AppName is the CLI application name (used in output)
AppName string AppName string
// Version is the CLI version string
Version string Version string
} }
// Init initialises the global CLI runtime. // Init initialises the global CLI runtime.
// Call this once at startup (typically in main.go). // Call this once at startup (typically in main.go).
func Init(opts RuntimeOptions) error { func Init(opts Options) error {
var initErr error var initErr error
once.Do(func() { once.Do(func() {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
core, err := framework.New( c, err := framework.New(
framework.WithService(NewOutputService(OutputServiceOptions{ framework.WithName("signal", newSignalService(cancel)),
AppName: opts.AppName,
})),
framework.WithService(NewSignalService(SignalServiceOptions{
Cancel: cancel,
})),
framework.WithServiceLock(), framework.WithServiceLock(),
) )
if err != nil { if err != nil {
@ -60,14 +59,13 @@ func Init(opts RuntimeOptions) error {
return return
} }
instance = &Runtime{ instance = &runtime{
Core: core, core: c,
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
} }
// Start services if err := c.ServiceStartup(ctx, nil); err != nil {
if err := core.ServiceStartup(ctx, nil); err != nil {
initErr = err initErr = err
return return
} }
@ -75,114 +73,91 @@ func Init(opts RuntimeOptions) error {
return initErr return initErr
} }
// App returns the global CLI runtime. func mustInit() {
// Panics if Init() hasn't been called.
func App() *Runtime {
if instance == nil { 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. // Context returns the CLI's root context.
// This context is cancelled on shutdown signals. // Cancelled on SIGINT/SIGTERM.
func (r *Runtime) Context() context.Context { func Context() context.Context {
return r.ctx mustInit()
return instance.ctx
} }
// Shutdown gracefully shuts down the CLI runtime. // Shutdown gracefully shuts down the CLI.
func (r *Runtime) Shutdown() { func Shutdown() {
r.cancel() if instance == nil {
r.Core.ServiceShutdown(r.ctx) return
}
// 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
} }
instance.cancel()
instance.core.ServiceShutdown(instance.ctx)
} }
// --- Output Functions ---
// Success prints a success message with checkmark. // Success prints a success message with checkmark.
func (s *OutputService) Success(msg string) { func Success(msg string) {
fmt.Println(SuccessStyle.Render(SymbolCheck + " " + msg)) fmt.Println(SuccessStyle.Render(SymbolCheck + " " + msg))
} }
// Error prints an error message with cross. // Error prints an error message with cross.
func (s *OutputService) Error(msg string) { func Error(msg string) {
fmt.Println(ErrorStyle.Render(SymbolCross + " " + msg)) fmt.Println(ErrorStyle.Render(SymbolCross + " " + msg))
} }
// Warning prints a warning message. // Warning prints a warning message.
func (s *OutputService) Warning(msg string) { func Warning(msg string) {
fmt.Println(WarningStyle.Render(SymbolWarning + " " + msg)) fmt.Println(WarningStyle.Render(SymbolWarning + " " + msg))
} }
// Info prints an info message. // Info prints an info message.
func (s *OutputService) Info(msg string) { func Info(msg string) {
fmt.Println(InfoStyle.Render(SymbolInfo + " " + msg)) fmt.Println(InfoStyle.Render(SymbolInfo + " " + msg))
} }
// Title prints a title/header. // Title prints a title/header.
func (s *OutputService) Title(msg string) { func Title(msg string) {
fmt.Println(TitleStyle.Render(msg)) fmt.Println(TitleStyle.Render(msg))
} }
// Dim prints dimmed/subtle text. // Dim prints dimmed/subtle text.
func (s *OutputService) Dim(msg string) { func Dim(msg string) {
fmt.Println(DimStyle.Render(msg)) fmt.Println(DimStyle.Render(msg))
} }
// --- Signal Service --- // --- Signal Service (internal) ---
// SignalServiceOptions configures the signal service. type signalService struct {
type SignalServiceOptions struct { cancel context.CancelFunc
Cancel context.CancelFunc
}
// SignalService handles OS signals for graceful shutdown.
type SignalService struct {
*framework.ServiceRuntime[SignalServiceOptions]
sigChan chan os.Signal sigChan chan os.Signal
} }
// NewSignalService creates a signal service factory. func newSignalService(cancel context.CancelFunc) func(*framework.Core) (any, error) {
func NewSignalService(opts SignalServiceOptions) func(*framework.Core) (any, error) {
return func(c *framework.Core) (any, error) { return func(c *framework.Core) (any, error) {
return &SignalService{ return &signalService{
ServiceRuntime: framework.NewServiceRuntime(c, opts), cancel: cancel,
sigChan: make(chan os.Signal, 1), sigChan: make(chan os.Signal, 1),
}, nil }, 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) signal.Notify(s.sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() { go func() {
select { select {
case <-s.sigChan: case <-s.sigChan:
s.Opts().Cancel() s.cancel()
case <-ctx.Done(): case <-ctx.Done():
} }
}() }()
@ -190,8 +165,7 @@ func (s *SignalService) OnStartup(ctx context.Context) error {
return nil 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) signal.Stop(s.sigChan)
close(s.sigChan) close(s.sigChan)
return nil return nil

View file

@ -327,23 +327,23 @@ var (
// Helper Functions // Helper Functions
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// Success returns a styled success message with checkmark. // FmtSuccess returns a styled success message with checkmark.
func Success(msg string) string { func FmtSuccess(msg string) string {
return fmt.Sprintf("%s %s", SuccessStyle.Render(SymbolCheck), msg) return fmt.Sprintf("%s %s", SuccessStyle.Render(SymbolCheck), msg)
} }
// Error returns a styled error message with cross. // FmtError returns a styled error message with cross.
func Error(msg string) string { func FmtError(msg string) string {
return fmt.Sprintf("%s %s", ErrorStyle.Render(SymbolCross), msg) return fmt.Sprintf("%s %s", ErrorStyle.Render(SymbolCross), msg)
} }
// Warning returns a styled warning message with warning symbol. // FmtWarning returns a styled warning message with warning symbol.
func Warning(msg string) string { func FmtWarning(msg string) string {
return fmt.Sprintf("%s %s", WarningStyle.Render(SymbolWarning), msg) return fmt.Sprintf("%s %s", WarningStyle.Render(SymbolWarning), msg)
} }
// Info returns a styled info message with info symbol. // FmtInfo returns a styled info message with info symbol.
func Info(msg string) string { func FmtInfo(msg string) string {
return fmt.Sprintf("%s %s", InfoStyle.Render(SymbolInfo), msg) 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)) return fmt.Sprintf("\n%s", HeaderStyle.Render(title))
} }
// Title returns a styled command/section title. // FmtTitle returns a styled command/section title.
func Title(text string) string { func FmtTitle(text string) string {
return TitleStyle.Render(text) return TitleStyle.Render(text)
} }
// Dim returns dimmed text. // FmtDim returns dimmed text.
func Dim(text string) string { func FmtDim(text string) string {
return DimStyle.Render(text) return DimStyle.Render(text)
} }