[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1 @@
|
||||
XingfenD
|
||||
@@ -0,0 +1,205 @@
|
||||
---
|
||||
name: golang-cli
|
||||
description: "Golang CLI application development. Use when building, modifying, or reviewing a Go CLI tool — especially for command structure, flag handling, configuration layering, version embedding, exit codes, I/O patterns, signal handling, shell completion, argument validation, and CLI unit testing. Also triggers when code uses cobra, viper, or urfave/cli. For cobra-specific APIs → See `samber/cc-skills-golang@golang-spf13-cobra` skill; for viper configuration layering → See `samber/cc-skills-golang@golang-spf13-viper` skill."
|
||||
user-invocable: true
|
||||
license: MIT
|
||||
compatibility: Designed for Claude Code or similar AI coding agents, and for projects using Golang.
|
||||
metadata:
|
||||
author: samber
|
||||
version: "1.2.0"
|
||||
openclaw:
|
||||
emoji: "💻"
|
||||
homepage: https://github.com/samber/cc-skills-golang
|
||||
requires:
|
||||
bins:
|
||||
- go
|
||||
install: []
|
||||
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*) Bash(git:*) Agent AskUserQuestion
|
||||
---
|
||||
|
||||
**Persona:** You are a Go CLI engineer. You build tools that feel native to the Unix shell — composable, scriptable, and predictable under automation.
|
||||
|
||||
**Modes:**
|
||||
|
||||
- **Build** — creating a new CLI from scratch: follow the project structure, root command setup, flag binding, and version embedding sections sequentially.
|
||||
- **Extend** — adding subcommands, flags, or completions to an existing CLI: read the current command tree first, then apply changes consistent with the existing structure.
|
||||
- **Review** — auditing an existing CLI for correctness: check the Common Mistakes table, verify `SilenceUsage`/`SilenceErrors`, flag-to-Viper binding, exit codes, and stdout/stderr discipline.
|
||||
|
||||
# Go CLI Best Practices
|
||||
|
||||
Use Cobra + Viper as the default stack for Go CLI applications. Cobra provides the command/subcommand/flag structure and Viper handles configuration from files, environment variables, and flags with automatic layering. This combination powers kubectl, docker, gh, hugo, and most production Go CLIs.
|
||||
|
||||
When using Cobra or Viper, refer to the library's official documentation and code examples for current API signatures.
|
||||
|
||||
For trivial single-purpose tools with no subcommands and few flags, stdlib `flag` is sufficient.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Concern | Package / Tool |
|
||||
| ------------------- | ------------------------------------ |
|
||||
| Commands & flags | `github.com/spf13/cobra` |
|
||||
| Configuration | `github.com/spf13/viper` |
|
||||
| Flag parsing | `github.com/spf13/pflag` (via Cobra) |
|
||||
| Colored output | `github.com/fatih/color` |
|
||||
| Table output | `github.com/olekukonko/tablewriter` |
|
||||
| Interactive prompts | `github.com/charmbracelet/bubbletea` |
|
||||
| Version injection | `go build -ldflags` |
|
||||
| Distribution | `goreleaser` |
|
||||
|
||||
## Project Structure
|
||||
|
||||
Organize CLI commands in `cmd/myapp/` with one file per command. Keep `main.go` minimal — it only calls `Execute()`.
|
||||
|
||||
```
|
||||
myapp/
|
||||
├── cmd/
|
||||
│ └── myapp/
|
||||
│ ├── main.go # package main, only calls Execute()
|
||||
│ ├── root.go # Root command + Viper init
|
||||
│ ├── serve.go # "serve" subcommand
|
||||
│ ├── migrate.go # "migrate" subcommand
|
||||
│ └── version.go # "version" subcommand
|
||||
├── go.mod
|
||||
└── go.sum
|
||||
```
|
||||
|
||||
`main.go` should be minimal — see [assets/examples/main.go](assets/examples/main.go).
|
||||
|
||||
## Root Command Setup
|
||||
|
||||
The root command initializes Viper configuration and sets up global behavior via `PersistentPreRunE`. See [assets/examples/root.go](assets/examples/root.go).
|
||||
|
||||
Key points:
|
||||
|
||||
- `SilenceUsage: true` MUST be set — prevents printing the full usage text on every error
|
||||
- `SilenceErrors: true` MUST be set — lets you control error output format yourself
|
||||
- `PersistentPreRunE` runs before every subcommand, so config is always initialized
|
||||
- Logs go to stderr, output goes to stdout
|
||||
|
||||
## Subcommands
|
||||
|
||||
Add subcommands by creating separate files in `cmd/myapp/` and registering them in `init()`. See [assets/examples/serve.go](assets/examples/serve.go) for a complete subcommand example including command groups.
|
||||
|
||||
## Flags
|
||||
|
||||
See [assets/examples/flags.go](assets/examples/flags.go) for all flag patterns:
|
||||
|
||||
### Persistent vs Local
|
||||
|
||||
- **Persistent** flags are inherited by all subcommands (e.g., `--config`)
|
||||
- **Local** flags only apply to the command they're defined on (e.g., `--port`)
|
||||
|
||||
### Required Flags
|
||||
|
||||
Use `MarkFlagRequired`, `MarkFlagsMutuallyExclusive`, and `MarkFlagsOneRequired` for flag constraints.
|
||||
|
||||
### Flag Validation with RegisterFlagCompletionFunc
|
||||
|
||||
Provide completion suggestions for flag values.
|
||||
|
||||
### Always Bind Flags to Viper
|
||||
|
||||
This ensures `viper.GetInt("port")` returns the flag value, env var `MYAPP_PORT`, or config file value — whichever has highest precedence.
|
||||
|
||||
## Argument Validation
|
||||
|
||||
Cobra provides built-in validators for positional arguments. See [assets/examples/args.go](assets/examples/args.go) for both built-in and custom validation examples.
|
||||
|
||||
| Validator | Description |
|
||||
| --------------------------- | ------------------------------------ |
|
||||
| `cobra.NoArgs` | Fails if any args provided |
|
||||
| `cobra.ExactArgs(n)` | Requires exactly n args |
|
||||
| `cobra.MinimumNArgs(n)` | Requires at least n args |
|
||||
| `cobra.MaximumNArgs(n)` | Allows at most n args |
|
||||
| `cobra.RangeArgs(min, max)` | Requires between min and max |
|
||||
| `cobra.ExactValidArgs(n)` | Exactly n args, must be in ValidArgs |
|
||||
|
||||
## Configuration with Viper
|
||||
|
||||
Viper resolves configuration values in this order (highest to lowest precedence):
|
||||
|
||||
1. **CLI flags** (explicit user input)
|
||||
2. **Environment variables** (deployment config)
|
||||
3. **Config file** (persistent settings)
|
||||
4. **Defaults** (set in code)
|
||||
|
||||
See [assets/examples/config.go](assets/examples/config.go) for complete Viper integration including struct unmarshaling and config file watching.
|
||||
|
||||
### Example Config File (.myapp.yaml)
|
||||
|
||||
```yaml
|
||||
port: 8080
|
||||
host: localhost
|
||||
log-level: info
|
||||
database:
|
||||
dsn: postgres://localhost:5432/myapp
|
||||
max-conn: 25
|
||||
```
|
||||
|
||||
With the setup above, these are all equivalent:
|
||||
|
||||
- Flag: `--port 9090`
|
||||
- Env var: `MYAPP_PORT=9090`
|
||||
- Config file: `port: 9090`
|
||||
|
||||
## Version and Build Info
|
||||
|
||||
Version SHOULD be embedded at compile time using `ldflags`. See [assets/examples/version.go](assets/examples/version.go) for the version command and build instructions.
|
||||
|
||||
## Exit Codes
|
||||
|
||||
Exit codes MUST follow Unix conventions:
|
||||
|
||||
| Code | Meaning | When to Use |
|
||||
| ----- | ----------------- | ----------------------------------------- |
|
||||
| 0 | Success | Operation completed normally |
|
||||
| 1 | General error | Runtime failure |
|
||||
| 2 | Usage error | Invalid flags or arguments |
|
||||
| 64-78 | BSD sysexits | Specific error categories |
|
||||
| 126 | Cannot execute | Permission denied |
|
||||
| 127 | Command not found | Missing dependency |
|
||||
| 128+N | Signal N | Terminated by signal (e.g., 130 = SIGINT) |
|
||||
|
||||
See [assets/examples/exit_codes.go](assets/examples/exit_codes.go) for a pattern mapping errors to exit codes.
|
||||
|
||||
## I/O Patterns
|
||||
|
||||
See [assets/examples/output.go](assets/examples/output.go) for all I/O patterns:
|
||||
|
||||
- **stdout vs stderr**: NEVER write diagnostic output to stdout — stdout is for program output (pipeable), stderr for logs/errors/diagnostics
|
||||
- **Detecting pipe vs terminal**: check `os.ModeCharDevice` on stdout
|
||||
- **Machine-readable output**: support `--output` flag for table/json/plain formats
|
||||
- **Colors**: use `fatih/color` which auto-disables when output is not a terminal
|
||||
|
||||
## Signal Handling
|
||||
|
||||
Signal handling MUST use `signal.NotifyContext` to propagate cancellation through context. See [assets/examples/signal.go](assets/examples/signal.go) for graceful HTTP server shutdown.
|
||||
|
||||
## Shell Completions
|
||||
|
||||
Cobra generates completions for bash, zsh, fish, and PowerShell automatically. See [assets/examples/completion.go](assets/examples/completion.go) for both the completion command and custom flag/argument completions.
|
||||
|
||||
## Testing CLI Commands
|
||||
|
||||
Test commands by executing them programmatically and capturing output. See [assets/examples/cli_test.go](assets/examples/cli_test.go).
|
||||
|
||||
Use `cmd.OutOrStdout()` and `cmd.ErrOrStderr()` in commands (instead of `os.Stdout` / `os.Stderr`) so output can be captured in tests.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Fix |
|
||||
| --- | --- |
|
||||
| Writing to `os.Stdout` directly | Tests can't capture output. Use `cmd.OutOrStdout()` which tests can redirect to a buffer |
|
||||
| Calling `os.Exit()` inside `RunE` | Cobra's error handling, deferred functions, and cleanup code never run. Return an error, let `main()` decide |
|
||||
| Not binding flags to Viper | Flags won't be configurable via env/config. Call `viper.BindPFlag` for every configurable flag |
|
||||
| Missing `viper.SetEnvPrefix` | `PORT` collides with other tools. Use a prefix (`MYAPP_PORT`) to namespace env vars |
|
||||
| Logging to stdout | Unix pipes chain stdout — logs corrupt the data stream for the next program. Logs go to stderr |
|
||||
| Printing usage on every error | Full help text on every error is noise. Set `SilenceUsage: true`, save full usage for `--help` |
|
||||
| Config file required | Users without a config file get a crash. Ignore `viper.ConfigFileNotFoundError` — config should be optional |
|
||||
| Not using `PersistentPreRunE` | Config initialization must happen before any subcommand. Use root's `PersistentPreRunE` |
|
||||
| Hardcoded version string | Version gets out of sync with tags. Inject via `ldflags` at build time from git tags |
|
||||
| Not supporting `--output` format | Scripts can't parse human-readable output. Add JSON/table/plain for machine consumption |
|
||||
|
||||
## Related Skills
|
||||
|
||||
See `samber/cc-skills-golang@golang-project-layout`, `samber/cc-skills-golang@golang-dependency-injection`, `samber/cc-skills-golang@golang-testing`, `samber/cc-skills-golang@golang-design-patterns` skills.
|
||||
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Cobra provides built-in validators for positional arguments.
|
||||
// See the table in SKILL.md for all available validators.
|
||||
var deployCmd = &cobra.Command{
|
||||
Use: "deploy [environment]",
|
||||
Short: "Deploy to an environment",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
env := args[0]
|
||||
_ = env
|
||||
// deploy...
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Custom validation example:
|
||||
var deployWithValidationCmd = &cobra.Command{
|
||||
Use: "deploy [environment]",
|
||||
Short: "Deploy to an environment",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("expected exactly 1 argument, got %d", len(args))
|
||||
}
|
||||
valid := map[string]bool{"dev": true, "staging": true, "prod": true}
|
||||
if !valid[args[0]] {
|
||||
return fmt.Errorf("invalid environment %q, must be one of: dev, staging, prod", args[0])
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// deploy...
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Test commands by executing them programmatically and capturing output.
|
||||
// Use cmd.OutOrStdout() and cmd.ErrOrStderr() in commands (instead of
|
||||
// os.Stdout / os.Stderr) so output can be captured in tests.
|
||||
|
||||
func executeCommand(root *cobra.Command, args ...string) (string, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
root.SetOut(buf)
|
||||
root.SetErr(buf)
|
||||
root.SetArgs(args)
|
||||
err := root.Execute()
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
func TestServeCommand(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "default port",
|
||||
args: []string{"serve"},
|
||||
want: "listening on :8080\n",
|
||||
},
|
||||
{
|
||||
name: "custom port",
|
||||
args: []string{"serve", "--port", "9090"},
|
||||
want: "listening on :9090\n",
|
||||
},
|
||||
{
|
||||
name: "missing required flag",
|
||||
args: []string{"serve", "--host", ""},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := executeCommand(rootCmd, tt.args...)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if !tt.wantErr && got != tt.want {
|
||||
t.Errorf("output = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// === Shell Completion Command ===
|
||||
// Cobra generates completions for bash, zsh, fish, and PowerShell automatically.
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(&cobra.Command{
|
||||
Use: "completion [bash|zsh|fish|powershell]",
|
||||
Short: "Generate shell completion script",
|
||||
Args: cobra.ExactValidArgs(1),
|
||||
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
switch args[0] {
|
||||
case "bash":
|
||||
return rootCmd.GenBashCompletionV2(os.Stdout, true)
|
||||
case "zsh":
|
||||
return rootCmd.GenZshCompletion(os.Stdout)
|
||||
case "fish":
|
||||
return rootCmd.GenFishCompletion(os.Stdout, true)
|
||||
case "powershell":
|
||||
return rootCmd.GenPowerShellCompletionWithDesc(os.Stdout)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// === Custom Completions ===
|
||||
// Add custom completions for flags and arguments.
|
||||
|
||||
func customCompletionExamples() {
|
||||
deployCmd.RegisterFlagCompletionFunc("env", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return []string{
|
||||
"dev\tDevelopment environment",
|
||||
"staging\tStaging environment",
|
||||
"prod\tProduction environment",
|
||||
}, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
|
||||
// Dynamic argument completion
|
||||
deployCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) != 0 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return getAvailableServices(), cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
func getAvailableServices() []string {
|
||||
// fetch available services dynamically
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// === Complete Cobra + Viper Integration ===
|
||||
|
||||
func initConfigComplete() error {
|
||||
// 1. Config file
|
||||
if cfgFile != "" {
|
||||
viper.SetConfigFile(cfgFile) // explicit path
|
||||
} else {
|
||||
home, _ := os.UserHomeDir()
|
||||
viper.AddConfigPath(home) // search $HOME
|
||||
viper.AddConfigPath(".") // search current dir
|
||||
viper.SetConfigName(".myapp")
|
||||
viper.SetConfigType("yaml")
|
||||
}
|
||||
|
||||
// 2. Environment variables
|
||||
viper.SetEnvPrefix("MYAPP") // MYAPP_PORT, MYAPP_LOG_LEVEL
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) // log-level → MYAPP_LOG_LEVEL
|
||||
viper.AutomaticEnv() // bind all env vars automatically
|
||||
|
||||
// 3. Read config file (ignore "not found")
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
return fmt.Errorf("reading config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// === Unmarshaling into Structs ===
|
||||
|
||||
type Config struct {
|
||||
Port int `mapstructure:"port"`
|
||||
Host string `mapstructure:"host"`
|
||||
LogLevel string `mapstructure:"log-level"`
|
||||
Database struct {
|
||||
DSN string `mapstructure:"dsn"`
|
||||
MaxConn int `mapstructure:"max-conn"`
|
||||
} `mapstructure:"database"`
|
||||
}
|
||||
|
||||
func loadConfig() (Config, error) {
|
||||
var cfg Config
|
||||
if err := viper.Unmarshal(&cfg); err != nil {
|
||||
return Config{}, fmt.Errorf("unmarshaling config: %w", err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// === Watching Config File Changes ===
|
||||
// For long-running CLIs (servers, daemons):
|
||||
|
||||
func watchConfig() {
|
||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||
slog.Info("config file changed", "file", e.Name)
|
||||
// re-read and apply config
|
||||
})
|
||||
viper.WatchConfig()
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/you/myapp/cmd"
|
||||
)
|
||||
|
||||
// Pattern for mapping errors to exit codes.
|
||||
func mainWithExitCodes() {
|
||||
if err := cmd.Execute(); err != nil {
|
||||
// Cobra already printed the error via RunE
|
||||
var exitErr *ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
os.Exit(exitErr.Code)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
type ExitError struct {
|
||||
Code int
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *ExitError) Error() string { return e.Err.Error() }
|
||||
func (e *ExitError) Unwrap() error { return e.Err }
|
||||
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func flagExamples() {
|
||||
// === Persistent vs Local ===
|
||||
|
||||
// Persistent — inherited by all subcommands
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file path")
|
||||
|
||||
// Local — only for this command
|
||||
serveCmd.Flags().IntP("port", "p", 8080, "port to listen on")
|
||||
|
||||
// === Required Flags ===
|
||||
|
||||
serveCmd.Flags().String("host", "", "hostname to bind to")
|
||||
serveCmd.MarkFlagRequired("host")
|
||||
|
||||
// Mutually exclusive flags
|
||||
rootCmd.MarkFlagsMutuallyExclusive("json", "yaml")
|
||||
|
||||
// At least one required
|
||||
rootCmd.MarkFlagsOneRequired("output-file", "stdout")
|
||||
|
||||
// === Flag Validation with RegisterFlagCompletionFunc ===
|
||||
|
||||
serveCmd.Flags().String("env", "dev", "environment (dev, staging, prod)")
|
||||
serveCmd.RegisterFlagCompletionFunc("env", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return []string{"dev", "staging", "prod"}, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
|
||||
// === Always Bind Flags to Viper ===
|
||||
// This ensures viper.GetInt("port") returns the flag value, env var MYAPP_PORT,
|
||||
// or config file value — whichever has highest precedence.
|
||||
|
||||
serveCmd.Flags().IntP("port", "p", 8080, "port to listen on")
|
||||
viper.BindPFlag("port", serveCmd.Flags().Lookup("port"))
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// cmd/myapp/main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// === stdout vs stderr ===
|
||||
// stdout: Program output (data, results). This is what gets piped.
|
||||
// stderr: Logs, progress, errors, diagnostics. Not piped by default.
|
||||
|
||||
func outputExample(cmd *cobra.Command, result string, err error) {
|
||||
// Output data to stdout (pipeable)
|
||||
fmt.Fprintln(cmd.OutOrStdout(), result)
|
||||
|
||||
// Logs and errors to stderr (use slog)
|
||||
// slog.Error("operation failed", "error", err)
|
||||
}
|
||||
|
||||
// === Detecting Pipe vs Terminal ===
|
||||
|
||||
func isTerminal() bool {
|
||||
fi, err := os.Stdout.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return fi.Mode()&os.ModeCharDevice != 0
|
||||
}
|
||||
|
||||
// === Machine-Readable Output ===
|
||||
// Support --output flag for different output formats.
|
||||
|
||||
type User struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
func printUsers(cmd *cobra.Command, users []User) error {
|
||||
format, _ := cmd.Flags().GetString("output")
|
||||
switch format {
|
||||
case "json":
|
||||
enc := json.NewEncoder(cmd.OutOrStdout())
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(users)
|
||||
case "plain":
|
||||
for _, u := range users {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%s\t%s\n", u.ID, u.Name)
|
||||
}
|
||||
default: // "table"
|
||||
w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "ID\tNAME")
|
||||
for _, u := range users {
|
||||
fmt.Fprintf(w, "%s\t%s\n", u.ID, u.Name)
|
||||
}
|
||||
w.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// === Colors ===
|
||||
// Use fatih/color — it auto-disables when output is not a terminal.
|
||||
|
||||
func colorExamples(cmd *cobra.Command, env string, err error) {
|
||||
color.Green("Success: deployed to %s", env)
|
||||
color.Red("Error: %v", err)
|
||||
|
||||
// Or for reusable styles
|
||||
success := color.New(color.FgGreen, color.Bold).SprintFunc()
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%s deployed\n", success("v1.2.3"))
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// cmd/myapp/root.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "myapp",
|
||||
Short: "A brief description of your application",
|
||||
Long: "A longer description with usage examples.",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return initConfig()
|
||||
},
|
||||
SilenceUsage: true, // don't print usage on errors from RunE
|
||||
SilenceErrors: true, // handle error printing yourself
|
||||
}
|
||||
|
||||
func Execute() error {
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default $HOME/.myapp.yaml)")
|
||||
rootCmd.PersistentFlags().String("log-level", "info", "log level (debug, info, warn, error)")
|
||||
viper.BindPFlag("log-level", rootCmd.PersistentFlags().Lookup("log-level"))
|
||||
}
|
||||
|
||||
func initConfig() error {
|
||||
if cfgFile != "" {
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding home directory: %w", err)
|
||||
}
|
||||
viper.AddConfigPath(home)
|
||||
viper.AddConfigPath(".")
|
||||
viper.SetConfigName(".myapp")
|
||||
viper.SetConfigType("yaml")
|
||||
}
|
||||
|
||||
viper.SetEnvPrefix("MYAPP")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
viper.AutomaticEnv()
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
return fmt.Errorf("reading config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up logging based on config
|
||||
level := slog.LevelInfo
|
||||
switch strings.ToLower(viper.GetString("log-level")) {
|
||||
case "debug":
|
||||
level = slog.LevelDebug
|
||||
case "warn":
|
||||
level = slog.LevelWarn
|
||||
case "error":
|
||||
level = slog.LevelError
|
||||
}
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level})))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// cmd/myapp/serve.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Start the HTTP server",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
port := viper.GetInt("port")
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "listening on :%d\n", port)
|
||||
// start server...
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
serveCmd.Flags().IntP("port", "p", 8080, "port to listen on")
|
||||
viper.BindPFlag("port", serveCmd.Flags().Lookup("port"))
|
||||
}
|
||||
|
||||
// For command groups, use AddGroup and set GroupID on commands:
|
||||
//
|
||||
// rootCmd.AddGroup(&cobra.Group{ID: "management", Title: "Management Commands:"})
|
||||
// serveCmd.GroupID = "management"
|
||||
@@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Use signal.NotifyContext to propagate cancellation through context.
|
||||
var serveWithSignalCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Start the server",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
srv := &http.Server{Addr: ":8080"}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
srv.Shutdown(shutdownCtx)
|
||||
}()
|
||||
|
||||
slog.Info("server starting", "addr", srv.Addr)
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
return fmt.Errorf("server failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// cmd/myapp/version.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Set via ldflags
|
||||
var (
|
||||
version = "dev"
|
||||
commit = "unknown"
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print version information",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "myapp %s (commit: %s, built: %s)\n", version, commit, date)
|
||||
|
||||
if info, ok := debug.ReadBuildInfo(); ok {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "go: %s\n", info.GoVersion)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
// Build with:
|
||||
//
|
||||
// go build -ldflags "-X github.com/you/myapp/cmd/myapp.version=1.2.3 \
|
||||
// -X github.com/you/myapp/cmd/myapp.commit=$(git rev-parse --short HEAD) \
|
||||
// -X github.com/you/myapp/cmd/myapp.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
// -o bin/myapp ./cmd/myapp
|
||||
@@ -0,0 +1,342 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "minimal-main-and-execute",
|
||||
"description": "Tests that main.go is minimal and only calls Execute(), with os.Exit handled in main not inside commands",
|
||||
"prompt": "Create the entry point for a Go CLI application called 'deploy' using Cobra. The app should have a root command and a 'push' subcommand. Write main.go and root.go.",
|
||||
"trap": "Model puts configuration logic, flag parsing, or complex setup directly in main.go instead of keeping it minimal. May also call os.Exit inside RunE instead of returning errors.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "1.1",
|
||||
"text": "main.go only calls Execute() (or rootCmd.Execute()) and os.Exit on error — no configuration, flag setup, or business logic in main()"
|
||||
},
|
||||
{
|
||||
"id": "1.2",
|
||||
"text": "The root command sets SilenceUsage: true to prevent printing full usage text on every error"
|
||||
},
|
||||
{
|
||||
"id": "1.3",
|
||||
"text": "The root command sets SilenceErrors: true to control error output format"
|
||||
},
|
||||
{
|
||||
"id": "1.4",
|
||||
"text": "Subcommands do NOT call os.Exit() inside RunE — they return errors and let main() decide the exit code"
|
||||
},
|
||||
{
|
||||
"id": "1.5",
|
||||
"text": "The push subcommand is registered via rootCmd.AddCommand() in an init() function or setup function"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "viper-config-layering",
|
||||
"description": "Tests proper Viper configuration precedence: flags > env > config file > defaults, with env prefix and config-file-not-required",
|
||||
"prompt": "I'm building a Go CLI server tool with Cobra. It needs a --port flag (default 3000) that can also be set via the MYSERVER_PORT env var or a config file at ~/.myserver.yaml. Write the configuration setup code.",
|
||||
"trap": "Model doesn't bind flags to Viper (so flags and env/config are disconnected), forgets SetEnvPrefix (causing env var collisions), or crashes when no config file exists instead of ignoring ConfigFileNotFoundError.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "2.1",
|
||||
"text": "Calls viper.BindPFlag to bind the port flag to Viper, ensuring viper.GetInt('port') returns the flag value when set"
|
||||
},
|
||||
{
|
||||
"id": "2.2",
|
||||
"text": "Sets an env prefix with viper.SetEnvPrefix('MYSERVER' or similar) to namespace env vars and avoid collisions"
|
||||
},
|
||||
{
|
||||
"id": "2.3",
|
||||
"text": "Calls viper.AutomaticEnv() to enable automatic env var binding"
|
||||
},
|
||||
{
|
||||
"id": "2.4",
|
||||
"text": "Handles viper.ConfigFileNotFoundError gracefully (ignores it) — config file is optional, not crashing when absent"
|
||||
},
|
||||
{
|
||||
"id": "2.5",
|
||||
"text": "The precedence order is correct: CLI flags > environment variables > config file > defaults"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "persistent-pre-run-config-init",
|
||||
"description": "Tests that configuration initialization happens in PersistentPreRunE on the root command",
|
||||
"prompt": "My Go CLI has a root command and three subcommands (serve, migrate, status). All of them need access to a database DSN from config. Where should I initialize the configuration so all subcommands have access? Write the code.",
|
||||
"trap": "Model initializes config inside each subcommand's RunE (duplicating logic), or uses a global init() function instead of PersistentPreRunE, or puts it in cobra.OnInitialize without connecting it to the command tree properly.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "3.1",
|
||||
"text": "Configuration initialization happens in PersistentPreRunE on the root command — this ensures it runs before every subcommand"
|
||||
},
|
||||
{
|
||||
"id": "3.2",
|
||||
"text": "Config init is NOT duplicated inside each subcommand's RunE — it happens once in the root"
|
||||
},
|
||||
{
|
||||
"id": "3.3",
|
||||
"text": "The --config flag (or equivalent) is a persistent flag on the root command so all subcommands inherit it"
|
||||
},
|
||||
{
|
||||
"id": "3.4",
|
||||
"text": "Environment variables use a replacer (SetEnvKeyReplacer) to handle hyphens-to-underscores mapping (e.g., log-level becomes LOG_LEVEL)"
|
||||
},
|
||||
{
|
||||
"id": "3.5",
|
||||
"text": "Logging is configured to write to stderr, not stdout"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "stdout-vs-stderr-separation",
|
||||
"description": "Tests that program output goes to stdout and diagnostics/errors/logs go to stderr",
|
||||
"prompt": "Write a Go CLI command 'list-users' using Cobra that fetches users from a database and prints them. It should log progress messages, handle errors, and support being piped to other commands (e.g., `myapp list-users | grep admin`). Write the RunE function.",
|
||||
"trap": "Model writes log messages, error messages, or progress indicators to stdout (using fmt.Println) instead of stderr, which would corrupt piped output. May also use os.Stdout directly instead of cmd.OutOrStdout().",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "4.1",
|
||||
"text": "Program output (the user list) goes to stdout via cmd.OutOrStdout() or fmt.Fprint(cmd.OutOrStdout(), ...) — NOT os.Stdout directly"
|
||||
},
|
||||
{
|
||||
"id": "4.2",
|
||||
"text": "Log messages, progress indicators, or diagnostic output go to stderr (via slog, log, or fmt.Fprint(os.Stderr, ...)) — NOT stdout"
|
||||
},
|
||||
{
|
||||
"id": "4.3",
|
||||
"text": "Error messages go to stderr (via cmd.ErrOrStderr() or os.Stderr), not mixed with program output on stdout"
|
||||
},
|
||||
{
|
||||
"id": "4.4",
|
||||
"text": "Uses cmd.OutOrStdout() instead of os.Stdout directly, enabling test capture"
|
||||
},
|
||||
{
|
||||
"id": "4.5",
|
||||
"text": "The function returns an error from RunE rather than calling os.Exit() or log.Fatal() on failure"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "version-ldflags-injection",
|
||||
"description": "Tests that version info is injected at build time via ldflags, not hardcoded",
|
||||
"prompt": "Add a 'version' command to my Go CLI app that shows the version, git commit, and build date. How should I handle the version string?",
|
||||
"trap": "Model hardcodes the version string as a constant (const version = \"1.0.0\") instead of using ldflags injection. May also not include the build command example.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "5.1",
|
||||
"text": "Version, commit, and date are package-level variables (var, not const) with placeholder defaults like 'dev' or 'unknown'"
|
||||
},
|
||||
{
|
||||
"id": "5.2",
|
||||
"text": "Shows or explains the -ldflags '-X ...' build command for injecting values at compile time"
|
||||
},
|
||||
{
|
||||
"id": "5.3",
|
||||
"text": "The version command uses cmd.OutOrStdout() for output, not fmt.Println or os.Stdout directly"
|
||||
},
|
||||
{
|
||||
"id": "5.4",
|
||||
"text": "Version is NOT hardcoded as a const string that would get out of sync with git tags"
|
||||
},
|
||||
{
|
||||
"id": "5.5",
|
||||
"text": "Optionally includes runtime/debug.ReadBuildInfo() as a fallback or supplement for Go module version info"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "exit-code-conventions",
|
||||
"description": "Tests proper Unix exit code mapping — 0 for success, 1 for general error, 2 for usage errors",
|
||||
"prompt": "My Go CLI tool needs to report different exit codes for different failure types: invalid arguments, missing config file, network timeout, and successful completion. Write the error handling and exit code logic in main.go.",
|
||||
"trap": "Model uses the same exit code (1) for all errors, or calls os.Exit deep inside command handlers instead of in main(). May also use non-standard exit codes.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "6.1",
|
||||
"text": "Exit code 0 for success, exit code 1 for general runtime errors, exit code 2 for usage/argument errors — follows Unix conventions"
|
||||
},
|
||||
{
|
||||
"id": "6.2",
|
||||
"text": "os.Exit() is called only in main(), not inside RunE functions or command handlers"
|
||||
},
|
||||
{
|
||||
"id": "6.3",
|
||||
"text": "Uses a typed error or error wrapping pattern (like ExitError with a Code field) to propagate exit codes from commands to main"
|
||||
},
|
||||
{
|
||||
"id": "6.4",
|
||||
"text": "Errors are returned from commands, not swallowed with os.Exit() calls that skip deferred cleanup"
|
||||
},
|
||||
{
|
||||
"id": "6.5",
|
||||
"text": "Different error categories map to different exit codes, not all errors producing exit code 1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "signal-handling-with-context",
|
||||
"description": "Tests that signal handling uses signal.NotifyContext for context-based cancellation",
|
||||
"prompt": "My Go CLI has a long-running 'serve' command that starts an HTTP server. I need graceful shutdown when the user presses Ctrl+C. Write the signal handling code.",
|
||||
"trap": "Model uses a raw signal channel with signal.Notify instead of signal.NotifyContext, missing context propagation. May also not handle the shutdown timeout or forget SIGTERM.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "7.1",
|
||||
"text": "Uses signal.NotifyContext to propagate cancellation through context — NOT a raw channel with signal.Notify and manual select"
|
||||
},
|
||||
{
|
||||
"id": "7.2",
|
||||
"text": "Handles both os.Interrupt (Ctrl+C / SIGINT) and syscall.SIGTERM (container orchestrators)"
|
||||
},
|
||||
{
|
||||
"id": "7.3",
|
||||
"text": "Creates a shutdown timeout context (e.g., 10-30 seconds) for graceful shutdown, not blocking indefinitely"
|
||||
},
|
||||
{
|
||||
"id": "7.4",
|
||||
"text": "Calls srv.Shutdown(ctx) for graceful HTTP server shutdown, not srv.Close() which drops in-flight requests"
|
||||
},
|
||||
{
|
||||
"id": "7.5",
|
||||
"text": "Distinguishes http.ErrServerClosed (normal shutdown) from unexpected server errors"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "flag-binding-and-constraints",
|
||||
"description": "Tests flag patterns: persistent vs local, required flags, mutual exclusion, and Viper binding",
|
||||
"prompt": "My Go CLI 'deploy' command needs these flags:\n- --env (required, must be one of: dev, staging, prod)\n- --tag (required, the docker image tag)\n- --dry-run and --force (mutually exclusive)\n- --verbose (available on all commands, not just deploy)\n\nWrite the flag setup code using Cobra.",
|
||||
"trap": "Model makes --verbose a local flag instead of persistent, doesn't use MarkFlagsMutuallyExclusive for dry-run/force, or forgets to bind flags to Viper.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "8.1",
|
||||
"text": "--verbose is a persistent flag (PersistentFlags) on the root command, not a local flag on deploy — it needs to be available on all commands"
|
||||
},
|
||||
{
|
||||
"id": "8.2",
|
||||
"text": "Uses MarkFlagRequired for --env and --tag flags"
|
||||
},
|
||||
{
|
||||
"id": "8.3",
|
||||
"text": "Uses MarkFlagsMutuallyExclusive for --dry-run and --force"
|
||||
},
|
||||
{
|
||||
"id": "8.4",
|
||||
"text": "Uses RegisterFlagCompletionFunc to provide completion values for --env (dev, staging, prod)"
|
||||
},
|
||||
{
|
||||
"id": "8.5",
|
||||
"text": "Binds configurable flags to Viper with viper.BindPFlag so they can be set via env vars or config file"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "argument-validation",
|
||||
"description": "Tests use of Cobra's built-in argument validators instead of manual validation in RunE",
|
||||
"prompt": "I have three Cobra commands:\n1. 'status' — takes no arguments\n2. 'deploy' — takes exactly one argument (the service name)\n3. 'scale' — takes 2-3 arguments (service, replica count, optional region)\n\nHow should I validate the arguments for each command?",
|
||||
"trap": "Model manually validates len(args) inside RunE instead of using Cobra's declarative validators (cobra.NoArgs, cobra.ExactArgs, cobra.RangeArgs). May also use custom validation where built-in validators suffice.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "9.1",
|
||||
"text": "Uses cobra.NoArgs for the status command — not manual len(args) == 0 check"
|
||||
},
|
||||
{
|
||||
"id": "9.2",
|
||||
"text": "Uses cobra.ExactArgs(1) for the deploy command — not manual len(args) != 1 check"
|
||||
},
|
||||
{
|
||||
"id": "9.3",
|
||||
"text": "Uses cobra.RangeArgs(2, 3) for the scale command — not manual len(args) < 2 || len(args) > 3 check"
|
||||
},
|
||||
{
|
||||
"id": "9.4",
|
||||
"text": "Validators are set on the Args field of the command struct, not implemented inside RunE"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "cli-testing-pattern",
|
||||
"description": "Tests that CLI commands are tested by executing programmatically with captured output",
|
||||
"prompt": "Write tests for a Cobra CLI command 'greet' that takes a --name flag and prints a greeting. Test the default behavior and a custom name. Show the test helper and test function.",
|
||||
"trap": "Model tests by running os.exec on the compiled binary instead of executing commands programmatically. May also not capture output via cmd.SetOut/cmd.SetErr buffers.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "10.1",
|
||||
"text": "Creates an executeCommand helper that sets up a buffer, calls cmd.SetOut(buf) and cmd.SetErr(buf), sets args, and executes"
|
||||
},
|
||||
{
|
||||
"id": "10.2",
|
||||
"text": "Tests are table-driven with multiple cases (at least default and custom name)"
|
||||
},
|
||||
{
|
||||
"id": "10.3",
|
||||
"text": "Uses cmd.SetArgs() to pass arguments programmatically — not os/exec.Command"
|
||||
},
|
||||
{
|
||||
"id": "10.4",
|
||||
"text": "Captures output via a bytes.Buffer set on the command — not by redirecting os.Stdout"
|
||||
},
|
||||
{
|
||||
"id": "10.5",
|
||||
"text": "Tests check both the output string and the error return value"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "machine-readable-output-format",
|
||||
"description": "Tests support for --output flag with multiple formats (json/table/plain) for scriptability",
|
||||
"prompt": "My Go CLI command 'list-services' shows running services. I need it to support both human-readable and machine-parseable output for scripting. What's the best approach?",
|
||||
"trap": "Model only supports a single output format, or adds a --json boolean flag instead of a flexible --output format flag. May also not use tabwriter for table output.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "11.1",
|
||||
"text": "Supports an --output flag with at least json and table formats (not just a --json boolean toggle)"
|
||||
},
|
||||
{
|
||||
"id": "11.2",
|
||||
"text": "JSON output uses encoding/json encoder writing to cmd.OutOrStdout()"
|
||||
},
|
||||
{
|
||||
"id": "11.3",
|
||||
"text": "Table output uses text/tabwriter or similar for aligned columns — not ad-hoc spacing"
|
||||
},
|
||||
{
|
||||
"id": "11.4",
|
||||
"text": "The default format is human-readable (table), with JSON/plain as opt-in machine formats"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "shell-completion-setup",
|
||||
"description": "Tests proper shell completion command and custom completions for flags",
|
||||
"prompt": "Add shell completion support to my Go CLI built with Cobra. I want users to be able to run 'myapp completion bash' to generate a completion script. Also, the --env flag should suggest 'dev', 'staging', 'prod' during tab completion.",
|
||||
"trap": "Model implements completion from scratch instead of using Cobra's built-in generators. May forget the ValidArgs field or RegisterFlagCompletionFunc for custom completions.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "12.1",
|
||||
"text": "Creates a 'completion' subcommand that supports bash, zsh, fish, and powershell as arguments"
|
||||
},
|
||||
{
|
||||
"id": "12.2",
|
||||
"text": "Uses Cobra's built-in completion generators (GenBashCompletionV2, GenZshCompletion, GenFishCompletion, GenPowerShellCompletionWithDesc) — not custom completion scripts"
|
||||
},
|
||||
{
|
||||
"id": "12.3",
|
||||
"text": "Uses RegisterFlagCompletionFunc for the --env flag to suggest dev/staging/prod values"
|
||||
},
|
||||
{
|
||||
"id": "12.4",
|
||||
"text": "Uses cobra.ExactValidArgs or ValidArgs to validate completion arguments for the completion command itself"
|
||||
},
|
||||
{
|
||||
"id": "12.5",
|
||||
"text": "Returns cobra.ShellCompDirectiveNoFileComp for flags that don't accept file paths"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user