1 Framework Integration
Virgil edited this page 2026-02-23 04:54:00 +00:00

Framework Integration

How Commands Register

Commands register through the Core framework lifecycle using WithCommands:

// In main.go
cli.Main(
    cli.WithCommands("score", score.AddScoreCommands),
    cli.WithCommands("gen", gen.AddGenCommands),
)

Internally, WithCommands wraps the registration function in a commandService that implements Startable.OnStartup(). During startup, it casts Core.App to *cobra.Command and calls the registration function.

Startup order:

  1. Core services start (i18n, log, crypt, workspace)
  2. Command services start (your WithCommands functions)
  3. Execute() runs the matched command

Registration Function Pattern

// score/commands.go
package score

import "forge.lthn.ai/core/cli/pkg/cli"

func AddScoreCommands(root *cli.Command) {
    scoreCmd := cli.NewGroup("score", "Scoring commands", "")

    grammarCmd := cli.NewCommand("grammar", "Grammar analysis", "", runGrammar)
    cli.StringFlag(grammarCmd, &inputPath, "input", "i", "", "Input file")
    scoreCmd.AddCommand(grammarCmd)

    root.AddCommand(scoreCmd)
}

Accessing Core Services

func runMyCommand(cmd *cli.Command, args []string) error {
    ctx := cli.Context()     // Root context (cancelled on signal)
    core := cli.Core()       // Framework Core instance
    root := cli.RootCmd()    // Root cobra command

    // Type-safe service retrieval
    ws, err := framework.ServiceFor[*workspace.Service](core)
    if err != nil {
        return cli.WrapVerb(err, "get", "workspace service")
    }

    return nil
}

Built-in Services

Service Name Purpose
i18n i18n Internationalisation, grammar
log log Structured logging (slog)
crypt crypt OpenPGP encryption
workspace workspace Project root detection
signal signal SIGINT/SIGTERM/SIGHUP handling

Main() Lifecycle

  1. Recovery defer (catches panics)
  2. Register core services (i18n, log, crypt, workspace)
  3. Append user command services
  4. Init() — creates cobra root, signals, starts all services
  5. Add completion command
  6. Execute() — runs matched command
  7. Shutdown() — stops all services in reverse order

Legacy: RegisterCommands

For packages that need init()-time registration (not recommended):

func init() {
    cli.RegisterCommands(func(root *cobra.Command) {
        root.AddCommand(myCmd)
    })
}

Prefer WithCommands — it's explicit and doesn't rely on import side effects.

Building a CLI Binary

// cmd/lem/main.go
package main

import (
    "forge.lthn.ai/core/cli/pkg/cli"
    "forge.lthn.ai/lthn/lem/cmd/lemcmd"
)

func main() {
    cli.WithAppName("lem")
    cli.Main(lemcmd.Commands()...)
}

Where Commands() returns []framework.Option:

func Commands() []framework.Option {
    return []framework.Option{
        cli.WithCommands("score", addScoreCommands),
        cli.WithCommands("gen", addGenCommands),
        cli.WithCommands("data", addDataCommands),
    }
}