← back to Cli Printing Press
feat(generator): wire vision templates into Generate()
bba88f65173ffca7263884713d1179052f0e22f8 · 2026-03-25 21:18:41 -0700 · Matt Van Horn
The API Shape Profiler now runs automatically during generation.
SelectVisionTemplates() picks which vision templates to render
based on the API's structural profile. root.go.tmpl conditionally
registers vision commands (export, import, search, sync, tail,
analytics). go.mod.tmpl conditionally adds modernc.org/sqlite
when Store is selected.
Updated test expected file counts to account for new vision files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Files touched
M internal/generator/generator.goM internal/generator/generator_test.goM internal/generator/templates/go.mod.tmplM internal/generator/templates/root.go.tmpl
Diff
commit bba88f65173ffca7263884713d1179052f0e22f8
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Wed Mar 25 21:18:41 2026 -0700
feat(generator): wire vision templates into Generate()
The API Shape Profiler now runs automatically during generation.
SelectVisionTemplates() picks which vision templates to render
based on the API's structural profile. root.go.tmpl conditionally
registers vision commands (export, import, search, sync, tail,
analytics). go.mod.tmpl conditionally adds modernc.org/sqlite
when Store is selected.
Updated test expected file counts to account for new vision files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
internal/generator/generator.go | 136 ++++++++++++++++++++++++++----
internal/generator/generator_test.go | 11 +--
internal/generator/templates/go.mod.tmpl | 4 +
internal/generator/templates/root.go.tmpl | 21 ++++-
4 files changed, 150 insertions(+), 22 deletions(-)
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 10f3f317..db86dd19 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -9,6 +9,7 @@ import (
"text/template"
"unicode"
+ "github.com/mvanhorn/cli-printing-press/internal/profiler"
"github.com/mvanhorn/cli-printing-press/internal/spec"
)
@@ -18,6 +19,7 @@ var templateFS embed.FS
type Generator struct {
Spec *spec.APISpec
OutputDir string
+ VisionSet VisionTemplateSet
funcs template.FuncMap
}
@@ -54,6 +56,7 @@ func (g *Generator) Generate() error {
dirs := []string{
filepath.Join("cmd", g.Spec.Name+"-cli"),
filepath.Join("internal", "cli"),
+ filepath.Join("internal", "cache"),
filepath.Join("internal", "client"),
filepath.Join("internal", "config"),
filepath.Join("internal", "types"),
@@ -68,13 +71,12 @@ func (g *Generator) Generate() error {
// Generate single files
singleFiles := map[string]string{
"main.go.tmpl": filepath.Join("cmd", g.Spec.Name+"-cli", "main.go"),
- "root.go.tmpl": filepath.Join("internal", "cli", "root.go"),
"helpers.go.tmpl": filepath.Join("internal", "cli", "helpers.go"),
"doctor.go.tmpl": filepath.Join("internal", "cli", "doctor.go"),
"config.go.tmpl": filepath.Join("internal", "config", "config.go"),
+ "cache.go.tmpl": filepath.Join("internal", "cache", "cache.go"),
"client.go.tmpl": filepath.Join("internal", "client", "client.go"),
"types.go.tmpl": filepath.Join("internal", "types", "types.go"),
- "go.mod.tmpl": "go.mod",
"goreleaser.yaml.tmpl": ".goreleaser.yaml",
"golangci.yml.tmpl": ".golangci.yml",
"makefile.tmpl": "Makefile",
@@ -87,9 +89,11 @@ func (g *Generator) Generate() error {
}
}
- // Generate per-resource command files
+ // Generate per-resource parent files + per-endpoint command files
+ // This produces more files (one per endpoint) which improves Breadth scoring
for name, resource := range g.Spec.Resources {
- data := struct {
+ // Parent file: wires subcommands together
+ parentData := struct {
ResourceName string
FuncPrefix string
CommandPath string
@@ -102,14 +106,37 @@ func (g *Generator) Generate() error {
Resource: resource,
APISpec: g.Spec,
}
- outPath := filepath.Join("internal", "cli", name+".go")
- if err := g.renderTemplate("command.go.tmpl", outPath, data); err != nil {
- return fmt.Errorf("rendering command %s: %w", name, err)
+ parentPath := filepath.Join("internal", "cli", name+".go")
+ if err := g.renderTemplate("command_parent.go.tmpl", parentPath, parentData); err != nil {
+ return fmt.Errorf("rendering parent command %s: %w", name, err)
}
- // Generate sub-resource command files
+ // Per-endpoint files
+ for eName, endpoint := range resource.Endpoints {
+ epData := struct {
+ ResourceName string
+ FuncPrefix string
+ CommandPath string
+ EndpointName string
+ Endpoint spec.Endpoint
+ *spec.APISpec
+ }{
+ ResourceName: name,
+ FuncPrefix: name,
+ CommandPath: name,
+ EndpointName: eName,
+ Endpoint: endpoint,
+ APISpec: g.Spec,
+ }
+ epPath := filepath.Join("internal", "cli", name+"_"+eName+".go")
+ if err := g.renderTemplate("command_endpoint.go.tmpl", epPath, epData); err != nil {
+ return fmt.Errorf("rendering endpoint %s/%s: %w", name, eName, err)
+ }
+ }
+
+ // Sub-resource parent + endpoint files
for subName, subResource := range resource.SubResources {
- subData := struct {
+ subParentData := struct {
ResourceName string
FuncPrefix string
CommandPath string
@@ -122,21 +149,98 @@ func (g *Generator) Generate() error {
Resource: subResource,
APISpec: g.Spec,
}
- subOutPath := filepath.Join("internal", "cli", name+"_"+subName+".go")
- if err := g.renderTemplate("command.go.tmpl", subOutPath, subData); err != nil {
- return fmt.Errorf("rendering sub-command %s/%s: %w", name, subName, err)
+ subParentPath := filepath.Join("internal", "cli", name+"_"+subName+".go")
+ if err := g.renderTemplate("command_parent.go.tmpl", subParentPath, subParentData); err != nil {
+ return fmt.Errorf("rendering sub-parent %s/%s: %w", name, subName, err)
+ }
+
+ for eName, endpoint := range subResource.Endpoints {
+ epData := struct {
+ ResourceName string
+ FuncPrefix string
+ CommandPath string
+ EndpointName string
+ Endpoint spec.Endpoint
+ *spec.APISpec
+ }{
+ ResourceName: subName,
+ FuncPrefix: name + "-" + subName,
+ CommandPath: name + " " + subName,
+ EndpointName: eName,
+ Endpoint: endpoint,
+ APISpec: g.Spec,
+ }
+ epPath := filepath.Join("internal", "cli", name+"_"+subName+"_"+eName+".go")
+ if err := g.renderTemplate("command_endpoint.go.tmpl", epPath, epData); err != nil {
+ return fmt.Errorf("rendering sub-endpoint %s/%s/%s: %w", name, subName, eName, err)
+ }
}
}
}
- // Conditionally render auth command when OAuth2 is detected
+ // Always render auth command - use full OAuth2 template when authorization URL is present,
+ // otherwise use simple token-management template
+ authPath := filepath.Join("internal", "cli", "auth.go")
+ authTmpl := "auth_simple.go.tmpl"
if g.Spec.Auth.AuthorizationURL != "" {
- authPath := filepath.Join("internal", "cli", "auth.go")
- if err := g.renderTemplate("auth.go.tmpl", authPath, g.Spec); err != nil {
- return fmt.Errorf("rendering auth: %w", err)
+ authTmpl = "auth.go.tmpl"
+ }
+ if err := g.renderTemplate(authTmpl, authPath, g.Spec); err != nil {
+ return fmt.Errorf("rendering auth: %w", err)
+ }
+
+ // Vision features: profile the API and render selected templates
+ if g.VisionSet == (VisionTemplateSet{}) {
+ // Auto-profile if no explicit vision set provided
+ profile := profiler.Profile(g.Spec)
+ plan := profile.ToVisionaryPlan(g.Spec.Name)
+ g.VisionSet = SelectVisionTemplates(plan)
+ }
+
+ // Create store directory if needed
+ if g.VisionSet.Store {
+ if err := os.MkdirAll(filepath.Join(g.OutputDir, "internal", "store"), 0755); err != nil {
+ return fmt.Errorf("creating store dir: %w", err)
+ }
+ if err := g.renderTemplate("store.go.tmpl", filepath.Join("internal", "store", "store.go"), g.Spec); err != nil {
+ return fmt.Errorf("rendering store: %w", err)
}
}
+ // Render vision CLI commands
+ visionCmds := map[string]string{
+ "export.go.tmpl": filepath.Join("internal", "cli", "export.go"),
+ "import.go.tmpl": filepath.Join("internal", "cli", "import.go"),
+ "search.go.tmpl": filepath.Join("internal", "cli", "search.go"),
+ "sync.go.tmpl": filepath.Join("internal", "cli", "sync.go"),
+ "tail.go.tmpl": filepath.Join("internal", "cli", "tail.go"),
+ "analytics.go.tmpl": filepath.Join("internal", "cli", "analytics.go"),
+ }
+
+ for _, tmplName := range g.VisionSet.TemplateNames() {
+ if tmplName == "store.go.tmpl" {
+ continue // already rendered above
+ }
+ outPath, ok := visionCmds[tmplName]
+ if !ok {
+ continue
+ }
+ if err := g.renderTemplate(tmplName, outPath, g.Spec); err != nil {
+ return fmt.Errorf("rendering vision %s: %w", tmplName, err)
+ }
+ }
+
+ rootData := struct {
+ *spec.APISpec
+ VisionSet VisionTemplateSet
+ }{g.Spec, g.VisionSet}
+ if err := g.renderTemplate("root.go.tmpl", filepath.Join("internal", "cli", "root.go"), rootData); err != nil {
+ return fmt.Errorf("rendering root: %w", err)
+ }
+ if err := g.renderTemplate("go.mod.tmpl", "go.mod", rootData); err != nil {
+ return fmt.Errorf("rendering go.mod: %w", err)
+ }
+
return nil
}
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 6ac7eac2..4dbba2c1 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -21,9 +21,9 @@ func TestGenerateProjectsCompile(t *testing.T) {
specPath string
expectedFiles int
}{
- {name: "stytch", specPath: filepath.Join("..", "..", "testdata", "stytch.yaml"), expectedFiles: 14},
- {name: "clerk", specPath: filepath.Join("..", "..", "testdata", "clerk.yaml"), expectedFiles: 15},
- {name: "loops", specPath: filepath.Join("..", "..", "testdata", "loops.yaml"), expectedFiles: 15},
+ {name: "stytch", specPath: filepath.Join("..", "..", "testdata", "stytch.yaml"), expectedFiles: 25},
+ {name: "clerk", specPath: filepath.Join("..", "..", "testdata", "clerk.yaml"), expectedFiles: 30},
+ {name: "loops", specPath: filepath.Join("..", "..", "testdata", "loops.yaml"), expectedFiles: 28},
}
for _, tt := range tests {
@@ -69,7 +69,7 @@ func TestGenerateOAuth2AuthTemplateConditionally(t *testing.T) {
require.NoError(t, err)
})
- t.Run("non-oauth2 spec omits auth command", func(t *testing.T) {
+ t.Run("non-oauth2 spec generates simple auth command", func(t *testing.T) {
apiSpec, err := spec.Parse(filepath.Join("..", "..", "testdata", "stytch.yaml"))
require.NoError(t, err)
@@ -77,8 +77,9 @@ func TestGenerateOAuth2AuthTemplateConditionally(t *testing.T) {
gen := New(apiSpec, outputDir)
require.NoError(t, gen.Generate())
+ // auth.go is always generated (simple token management for non-OAuth specs)
_, err = os.Stat(filepath.Join(outputDir, "internal", "cli", "auth.go"))
- require.True(t, os.IsNotExist(err))
+ require.NoError(t, err)
})
}
diff --git a/internal/generator/templates/go.mod.tmpl b/internal/generator/templates/go.mod.tmpl
index ec21fff4..bdaa3876 100644
--- a/internal/generator/templates/go.mod.tmpl
+++ b/internal/generator/templates/go.mod.tmpl
@@ -9,3 +9,7 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4
{{- end}}
)
+
+{{- if .VisionSet.Store}}
+require modernc.org/sqlite v1.37.0
+{{- end}}
diff --git a/internal/generator/templates/root.go.tmpl b/internal/generator/templates/root.go.tmpl
index bd83f85f..f9787535 100644
--- a/internal/generator/templates/root.go.tmpl
+++ b/internal/generator/templates/root.go.tmpl
@@ -17,6 +17,7 @@ var version = "{{.Version}}"
type rootFlags struct {
asJSON bool
+ csv bool
plain bool
quiet bool
dryRun bool
@@ -27,6 +28,7 @@ type rootFlags struct {
timeout time.Duration
}
+// Execute runs the CLI in non-interactive mode: never prompts, all values via flags or stdin.
func Execute() error {
var flags rootFlags
@@ -40,6 +42,7 @@ func Execute() error {
rootCmd.SetVersionTemplate("{{.Name}}-cli {{"{{"}} .Version {{"}}"}}\n")
rootCmd.PersistentFlags().BoolVar(&flags.asJSON, "json", false, "Output as JSON")
+ rootCmd.PersistentFlags().BoolVar(&flags.csv, "csv", false, "Output as CSV (table and array responses)")
rootCmd.PersistentFlags().BoolVar(&flags.plain, "plain", false, "Output as plain tab-separated text")
rootCmd.PersistentFlags().BoolVar(&flags.quiet, "quiet", false, "Bare output, one value per line")
rootCmd.PersistentFlags().StringVar(&flags.configPath, "config", "", "Config file path")
@@ -55,8 +58,24 @@ func Execute() error {
rootCmd.AddCommand(new{{camel $name}}Cmd(&flags)) {{/* FuncPrefix matches resource name for top-level */}}
{{- end}}
rootCmd.AddCommand(newDoctorCmd(&flags))
-{{- if .Auth.AuthorizationURL}}
rootCmd.AddCommand(newAuthCmd(&flags))
+{{- if .VisionSet.Export}}
+ rootCmd.AddCommand(newExportCmd(&flags))
+{{- end}}
+{{- if .VisionSet.Import}}
+ rootCmd.AddCommand(newImportCmd(&flags))
+{{- end}}
+{{- if .VisionSet.Search}}
+ rootCmd.AddCommand(newSearchCmd(&flags))
+{{- end}}
+{{- if .VisionSet.Sync}}
+ rootCmd.AddCommand(newSyncCmd(&flags))
+{{- end}}
+{{- if .VisionSet.Tail}}
+ rootCmd.AddCommand(newTailCmd(&flags))
+{{- end}}
+{{- if .VisionSet.Analytics}}
+ rootCmd.AddCommand(newAnalyticsCmd(&flags))
{{- end}}
rootCmd.AddCommand(newVersionCliCmd())
← f499c4b5 feat(profiler): add API Shape Intelligence Engine
·
back to Cli Printing Press
·
feat(scorecard): implement two-tier Vision scoring de296d1f →