Skip to content

Commit 1bc2946

Browse files
akoclaude
andcommitted
feat: add unified --json output format for all commands (#134)
Add a global --json flag that produces machine-readable JSON output from any SHOW or DESCRIBE command, eliminating truncation and table parsing overhead for programmatic/AI consumption. - Add OutputFormat type, TableResult struct, and writeResult/writeDescribeJSON helpers in mdl/executor/format.go - Convert ~30 SHOW handlers to use writeResult() instead of inline fprintf - Wrap all DESCRIBE handlers with writeDescribeJSON() at the dispatch level - Wire --json persistent flag in CLI, with resolveFormat() for commands that already have --format flags (search, lint, check, report, describe) - Add format_test.go with table, JSON, empty, and describe passthrough tests - Update MDL_QUICK_REFERENCE.md with --json usage Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2907a8e commit 1bc2946

39 files changed

+750
-1279
lines changed

cmd/mxcli/cmd_check.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Examples:
4545
filePath := args[0]
4646
projectPath, _ := cmd.Flags().GetString("project")
4747
checkRefs, _ := cmd.Flags().GetBool("references")
48-
format, _ := cmd.Flags().GetString("format")
48+
format := resolveFormat(cmd, "text")
4949

5050
// Read the file
5151
content, err := os.ReadFile(filePath)

cmd/mxcli/cmd_describe.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"strings"
99

10+
"github.com/mendixlabs/mxcli/mdl/executor"
1011
"github.com/mendixlabs/mxcli/mdl/visitor"
1112
"github.com/spf13/cobra"
1213
)
@@ -157,8 +158,11 @@ Example:
157158
}
158159
}
159160

160-
// Check for mermaid/elk format - bypass MDL parser and call directly
161-
format, _ := cmd.Flags().GetString("format")
161+
// Check for format overrides - bypass MDL parser for mermaid/elk, set executor for json
162+
format := resolveFormat(cmd, "mdl")
163+
if format == "json" {
164+
exec.SetFormat(executor.FormatJSON)
165+
}
162166
typeArg := strings.Join(args[:len(args)-1], " ")
163167
if format == "mermaid" {
164168
if err := exec.DescribeMermaid(typeArg, name); err != nil {

cmd/mxcli/cmd_lint.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Examples:
7979
`,
8080
Run: func(cmd *cobra.Command, args []string) {
8181
projectPath, _ := cmd.Flags().GetString("project")
82-
format, _ := cmd.Flags().GetString("format")
82+
format := resolveFormat(cmd, "text")
8383
useColor, _ := cmd.Flags().GetBool("color")
8484
listRules, _ := cmd.Flags().GetBool("list-rules")
8585
excludeModules, _ := cmd.Flags().GetStringSlice("exclude")

cmd/mxcli/cmd_query.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ Examples:
226226
Args: cobra.ExactArgs(1),
227227
Run: func(cmd *cobra.Command, args []string) {
228228
projectPath, _ := cmd.Flags().GetString("project")
229-
format, _ := cmd.Flags().GetString("format")
229+
format := resolveFormat(cmd, "table")
230230
quiet, _ := cmd.Flags().GetBool("quiet")
231231

232232
if projectPath == "" {

cmd/mxcli/cmd_report.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Examples:
3838
`,
3939
Run: func(cmd *cobra.Command, args []string) {
4040
projectPath, _ := cmd.Flags().GetString("project")
41-
format, _ := cmd.Flags().GetString("format")
41+
format := resolveFormat(cmd, "markdown")
4242
outputPath, _ := cmd.Flags().GetString("output")
4343
excludeModules, _ := cmd.Flags().GetStringSlice("exclude")
4444

cmd/mxcli/main.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ Examples:
106106
fmt.Fprintf(os.Stderr, "Using project: %s\n", discovered)
107107
}
108108
}
109+
globalJSONFlag, _ = cmd.Flags().GetBool("json")
109110
},
110111
Run: func(cmd *cobra.Command, args []string) {
111112
// Get flags
@@ -171,12 +172,32 @@ Examples:
171172
},
172173
}
173174

175+
// globalJSONFlag is set by PersistentPreRun when --json is passed.
176+
var globalJSONFlag bool
177+
178+
// resolveFormat returns the effective output format for a command.
179+
// If the global --json flag is set and the command has a --format flag, it returns "json".
180+
// Otherwise it returns the command's --format flag value (or the provided default).
181+
func resolveFormat(cmd *cobra.Command, defaultFormat string) string {
182+
if globalJSONFlag {
183+
return "json"
184+
}
185+
if cmd.Flags().Lookup("format") != nil {
186+
f, _ := cmd.Flags().GetString("format")
187+
return f
188+
}
189+
return defaultFormat
190+
}
191+
174192
// newLoggedExecutor creates an executor with diagnostics logging attached.
175193
// The caller must call logger.Close() when done (safe on nil).
176194
func newLoggedExecutor(mode string) (*executor.Executor, *diaglog.Logger) {
177195
logger := diaglog.Init(version, mode)
178196
exec := executor.New(os.Stdout)
179197
exec.SetLogger(logger)
198+
if globalJSONFlag {
199+
exec.SetFormat(executor.FormatJSON)
200+
}
180201
return exec, logger
181202
}
182203

@@ -216,6 +237,7 @@ func init() {
216237

217238
// Global flags
218239
rootCmd.PersistentFlags().StringP("project", "p", "", "Path to Mendix project (.mpr file)")
240+
rootCmd.PersistentFlags().Bool("json", false, "Output in JSON format")
219241
rootCmd.Flags().StringP("command", "c", "", "Execute MDL command(s) and exit")
220242

221243
// Check command flags
@@ -234,7 +256,7 @@ func init() {
234256
diffLocalCmd.Flags().IntP("width", "w", 120, "Terminal width for side-by-side format")
235257

236258
// Describe command flags
237-
describeCmd.Flags().StringP("format", "f", "mdl", "Output format: mdl, mermaid, elk")
259+
describeCmd.Flags().StringP("format", "f", "mdl", "Output format: mdl, json, mermaid, elk")
238260

239261
// Search command flags
240262
searchCmd.Flags().StringP("format", "f", "table", "Output format: table, names, json")

docs/01-project/MDL_QUICK_REFERENCE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,7 @@ Cross-reference commands require `REFRESH CATALOG FULL` to populate reference da
764764
|---------|--------|-------|
765765
| Interactive REPL | `mxcli` | Interactive MDL shell |
766766
| Execute command | `mxcli -p app.mpr -c "SHOW ENTITIES"` | Single command |
767+
| JSON output | `mxcli -p app.mpr -c "SHOW ENTITIES" --json` | JSON for any command |
767768
| Execute script | `mxcli exec script.mdl -p app.mpr` | Script file |
768769
| Check syntax | `mxcli check script.mdl` | Parse-only validation |
769770
| Check references | `mxcli check script.mdl -p app.mpr --references` | With reference validation |

mdl/executor/cmd_associations.go

Lines changed: 10 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ func (e *Executor) showAssociations(moduleName string) error {
272272
}
273273
}
274274

275-
// Collect rows and calculate column widths
275+
// Collect rows
276276
type row struct {
277277
qualifiedName string
278278
module string
@@ -284,42 +284,6 @@ func (e *Executor) showAssociations(moduleName string) error {
284284
storage string
285285
}
286286
var rows []row
287-
qnWidth := len("Qualified Name")
288-
modWidth := len("Module")
289-
nameWidth := len("Name")
290-
parentWidth := len("Parent")
291-
childWidth := len("Child")
292-
typeWidth := len("Type")
293-
ownerWidth := len("Owner")
294-
storageWidth := len("Storage")
295-
296-
addRow := func(qualifiedName, modName, assocName, parent, child string, assocType, owner, storage string) {
297-
rows = append(rows, row{qualifiedName, modName, assocName, parent, child, assocType, owner, storage})
298-
if len(qualifiedName) > qnWidth {
299-
qnWidth = len(qualifiedName)
300-
}
301-
if len(modName) > modWidth {
302-
modWidth = len(modName)
303-
}
304-
if len(assocName) > nameWidth {
305-
nameWidth = len(assocName)
306-
}
307-
if len(parent) > parentWidth {
308-
parentWidth = len(parent)
309-
}
310-
if len(child) > childWidth {
311-
childWidth = len(child)
312-
}
313-
if len(assocType) > typeWidth {
314-
typeWidth = len(assocType)
315-
}
316-
if len(owner) > ownerWidth {
317-
ownerWidth = len(owner)
318-
}
319-
if len(storage) > storageWidth {
320-
storageWidth = len(storage)
321-
}
322-
}
323287

324288
for _, dm := range domainModels {
325289
modName := moduleNames[dm.ContainerID]
@@ -338,7 +302,7 @@ func (e *Executor) showAssociations(moduleName string) error {
338302
if child == "" {
339303
child = string(assoc.ChildID)
340304
}
341-
addRow(qualifiedName, modName, assoc.Name, parent, child, string(assoc.Type), string(assoc.Owner), string(assoc.StorageFormat))
305+
rows = append(rows, row{qualifiedName, modName, assoc.Name, parent, child, string(assoc.Type), string(assoc.Owner), string(assoc.StorageFormat)})
342306
}
343307
// Cross-module associations
344308
for _, ca := range dm.CrossAssociations {
@@ -347,7 +311,7 @@ func (e *Executor) showAssociations(moduleName string) error {
347311
if parent == "" {
348312
parent = string(ca.ParentID)
349313
}
350-
addRow(qualifiedName, modName, ca.Name, parent, ca.ChildRef, string(ca.Type), string(ca.Owner), string(ca.StorageFormat))
314+
rows = append(rows, row{qualifiedName, modName, ca.Name, parent, ca.ChildRef, string(ca.Type), string(ca.Owner), string(ca.StorageFormat)})
351315
}
352316
}
353317

@@ -356,21 +320,15 @@ func (e *Executor) showAssociations(moduleName string) error {
356320
return strings.ToLower(rows[i].qualifiedName) < strings.ToLower(rows[j].qualifiedName)
357321
})
358322

359-
// Markdown table with aligned columns
360-
fmt.Fprintf(e.output, "| %-*s | %-*s | %-*s | %-*s | %-*s | %-*s | %-*s | %-*s |\n",
361-
qnWidth, "Qualified Name", modWidth, "Module", nameWidth, "Name",
362-
parentWidth, "Parent", childWidth, "Child", typeWidth, "Type", ownerWidth, "Owner", storageWidth, "Storage")
363-
fmt.Fprintf(e.output, "|-%s-|-%s-|-%s-|-%s-|-%s-|-%s-|-%s-|-%s-|\n",
364-
strings.Repeat("-", qnWidth), strings.Repeat("-", modWidth), strings.Repeat("-", nameWidth),
365-
strings.Repeat("-", parentWidth), strings.Repeat("-", childWidth),
366-
strings.Repeat("-", typeWidth), strings.Repeat("-", ownerWidth), strings.Repeat("-", storageWidth))
323+
// Build TableResult
324+
result := &TableResult{
325+
Columns: []string{"Qualified Name", "Module", "Name", "Parent", "Child", "Type", "Owner", "Storage"},
326+
Summary: fmt.Sprintf("(%d associations)", len(rows)),
327+
}
367328
for _, r := range rows {
368-
fmt.Fprintf(e.output, "| %-*s | %-*s | %-*s | %-*s | %-*s | %-*s | %-*s | %-*s |\n",
369-
qnWidth, r.qualifiedName, modWidth, r.module, nameWidth, r.name,
370-
parentWidth, r.parent, childWidth, r.child, typeWidth, r.assocType, ownerWidth, r.owner, storageWidth, r.storage)
329+
result.Rows = append(result.Rows, []any{r.qualifiedName, r.module, r.name, r.parent, r.child, r.assocType, r.owner, r.storage})
371330
}
372-
fmt.Fprintf(e.output, "\n(%d associations)\n", len(rows))
373-
return nil
331+
return e.writeResult(result)
374332
}
375333

376334
// showAssociation handles SHOW ASSOCIATION command.

mdl/executor/cmd_businessevents.go

Lines changed: 14 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ func (e *Executor) showBusinessEventServices(inModule string) error {
5151
msgCount, publishCount, subscribeCount int
5252
}
5353
var rows []row
54-
modWidth, qnWidth, nameWidth := len("Module"), len("QualifiedName"), len("Service")
5554

5655
for _, svc := range filtered {
5756
modID := h.FindModuleID(svc.ContainerID)
@@ -73,32 +72,17 @@ func (e *Executor) showBusinessEventServices(inModule string) error {
7372
}
7473
}
7574

76-
if len(moduleName) > modWidth {
77-
modWidth = len(moduleName)
78-
}
79-
if len(qn) > qnWidth {
80-
qnWidth = len(qn)
81-
}
82-
if len(svc.Name) > nameWidth {
83-
nameWidth = len(svc.Name)
84-
}
8575
rows = append(rows, r)
8676
}
8777

88-
fmt.Fprintf(e.output, "| %-*s | %-*s | %-*s | %-10s | %-10s | %-10s |\n",
89-
modWidth, "Module", qnWidth, "QualifiedName", nameWidth, "Service", "Messages", "Publish", "Subscribe")
90-
fmt.Fprintf(e.output, "|-%s-|-%s-|-%s-|-%s-|-%s-|-%s-|\n",
91-
strings.Repeat("-", modWidth), strings.Repeat("-", qnWidth), strings.Repeat("-", nameWidth),
92-
strings.Repeat("-", 10), strings.Repeat("-", 10), strings.Repeat("-", 10))
93-
78+
result := &TableResult{
79+
Columns: []string{"Module", "QualifiedName", "Service", "Messages", "Publish", "Subscribe"},
80+
Summary: fmt.Sprintf("(%d business event services)", len(filtered)),
81+
}
9482
for _, r := range rows {
95-
fmt.Fprintf(e.output, "| %-*s | %-*s | %-*s | %10d | %10d | %10d |\n",
96-
modWidth, r.module, qnWidth, r.qualifiedName, nameWidth, r.name,
97-
r.msgCount, r.publishCount, r.subscribeCount)
83+
result.Rows = append(result.Rows, []any{r.module, r.qualifiedName, r.name, r.msgCount, r.publishCount, r.subscribeCount})
9884
}
99-
100-
fmt.Fprintf(e.output, "\n(%d business event services)\n", len(filtered))
101-
return nil
85+
return e.writeResult(result)
10286
}
10387

10488
// showBusinessEventClients displays a table of all business event client documents.
@@ -128,10 +112,6 @@ func (e *Executor) showBusinessEvents(inModule string) error {
128112
attrs int
129113
}
130114
var rows []row
131-
svcWidth := len("Service")
132-
msgWidth := len("Message")
133-
opWidth := len("Operation")
134-
entityWidth := len("Entity")
135115

136116
for _, svc := range services {
137117
modID := h.FindModuleID(svc.ContainerID)
@@ -157,26 +137,13 @@ func (e *Executor) showBusinessEvents(inModule string) error {
157137
opStr = strings.ToUpper(op.Operation)
158138
entityStr = op.Entity
159139
}
160-
r := row{
140+
rows = append(rows, row{
161141
service: svcQN,
162142
message: msg.MessageName,
163143
operation: opStr,
164144
entity: entityStr,
165145
attrs: len(msg.Attributes),
166-
}
167-
if len(svcQN) > svcWidth {
168-
svcWidth = len(svcQN)
169-
}
170-
if len(msg.MessageName) > msgWidth {
171-
msgWidth = len(msg.MessageName)
172-
}
173-
if len(opStr) > opWidth {
174-
opWidth = len(opStr)
175-
}
176-
if len(entityStr) > entityWidth {
177-
entityWidth = len(entityStr)
178-
}
179-
rows = append(rows, r)
146+
})
180147
}
181148
}
182149
}
@@ -191,20 +158,14 @@ func (e *Executor) showBusinessEvents(inModule string) error {
191158
return nil
192159
}
193160

194-
fmt.Fprintf(e.output, "| %-*s | %-*s | %-*s | %-*s | %-10s |\n",
195-
svcWidth, "Service", msgWidth, "Message", opWidth, "Operation", entityWidth, "Entity", "Attributes")
196-
fmt.Fprintf(e.output, "|-%s-|-%s-|-%s-|-%s-|-%s-|\n",
197-
strings.Repeat("-", svcWidth), strings.Repeat("-", msgWidth),
198-
strings.Repeat("-", opWidth), strings.Repeat("-", entityWidth), strings.Repeat("-", 10))
199-
161+
result := &TableResult{
162+
Columns: []string{"Service", "Message", "Operation", "Entity", "Attributes"},
163+
Summary: fmt.Sprintf("(%d business events)", len(rows)),
164+
}
200165
for _, r := range rows {
201-
fmt.Fprintf(e.output, "| %-*s | %-*s | %-*s | %-*s | %10d |\n",
202-
svcWidth, r.service, msgWidth, r.message, opWidth, r.operation,
203-
entityWidth, r.entity, r.attrs)
166+
result.Rows = append(result.Rows, []any{r.service, r.message, r.operation, r.entity, r.attrs})
204167
}
205-
206-
fmt.Fprintf(e.output, "\n(%d business events)\n", len(rows))
207-
return nil
168+
return e.writeResult(result)
208169
}
209170

210171
// describeBusinessEventService outputs the full MDL description of a business event service.

0 commit comments

Comments
 (0)