# 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` - `--verbose` is an alias for `--debug` (both set `flagDebug`) - 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** ```go // 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** ```bash 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` — update `newClient` calls - Modify: `internal/provider/rainyun/server_test.go` — update `newClient` calls **Interfaces:** - Consumes: `debug bool` parameter - Produces: `newClient(endpoint, apiKey string, debug bool) *client` - Produces: stderr debug output with `[debug]` prefix - [ ] **Step 1: Update client struct and newClient** ```go // 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: ```go 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: ```go 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** ```bash 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()** ```go 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** ```bash 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: ```go 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: ```go 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`: ```go 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`: ```go 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** ```bash 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** ```bash 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** ```bash ./ry --debug server list 2>&1 | head -5 ``` Expected: same `[debug]` output - [ ] **Step 3: Verify no debug output without flag** ```bash ./ry server list 2>/dev/null ``` Expected: clean table output, no debug lines