go/pkg/cli/runtime.go

199 lines
4.6 KiB
Go
Raw Normal View History

// 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
package cli
import (
"context"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"github.com/host-uk/core/pkg/framework"
)
var (
instance *Runtime
once sync.Once
)
// Runtime is the CLI's 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)
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 {
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,
})),
framework.WithServiceLock(),
)
if err != nil {
initErr = err
cancel()
return
}
instance = &Runtime{
Core: core,
ctx: ctx,
cancel: cancel,
}
// Start services
if err := core.ServiceStartup(ctx, nil); err != nil {
initErr = err
return
}
})
return initErr
}
// App returns the global CLI runtime.
// Panics if Init() hasn't been called.
func App() *Runtime {
if instance == nil {
panic("cli.App() called before cli.Init()")
}
return instance
}
// Context returns the CLI's root context.
// This context is cancelled on shutdown signals.
func (r *Runtime) Context() context.Context {
return r.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
}
}
// Success prints a success message with checkmark.
func (s *OutputService) Success(msg string) {
fmt.Println(SuccessStyle.Render(SymbolCheck + " " + msg))
}
// Error prints an error message with cross.
func (s *OutputService) Error(msg string) {
fmt.Println(ErrorStyle.Render(SymbolCross + " " + msg))
}
// Warning prints a warning message.
func (s *OutputService) Warning(msg string) {
fmt.Println(WarningStyle.Render(SymbolWarning + " " + msg))
}
// Info prints an info message.
func (s *OutputService) Info(msg string) {
fmt.Println(InfoStyle.Render(SymbolInfo + " " + msg))
}
// Title prints a title/header.
func (s *OutputService) Title(msg string) {
fmt.Println(TitleStyle.Render(msg))
}
// Dim prints dimmed/subtle text.
func (s *OutputService) Dim(msg string) {
fmt.Println(DimStyle.Render(msg))
}
// --- Signal Service ---
// SignalServiceOptions configures the signal service.
type SignalServiceOptions struct {
Cancel context.CancelFunc
}
// SignalService handles OS signals for graceful shutdown.
type SignalService struct {
*framework.ServiceRuntime[SignalServiceOptions]
sigChan chan os.Signal
}
// NewSignalService creates a signal service factory.
func NewSignalService(opts SignalServiceOptions) 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),
}, nil
}
}
// OnStartup starts listening for signals.
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()
case <-ctx.Done():
}
}()
return nil
}
// OnShutdown stops listening for signals.
func (s *SignalService) OnShutdown(ctx context.Context) error {
signal.Stop(s.sigChan)
close(s.sigChan)
return nil
}