Creates CLI commands for the ago tool following established patterns. Use when adding new subcommands to ago, implementing command actions, or working with the cmdexec package.
Creates CLI commands for the ago tool using consistent patterns for command execution, configuration access, and testing.
Config is loaded lazily when an action runs, not in a global Before hook. This allows:
ago (no args) → shows help without requiring configago dev → shows subcommand help without requiring configago dev fmt → loads config when the action executesThe config.RunWithConfig() wrapper handles lazy loading via config.Ensure().
Commands that require project config use config.RunWithConfig:
// cmd/ago/example.go
package main
import (
"context"
"os"
"github.com/advdv/ago/cmd/ago/internal/cmdexec"
"github.com/advdv/ago/cmd/ago/internal/config"
"github.com/urfave/cli/v3"
)
func exampleCmd() *cli.Command {
return &cli.Command{
Name: "example",
Usage: "Example command description",
Action: config.RunWithConfig(runExample),
}
}
func runExample(ctx context.Context, cmd *cli.Command, cfg config.Config) error {
return doExample(ctx, cfg, exampleOptions{
SomeFlag: cmd.Bool("some-flag"),
Output: os.Stdout,
})
}
type exampleOptions struct {
SomeFlag bool
Output io.Writer
}
func doExample(ctx context.Context, cfg config.Config, opts exampleOptions) error {
exec := cmdexec.New(cfg).WithOutput(opts.Output, opts.Output)
return exec.Run(ctx, "some-command", "arg1", "arg2")
}
Commands that run before config exists use cmdexec.NewWithDir:
func runInit(ctx context.Context, cmd *cli.Command) error {
dir := cmd.Args().First()
if dir == "" {
dir, _ = os.Getwd()
}
exec := cmdexec.NewWithDir(dir).WithOutput(os.Stdout, os.Stderr)
return exec.Run(ctx, "git", "init")
}
The cmdexec package provides consistent command execution with proper working directory handling.
| Function | Use Case |
|---|---|
cmdexec.New(cfg config.Context) | Standard commands with config |
cmdexec.NewWithDir(dir string) | Init-like commands, tests |
exec := cmdexec.New(cfg).
WithOutput(os.Stdout, os.Stderr). // Set stdout/stderr
InSubdir("infra/cdk/cdk") // Change to subdirectory
| Method | Returns | Use Case |
|---|---|---|
Run(ctx, name, args...) | error | Stream output to configured writers |
Output(ctx, name, args...) | (string, error) | Capture stdout as trimmed string |
Mise(ctx, name, args...) | error | Run via mise exec -- |
MiseOutput(ctx, name, args...) | (string, error) | Capture output from mise command |
exec.Dir() // Returns the working directory
cmd/ago/
├── main.go # CLI setup (no Before hook, config loaded lazily)
├── example.go # Parent command with subcommands
├── example_action.go # Individual action implementation
└── example_test.go # Tests
// cmd/ago/check.go
func checkCmd() *cli.Command {
return &cli.Command{
Name: "check",
Usage: "Run various checks",
Commands: []*cli.Command{
{
Name: "tests",
Usage: "Run Go tests",
Action: config.RunWithConfig(checkTests),
},
{
Name: "lint",
Usage: "Lint Go code",
Action: config.RunWithConfig(checkLint),
},
},
}
}
Separate CLI parsing (runXxx) from business logic (doXxx) for testability:
// cmd/ago/example_test.go
func TestDoExample(t *testing.T) {
t.Parallel()
dir := t.TempDir()
cfg := config.Context{ProjectDir: dir}
var output bytes.Buffer
opts := exampleOptions{
SomeFlag: true,
Output: &output,
}
err := doExample(context.Background(), cfg, opts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
For init-style commands without config:
func TestInitCommand(t *testing.T) {
dir := t.TempDir()
exec := cmdexec.NewWithDir(dir)
err := exec.Run(context.Background(), "git", "init")
// ...
}
ctx parameter, never context.Background() in command handlersfunc longFunctionName(
ctx context.Context, exec cmdexec.Executor, opts someOptions,
) error {
--project-dir flags: Commands get cfg.ProjectDir from contextOutput io.Writer for testable outputgithub.com/cockroachdb/errorsAdd to main.go:
cmd := &cli.Command{
Name: "ago",
Commands: []*cli.Command{
cdkCmd(),
checkCmd(),
devCmd(),
initCmd(),
exampleCmd(), // Add new command here
},
// ...
}
exec.Run(ctx, "go", "test", "./...")
exec.Run(ctx, "go", "build", "./...")
exec.Run(ctx, "golangci-lint", "run", "./...")
exec.Mise(ctx, "aws", "cloudformation", "deploy", "--stack-name", name, ...)
output, err := exec.MiseOutput(ctx, "aws", "sts", "get-caller-identity", ...)
cdkExec := exec.InSubdir("infra/cdk/cdk")
cdkExec.Mise(ctx, "cdk", "deploy", "--profile", profile, ...)
shellFiles, err := FindShellScripts(exec.Dir())
if len(shellFiles) > 0 {
exec.Run(ctx, "shellcheck", shellFiles...)
}