Skip to content

Commit 242be3f

Browse files
committed
feat: replace config validate with doctor command
Add extensible health check system with styled output. Config validation is now the first check under `hive doctor`.
1 parent 3e5824d commit 242be3f

7 files changed

Lines changed: 313 additions & 160 deletions

File tree

internal/commands/cmd_config_validate.go

Lines changed: 0 additions & 145 deletions
This file was deleted.

internal/commands/cmd_doctor.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
7+
"github.com/hay-kot/hive/internal/commands/doctor"
8+
"github.com/hay-kot/hive/internal/printer"
9+
"github.com/urfave/cli/v3"
10+
)
11+
12+
type DoctorCmd struct {
13+
flags *Flags
14+
format string
15+
}
16+
17+
func NewDoctorCmd(flags *Flags) *DoctorCmd {
18+
return &DoctorCmd{flags: flags}
19+
}
20+
21+
func (cmd *DoctorCmd) Register(app *cli.Command) *cli.Command {
22+
app.Commands = append(app.Commands, &cli.Command{
23+
Name: "doctor",
24+
Usage: "Run health checks on your hive setup",
25+
UsageText: "hive doctor [options]",
26+
Description: "Runs diagnostic checks on configuration, environment, and dependencies.",
27+
Flags: []cli.Flag{
28+
&cli.StringFlag{
29+
Name: "format",
30+
Usage: "output format (text, json)",
31+
Value: "text",
32+
Destination: &cmd.format,
33+
},
34+
},
35+
Action: cmd.run,
36+
})
37+
return app
38+
}
39+
40+
func (cmd *DoctorCmd) run(ctx context.Context, c *cli.Command) error {
41+
checks := []doctor.Check{
42+
doctor.NewConfigCheck(cmd.flags.Config, cmd.flags.ConfigPath),
43+
}
44+
45+
results := doctor.RunAll(ctx, checks)
46+
47+
if cmd.format == "json" {
48+
return cmd.outputJSON(c, results)
49+
}
50+
51+
return cmd.outputText(ctx, results)
52+
}
53+
54+
func (cmd *DoctorCmd) outputJSON(c *cli.Command, results []doctor.Result) error {
55+
passed, warned, failed := doctor.Summary(results)
56+
57+
out := struct {
58+
Healthy bool `json:"healthy"`
59+
Summary summaryJSON `json:"summary"`
60+
Checks []doctor.Result `json:"checks"`
61+
}{
62+
Healthy: failed == 0,
63+
Summary: summaryJSON{Passed: passed, Warned: warned, Failed: failed},
64+
Checks: results,
65+
}
66+
67+
enc := json.NewEncoder(c.Root().Writer)
68+
enc.SetIndent("", " ")
69+
return enc.Encode(out)
70+
}
71+
72+
type summaryJSON struct {
73+
Passed int `json:"passed"`
74+
Warned int `json:"warned"`
75+
Failed int `json:"failed"`
76+
}
77+
78+
func (cmd *DoctorCmd) outputText(ctx context.Context, results []doctor.Result) error {
79+
p := printer.Ctx(ctx)
80+
81+
for _, result := range results {
82+
p.Section(result.Name)
83+
84+
for _, item := range result.Items {
85+
switch item.Status {
86+
case doctor.StatusPass:
87+
p.CheckItem(item.Label, item.Detail)
88+
case doctor.StatusWarn:
89+
p.WarnItem(item.Label, item.Detail)
90+
case doctor.StatusFail:
91+
p.FailItem(item.Label, item.Detail)
92+
}
93+
}
94+
95+
p.Printf("")
96+
}
97+
98+
passed, warned, failed := doctor.Summary(results)
99+
p.Printf("Summary: %d passed, %d warnings, %d failed", passed, warned, failed)
100+
101+
if failed > 0 {
102+
return cli.Exit("", 1)
103+
}
104+
105+
return nil
106+
}

internal/commands/doctor/config.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package doctor
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
"github.com/hay-kot/criterio"
8+
"github.com/hay-kot/hive/internal/core/config"
9+
)
10+
11+
// ConfigCheck validates the configuration file.
12+
type ConfigCheck struct {
13+
config *config.Config
14+
configPath string
15+
}
16+
17+
// NewConfigCheck creates a new configuration check.
18+
func NewConfigCheck(cfg *config.Config, configPath string) *ConfigCheck {
19+
return &ConfigCheck{
20+
config: cfg,
21+
configPath: configPath,
22+
}
23+
}
24+
25+
func (c *ConfigCheck) Name() string {
26+
return "Configuration"
27+
}
28+
29+
func (c *ConfigCheck) Run(ctx context.Context) Result {
30+
result := Result{Name: c.Name()}
31+
32+
if c.config == nil {
33+
result.Items = append(result.Items, CheckItem{
34+
Label: "Config loaded",
35+
Status: StatusFail,
36+
Detail: "configuration not loaded",
37+
})
38+
return result
39+
}
40+
41+
err := c.config.ValidateDeep(c.configPath)
42+
warnings := c.config.Warnings()
43+
44+
// If no errors and no warnings, report success
45+
if err == nil && len(warnings) == 0 {
46+
result.Items = append(result.Items, CheckItem{
47+
Label: "Config valid",
48+
Status: StatusPass,
49+
})
50+
return result
51+
}
52+
53+
// Extract and report errors
54+
if err != nil {
55+
var fieldErrs criterio.FieldErrors
56+
if errors.As(err, &fieldErrs) {
57+
for _, fe := range fieldErrs {
58+
label := fe.Field
59+
if label == "" {
60+
label = "validation"
61+
}
62+
result.Items = append(result.Items, CheckItem{
63+
Label: label,
64+
Status: StatusFail,
65+
Detail: fe.Err.Error(),
66+
})
67+
}
68+
} else {
69+
result.Items = append(result.Items, CheckItem{
70+
Label: "validation",
71+
Status: StatusFail,
72+
Detail: err.Error(),
73+
})
74+
}
75+
}
76+
77+
// Extract and report warnings
78+
for _, w := range warnings {
79+
label := w.Category
80+
if w.Item != "" {
81+
label += " (" + w.Item + ")"
82+
}
83+
result.Items = append(result.Items, CheckItem{
84+
Label: label,
85+
Status: StatusWarn,
86+
Detail: w.Message,
87+
})
88+
}
89+
90+
return result
91+
}

0 commit comments

Comments
 (0)