Skip to content

Commit 31b668b

Browse files
authored
Merge pull request #83 from LAA-Software-Engineering/feat/cli-inspect-command
feat(cli): agentctl inspect (effective normalized resource)
2 parents 25c900d + c2c2396 commit 31b668b

8 files changed

Lines changed: 420 additions & 42 deletions

File tree

internal/cli/diff.go

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -37,44 +37,6 @@ Exit codes (§11.2):
3737
}
3838
}
3939

40-
func parseDiffResourceRef(s string) (spec.ResourceID, error) {
41-
s = strings.TrimSpace(s)
42-
if s == "" {
43-
return spec.ResourceID{}, fmt.Errorf("empty Kind/name")
44-
}
45-
i := strings.IndexByte(s, '/')
46-
if i <= 0 || i == len(s)-1 {
47-
return spec.ResourceID{}, fmt.Errorf("resource must be Kind/name (e.g. Policy/default), got %q", s)
48-
}
49-
kindIn, name := s[:i], s[i+1:]
50-
kind, err := normalizeKindName(kindIn)
51-
if err != nil {
52-
return spec.ResourceID{}, err
53-
}
54-
if strings.TrimSpace(name) == "" {
55-
return spec.ResourceID{}, fmt.Errorf("resource name is empty in %q", s)
56-
}
57-
return spec.ResourceID{Kind: kind, Name: name}, nil
58-
}
59-
60-
func normalizeKindName(s string) (string, error) {
61-
s = strings.TrimSpace(s)
62-
known := []string{
63-
spec.KindProject,
64-
spec.KindAgent,
65-
spec.KindTool,
66-
spec.KindWorkflow,
67-
spec.KindPolicy,
68-
spec.KindEnvironment,
69-
}
70-
for _, k := range known {
71-
if strings.EqualFold(s, k) {
72-
return k, nil
73-
}
74-
}
75-
return "", fmt.Errorf("unknown resource kind %q (want Project, Agent, Tool, Workflow, Policy, or Environment)", s)
76-
}
77-
7840
func desiredContainsID(ids []spec.ResourceID, id spec.ResourceID) bool {
7941
for _, x := range ids {
8042
if x.Kind == id.Kind && x.Name == id.Name {
@@ -171,7 +133,7 @@ func runDiff(cmd *cobra.Command, args []string) error {
171133
appliedByKey := appliedIndex(appliedList)
172134

173135
if len(args) == 1 {
174-
id, err := parseDiffResourceRef(args[0])
136+
id, err := ParseResourceRef(args[0])
175137
if err != nil {
176138
return NewExitError(ExitValidationError, fmt.Errorf("diff: %w", err))
177139
}

internal/cli/doc.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
// The validate command (section 10.2) loads the project, applies defaults and optional environment
77
// overlays, then runs [spec.ValidateProjectGraph].
88
//
9+
// The inspect command (section 10.2) uses the same preparation pipeline and prints one effective
10+
// normalized resource envelope (Kind/name) as JSON, YAML, or indented JSON for table output.
11+
//
912
// The plan command compares that prepared graph to the SQLite deployment store (default
1013
// .agentic/state.db, or project.spec.state.dsn / --state) and prints a diff plus risk delta
1114
// via [plan.ComputePlan] and [plan.FormatPlan].

internal/cli/inspect.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package cli
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/LAA-Software-Engineering/agentic-control-plane/internal/render"
9+
"github.com/LAA-Software-Engineering/agentic-control-plane/internal/spec"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
func newInspectCmd() *cobra.Command {
14+
return &cobra.Command{
15+
Use: "inspect Kind/name",
16+
Short: "Print the effective normalized resource (after defaults and env overlay)",
17+
Long: `Load the project the same way as validate, plan, and run (defaults, optional -e / --env
18+
overlay via Environment resources, then validation), then print one resource.
19+
20+
Argument must be Kind/name (e.g. Agent/reviewer, workflow/demo). Kind is matched case-insensitively.
21+
22+
Output is the full resource envelope: apiVersion, kind, metadata, and spec (design doc §6.1).
23+
24+
Exit code 2 for validation failure, unknown resource, or bad Kind/name (§11.2).`,
25+
Example: ` agentctl inspect Workflow/pr-review
26+
agentctl inspect Agent/reviewer -o yaml
27+
agentctl inspect Policy/default -e staging -o json`,
28+
SilenceUsage: true,
29+
RunE: runInspect,
30+
}
31+
}
32+
33+
func environmentLabel(g *Global) string {
34+
if g == nil || strings.TrimSpace(g.Env) == "" {
35+
return "(none)"
36+
}
37+
return strings.TrimSpace(g.Env)
38+
}
39+
40+
// lookupEffectiveResource returns the in-memory resource after normalization and environment merge.
41+
func lookupEffectiveResource(g *spec.ProjectGraph, id spec.ResourceID) (any, error) {
42+
if g == nil {
43+
return nil, fmt.Errorf("inspect: nil project graph")
44+
}
45+
switch id.Kind {
46+
case spec.KindProject:
47+
want := strings.TrimSpace(id.Name)
48+
got := strings.TrimSpace(g.Meta.Name)
49+
if want == "" || got == "" || want != got {
50+
return nil, fmt.Errorf("inspect: unknown resource %s (project metadata.name is %q)", id.String(), got)
51+
}
52+
return &spec.ProjectResource{
53+
APIVersion: spec.APIVersionV0,
54+
Kind: spec.KindProject,
55+
Metadata: g.Meta,
56+
Spec: g.Spec,
57+
}, nil
58+
case spec.KindAgent:
59+
a := g.Agents[id.Name]
60+
if a == nil {
61+
return nil, fmt.Errorf("inspect: unknown resource %s", id.String())
62+
}
63+
return a, nil
64+
case spec.KindTool:
65+
t := g.Tools[id.Name]
66+
if t == nil {
67+
return nil, fmt.Errorf("inspect: unknown resource %s", id.String())
68+
}
69+
return t, nil
70+
case spec.KindWorkflow:
71+
w := g.Workflows[id.Name]
72+
if w == nil {
73+
return nil, fmt.Errorf("inspect: unknown resource %s", id.String())
74+
}
75+
return w, nil
76+
case spec.KindPolicy:
77+
p := g.Policies[id.Name]
78+
if p == nil {
79+
return nil, fmt.Errorf("inspect: unknown resource %s", id.String())
80+
}
81+
return p, nil
82+
case spec.KindEnvironment:
83+
e := g.Environments[id.Name]
84+
if e == nil {
85+
return nil, fmt.Errorf("inspect: unknown resource %s", id.String())
86+
}
87+
return e, nil
88+
default:
89+
return nil, fmt.Errorf("inspect: unsupported kind %q", id.Kind)
90+
}
91+
}
92+
93+
func runInspect(cmd *cobra.Command, args []string) error {
94+
if len(args) != 1 {
95+
return NewExitError(ExitValidationError, fmt.Errorf("inspect: requires exactly one Kind/name argument"))
96+
}
97+
id, err := ParseResourceRef(args[0])
98+
if err != nil {
99+
return NewExitError(ExitValidationError, fmt.Errorf("inspect: %w", err))
100+
}
101+
gl := Globals()
102+
graph, _, err := prepareProjectGraph(gl.ProjectRoot, gl)
103+
if err != nil {
104+
return NewExitError(ExitValidationError, err)
105+
}
106+
res, err := lookupEffectiveResource(graph, id)
107+
if err != nil {
108+
return NewExitError(ExitValidationError, err)
109+
}
110+
return writeInspectOutput(cmd, id.String(), res, gl)
111+
}
112+
113+
func writeInspectOutput(cmd *cobra.Command, target string, resource any, g *Global) error {
114+
out := cmd.OutOrStdout()
115+
env := environmentLabel(g)
116+
switch g.Output {
117+
case render.FormatJSON:
118+
raw, err := json.Marshal(resource)
119+
if err != nil {
120+
return err
121+
}
122+
var resObj map[string]any
123+
if err := json.Unmarshal(raw, &resObj); err != nil {
124+
return err
125+
}
126+
return render.WriteJSON(out, map[string]any{
127+
"environment": env,
128+
"resource": resObj,
129+
})
130+
case render.FormatYAML:
131+
return render.WriteYAML(out, map[string]any{
132+
"environment": env,
133+
"resource": resource,
134+
})
135+
default:
136+
if _, err := fmt.Fprintf(out, "Resource: %s\nEnvironment: %s\n\n", target, env); err != nil {
137+
return err
138+
}
139+
b, err := json.MarshalIndent(resource, "", " ")
140+
if err != nil {
141+
return err
142+
}
143+
_, err = out.Write(b)
144+
if err != nil {
145+
return err
146+
}
147+
_, err = out.Write([]byte("\n"))
148+
return err
149+
}
150+
}

0 commit comments

Comments
 (0)