litclaude-ai 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +155 -0
- package/LICENSE +21 -0
- package/README.md +369 -0
- package/README_ko-KR.md +374 -0
- package/RELEASE_CHECKLIST.md +165 -0
- package/bin/litclaude-ai.js +643 -0
- package/cover.png +0 -0
- package/docs/agents.md +67 -0
- package/docs/hooks.md +134 -0
- package/docs/lsp.md +40 -0
- package/docs/migration.md +209 -0
- package/docs/workflow-compatibility-audit.md +119 -0
- package/generate_cover.py +123 -0
- package/package.json +48 -0
- package/plugins/litclaude/.claude-plugin/plugin.json +25 -0
- package/plugins/litclaude/.lsp.json +13 -0
- package/plugins/litclaude/.mcp.json +9 -0
- package/plugins/litclaude/agents/boulder-executor.md +12 -0
- package/plugins/litclaude/agents/librarian-researcher.md +15 -0
- package/plugins/litclaude/agents/oracle-verifier.md +16 -0
- package/plugins/litclaude/agents/prometheus-planner.md +13 -0
- package/plugins/litclaude/agents/qa-runner.md +16 -0
- package/plugins/litclaude/agents/quality-reviewer.md +17 -0
- package/plugins/litclaude/bin/litclaude-hook.js +110 -0
- package/plugins/litclaude/bin/litclaude-hud.js +271 -0
- package/plugins/litclaude/bin/litclaude-lsp-doctor.js +15 -0
- package/plugins/litclaude/bin/litclaude-mcp.js +70 -0
- package/plugins/litclaude/commands/deep-interview.md +21 -0
- package/plugins/litclaude/commands/dynamic-workflow.md +36 -0
- package/plugins/litclaude/commands/lit-loop.md +40 -0
- package/plugins/litclaude/commands/lit-plan.md +35 -0
- package/plugins/litclaude/commands/litgoal.md +30 -0
- package/plugins/litclaude/commands/review-work.md +35 -0
- package/plugins/litclaude/commands/start-work.md +36 -0
- package/plugins/litclaude/hooks/hooks.json +54 -0
- package/plugins/litclaude/lib/context-pressure.mjs +25 -0
- package/plugins/litclaude/lib/hud-accent-palette.mjs +58 -0
- package/plugins/litclaude/lib/litgoal/cli.mjs +266 -0
- package/plugins/litclaude/lib/litgoal/ledger.mjs +16 -0
- package/plugins/litclaude/lib/litgoal/paths.mjs +7 -0
- package/plugins/litclaude/lib/litgoal/state.mjs +67 -0
- package/plugins/litclaude/lib/mutated-file-paths.mjs +63 -0
- package/plugins/litclaude/lib/start-work-continuation.mjs +99 -0
- package/plugins/litclaude/lib/workflow-check.mjs +83 -0
- package/plugins/litclaude/skills/ai-slop-remover/SKILL.md +142 -0
- package/plugins/litclaude/skills/comment-checker/SKILL.md +55 -0
- package/plugins/litclaude/skills/debugging/SKILL.md +70 -0
- package/plugins/litclaude/skills/debugging/references/methodology/00-setup.md +108 -0
- package/plugins/litclaude/skills/debugging/references/methodology/02-investigate.md +126 -0
- package/plugins/litclaude/skills/debugging/references/methodology/04-oracle-triple.md +106 -0
- package/plugins/litclaude/skills/debugging/references/methodology/05-escalate.md +69 -0
- package/plugins/litclaude/skills/debugging/references/methodology/06-fix.md +116 -0
- package/plugins/litclaude/skills/debugging/references/methodology/08-qa.md +94 -0
- package/plugins/litclaude/skills/debugging/references/methodology/09-cleanup.md +164 -0
- package/plugins/litclaude/skills/debugging/references/methodology/partial-runtime-evidence.md +228 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/go.md +252 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/native-binary.md +484 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/node.md +260 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/python.md +248 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/rust.md +234 -0
- package/plugins/litclaude/skills/debugging/references/tools/ghidra.md +212 -0
- package/plugins/litclaude/skills/debugging/references/tools/playwright-cli.md +194 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwndbg.md +263 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwntools.md +265 -0
- package/plugins/litclaude/skills/deep-interview/SKILL.md +323 -0
- package/plugins/litclaude/skills/deep-interview/scripts/render_progress.py +193 -0
- package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +62 -0
- package/plugins/litclaude/skills/lit-loop/SKILL.md +144 -0
- package/plugins/litclaude/skills/lit-plan/SKILL.md +125 -0
- package/plugins/litclaude/skills/litgoal/SKILL.md +219 -0
- package/plugins/litclaude/skills/lsp/SKILL.md +63 -0
- package/plugins/litclaude/skills/programming/SKILL.md +106 -0
- package/plugins/litclaude/skills/programming/references/go/README.md +90 -0
- package/plugins/litclaude/skills/programming/references/go/backend-stack.md +641 -0
- package/plugins/litclaude/skills/programming/references/go/bootstrap.md +328 -0
- package/plugins/litclaude/skills/programming/references/go/bubbletea-v2.md +360 -0
- package/plugins/litclaude/skills/programming/references/go/cobra-stack.md +468 -0
- package/plugins/litclaude/skills/programming/references/go/concurrency.md +362 -0
- package/plugins/litclaude/skills/programming/references/go/data-modeling.md +329 -0
- package/plugins/litclaude/skills/programming/references/go/error-handling.md +359 -0
- package/plugins/litclaude/skills/programming/references/go/golangci-strict.md +236 -0
- package/plugins/litclaude/skills/programming/references/go/grpc-connect.md +375 -0
- package/plugins/litclaude/skills/programming/references/go/libraries.md +337 -0
- package/plugins/litclaude/skills/programming/references/go/one-liners.md +202 -0
- package/plugins/litclaude/skills/programming/references/go/sqlc-pgx.md +471 -0
- package/plugins/litclaude/skills/programming/references/go/testing.md +467 -0
- package/plugins/litclaude/skills/programming/references/go/type-patterns.md +298 -0
- package/plugins/litclaude/skills/programming/references/python/README.md +314 -0
- package/plugins/litclaude/skills/programming/references/python/async-anyio.md +442 -0
- package/plugins/litclaude/skills/programming/references/python/data-modeling.md +233 -0
- package/plugins/litclaude/skills/programming/references/python/data-processing.md +133 -0
- package/plugins/litclaude/skills/programming/references/python/error-handling.md +218 -0
- package/plugins/litclaude/skills/programming/references/python/fastapi-stack.md +316 -0
- package/plugins/litclaude/skills/programming/references/python/httpx2-optimization.md +360 -0
- package/plugins/litclaude/skills/programming/references/python/libraries.md +307 -0
- package/plugins/litclaude/skills/programming/references/python/one-liners.md +268 -0
- package/plugins/litclaude/skills/programming/references/python/orjson-stack.md +378 -0
- package/plugins/litclaude/skills/programming/references/python/pydantic-ai.md +285 -0
- package/plugins/litclaude/skills/programming/references/python/pyproject-strict.md +232 -0
- package/plugins/litclaude/skills/programming/references/python/textual-tui.md +201 -0
- package/plugins/litclaude/skills/programming/references/python/type-patterns.md +176 -0
- package/plugins/litclaude/skills/programming/references/rust/README.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/async-tokio.md +299 -0
- package/plugins/litclaude/skills/programming/references/rust/axum-stack.md +467 -0
- package/plugins/litclaude/skills/programming/references/rust/cargo-strict.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/clap-stack.md +409 -0
- package/plugins/litclaude/skills/programming/references/rust/concurrency.md +375 -0
- package/plugins/litclaude/skills/programming/references/rust/libraries.md +439 -0
- package/plugins/litclaude/skills/programming/references/rust/one-liners.md +291 -0
- package/plugins/litclaude/skills/programming/references/rust/proptest-insta.md +429 -0
- package/plugins/litclaude/skills/programming/references/rust/type-state.md +354 -0
- package/plugins/litclaude/skills/programming/references/rust/unsafe-discipline.md +250 -0
- package/plugins/litclaude/skills/programming/references/rust/zero-cost-safety.md +527 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/README.md +289 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
- package/plugins/litclaude/skills/programming/references/typescript/README.md +195 -0
- package/plugins/litclaude/skills/programming/references/typescript/backend-hono.md +672 -0
- package/plugins/litclaude/skills/programming/references/typescript/bootstrap.md +199 -0
- package/plugins/litclaude/skills/programming/references/typescript/data-modeling.md +202 -0
- package/plugins/litclaude/skills/programming/references/typescript/error-handling.md +169 -0
- package/plugins/litclaude/skills/programming/references/typescript/tsconfig-strict.md +152 -0
- package/plugins/litclaude/skills/programming/references/typescript/type-patterns.md +196 -0
- package/plugins/litclaude/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
- package/plugins/litclaude/skills/programming/scripts/go/new-project.py +138 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.editorconfig +13 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.golangci.yml +95 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/ci.yml +37 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/config.go +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/gitignore +15 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/run.go +15 -0
- package/plugins/litclaude/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-project.py +172 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-script.py +116 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
- package/plugins/litclaude/skills/programming/scripts/rust/new-project.py +175 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/new-project.ts +177 -0
- package/plugins/litclaude/skills/refactor/SKILL.md +73 -0
- package/plugins/litclaude/skills/remove-ai-slops/SKILL.md +52 -0
- package/plugins/litclaude/skills/review-work/SKILL.md +331 -0
- package/plugins/litclaude/skills/rules/SKILL.md +66 -0
- package/plugins/litclaude/skills/start-work/SKILL.md +132 -0
- package/scripts/audit-plan-checkboxes.mjs +37 -0
- package/scripts/doctor.mjs +41 -0
- package/scripts/inspect-agent-tools.mjs +27 -0
- package/scripts/postinstall.mjs +50 -0
- package/scripts/qa-claude-plugin-smoke.sh +60 -0
- package/scripts/qa-portable-install.sh +136 -0
- package/scripts/validate-plugin.mjs +72 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
# CLI Stack — cobra + slog + caarlos0/env + signal handling
|
|
2
|
+
|
|
3
|
+
The canonical Go CLI skeleton. `cobra` is the de facto framework — Kubernetes, Docker CLI, Helm, GitHub CLI, gh, Hugo all use it. Use it.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Toolchain
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
go install github.com/spf13/cobra-cli@latest
|
|
11
|
+
cobra-cli init mytool
|
|
12
|
+
cobra-cli add server
|
|
13
|
+
cobra-cli add migrate
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`cobra-cli` scaffolds the `cmd/` package. Edit the result; do not regenerate.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Layout
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
mytool/
|
|
24
|
+
├── go.mod
|
|
25
|
+
├── main.go # ≤ 30 LOC, calls cmd.Execute
|
|
26
|
+
├── cmd/
|
|
27
|
+
│ ├── root.go # rootCmd, persistent flags, slog setup
|
|
28
|
+
│ ├── server.go # `mytool server` subcommand
|
|
29
|
+
│ ├── migrate.go # `mytool migrate` subcommand
|
|
30
|
+
│ └── version.go # `mytool version` — auto-injected version
|
|
31
|
+
├── internal/
|
|
32
|
+
│ ├── config/
|
|
33
|
+
│ └── server/
|
|
34
|
+
└── Taskfile.yml
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## `main.go`
|
|
40
|
+
|
|
41
|
+
```go
|
|
42
|
+
package main
|
|
43
|
+
|
|
44
|
+
import (
|
|
45
|
+
"context"
|
|
46
|
+
"log/slog"
|
|
47
|
+
"os"
|
|
48
|
+
"os/signal"
|
|
49
|
+
"syscall"
|
|
50
|
+
|
|
51
|
+
"github.com/your-org/mytool/cmd"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
func main() {
|
|
55
|
+
ctx, stop := signal.NotifyContext(context.Background(),
|
|
56
|
+
syscall.SIGINT, syscall.SIGTERM)
|
|
57
|
+
defer stop()
|
|
58
|
+
|
|
59
|
+
if err := cmd.Execute(ctx); err != nil {
|
|
60
|
+
slog.Error("fatal", slog.Any("err", err))
|
|
61
|
+
os.Exit(1)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`signal.NotifyContext` (Go 1.16+) gives every subcommand a ctx that cancels on Ctrl-C. Subcommands plumb the ctx into their workers.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## `cmd/root.go`
|
|
71
|
+
|
|
72
|
+
```go
|
|
73
|
+
package cmd
|
|
74
|
+
|
|
75
|
+
import (
|
|
76
|
+
"context"
|
|
77
|
+
"log/slog"
|
|
78
|
+
"os"
|
|
79
|
+
|
|
80
|
+
"github.com/spf13/cobra"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
var (
|
|
84
|
+
verbose bool
|
|
85
|
+
logFormat string
|
|
86
|
+
configPath string
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
var rootCmd = &cobra.Command{
|
|
90
|
+
Use: "mytool",
|
|
91
|
+
Short: "Short description of mytool",
|
|
92
|
+
Long: `Long description, prose; cobra wraps it for --help.`,
|
|
93
|
+
PersistentPreRunE: func(c *cobra.Command, args []string) error {
|
|
94
|
+
return setupLogger()
|
|
95
|
+
},
|
|
96
|
+
SilenceUsage: true, // don't print --help on every error
|
|
97
|
+
SilenceErrors: true, // we log them ourselves in Execute
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func init() {
|
|
101
|
+
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false,
|
|
102
|
+
"enable debug logging")
|
|
103
|
+
rootCmd.PersistentFlags().StringVar(&logFormat, "log-format", "text",
|
|
104
|
+
"log format: text or json")
|
|
105
|
+
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "",
|
|
106
|
+
"path to config file (optional)")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func Execute(ctx context.Context) error {
|
|
110
|
+
return rootCmd.ExecuteContext(ctx)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func setupLogger() error {
|
|
114
|
+
level := slog.LevelInfo
|
|
115
|
+
if verbose { level = slog.LevelDebug }
|
|
116
|
+
opts := &slog.HandlerOptions{Level: level}
|
|
117
|
+
|
|
118
|
+
var h slog.Handler
|
|
119
|
+
switch logFormat {
|
|
120
|
+
case "json":
|
|
121
|
+
h = slog.NewJSONHandler(os.Stderr, opts)
|
|
122
|
+
case "text":
|
|
123
|
+
h = slog.NewTextHandler(os.Stderr, opts)
|
|
124
|
+
default:
|
|
125
|
+
return fmt.Errorf("invalid log-format %q", logFormat)
|
|
126
|
+
}
|
|
127
|
+
slog.SetDefault(slog.New(h))
|
|
128
|
+
return nil
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Notes:
|
|
133
|
+
|
|
134
|
+
- `RunE` / `PersistentPreRunE` (the `E` variants) return errors. Use these; never use `Run` (no error return, encourages `log.Fatal`).
|
|
135
|
+
- `SilenceUsage: true` + `SilenceErrors: true` together: cobra stops printing the full `--help` on every command failure (the default behavior is rude in production scripts).
|
|
136
|
+
- `ExecuteContext` (cobra 1.8+) plumbs the ctx into every subcommand's `cmd.Context()`.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## `cmd/server.go`
|
|
141
|
+
|
|
142
|
+
```go
|
|
143
|
+
package cmd
|
|
144
|
+
|
|
145
|
+
import (
|
|
146
|
+
"log/slog"
|
|
147
|
+
|
|
148
|
+
"github.com/spf13/cobra"
|
|
149
|
+
"github.com/your-org/mytool/internal/server"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
var (
|
|
153
|
+
serverAddr string
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
var serverCmd = &cobra.Command{
|
|
157
|
+
Use: "server",
|
|
158
|
+
Short: "Run the HTTP server",
|
|
159
|
+
RunE: func(c *cobra.Command, args []string) error {
|
|
160
|
+
ctx := c.Context()
|
|
161
|
+
slog.InfoContext(ctx, "starting", slog.String("addr", serverAddr))
|
|
162
|
+
return server.Run(ctx, serverAddr)
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
func init() {
|
|
167
|
+
serverCmd.Flags().StringVar(&serverAddr, "addr", ":8080",
|
|
168
|
+
"listen address")
|
|
169
|
+
rootCmd.AddCommand(serverCmd)
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The subcommand is a thin shim — flags + log line + delegate to `internal/server`. Anything bigger violates the 250-LOC ceiling and belongs in `internal/`.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Subcommands with arguments
|
|
178
|
+
|
|
179
|
+
```go
|
|
180
|
+
var migrateUpCmd = &cobra.Command{
|
|
181
|
+
Use: "up [N]",
|
|
182
|
+
Short: "Apply N migrations (default: all)",
|
|
183
|
+
Args: cobra.MaximumNArgs(1),
|
|
184
|
+
RunE: func(c *cobra.Command, args []string) error {
|
|
185
|
+
n := -1 // all
|
|
186
|
+
if len(args) == 1 {
|
|
187
|
+
var err error
|
|
188
|
+
n, err = strconv.Atoi(args[0])
|
|
189
|
+
if err != nil {
|
|
190
|
+
return fmt.Errorf("invalid N: %w", err)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return migrate.Up(c.Context(), n)
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Use cobra's argument validators (`cobra.ExactArgs`, `cobra.MaximumNArgs`, `cobra.OnlyValidArgs`). They produce clean help text.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Flag types — typed, not strings
|
|
203
|
+
|
|
204
|
+
```go
|
|
205
|
+
// GOOD
|
|
206
|
+
serverCmd.Flags().DurationVar(&timeout, "timeout", 30*time.Second, "request timeout")
|
|
207
|
+
serverCmd.Flags().IntVar(&port, "port", 8080, "port")
|
|
208
|
+
serverCmd.Flags().StringSliceVar(&hosts, "host", nil, "allowed hosts (repeatable)")
|
|
209
|
+
|
|
210
|
+
// BAD — manual parsing
|
|
211
|
+
serverCmd.Flags().StringVar(&timeoutStr, "timeout", "30s", "")
|
|
212
|
+
// ...then later: time.ParseDuration(timeoutStr)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`pflag` (cobra's flag lib) has typed variants for every common type. Use them; the parsing and error messages are free.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Bind flags to env vars
|
|
220
|
+
|
|
221
|
+
cobra + viper is overkill for env binding. Use `caarlos0/env/v11`:
|
|
222
|
+
|
|
223
|
+
```go
|
|
224
|
+
type ServerOpts struct {
|
|
225
|
+
Addr string `env:"ADDR" envDefault:":8080"`
|
|
226
|
+
Timeout time.Duration `env:"TIMEOUT" envDefault:"30s"`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
var opts ServerOpts
|
|
230
|
+
|
|
231
|
+
var serverCmd = &cobra.Command{
|
|
232
|
+
Use: "server",
|
|
233
|
+
PersistentPreRunE: func(c *cobra.Command, args []string) error {
|
|
234
|
+
// 1. Parse env first.
|
|
235
|
+
if err := env.Parse(&opts); err != nil { return err }
|
|
236
|
+
// 2. Flags override env if explicitly set.
|
|
237
|
+
if c.Flags().Changed("addr") {
|
|
238
|
+
opts.Addr, _ = c.Flags().GetString("addr")
|
|
239
|
+
}
|
|
240
|
+
return nil
|
|
241
|
+
},
|
|
242
|
+
RunE: func(c *cobra.Command, args []string) error {
|
|
243
|
+
return server.Run(c.Context(), opts)
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
func init() {
|
|
248
|
+
serverCmd.Flags().String("addr", "", "listen address (env: ADDR)")
|
|
249
|
+
serverCmd.Flags().Duration("timeout", 0, "request timeout (env: TIMEOUT)")
|
|
250
|
+
rootCmd.AddCommand(serverCmd)
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Precedence: **flag (if set) > env > default**. Document the env var in the flag usage string.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Version subcommand — build-injected
|
|
259
|
+
|
|
260
|
+
```go
|
|
261
|
+
// cmd/version.go
|
|
262
|
+
package cmd
|
|
263
|
+
|
|
264
|
+
import (
|
|
265
|
+
"fmt"
|
|
266
|
+
"runtime/debug"
|
|
267
|
+
|
|
268
|
+
"github.com/spf13/cobra"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
// Set by -ldflags at build time, falls back to debug.BuildInfo.
|
|
272
|
+
var (
|
|
273
|
+
version = ""
|
|
274
|
+
commit = ""
|
|
275
|
+
date = ""
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
var versionCmd = &cobra.Command{
|
|
279
|
+
Use: "version",
|
|
280
|
+
Short: "Print version",
|
|
281
|
+
Run: func(c *cobra.Command, args []string) {
|
|
282
|
+
v, c2, d := resolveVersion()
|
|
283
|
+
fmt.Printf("mytool %s (commit %s, built %s)\n", v, c2, d)
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
func resolveVersion() (string, string, string) {
|
|
288
|
+
if version != "" { return version, commit, date }
|
|
289
|
+
info, ok := debug.ReadBuildInfo()
|
|
290
|
+
if !ok { return "dev", "unknown", "unknown" }
|
|
291
|
+
|
|
292
|
+
var vcs, hash, time string
|
|
293
|
+
for _, s := range info.Settings {
|
|
294
|
+
switch s.Key {
|
|
295
|
+
case "vcs.revision": hash = s.Value
|
|
296
|
+
case "vcs.time": time = s.Value
|
|
297
|
+
case "vcs": vcs = s.Value
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return info.Main.Version, hash, time + " (" + vcs + ")"
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
func init() { rootCmd.AddCommand(versionCmd) }
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Build with version injection:
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
go build \
|
|
310
|
+
-ldflags="-X 'github.com/your-org/mytool/cmd.version=v1.2.3' -X 'github.com/your-org/mytool/cmd.commit=$(git rev-parse --short HEAD)' -X 'github.com/your-org/mytool/cmd.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)'" \
|
|
311
|
+
-o bin/mytool ./
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
The `debug.BuildInfo` fallback means a `go install`'d binary also has version info — no manual `-ldflags` needed.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Shell completions
|
|
319
|
+
|
|
320
|
+
```go
|
|
321
|
+
var completionCmd = &cobra.Command{
|
|
322
|
+
Use: "completion [bash|zsh|fish|powershell]",
|
|
323
|
+
Short: "Generate shell completion",
|
|
324
|
+
Args: cobra.ExactValidArgs(1),
|
|
325
|
+
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
|
|
326
|
+
DisableFlagsInUseLine: true,
|
|
327
|
+
RunE: func(c *cobra.Command, args []string) error {
|
|
328
|
+
switch args[0] {
|
|
329
|
+
case "bash": return rootCmd.GenBashCompletionV2(os.Stdout, true)
|
|
330
|
+
case "zsh": return rootCmd.GenZshCompletion(os.Stdout)
|
|
331
|
+
case "fish": return rootCmd.GenFishCompletion(os.Stdout, true)
|
|
332
|
+
case "powershell": return rootCmd.GenPowerShellCompletion(os.Stdout)
|
|
333
|
+
}
|
|
334
|
+
return nil
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
func init() { rootCmd.AddCommand(completionCmd) }
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
User:
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
mytool completion zsh > "${fpath[1]}/_mytool"
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Interactive prompts — `huh` from charm
|
|
350
|
+
|
|
351
|
+
For prompts/forms (`Are you sure?`, "Pick an environment", multi-field forms):
|
|
352
|
+
|
|
353
|
+
```go
|
|
354
|
+
import "github.com/charmbracelet/huh"
|
|
355
|
+
|
|
356
|
+
var confirm bool
|
|
357
|
+
err := huh.NewConfirm().
|
|
358
|
+
Title("Apply migrations to PRODUCTION?").
|
|
359
|
+
Affirmative("Yes, do it").
|
|
360
|
+
Negative("Abort").
|
|
361
|
+
Value(&confirm).
|
|
362
|
+
Run()
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
`huh` replaces `survey` (which is no longer maintained). It composes with `lipgloss` for styling.
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Progress / spinners
|
|
370
|
+
|
|
371
|
+
```go
|
|
372
|
+
import "github.com/charmbracelet/huh/spinner"
|
|
373
|
+
|
|
374
|
+
err := spinner.New().Title("Fetching...").Action(func() {
|
|
375
|
+
// long-running work
|
|
376
|
+
}).Run()
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
For determinate progress (downloads, batch processing), use `vbauerster/mpb/v8`:
|
|
380
|
+
|
|
381
|
+
```go
|
|
382
|
+
import "github.com/vbauerster/mpb/v8"
|
|
383
|
+
|
|
384
|
+
p := mpb.New(mpb.WithWidth(60))
|
|
385
|
+
bar := p.AddBar(int64(total), /* decorators */)
|
|
386
|
+
for i := 0; i < total; i++ {
|
|
387
|
+
work()
|
|
388
|
+
bar.Increment()
|
|
389
|
+
}
|
|
390
|
+
p.Wait()
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Output — JSON vs text
|
|
396
|
+
|
|
397
|
+
Honor `--output json` for any CLI that scripts will parse:
|
|
398
|
+
|
|
399
|
+
```go
|
|
400
|
+
var outputFmt string
|
|
401
|
+
|
|
402
|
+
rootCmd.PersistentFlags().StringVar(&outputFmt, "output", "text",
|
|
403
|
+
"output format: text or json")
|
|
404
|
+
|
|
405
|
+
func render(v any) error {
|
|
406
|
+
switch outputFmt {
|
|
407
|
+
case "json":
|
|
408
|
+
enc := json.NewEncoder(os.Stdout)
|
|
409
|
+
enc.SetIndent("", " ")
|
|
410
|
+
return enc.Encode(v)
|
|
411
|
+
case "text":
|
|
412
|
+
return renderText(v)
|
|
413
|
+
default:
|
|
414
|
+
return fmt.Errorf("invalid --output %q", outputFmt)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The `text` format uses `lipgloss` tables or `aquasecurity/table` for nicely-aligned columns. The `json` format is for `jq`-style piping.
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## Error semantics
|
|
424
|
+
|
|
425
|
+
- Return errors from `RunE`. Cobra catches them and the `Execute` wrapper logs + exits non-zero.
|
|
426
|
+
- `os.Exit(1)` should appear **only in `main.go`**. Anywhere else means a subcommand cannot be tested.
|
|
427
|
+
- For graceful early termination ("user cancelled"), return a sentinel and check it in `Execute`:
|
|
428
|
+
```go
|
|
429
|
+
var ErrCancelled = errors.New("cancelled by user")
|
|
430
|
+
// ... return ErrCancelled
|
|
431
|
+
// in main:
|
|
432
|
+
if errors.Is(err, cmd.ErrCancelled) { os.Exit(130) } // 128 + SIGINT
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Testing CLI commands
|
|
438
|
+
|
|
439
|
+
```go
|
|
440
|
+
func TestServerCmd_runs_with_default_addr(t *testing.T) {
|
|
441
|
+
// Given
|
|
442
|
+
buf := &bytes.Buffer{}
|
|
443
|
+
rootCmd.SetOut(buf)
|
|
444
|
+
rootCmd.SetErr(buf)
|
|
445
|
+
rootCmd.SetArgs([]string{"server", "--addr", ":0"})
|
|
446
|
+
|
|
447
|
+
// When
|
|
448
|
+
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
449
|
+
defer cancel()
|
|
450
|
+
err := rootCmd.ExecuteContext(ctx)
|
|
451
|
+
|
|
452
|
+
// Then
|
|
453
|
+
require.NoError(t, err)
|
|
454
|
+
require.Contains(t, buf.String(), "starting")
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
`SetArgs` + `ExecuteContext` is the canonical pattern. Bind a ctx with a short deadline for tests that would otherwise block.
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Sources
|
|
463
|
+
|
|
464
|
+
- cobra docs: https://github.com/spf13/cobra/blob/main/site/content/user_guide.md
|
|
465
|
+
- pflag: https://github.com/spf13/pflag
|
|
466
|
+
- huh: https://github.com/charmbracelet/huh
|
|
467
|
+
- caarlos0/env: https://github.com/caarlos0/env
|
|
468
|
+
- signal.NotifyContext: https://pkg.go.dev/os/signal#NotifyContext
|