Skip to content

Commit 75fe32c

Browse files
committed
feat: add --json flag to all commands
- Global --json persistent flag on root command - Shared json.go types and output.go helpers - JSON output for list, info, search, doctor, enable, disable, restart, load, unload, logs, create, edit, import - export already outputs JSON natively (unchanged) - 15 new tests for JSON output paths - All existing tests still passing
1 parent 70d28da commit 75fe32c

18 files changed

Lines changed: 847 additions & 49 deletions

internal/cli/create.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,17 +131,33 @@ var createCmd = &cobra.Command{
131131
return fmt.Errorf("failed to write plist: %w", err)
132132
}
133133

134-
fmt.Printf("Created %s\n", outputPath)
135-
136134
// Optionally load the plist.
135+
loaded := false
137136
if createLoad {
138137
_, manager, _ := buildDeps()
139138
if err := manager.Load(outputPath); err != nil {
139+
if !jsonFlag {
140+
fmt.Printf("Created %s\n", outputPath)
141+
}
140142
return fmt.Errorf("failed to load plist: %w", err)
141143
}
142-
fmt.Printf("Loaded %s\n", pl.Label)
144+
loaded = true
143145
}
144146

147+
if jsonFlag {
148+
return printJSON(jsonCreate{
149+
OK: true,
150+
Action: "create",
151+
Label: pl.Label,
152+
PlistPath: outputPath,
153+
Loaded: loaded,
154+
})
155+
}
156+
157+
fmt.Printf("Created %s\n", outputPath)
158+
if loaded {
159+
fmt.Printf("Loaded %s\n", pl.Label)
160+
}
145161
return nil
146162
},
147163
}

internal/cli/disable.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ var disableCmd = &cobra.Command{
1919
return err
2020
}
2121

22+
if jsonFlag {
23+
return printJSON(jsonAction{OK: true, Action: "disable", Label: label})
24+
}
25+
2226
fmt.Printf("Disabled %s\n", label)
2327
return nil
2428
},

internal/cli/doctor.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ var doctorCmd = &cobra.Command{
1919
return fmt.Errorf("failed to run doctor: %w", err)
2020
}
2121

22+
if jsonFlag {
23+
return printJSON(toJSONDoctor(findings))
24+
}
25+
2226
if len(findings) == 0 {
2327
fmt.Println("DOCTOR REPORT")
2428
fmt.Println("=============")

internal/cli/edit.go

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,22 +54,44 @@ var editCmd = &cobra.Command{
5454
// Validate the plist after editing.
5555
validateCmd := exec.Command("plutil", "-lint", svc.PlistPath)
5656
validateOut, err := validateCmd.CombinedOutput()
57-
if err != nil {
58-
fmt.Printf("Warning: plist validation failed:\n%s\n", string(validateOut))
59-
} else {
60-
fmt.Println("Plist validation passed.")
57+
validationOK := err == nil
58+
59+
if !jsonFlag {
60+
if !validationOK {
61+
fmt.Printf("Warning: plist validation failed:\n%s\n", string(validateOut))
62+
} else {
63+
fmt.Println("Plist validation passed.")
64+
}
6165
}
6266

6367
// Optionally reload.
68+
reloaded := false
6469
if editReload {
65-
fmt.Printf("Reloading %s...\n", label)
70+
if !jsonFlag {
71+
fmt.Printf("Reloading %s...\n", label)
72+
}
6673

6774
// Bootout then bootstrap.
6875
_ = manager.Unload(label)
6976
if err := manager.Load(svc.PlistPath); err != nil {
7077
return fmt.Errorf("failed to reload service: %w", err)
7178
}
72-
fmt.Println("Service reloaded.")
79+
reloaded = true
80+
81+
if !jsonFlag {
82+
fmt.Println("Service reloaded.")
83+
}
84+
}
85+
86+
if jsonFlag {
87+
return printJSON(jsonEdit{
88+
OK: true,
89+
Action: "edit",
90+
Label: label,
91+
PlistPath: svc.PlistPath,
92+
ValidationOK: validationOK,
93+
Reloaded: reloaded,
94+
})
7395
}
7496

7597
return nil

internal/cli/enable.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ var enableCmd = &cobra.Command{
1919
return err
2020
}
2121

22+
if jsonFlag {
23+
return printJSON(jsonAction{OK: true, Action: "enable", Label: label})
24+
}
25+
2226
fmt.Printf("Enabled %s\n", label)
2327
return nil
2428
},

internal/cli/import.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,33 @@ var importCmd = &cobra.Command{
4848
return fmt.Errorf("failed to write plist: %w", err)
4949
}
5050

51-
fmt.Printf("Imported %s to %s\n", bundle.Label, outputPath)
52-
5351
// Optionally load the plist.
52+
loaded := false
5453
if importLoad {
5554
_, manager, _ := buildDeps()
5655
if err := manager.Load(outputPath); err != nil {
56+
if !jsonFlag {
57+
fmt.Printf("Imported %s to %s\n", bundle.Label, outputPath)
58+
}
5759
return fmt.Errorf("failed to load plist: %w", err)
5860
}
59-
fmt.Printf("Loaded %s\n", bundle.Label)
61+
loaded = true
62+
}
63+
64+
if jsonFlag {
65+
return printJSON(jsonImport{
66+
OK: true,
67+
Action: "import",
68+
Label: bundle.Label,
69+
PlistPath: outputPath,
70+
Loaded: loaded,
71+
})
6072
}
6173

74+
fmt.Printf("Imported %s to %s\n", bundle.Label, outputPath)
75+
if loaded {
76+
fmt.Printf("Loaded %s\n", bundle.Label)
77+
}
6278
return nil
6379
},
6480
}

internal/cli/info.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ var infoCmd = &cobra.Command{
2121
return fmt.Errorf("failed to get info for %q: %w", label, err)
2222
}
2323

24+
if jsonFlag {
25+
return printJSON(toJSONServiceDetail(svc))
26+
}
27+
2428
printField := func(name, value string) {
2529
fmt.Printf("%-22s %s\n", name+":", value)
2630
}

internal/cli/json.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package cli
2+
3+
import "github.com/lu-zhengda/lanchr/internal/agent"
4+
5+
// ---------------------------------------------------------------------------
6+
// Service JSON types (list, search, info)
7+
// ---------------------------------------------------------------------------
8+
9+
// jsonService is the compact representation used by list and search.
10+
type jsonService struct {
11+
Label string `json:"label"`
12+
Domain string `json:"domain"`
13+
Type string `json:"type"`
14+
Status string `json:"status"`
15+
PID int `json:"pid"`
16+
LastExitStatus int `json:"last_exit_status"`
17+
PlistPath string `json:"plist_path,omitempty"`
18+
Program string `json:"program,omitempty"`
19+
}
20+
21+
// toJSONServices converts a slice of agent.Service to JSON-serializable form.
22+
func toJSONServices(services []agent.Service) []jsonService {
23+
out := make([]jsonService, 0, len(services))
24+
for _, svc := range services {
25+
out = append(out, jsonService{
26+
Label: svc.Label,
27+
Domain: svc.Domain.String(),
28+
Type: svc.Type.String(),
29+
Status: svc.Status.String(),
30+
PID: svc.PID,
31+
LastExitStatus: svc.LastExitStatus,
32+
PlistPath: svc.PlistPath,
33+
Program: svc.BinaryPath(),
34+
})
35+
}
36+
return out
37+
}
38+
39+
// jsonServiceDetail is the full representation used by info.
40+
type jsonServiceDetail struct {
41+
Label string `json:"label"`
42+
Domain string `json:"domain"`
43+
Type string `json:"type"`
44+
Status string `json:"status"`
45+
PID int `json:"pid"`
46+
LastExitStatus int `json:"last_exit_status"`
47+
PlistPath string `json:"plist_path,omitempty"`
48+
Program string `json:"program,omitempty"`
49+
ProgramArgs []string `json:"program_args,omitempty"`
50+
RunAtLoad bool `json:"run_at_load"`
51+
KeepAlive interface{} `json:"keep_alive"`
52+
StartInterval int `json:"start_interval,omitempty"`
53+
WatchPaths []string `json:"watch_paths,omitempty"`
54+
QueueDirectories []string `json:"queue_directories,omitempty"`
55+
WorkingDirectory string `json:"working_directory,omitempty"`
56+
StandardOutPath string `json:"stdout_path,omitempty"`
57+
StandardErrorPath string `json:"stderr_path,omitempty"`
58+
EnvironmentVars map[string]string `json:"environment,omitempty"`
59+
ExitTimeout int `json:"exit_timeout,omitempty"`
60+
Disabled bool `json:"disabled"`
61+
BlameLine string `json:"blame,omitempty"`
62+
}
63+
64+
// toJSONServiceDetail converts an agent.Service to its full JSON representation.
65+
func toJSONServiceDetail(svc *agent.Service) jsonServiceDetail {
66+
return jsonServiceDetail{
67+
Label: svc.Label,
68+
Domain: svc.Domain.String(),
69+
Type: svc.Type.String(),
70+
Status: svc.Status.String(),
71+
PID: svc.PID,
72+
LastExitStatus: svc.LastExitStatus,
73+
PlistPath: svc.PlistPath,
74+
Program: svc.BinaryPath(),
75+
ProgramArgs: svc.ProgramArgs,
76+
RunAtLoad: svc.RunAtLoad,
77+
KeepAlive: svc.KeepAlive,
78+
StartInterval: svc.StartInterval,
79+
WatchPaths: svc.WatchPaths,
80+
QueueDirectories: svc.QueueDirectories,
81+
WorkingDirectory: svc.WorkingDirectory,
82+
StandardOutPath: svc.StandardOutPath,
83+
StandardErrorPath: svc.StandardErrorPath,
84+
EnvironmentVars: svc.EnvironmentVars,
85+
ExitTimeout: svc.ExitTimeout,
86+
Disabled: svc.Disabled,
87+
BlameLine: svc.BlameLine,
88+
}
89+
}
90+
91+
// ---------------------------------------------------------------------------
92+
// Action JSON type (enable, disable, restart, load, unload)
93+
// ---------------------------------------------------------------------------
94+
95+
type jsonAction struct {
96+
OK bool `json:"ok"`
97+
Action string `json:"action"`
98+
Label string `json:"label"`
99+
}
100+
101+
// ---------------------------------------------------------------------------
102+
// Doctor JSON types
103+
// ---------------------------------------------------------------------------
104+
105+
type jsonDoctor struct {
106+
Findings []jsonFinding `json:"findings"`
107+
Summary jsonSummary `json:"summary"`
108+
}
109+
110+
type jsonFinding struct {
111+
Severity string `json:"severity"`
112+
Label string `json:"label"`
113+
PlistPath string `json:"plist_path,omitempty"`
114+
Message string `json:"message"`
115+
Suggestion string `json:"suggestion,omitempty"`
116+
}
117+
118+
type jsonSummary struct {
119+
Critical int `json:"critical"`
120+
Warning int `json:"warning"`
121+
OK int `json:"ok"`
122+
}
123+
124+
// toJSONDoctor converts doctor findings to a JSON-serializable structure.
125+
func toJSONDoctor(findings []agent.Finding) jsonDoctor {
126+
critical, warning, ok := agent.CountBySeverity(findings)
127+
128+
jf := make([]jsonFinding, 0, len(findings))
129+
for _, f := range findings {
130+
jf = append(jf, jsonFinding{
131+
Severity: f.Severity.String(),
132+
Label: f.Label,
133+
PlistPath: f.PlistPath,
134+
Message: f.Message,
135+
Suggestion: f.Suggestion,
136+
})
137+
}
138+
139+
return jsonDoctor{
140+
Findings: jf,
141+
Summary: jsonSummary{
142+
Critical: critical,
143+
Warning: warning,
144+
OK: ok,
145+
},
146+
}
147+
}
148+
149+
// ---------------------------------------------------------------------------
150+
// Create JSON type
151+
// ---------------------------------------------------------------------------
152+
153+
type jsonCreate struct {
154+
OK bool `json:"ok"`
155+
Action string `json:"action"`
156+
Label string `json:"label"`
157+
PlistPath string `json:"plist_path"`
158+
Loaded bool `json:"loaded"`
159+
}
160+
161+
// ---------------------------------------------------------------------------
162+
// Import JSON type
163+
// ---------------------------------------------------------------------------
164+
165+
type jsonImport struct {
166+
OK bool `json:"ok"`
167+
Action string `json:"action"`
168+
Label string `json:"label"`
169+
PlistPath string `json:"plist_path"`
170+
Loaded bool `json:"loaded"`
171+
}
172+
173+
// ---------------------------------------------------------------------------
174+
// Logs JSON type
175+
// ---------------------------------------------------------------------------
176+
177+
type jsonLogs struct {
178+
Label string `json:"label"`
179+
Source string `json:"source"`
180+
Lines []string `json:"lines"`
181+
}
182+
183+
// ---------------------------------------------------------------------------
184+
// Edit JSON type
185+
// ---------------------------------------------------------------------------
186+
187+
type jsonEdit struct {
188+
OK bool `json:"ok"`
189+
Action string `json:"action"`
190+
Label string `json:"label"`
191+
PlistPath string `json:"plist_path"`
192+
ValidationOK bool `json:"validation_ok"`
193+
Reloaded bool `json:"reloaded"`
194+
}

0 commit comments

Comments
 (0)