Skip to content
10 changes: 5 additions & 5 deletions internal/cli/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func authedClient() (*api.Client, *config.Config, error) {
func runClientCreate(ctx context.Context, p *ui.Printer, pr prompter, opts clientCreateOpts) (err error) {
// Always leave a full provision trace on disk, even on a quiet/headless run
// (RFC-0001 §8.5). On any failure, point at the (idempotent) resume command
// + `cluster doctor`, so a zero-prompt connect that breaks isn't a dead end.
// + `doctor`, so a zero-prompt connect that breaks isn't a dead end.
ilog, logPath := newInstallLog()
defer ilog.Close()
ilog.Logf("client create: name=%q location=%q", opts.name, opts.location)
Expand All @@ -166,7 +166,7 @@ func runClientCreate(ctx context.Context, p *ui.Printer, pr prompter, opts clien
p.Newline()
p.Hintf("Provisioning didn't complete. Re-running is safe — on the same cluster it adopts the existing client instead of minting a duplicate (idempotent):")
p.Hintf(" %s", resumeCommand(opts))
p.Hintf("Diagnose auth / cluster problems with: tracebloc cluster doctor")
p.Hintf("Diagnose auth / cluster problems with: tracebloc doctor")
if logPath != "" {
p.Hintf("Full log: %s", logPath)
}
Expand Down Expand Up @@ -528,7 +528,7 @@ func adoptLiveInClusterClient(
"couldn't check whether a tracebloc client is already running on this cluster (%w) — "+
"provisioning now could mint a duplicate that never deploys and locks the cluster to it. "+
"Re-run (if this was transient); if it persists, ensure your kubeconfig/context can list "+
"deployments and secrets across namespaces. Diagnose with `tracebloc cluster doctor`", err)}
"deployments and secrets across namespaces. Diagnose with `tracebloc doctor`", err)}
}
ilog.Logf("in-cluster client discovery skipped — cluster unreachable (non-fatal): %v", err)
return nil, false, nil
Expand Down Expand Up @@ -846,11 +846,11 @@ func runClientStatus(ctx context.Context, p *ui.Printer, wait bool, timeout time
case lastState >= 0:
return &exitError{code: 1, err: fmt.Errorf(
"timed out after %s waiting for tracebloc to report this client online (last state: %s). "+
"Run `tracebloc cluster doctor` to diagnose, or re-run the installer.", timeout, clientStateLabel(lastState))}
"Run `tracebloc doctor` to diagnose, or re-run the installer.", timeout, clientStateLabel(lastState))}
default:
return &exitError{code: 1, err: fmt.Errorf(
"timed out after %s before tracebloc could confirm this client — retry, "+
"or run `tracebloc cluster doctor`.", timeout)}
"or run `tracebloc doctor`.", timeout)}
}
}
wait := clientStatusPollInterval
Expand Down
5 changes: 4 additions & 1 deletion internal/cli/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ the wrong cluster).`,
}

cmd.AddCommand(newClusterInfoCmd())
cmd.AddCommand(newClusterDoctorCmd())
// `cluster doctor` stays as a hidden alias of the top-level `doctor` (both
// share one RunE — see newDoctorCmd) so existing docs / muscle memory keep
// working while the home screen points at the shorter top-level command.
cmd.AddCommand(newDoctorCmd(true))
return cmd
}

Expand Down
27 changes: 15 additions & 12 deletions internal/cli/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,28 @@ import (
"github.com/tracebloc/cli/internal/ui"
)

// newClusterDoctorCmd implements `tracebloc cluster doctor` — the sibling of
// `cluster info` that cluster.go's doc comment anticipated. Where `info`
// answers "is the CLI pointing at the right cluster?", `doctor` answers "is
// this running cluster healthy enough to run an experiment, and if not, what
// do I fix?" — a read-only, post-install health sweep with remedies
// (epic client-runtime#116, WS3).
// newDoctorCmd builds the `doctor` command. The SAME command is registered two
// ways: as the top-level `tracebloc doctor` (visible — the home screen and its
// env-status lines point here), and, hidden, as `tracebloc cluster doctor` (its
// original path, kept working so existing docs, muscle memory, and scripts don't
// break). Pass hidden=true for the cluster-subtree alias. Both entry points
// share one RunE (runClusterDoctor), so there is a single diagnostic code path.
//
// The three kubeconfig flags match `cluster info` exactly so muscle memory
// carries over; all are zero-value-safe.
func newClusterDoctorCmd() *cobra.Command {
// It answers "is this running cluster healthy enough to run an experiment, and
// if not, what do I fix?" — a read-only, post-install health sweep with remedies
// (epic client-runtime#116, WS3). The three kubeconfig flags match `cluster
// info` exactly so muscle memory carries over; all are zero-value-safe.
func newDoctorCmd(hidden bool) *cobra.Command {
var (
kubeconfigPath string
contextOverride string
nsOverride string
)

cmd := &cobra.Command{
Use: "doctor",
Short: "Diagnose a running tracebloc client cluster (✔/⚠/✖ health checks + remedies)",
Use: "doctor",
Hidden: hidden,
Short: "Diagnose a running tracebloc client cluster (✔/⚠/✖ health checks + remedies)",
Long: `Runs a read-only health sweep over the tracebloc client release in the
configured cluster + namespace and prints a ✔/⚠/✖ line per check with a
remedy for anything that isn't green:
Expand Down Expand Up @@ -76,7 +79,7 @@ func runClusterDoctor(
p *ui.Printer,
kubeconfigPath, contextOverride, nsOverride string,
) error {
p.Banner("tracebloc", "cluster doctor")
p.Banner("tracebloc", "doctor")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doctor ignores active-client binding

Medium Severity

The new home screen resolves environment status with bindActiveClientNamespace and the same discovery rules as data and cluster info, and its status lines tell users to run top-level doctor. runClusterDoctor still loads the kubeconfig without that binding and runs checks only in the context default namespace, so a typical install whose client lives in the cached slug namespace can show one state on the home screen and a conflicting “no client here” diagnosis when the user follows the on-screen doctor hint.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1584849. Configure here.


// Auth / config checks run FIRST and don't need a cluster — so `doctor` can
// diagnose a failed provision (bad/expired token, wrong env, no active
Expand Down
14 changes: 8 additions & 6 deletions internal/cli/group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ func TestGroup_UnknownSubcommand_Suggests(t *testing.T) {
}{
{[]string{"data", "ingst"}, "ingest"},
{[]string{"cluster", "inf"}, "info"},
{[]string{"cluster", "doctr"}, "doctor"},
}
for _, c := range cases {
t.Run(strings.Join(c.args, " "), func(t *testing.T) {
Expand All @@ -74,14 +73,17 @@ func TestGroup_UnknownSubcommand_Suggests(t *testing.T) {
}
}

// The hidden `client list` (Hidden per Rev-9 §7.10) must NOT be offered as a
// suggestion even though `lst` is one edit from `list` — SuggestionsFor skips
// unavailable commands.
// Hidden subcommands must NOT be offered as suggestions even when the typo is
// one edit away — SuggestionsFor skips unavailable commands. Two are hidden:
// `client list` (Rev-9 §7.10) and `cluster doctor` (now a hidden alias of the
// top-level `doctor`, cli#244).
func TestGroup_HiddenSubcommand_NotSuggested(t *testing.T) {
_, out := execGroup(t, "client", "lst")
if strings.Contains(out, "list") {
if _, out := execGroup(t, "client", "lst"); strings.Contains(out, "list") {
t.Errorf("hidden `client list` must not be suggested:\n%s", out)
}
if _, out := execGroup(t, "cluster", "doctr"); strings.Contains(out, "doctor") {
t.Errorf("hidden `cluster doctor` must not be suggested:\n%s", out)
}
}

// A bare group (no subcommand) still prints its help and exits 0 — runnable
Expand Down
Loading
Loading