diff --git a/docs/plan/2026-07-28-verbose-debug-plan.md b/docs/plan/2026-07-28-verbose-debug-plan.md new file mode 100644 index 0000000..93eec7a --- /dev/null +++ b/docs/plan/2026-07-28-verbose-debug-plan.md @@ -0,0 +1,316 @@ +# 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