Skip to content

Commit 2d27162

Browse files
jingle2008claude
andcommitted
refactor(cli): flatten grouped categories to []T in json/jsonl/yaml
Some `toolkit get -o json` outputs were returning objects ({"pool-a": [...], "pool-b": [...]}) and others arrays, mirroring the Go shape of the underlying loader (map vs slice). Inconsistent for consumers and an early leak that the upcoming MCP layer would inherit. Add output.FlattenWithKey[T](map[string][]T, groupField) which round-trips each element through JSON and injects the map key into each object under groupField (sorted-key iteration for determinism). writeMap now flattens for json/jsonl/yaml; the table path keeps the grouped input because the per-category renderer needs the group column. Group field names per category: GpuNode/GpuNodeMap -> "pool" DAC/DedicatedAIClusterMap -> "tenant" ModelArtifact/.../Map -> "model" TenancyOverride (3 kinds) -> "tenant" CLI consumers and the future MCP server now see a uniform Array<T> for every category, with the group key carried in a named field. The TUI is unaffected (it operates on the typed maps directly). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8c2f853 commit 2d27162

2 files changed

Lines changed: 49 additions & 9 deletions

File tree

internal/cli/get.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -203,13 +203,13 @@ func emitCategory(
203203
if err != nil {
204204
return fmt.Errorf("load gpu nodes: %w", err)
205205
}
206-
return writeMap(w, filterMap(grouped, filter), opts, gpuNodeTable)
206+
return writeMap(w, filterMap(grouped, filter), opts, gpuNodeTable, "pool")
207207
case domain.DedicatedAICluster:
208208
grouped, err := ld.LoadDedicatedAIClusters(ctx, cfg.KubeConfig, env)
209209
if err != nil {
210210
return fmt.Errorf("load dedicated AI clusters: %w", err)
211211
}
212-
return writeMap(w, filterMap(grouped, filter), opts, dacTable)
212+
return writeMap(w, filterMap(grouped, filter), opts, dacTable, "tenant")
213213
case domain.Tenant,
214214
domain.LimitTenancyOverride,
215215
domain.ConsolePropertyTenancyOverride,
@@ -273,19 +273,25 @@ func writeSlice[T any](
273273
}
274274
}
275275

276+
// writeMap renders a grouped slice. For json/jsonl/yaml the input is
277+
// flattened to []map[string]any with `groupField` carrying the original
278+
// map key, so consumers see a uniform array of objects (matches the
279+
// shape MCP tools return). The table path keeps the grouped input so
280+
// the per-category renderer can show a group column.
276281
func writeMap[T any](
277282
w writer,
278283
grouped map[string][]T,
279284
opts output.Options,
280285
toTable func(map[string][]T) (headers []string, rows [][]string),
286+
groupField string,
281287
) error {
282288
switch opts.Format {
283289
case output.FormatJSON:
284-
return output.WriteJSON(w, grouped, opts)
290+
return output.WriteJSON(w, output.FlattenWithKey(grouped, groupField), opts)
285291
case output.FormatJSONL:
286-
return output.WriteJSONL(w, grouped, opts)
292+
return output.WriteJSONL(w, output.FlattenWithKey(grouped, groupField), opts)
287293
case output.FormatYAML:
288-
return output.WriteYAML(w, grouped, opts)
294+
return output.WriteYAML(w, output.FlattenWithKey(grouped, groupField), opts)
289295
case output.FormatTable:
290296
headers, rows := toTable(grouped)
291297
return output.WriteTable(w, headers, rows, opts)
@@ -313,13 +319,13 @@ func emitTenancyGroup(
313319
return writeSlice(w, collections.FilterSlice(group.Tenants, nil, filter, nil), opts, tenantTable)
314320
case domain.LimitTenancyOverride:
315321
return writeMap(w, filterMap(group.LimitTenancyOverrideMap, filter), opts,
316-
tenancyOverrideTable[models.LimitTenancyOverride])
322+
tenancyOverrideTable[models.LimitTenancyOverride], "tenant")
317323
case domain.ConsolePropertyTenancyOverride:
318324
return writeMap(w, filterMap(group.ConsolePropertyTenancyOverrideMap, filter), opts,
319-
tenancyOverrideTable[models.ConsolePropertyTenancyOverride])
325+
tenancyOverrideTable[models.ConsolePropertyTenancyOverride], "tenant")
320326
case domain.PropertyTenancyOverride:
321327
return writeMap(w, filterMap(group.PropertyTenancyOverrideMap, filter), opts,
322-
tenancyOverrideTable[models.PropertyTenancyOverride])
328+
tenancyOverrideTable[models.PropertyTenancyOverride], "tenant")
323329
default:
324330
return fmt.Errorf("category %s not in tenancy group", cat)
325331
}
@@ -354,7 +360,7 @@ func emitFromDataset(
354360
collections.FilterSlice(dataset.ServiceTenancies, nil, filter, nil),
355361
opts, serviceTenancyTable)
356362
case domain.ModelArtifact:
357-
return writeMap(w, filterMap(dataset.ModelArtifactMap, filter), opts, modelArtifactTable)
363+
return writeMap(w, filterMap(dataset.ModelArtifactMap, filter), opts, modelArtifactTable, "model")
358364
case domain.Alias:
359365
return writeAliases(w, filter, opts)
360366
default:

internal/cli/output/output.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"encoding/json"
1313
"fmt"
1414
"io"
15+
"sort"
1516
"strings"
1617
"text/tabwriter"
1718

@@ -135,6 +136,39 @@ func WriteYAML(w io.Writer, items any, opts Options) error {
135136
return enc.Encode(items)
136137
}
137138

139+
// FlattenWithKey turns a map[string][]T into []map[string]any, injecting
140+
// the map key into each element under groupField. Used to expose grouped
141+
// loader data (e.g. GpuNodeMap, DedicatedAIClusterMap) as a uniform
142+
// array of objects without leaking the underlying map shape to MCP /
143+
// JSON consumers.
144+
//
145+
// The output is stable: map keys are sorted before iteration. The
146+
// implementation round-trips through JSON so the caller's struct tags
147+
// (omitempty, custom names, etc.) are honored.
148+
func FlattenWithKey[T any](grouped map[string][]T, groupField string) []map[string]any {
149+
keys := make([]string, 0, len(grouped))
150+
for k := range grouped {
151+
keys = append(keys, k)
152+
}
153+
sort.Strings(keys)
154+
var out []map[string]any
155+
for _, k := range keys {
156+
for _, v := range grouped[k] {
157+
raw, err := json.Marshal(v)
158+
if err != nil {
159+
continue
160+
}
161+
var m map[string]any
162+
if err := json.Unmarshal(raw, &m); err != nil {
163+
continue
164+
}
165+
m[groupField] = k
166+
out = append(out, m)
167+
}
168+
}
169+
return out
170+
}
171+
138172
// WriteTable emits a tab-aligned table. opts.NoHeaders suppresses the
139173
// header row even when headers is non-empty.
140174
func WriteTable(w io.Writer, headers []string, rows [][]string, opts Options) error {

0 commit comments

Comments
 (0)