Files
rainyun-cli/docs/plan/2026-07-28-verbose-debug-plan.md
T

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
  • --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

// 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 — 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

// 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