From 05abcf3bac78d702531a607f1cf28662130a7733 Mon Sep 17 00:00:00 2001 From: Steve Cliff Date: Mon, 22 Jun 2026 00:00:25 +0100 Subject: [PATCH] feat(cli): JSON output envelope with stable error codes Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/cli/envelope.go | 49 +++++++++++++++++++++++++++++++++++ internal/cli/envelope_test.go | 43 ++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 internal/cli/envelope.go create mode 100644 internal/cli/envelope_test.go diff --git a/internal/cli/envelope.go b/internal/cli/envelope.go new file mode 100644 index 0000000..0034b4e --- /dev/null +++ b/internal/cli/envelope.go @@ -0,0 +1,49 @@ +// Package cli implements command dispatch and the agent JSON envelope. +package cli + +import ( + "encoding/json" + "io" +) + +const ( + CodeConfig = "config" + CodeDB = "db" + CodeNetwork = "network" + CodeAuth = "auth" + CodePolicy = "policy" + CodeNotFound = "not_found" + CodeUsage = "usage" +) + +// Envelope is the single JSON object every agent command emits. +type Envelope struct { + Error bool `json:"error"` + ErrorDetail map[string]any `json:"error_detail"` + Data any `json:"data"` +} + +func Success(data any) Envelope { + if data == nil { + data = map[string]any{} + } + return Envelope{Error: false, ErrorDetail: map[string]any{}, Data: data} +} + +func Failure(code, message string) Envelope { + return Envelope{ + Error: true, + ErrorDetail: map[string]any{"code": code, "message": message}, + Data: map[string]any{}, + } +} + +func (e Envelope) Write(w io.Writer) error { + b, err := json.Marshal(e) + if err != nil { + return err + } + b = append(b, '\n') + _, err = w.Write(b) + return err +} diff --git a/internal/cli/envelope_test.go b/internal/cli/envelope_test.go new file mode 100644 index 0000000..f965e7b --- /dev/null +++ b/internal/cli/envelope_test.go @@ -0,0 +1,43 @@ +package cli + +import ( + "bytes" + "encoding/json" + "testing" +) + +func TestSuccessEnvelope(t *testing.T) { + var buf bytes.Buffer + if err := Success(map[string]any{"count": 2}).Write(&buf); err != nil { + t.Fatalf("write: %v", err) + } + var got map[string]any + if err := json.Unmarshal(buf.Bytes(), &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if got["error"] != false { + t.Fatalf("error should be false: %v", got["error"]) + } + if _, ok := got["data"]; !ok { + t.Fatal("data key missing") + } + if ed, ok := got["error_detail"].(map[string]any); !ok || len(ed) != 0 { + t.Fatalf("error_detail should be empty object: %v", got["error_detail"]) + } +} + +func TestFailureEnvelope(t *testing.T) { + var buf bytes.Buffer + _ = Failure(CodeNotFound, "uid 7 not found").Write(&buf) + var got struct { + Error bool `json:"error"` + ErrorDetail struct { + Code string `json:"code"` + Message string `json:"message"` + } `json:"error_detail"` + } + _ = json.Unmarshal(buf.Bytes(), &got) + if !got.Error || got.ErrorDetail.Code != "not_found" { + t.Fatalf("bad failure envelope: %+v", got) + } +}