Skip to content

Commit de6555e

Browse files
gustavobertoiclaude
andcommitted
feat(cli): add config validate and config show (spec 01 DX)
Wire the M1 config loader into the CLI: config validate loads + validates the workspace and reports file:line:col errors (non-zero exit on failure); config show prints a human/--json summary of the resolved workspace (shared services, projects, services, uses). Makes the spec-01 loader usable from the binary ahead of up/status (M2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0d91698 commit de6555e

2 files changed

Lines changed: 154 additions & 0 deletions

File tree

‎internal/cli/config.go‎

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package cli
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"sort"
8+
9+
"github.com/spf13/cobra"
10+
11+
"github.com/open-source-cloud/devstack/internal/config"
12+
)
13+
14+
// newConfigCmd wires `config validate|show` — the M1 DX surface for the
15+
// two-file schema (spec 01). It discovers the workspace by walking up from the
16+
// CWD, loads + validates workspace.yaml and every project's devstack.yaml, and
17+
// reports problems as file:line:col.
18+
func newConfigCmd(g *GlobalOpts) *cobra.Command {
19+
cmd := &cobra.Command{
20+
Use: "config",
21+
Short: "Inspect and validate the workspace configuration",
22+
Long: "config loads workspace.yaml (found by walking up from the current directory)\n" +
23+
"and every referenced project's devstack.yaml, validates structure and\n" +
24+
"cross-references against the workspace graph, and reports errors as file:line:col.",
25+
}
26+
cmd.AddCommand(newConfigValidateCmd(g), newConfigShowCmd(g))
27+
return cmd
28+
}
29+
30+
func newConfigValidateCmd(g *GlobalOpts) *cobra.Command {
31+
return &cobra.Command{
32+
Use: "validate",
33+
Short: "Validate the workspace + project configuration",
34+
Args: cobra.NoArgs,
35+
RunE: func(cmd *cobra.Command, _ []string) error {
36+
m, err := loadWorkspace()
37+
if err != nil {
38+
return err
39+
}
40+
s := summarize(m)
41+
if g.JSON {
42+
return writeJSON(cmd, map[string]any{"ok": true, "config": s})
43+
}
44+
if !g.Quiet {
45+
fmt.Fprintf(cmd.OutOrStdout(), "ok: workspace %q — %d project(s), %d shared service(s)\n",
46+
s.Workspace, len(s.Projects), len(s.Shared))
47+
}
48+
return nil
49+
},
50+
}
51+
}
52+
53+
func newConfigShowCmd(g *GlobalOpts) *cobra.Command {
54+
return &cobra.Command{
55+
Use: "show",
56+
Short: "Print a summary of the resolved workspace configuration",
57+
Args: cobra.NoArgs,
58+
RunE: func(cmd *cobra.Command, _ []string) error {
59+
m, err := loadWorkspace()
60+
if err != nil {
61+
return err
62+
}
63+
s := summarize(m)
64+
if g.JSON {
65+
return writeJSON(cmd, s)
66+
}
67+
w := cmd.OutOrStdout()
68+
fmt.Fprintf(w, "workspace: %s\n", s.Workspace)
69+
fmt.Fprintf(w, "root: %s\n", s.Root)
70+
if len(s.Shared) > 0 {
71+
fmt.Fprintf(w, "shared: %v\n", s.Shared)
72+
}
73+
for _, p := range s.Projects {
74+
fmt.Fprintf(w, "project %s:\n", p.Name)
75+
for _, svc := range p.Services {
76+
if len(svc.Uses) > 0 {
77+
fmt.Fprintf(w, " - %s (uses: %v)\n", svc.Name, svc.Uses)
78+
} else {
79+
fmt.Fprintf(w, " - %s\n", svc.Name)
80+
}
81+
}
82+
}
83+
return nil
84+
},
85+
}
86+
}
87+
88+
// loadWorkspace discovers and loads the workspace from the current directory.
89+
func loadWorkspace() (*config.Model, error) {
90+
cwd, err := os.Getwd()
91+
if err != nil {
92+
return nil, err
93+
}
94+
return config.Load(cwd)
95+
}
96+
97+
// configSummary is the stable JSON/human shape for `config` output.
98+
type configSummary struct {
99+
Workspace string `json:"workspace"`
100+
Root string `json:"root"`
101+
Shared []string `json:"shared"`
102+
Projects []projectSummary `json:"projects"`
103+
}
104+
105+
type projectSummary struct {
106+
Name string `json:"name"`
107+
Services []serviceSummary `json:"services"`
108+
}
109+
110+
type serviceSummary struct {
111+
Name string `json:"name"`
112+
Template string `json:"template"`
113+
Uses []string `json:"uses,omitempty"`
114+
}
115+
116+
func summarize(m *config.Model) configSummary {
117+
s := configSummary{Workspace: m.Workspace.Name, Root: m.Root, Shared: m.SharedNames()}
118+
sort.Strings(s.Shared)
119+
for _, pname := range sortedProjectNames(m) {
120+
p := m.Projects[pname]
121+
ps := projectSummary{Name: pname}
122+
for _, sname := range sortedServiceNames(p.Services) {
123+
svc := p.Services[sname]
124+
ps.Services = append(ps.Services, serviceSummary{Name: sname, Template: svc.Template, Uses: svc.Uses})
125+
}
126+
s.Projects = append(s.Projects, ps)
127+
}
128+
return s
129+
}
130+
131+
func writeJSON(cmd *cobra.Command, v any) error {
132+
enc := json.NewEncoder(cmd.OutOrStdout())
133+
enc.SetIndent("", " ")
134+
return enc.Encode(v)
135+
}
136+
137+
func sortedProjectNames(m *config.Model) []string {
138+
out := make([]string, 0, len(m.Projects))
139+
for k := range m.Projects {
140+
out = append(out, k)
141+
}
142+
sort.Strings(out)
143+
return out
144+
}
145+
146+
func sortedServiceNames(svcs map[string]config.Service) []string {
147+
out := make([]string, 0, len(svcs))
148+
for k := range svcs {
149+
out = append(out, k)
150+
}
151+
sort.Strings(out)
152+
return out
153+
}

‎internal/cli/root.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ func NewRootCmd(opts Options) *cobra.Command {
6565

6666
root.AddCommand(
6767
newDoctorCmd(g),
68+
newConfigCmd(g),
6869
newAliasCmd(g),
6970
newVersionCmd(),
7071
)

0 commit comments

Comments
 (0)