Skip to content

Commit e7585f4

Browse files
Karlclaude
andcommitted
Feat: add yoloai system check command
Implements the health check identified in CRITIQUE.md. Reports backend connectivity, base image presence, and agent credential status. Exits 1 if any check fails for use in CI/CD pipelines. Supports --json output. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7252990 commit e7585f4

File tree

2 files changed

+157
-0
lines changed

2 files changed

+157
-0
lines changed

internal/cli/system.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func newSystemCmd(version, commit, date string) *cobra.Command {
2727
newSystemAgentsCmd(),
2828
newSystemBackendsCmd(),
2929
newSystemBuildCmd(),
30+
newSystemCheckCmd(),
3031
newSystemPruneCmd(),
3132
newSystemSetupCmd(),
3233
newCompletionCmd(),

internal/cli/system_check.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package cli
2+
3+
// ABOUTME: `yoloai system check` command — verifies prereqs for CI/CD pipelines.
4+
// ABOUTME: Checks backend connectivity, base image, and agent credentials. Exits 1 on failure.
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"os"
10+
"strings"
11+
12+
"github.com/kstenerud/yoloai/agent"
13+
"github.com/kstenerud/yoloai/runtime"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
func newSystemCheckCmd() *cobra.Command {
18+
cmd := &cobra.Command{
19+
Use: "check",
20+
Short: "Verify prerequisites (backend, image, credentials)",
21+
Long: `Check that yoloai prerequisites are satisfied.
22+
23+
Exits 0 if all checks pass, 1 if any check fails. Designed for CI/CD pipelines.
24+
25+
Checks performed:
26+
1. backend — runtime daemon is reachable
27+
2. image — yoloai-base image has been built
28+
3. agent — at least one API key is set for the selected agent`,
29+
Args: cobra.NoArgs,
30+
RunE: func(cmd *cobra.Command, _ []string) error {
31+
backend := resolveBackend(cmd)
32+
agentName, _ := cmd.Flags().GetString("agent")
33+
return runSystemCheck(cmd, backend, agentName)
34+
},
35+
}
36+
37+
cmd.Flags().String("backend", "", "Runtime backend (see 'yoloai system backends')")
38+
cmd.Flags().String("agent", "", "Agent to check credentials for (default: configured agent)")
39+
40+
return cmd
41+
}
42+
43+
// checkResult holds the outcome of a single check.
44+
type checkResult struct {
45+
Name string `json:"name"`
46+
OK bool `json:"ok"`
47+
Message string `json:"message,omitempty"`
48+
}
49+
50+
func runSystemCheck(cmd *cobra.Command, backend, agentName string) error {
51+
ctx := cmd.Context()
52+
out := cmd.OutOrStdout()
53+
isJSON := jsonEnabled(cmd)
54+
55+
var results []checkResult
56+
allOK := true
57+
58+
// 1. Backend connectivity.
59+
{
60+
available, note := checkBackend(ctx, backend)
61+
msg := ""
62+
if !available {
63+
msg = note
64+
allOK = false
65+
}
66+
results = append(results, checkResult{
67+
Name: "backend",
68+
OK: available,
69+
Message: msg,
70+
})
71+
}
72+
73+
// 2. Base image exists (only meaningful if backend is reachable).
74+
{
75+
r := checkResult{Name: "image"}
76+
err := withRuntime(ctx, backend, func(ctx context.Context, rt runtime.Runtime) error {
77+
exists, err := rt.ImageExists(ctx, "yoloai-base")
78+
if err != nil {
79+
return err
80+
}
81+
if !exists {
82+
return fmt.Errorf("yoloai-base image not found — run 'yoloai system build'")
83+
}
84+
return nil
85+
})
86+
if err != nil {
87+
r.OK = false
88+
r.Message = err.Error()
89+
allOK = false
90+
} else {
91+
r.OK = true
92+
}
93+
results = append(results, r)
94+
}
95+
96+
// 3. Agent credentials.
97+
{
98+
r := checkResult{Name: "agent"}
99+
if agentName == "" {
100+
agentName = resolveAgent(cmd)
101+
}
102+
def := agent.GetAgent(agentName)
103+
switch {
104+
case def == nil:
105+
r.OK = false
106+
r.Message = fmt.Sprintf("unknown agent %q", agentName)
107+
allOK = false
108+
case len(def.APIKeyEnvVars) == 0:
109+
// Agent needs no credentials (e.g. shell, test).
110+
r.OK = true
111+
r.Message = fmt.Sprintf("agent %q requires no credentials", agentName)
112+
default:
113+
var found []string
114+
for _, key := range def.APIKeyEnvVars {
115+
if os.Getenv(key) != "" {
116+
found = append(found, key)
117+
}
118+
}
119+
if len(found) == 0 {
120+
r.OK = false
121+
r.Message = fmt.Sprintf("no credentials set for agent %q (need one of: %s)",
122+
agentName, strings.Join(def.APIKeyEnvVars, ", "))
123+
allOK = false
124+
} else {
125+
r.OK = true
126+
r.Message = fmt.Sprintf("found: %s", strings.Join(found, ", "))
127+
}
128+
}
129+
results = append(results, r)
130+
}
131+
132+
if isJSON {
133+
return writeJSON(out, map[string]any{
134+
"ok": allOK,
135+
"checks": results,
136+
})
137+
}
138+
139+
// Human-readable table.
140+
for _, r := range results {
141+
status := "ok"
142+
if !r.OK {
143+
status = "FAIL"
144+
}
145+
if r.Message != "" {
146+
fmt.Fprintf(out, "%-10s %-4s %s\n", r.Name, status, r.Message) //nolint:errcheck
147+
} else {
148+
fmt.Fprintf(out, "%-10s %s\n", r.Name, status) //nolint:errcheck
149+
}
150+
}
151+
152+
if !allOK {
153+
return fmt.Errorf("one or more checks failed")
154+
}
155+
return nil
156+
}

0 commit comments

Comments
 (0)