199 lines
4.6 KiB
Go
199 lines
4.6 KiB
Go
|
|
// 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
|
||
|
|
}
|