7.9 KiB
Verbose/Debug Mode Implementation Plan
For agentic workers: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task.
Goal: Wire up --debug/--verbose flags to print HTTP request details and runtime info to stderr.
Architecture: Add Debug bool to ProviderConfig, pass it through to the HTTP client. Client's do() method logs request/response when debug is on. Root command prints runtime info in PersistentPreRunE.
Tech Stack: Go, cobra, net/http
Global Constraints
- All debug output goes to
os.Stderr --verboseis an alias for--debug(both setflagDebug)- Debug prefix:
[debug] - Existing tests must pass after each task
Task 1: Add Debug field to ProviderConfig
Files:
- Modify:
internal/provider/provider.go
Interfaces:
-
Produces:
ProviderConfig.Debug bool— consumed by Task 3 and Task 4 -
Step 1: Add Debug field
// internal/provider/provider.go — add to ProviderConfig
type ProviderConfig struct {
APIKey string
AccessKeyID string
AccessKeySecret string
Endpoint string
Debug bool
}
- Step 2: Verify build
Run: go build ./...
Expected: PASS (field addition is backward-compatible)
- Step 3: Commit
git add internal/provider/provider.go
git commit -m "feat: add Debug field to ProviderConfig"
Task 2: Update client to accept and use debug flag
Files:
- Modify:
internal/provider/rainyun/client.go - Modify:
internal/provider/rainyun/client_test.go— updatenewClientcalls - Modify:
internal/provider/rainyun/server_test.go— updatenewClientcalls
Interfaces:
-
Consumes:
debug boolparameter -
Produces:
newClient(endpoint, apiKey string, debug bool) *client -
Produces: stderr debug output with
[debug]prefix -
Step 1: Update client struct and newClient
// internal/provider/rainyun/client.go
type client struct {
httpClient *http.Client
baseURL string
apiKey string
debug bool
}
func newClient(endpoint, apiKey string, debug bool) *client {
if endpoint == "" {
endpoint = defaultEndpoint
}
return &client{
httpClient: &http.Client{Timeout: 30 * time.Second},
baseURL: endpoint,
apiKey: apiKey,
debug: debug,
}
}
- Step 2: Add debug logging to do()
Add to top of client.go imports: "os", "time" (time already imported).
In do(), after building the URL and before creating the request, add:
if c.debug {
fmt.Fprintf(os.Stderr, "[debug] > %s %s\n", method, u)
if body != nil {
fmt.Fprintf(os.Stderr, "[debug] %s\n", bodyReader.(*bytes.Reader). // wrong
}
}
Actually, the body bytes are already in b at that point. Better approach — log after marshaling:
func (c *client) do(ctx context.Context, method, path string, body, result any, query map[string]string) error {
u := c.baseURL + path
if len(query) > 0 {
params := url.Values{}
for k, v := range query {
params.Set(k, v)
}
u += "?" + params.Encode()
}
var bodyReader io.Reader
var bodyBytes []byte
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("marshal request: %w", err)
}
bodyBytes = b
bodyReader = bytes.NewReader(b)
}
if c.debug {
fmt.Fprintf(os.Stderr, "[debug] > %s %s\n", method, u)
if bodyBytes != nil {
fmt.Fprintf(os.Stderr, "[debug] %s\n", bodyBytes)
}
}
req, err := http.NewRequestWithContext(ctx, method, u, bodyReader)
if err != nil {
return fmt.Errorf("create request: %w", err)
}
req.Header.Set("X-Api-Key", c.apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
start := time.Now()
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read response: %w", err)
}
if c.debug {
fmt.Fprintf(os.Stderr, "[debug] < %d %s %s\n", resp.StatusCode, http.StatusText(resp.StatusCode), time.Since(start).Round(time.Millisecond))
fmt.Fprintf(os.Stderr, "[debug] %s\n", respBody)
}
// ... rest unchanged
- Step 3: Update test calls to newClient
In client_test.go, change all newClient(srv.URL, "sk-test") to newClient(srv.URL, "sk-test", false).
In server_test.go, change all newClient(srv.URL, "sk-test") to newClient(srv.URL, "sk-test", false).
- Step 4: Run tests
Run: go test ./internal/provider/rainyun/...
Expected: PASS
- Step 5: Commit
git add internal/provider/rainyun/
git commit -m "feat: HTTP debug logging in client.do()"
Task 3: Pass debug flag from provider to client
Files:
- Modify:
internal/provider/rainyun/rainyun.go
Interfaces:
-
Consumes:
ProviderConfig.Debug(from Task 1) -
Consumes:
newClient(endpoint, apiKey string, debug bool)(from Task 2) -
Step 1: Update New()
func New(cfg provider.ProviderConfig) (provider.Provider, error) {
c := newClient(cfg.Endpoint, cfg.APIKey, cfg.Debug)
// ... rest unchanged
}
- Step 2: Run tests
Run: go test ./internal/provider/rainyun/...
Expected: PASS
- Step 3: Commit
git add internal/provider/rainyun/rainyun.go
git commit -m "feat: pass Debug flag from config to client"
Task 4: Wire --verbose alias and runtime info in root
Files:
- Modify:
cmd/cli/root.go
Interfaces:
-
Consumes:
flagDebug(existing, now wired) -
Consumes:
ProviderConfig.Debug(from Task 1) -
Step 1: Add --verbose alias
In init(), add after the --debug flag registration:
rootCmd.PersistentFlags().BoolVarP(&flagDebug, "verbose", "v", false, "Enable verbose/debug output")
Note: cobra doesn't support two flags binding to the same var directly. Instead, use MarkHidden or just register --verbose as the primary and --debug as hidden alias. Simplest approach:
rootCmd.PersistentFlags().BoolVarP(&flagDebug, "verbose", "v", false, "Enable verbose/debug output (same as --debug)")
rootCmd.PersistentFlags().BoolVar(&flagDebug, "debug", false, "Enable debug output (same as --verbose)")
- Step 2: Wire flagDebug into ProviderConfig
In PersistentPreRunE, after creating pc:
prov, err = provider.New(providerName, provider.ProviderConfig{
APIKey: pc.APIKey,
AccessKeyID: pc.AccessKeyID,
AccessKeySecret: pc.AccessKeySecret,
Endpoint: pc.Endpoint,
Debug: flagDebug,
})
- Step 3: Print runtime info when debug
At the end of PersistentPreRunE, after creating out:
if flagDebug {
fmt.Fprintf(os.Stderr, "[debug] provider: %s\n", providerName)
fmt.Fprintf(os.Stderr, "[debug] config: %s\n", cfgPath)
fmt.Fprintf(os.Stderr, "[debug] output: %s\n", outputFormat)
}
- Step 4: Run full test suite
Run: go test ./...
Expected: PASS
- Step 5: Commit
git add cmd/cli/root.go
git commit -m "feat: wire --verbose/--debug flags with runtime info"
Task 5: Manual verification
- Step 1: Build and test verbose output
go build -o ry ./cmd/cli/
./ry --verbose server list 2>&1 | head -20
Expected stderr output:
[debug] provider: rainyun
[debug] config: /path/to/config.json
[debug] output: table
[debug] > GET /product/rcs/?Page=1&PageSize=999&PerPage=999
[debug] < 200 OK 123ms
[debug] {"code":200,"data":{...}}
- Step 2: Verify --debug works too
./ry --debug server list 2>&1 | head -5
Expected: same [debug] output
- Step 3: Verify no debug output without flag
./ry server list 2>/dev/null
Expected: clean table output, no debug lines