fix(cli): keep enable's internal warnings out of the terminal; humanize decode failures · Entire

fix(cli): keep enable's internal warnings out of the terminal; humanize decode failures

73d4c63·

peyton-alt·yesterday·3 files·+50 added/-0 removed

enable never called logging.Init, so ladder warnings fell back to slog's stderr default and leaked ogen's full decoder chain into the flow's output (seen live when a gateway 502 failed the mirror offer). Init logging to .entire/logs after the bootstrap block, so a declined bootstrap leaves no stray .entire/ behind.

renderCoreError now collapses undecodable responses (gateway error pages — no typed error exists on that path) to one line: 'the control plane returned HTTP 502 — try again shortly' instead of the raw wrap chain. Applies to every control-plane command.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

Sessions

01KXPKG9PAXZHXMP0GX6SRWEBFView transcript

[?
Fix Onboarding History and Scan Cache IssuesClaude Code·Fable 5·14 steps](/content/gh/entireio/cli/session/4c8ea8be-3447-426e-b8cb-07f2c8a04b71#timeline-01KXPKG9PAXZHXMP0GX6SRWEBF/index.html)

Changes

3

5 unmodified lines

6
7
8
9
10
11
12
475 unmodified lines

488
489
490
491
492
493
494
495
496
497
498
499
1 unmodified line

501
502
503
504
505
506
507
508
509
510
511
512
513
514
515

5 unmodified lines

"errors"
    "fmt"
    "io"
    "regexp"
    "strings"

"charm.land/huh/v2";
475 unmodified lines

// SilenceUsage, so the message reaches the user without a usage dump. (A
// SilentError here would be swallowed — main.go skips printing those —
// leaving e.g. a 409 conflict with no output.)
// coreErrorStatusRe extracts the HTTP status from ogen's generated decoder
// wrapping ("default (code 502)"), the only place the status survives when a
// response fails to decode — e.g. a gateway 502 serving a text/plain error
// page — since no typed error exists on that path.
var coreErrorStatusRe = regexp.MustCompile(`\(code (\d{3})\)`)

func renderCoreError(err error) error {
    if err == nil {
        return nil
    }

if msg := coreapi.APIError(err); msg != "" {
        return errors.New(msg)
    }
    // A response that failed decoding renders as ogen's raw wrap chain
    // ("decode response: … unexpected Content-Type: text/plain") — developer
    // noise where a user needs one line. Collapse it to the status.
    if m := coreErrorStatusRe.FindStringSubmatch(err.Error()); m != nil {
        if m[1][0] == '5' {
            return fmt.Errorf("the control plane returned HTTP %s — try again shortly", m[1])
        }
        return fmt.Errorf("control-plane request failed with HTTP %s", m[1])
    }
    return err
}

Mcmd/entire/cli/corecmd.go+16

146 unmodified lines

147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173

146 unmodified lines

t.Errorf("printFields output:\n%q\nwant:\n%q", got, want)
    }
}

// A response that fails decoding (e.g. a gateway 502 serving text/plain)
// carries its status only in ogen's wrap string; renderCoreError must
// collapse that developer noise to a one-line message instead of showing the
// raw decoder chain (it reached a user's terminal verbatim in the enable
// mirror offer).
func TestRenderCoreError_CollapsesDecodeFailures(t *testing.T) {
    t.Parallel()
    raw := errors.New(`decode response: default (code 502): unexpected Content-Type: text/plain`)
    got := renderCoreError(raw)
    if got.Error() != "the control plane returned HTTP 502 — try again shortly" {
        t.Errorf("renderCoreError(502 decode) = %q", got.Error())
    }

client := errors.New(`decode response: default (code 404): unexpected Content-Type: text/html`)
    if got := renderCoreError(client); got.Error() != "control-plane request failed with HTTP 404" {
        t.Errorf("renderCoreError(404 decode) = %q", got.Error())
    }

plain := errors.New("dial tcp: connection refused")
    if got := renderCoreError(plain); !errors.Is(got, plain) {
        t.Errorf("renderCoreError(plain) = %v, want passthrough", got)
    }
}

Mcmd/entire/cli/corecmd_test.go+24

897 unmodified lines

898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913

897 unmodified lines

}

// Operational logging (ladder probes, offer failures) belongs in
        // .entire/logs — without Init the logging package falls back to
        // slog's stderr default and WARN lines leak verbatim into
        // enable's output. After the bootstrap block, so a declined
        // bootstrap doesn't leave a stray .entire/ in a non-repo folder.
        logging.SetLogLevelGetter(GetLogLevel)
        if lerr := logging.Init(ctx, ""); lerr == nil {
            defer logging.Close()
        }

if err := validateSetupFlags(opts.UseLocalSettings, opts.UseProjectSettings); err != nil {
            return err
        }