Design for moving cmd/ into self-registering packages in pkg/: - RegisterCommands() pattern like RegisterLocales() - rootCmd stored in core.App - Build variants via import files with build tags - Smaller attack surface through selective compilation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
4.3 KiB
4.3 KiB
CLI Commands Registration Design
Overview
Move CLI commands from cmd/ into self-registering packages in pkg/. This enables build variants with reduced attack surface - only compiled code exists in the binary.
Pattern
Same pattern as i18n.RegisterLocales():
- Packages register themselves during
init() - Registration is stored until
cli.Init()runs - Build tags control which packages are imported
Registration API
// pkg/cli/commands.go
type CommandRegistration func(root *cobra.Command)
var (
registeredCommands []CommandRegistration
registeredCommandsMu sync.Mutex
)
// RegisterCommands registers a function that adds commands to the CLI.
func RegisterCommands(fn CommandRegistration) {
registeredCommandsMu.Lock()
defer registeredCommandsMu.Unlock()
registeredCommands = append(registeredCommands, fn)
}
func attachRegisteredCommands(root *cobra.Command) {
registeredCommandsMu.Lock()
defer registeredCommandsMu.Unlock()
for _, fn := range registeredCommands {
fn(root)
}
}
Integration with Core.App
The CLI stores rootCmd in core.App, unifying GUI and CLI under the same pattern:
// pkg/cli/runtime.go
func Init(opts Options) error {
once.Do(func() {
rootCmd := &cobra.Command{
Use: opts.AppName,
Version: opts.Version,
}
attachRegisteredCommands(rootCmd)
c, err := framework.New(
framework.WithApp(rootCmd),
// ... services ...
)
// ...
})
return initErr
}
func RootCmd() *cobra.Command {
return framework.App().(*cobra.Command)
}
func Execute() error {
return RootCmd().Execute()
}
Package Structure
Commands move from cmd/ to pkg/ with a cmd.go file:
pkg/
├── php/
│ ├── i18n.go # registers locales
│ ├── cmd.go # registers commands
│ ├── locales/
│ └── ...
├── dev/
│ ├── cmd.go # registers commands
│ └── ...
├── cli/
│ ├── commands.go # RegisterCommands API
│ ├── runtime.go # Init, Execute
│ └── ...
Each cmd.go:
// pkg/php/cmd.go
package php
import "github.com/host-uk/core/pkg/cli"
func init() {
cli.RegisterCommands(AddCommands)
}
func AddCommands(root *cobra.Command) {
// ... existing command setup ...
}
Build Variants
Import files with build tags in cmd/variants/:
cmd/
├── main.go
└── variants/
├── full.go # default: all packages
├── ci.go # CI/release only
├── php.go # PHP tooling only
└── minimal.go # core only
// cmd/variants/full.go
//go:build !ci && !php && !minimal
package variants
import (
_ "github.com/host-uk/core/pkg/ai"
_ "github.com/host-uk/core/pkg/build"
_ "github.com/host-uk/core/pkg/ci"
_ "github.com/host-uk/core/pkg/dev"
_ "github.com/host-uk/core/pkg/docs"
_ "github.com/host-uk/core/pkg/doctor"
_ "github.com/host-uk/core/pkg/go"
_ "github.com/host-uk/core/pkg/php"
_ "github.com/host-uk/core/pkg/pkg"
_ "github.com/host-uk/core/pkg/sdk"
_ "github.com/host-uk/core/pkg/setup"
_ "github.com/host-uk/core/pkg/test"
_ "github.com/host-uk/core/pkg/vm"
)
// cmd/variants/ci.go
//go:build ci
package variants
import (
_ "github.com/host-uk/core/pkg/build"
_ "github.com/host-uk/core/pkg/ci"
_ "github.com/host-uk/core/pkg/doctor"
_ "github.com/host-uk/core/pkg/sdk"
)
Build Commands
go build→ full variant (default)go build -tags ci→ CI variantgo build -tags php→ PHP-only variant
Benefits
- Smaller attack surface - only compiled code exists in binary
- Self-registering packages - same pattern as
i18n.RegisterLocales() - Uses existing
core.App- no new framework concepts - Simple build variants - just add
-tagsflag - Defence in depth - no code = no vulnerabilities
Migration Steps
- Add
RegisterCommands()topkg/cli/commands.go - Update
pkg/cli/runtime.goto usecore.Appfor rootCmd - Move each
cmd/*package topkg/*/cmd.go - Create
cmd/variants/with build tag files - Simplify
cmd/main.goto minimal entry point - Remove old
cmd/core_dev.goandcmd/core_ci.go