feat(cli): wire Core runtime with i18n and log services
- Add i18n service wrapping pkg/i18n for translations via cli.T() - Add log service with levels (quiet/error/warn/info/debug) - Wire cli.Init() in cmd.Execute() with explicit service names - Fix main.go to print errors to stderr and exit with code 1 - Update runtime.go to accept additional services via Options Services use WithName() to avoid name collision since both are defined in pkg/cli (WithService would auto-name both "cli"). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
22aa1df30a
commit
5e2d058b26
5 changed files with 335 additions and 11 deletions
25
cmd/core.go
25
cmd/core.go
|
|
@ -20,9 +20,15 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/host-uk/core/pkg/cli"
|
||||
"github.com/host-uk/core/pkg/framework"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
appName = "core"
|
||||
appVersion = "0.1.0"
|
||||
)
|
||||
|
||||
// Terminal styles using Tailwind colour palette (from shared package).
|
||||
var (
|
||||
// coreStyle is used for primary headings and the CLI name.
|
||||
|
|
@ -34,14 +40,29 @@ var (
|
|||
|
||||
// rootCmd is the base command for the CLI.
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "core",
|
||||
Use: appName,
|
||||
Short: "CLI tool for development and production",
|
||||
Version: "0.1.0",
|
||||
Version: appVersion,
|
||||
}
|
||||
|
||||
// Execute initialises and runs the CLI application.
|
||||
// Commands are registered based on build tags (see core_ci.go and core_dev.go).
|
||||
func Execute() error {
|
||||
// Initialise CLI runtime with services
|
||||
if err := cli.Init(cli.Options{
|
||||
AppName: appName,
|
||||
Version: appVersion,
|
||||
Services: []framework.Option{
|
||||
framework.WithName("i18n", cli.NewI18nService(cli.I18nOptions{})),
|
||||
framework.WithName("log", cli.NewLogService(cli.LogOptions{
|
||||
Level: cli.LogLevelInfo,
|
||||
})),
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Shutdown()
|
||||
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
|
|
|
|||
9
main.go
9
main.go
|
|
@ -1,12 +1,15 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/host-uk/core/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := cmd.Execute()
|
||||
if err != nil {
|
||||
return
|
||||
if err := cmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
109
pkg/cli/i18n.go
Normal file
109
pkg/cli/i18n.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/host-uk/core/pkg/framework"
|
||||
"github.com/host-uk/core/pkg/i18n"
|
||||
)
|
||||
|
||||
// I18nService wraps i18n as a Core service.
|
||||
type I18nService struct {
|
||||
*framework.ServiceRuntime[I18nOptions]
|
||||
svc *i18n.Service
|
||||
}
|
||||
|
||||
// I18nOptions configures the i18n service.
|
||||
type I18nOptions struct {
|
||||
// Language overrides auto-detection (e.g., "en-GB", "de")
|
||||
Language string
|
||||
}
|
||||
|
||||
// NewI18nService creates an i18n service factory.
|
||||
func NewI18nService(opts I18nOptions) func(*framework.Core) (any, error) {
|
||||
return func(c *framework.Core) (any, error) {
|
||||
svc, err := i18n.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.Language != "" {
|
||||
svc.SetLanguage(opts.Language)
|
||||
}
|
||||
|
||||
return &I18nService{
|
||||
ServiceRuntime: framework.NewServiceRuntime(c, opts),
|
||||
svc: svc,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// OnStartup initialises the i18n service.
|
||||
func (s *I18nService) OnStartup(ctx context.Context) error {
|
||||
s.Core().RegisterQuery(s.handleQuery)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Queries for i18n service
|
||||
|
||||
// QueryTranslate requests a translation.
|
||||
type QueryTranslate struct {
|
||||
Key string
|
||||
Args map[string]any
|
||||
}
|
||||
|
||||
func (s *I18nService) handleQuery(c *framework.Core, q framework.Query) (any, bool, error) {
|
||||
switch m := q.(type) {
|
||||
case QueryTranslate:
|
||||
return s.svc.T(m.Key, m.Args), true, nil
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// T translates a key with optional arguments.
|
||||
func (s *I18nService) T(key string, args ...map[string]any) string {
|
||||
if len(args) > 0 {
|
||||
return s.svc.T(key, args[0])
|
||||
}
|
||||
return s.svc.T(key)
|
||||
}
|
||||
|
||||
// SetLanguage changes the current language.
|
||||
func (s *I18nService) SetLanguage(lang string) {
|
||||
s.svc.SetLanguage(lang)
|
||||
}
|
||||
|
||||
// Language returns the current language.
|
||||
func (s *I18nService) Language() string {
|
||||
return s.svc.Language()
|
||||
}
|
||||
|
||||
// AvailableLanguages returns all available languages.
|
||||
func (s *I18nService) AvailableLanguages() []string {
|
||||
return s.svc.AvailableLanguages()
|
||||
}
|
||||
|
||||
// --- Package-level convenience ---
|
||||
|
||||
// T translates a key using the CLI's i18n service.
|
||||
// Falls back to the global i18n.T if CLI not initialised.
|
||||
func T(key string, args ...map[string]any) string {
|
||||
if instance == nil {
|
||||
// CLI not initialised, use global i18n
|
||||
if len(args) > 0 {
|
||||
return i18n.T(key, args[0])
|
||||
}
|
||||
return i18n.T(key)
|
||||
}
|
||||
|
||||
svc, err := framework.ServiceFor[*I18nService](instance.core, "i18n")
|
||||
if err != nil {
|
||||
// i18n service not registered, use global
|
||||
if len(args) > 0 {
|
||||
return i18n.T(key, args[0])
|
||||
}
|
||||
return i18n.T(key)
|
||||
}
|
||||
|
||||
return svc.T(key, args...)
|
||||
}
|
||||
186
pkg/cli/log.go
Normal file
186
pkg/cli/log.go
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/host-uk/core/pkg/framework"
|
||||
)
|
||||
|
||||
// LogLevel defines logging verbosity.
|
||||
type LogLevel int
|
||||
|
||||
const (
|
||||
LogLevelQuiet LogLevel = iota
|
||||
LogLevelError
|
||||
LogLevelWarn
|
||||
LogLevelInfo
|
||||
LogLevelDebug
|
||||
)
|
||||
|
||||
// LogService provides structured logging for the CLI.
|
||||
type LogService struct {
|
||||
*framework.ServiceRuntime[LogOptions]
|
||||
mu sync.RWMutex
|
||||
level LogLevel
|
||||
output io.Writer
|
||||
}
|
||||
|
||||
// LogOptions configures the log service.
|
||||
type LogOptions struct {
|
||||
Level LogLevel
|
||||
Output io.Writer // defaults to os.Stderr
|
||||
}
|
||||
|
||||
// NewLogService creates a log service factory.
|
||||
func NewLogService(opts LogOptions) func(*framework.Core) (any, error) {
|
||||
return func(c *framework.Core) (any, error) {
|
||||
output := opts.Output
|
||||
if output == nil {
|
||||
output = os.Stderr
|
||||
}
|
||||
|
||||
return &LogService{
|
||||
ServiceRuntime: framework.NewServiceRuntime(c, opts),
|
||||
level: opts.Level,
|
||||
output: output,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// OnStartup registers query handlers.
|
||||
func (s *LogService) OnStartup(ctx context.Context) error {
|
||||
s.Core().RegisterQuery(s.handleQuery)
|
||||
s.Core().RegisterTask(s.handleTask)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Queries and tasks for log service
|
||||
|
||||
// QueryLogLevel returns the current log level.
|
||||
type QueryLogLevel struct{}
|
||||
|
||||
// TaskSetLogLevel changes the log level.
|
||||
type TaskSetLogLevel struct {
|
||||
Level LogLevel
|
||||
}
|
||||
|
||||
func (s *LogService) handleQuery(c *framework.Core, q framework.Query) (any, bool, error) {
|
||||
switch q.(type) {
|
||||
case QueryLogLevel:
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.level, true, nil
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (s *LogService) handleTask(c *framework.Core, t framework.Task) (any, bool, error) {
|
||||
switch m := t.(type) {
|
||||
case TaskSetLogLevel:
|
||||
s.mu.Lock()
|
||||
s.level = m.Level
|
||||
s.mu.Unlock()
|
||||
return nil, true, nil
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// SetLevel changes the log level.
|
||||
func (s *LogService) SetLevel(level LogLevel) {
|
||||
s.mu.Lock()
|
||||
s.level = level
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Level returns the current log level.
|
||||
func (s *LogService) Level() LogLevel {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.level
|
||||
}
|
||||
|
||||
func (s *LogService) shouldLog(level LogLevel) bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return level <= s.level
|
||||
}
|
||||
|
||||
func (s *LogService) log(level, prefix, msg string) {
|
||||
timestamp := time.Now().Format("15:04:05")
|
||||
fmt.Fprintf(s.output, "%s %s %s\n", DimStyle.Render(timestamp), prefix, msg)
|
||||
}
|
||||
|
||||
// Debug logs a debug message.
|
||||
func (s *LogService) Debug(msg string) {
|
||||
if s.shouldLog(LogLevelDebug) {
|
||||
s.log("debug", DimStyle.Render("[DBG]"), msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Infof logs an info message.
|
||||
func (s *LogService) Infof(msg string) {
|
||||
if s.shouldLog(LogLevelInfo) {
|
||||
s.log("info", InfoStyle.Render("[INF]"), msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Warnf logs a warning message.
|
||||
func (s *LogService) Warnf(msg string) {
|
||||
if s.shouldLog(LogLevelWarn) {
|
||||
s.log("warn", WarningStyle.Render("[WRN]"), msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf logs an error message.
|
||||
func (s *LogService) Errorf(msg string) {
|
||||
if s.shouldLog(LogLevelError) {
|
||||
s.log("error", ErrorStyle.Render("[ERR]"), msg)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Package-level convenience ---
|
||||
|
||||
// Log returns the CLI's log service, or nil if not available.
|
||||
func Log() *LogService {
|
||||
if instance == nil {
|
||||
return nil
|
||||
}
|
||||
svc, err := framework.ServiceFor[*LogService](instance.core, "log")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return svc
|
||||
}
|
||||
|
||||
// LogDebug logs a debug message if log service is available.
|
||||
func LogDebug(msg string) {
|
||||
if l := Log(); l != nil {
|
||||
l.Debug(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// LogInfo logs an info message if log service is available.
|
||||
func LogInfo(msg string) {
|
||||
if l := Log(); l != nil {
|
||||
l.Infof(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// LogWarn logs a warning message if log service is available.
|
||||
func LogWarn(msg string) {
|
||||
if l := Log(); l != nil {
|
||||
l.Warnf(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// LogError logs an error message if log service is available.
|
||||
func LogError(msg string) {
|
||||
if l := Log(); l != nil {
|
||||
l.Errorf(msg)
|
||||
}
|
||||
}
|
||||
|
|
@ -38,21 +38,26 @@ type runtime struct {
|
|||
|
||||
// Options configures the CLI runtime.
|
||||
type Options struct {
|
||||
AppName string
|
||||
Version string
|
||||
AppName string
|
||||
Version string
|
||||
Services []framework.Option // Additional services to register
|
||||
}
|
||||
|
||||
// Init initialises the global CLI runtime.
|
||||
// Call this once at startup (typically in main.go).
|
||||
// Call this once at startup (typically in main.go or cmd.Execute).
|
||||
func Init(opts Options) error {
|
||||
var initErr error
|
||||
once.Do(func() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
c, err := framework.New(
|
||||
// Build options: signal service + any additional services
|
||||
coreOpts := []framework.Option{
|
||||
framework.WithName("signal", newSignalService(cancel)),
|
||||
framework.WithServiceLock(),
|
||||
)
|
||||
}
|
||||
coreOpts = append(coreOpts, opts.Services...)
|
||||
coreOpts = append(coreOpts, framework.WithServiceLock())
|
||||
|
||||
c, err := framework.New(coreOpts...)
|
||||
if err != nil {
|
||||
initErr = err
|
||||
cancel()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue