← back to Cli Printing Press
feat(scaffold): initial project structure with CLI skeleton
454739bd04333318247c1fb4c4d57cf47237b072 · 2026-03-23 09:01:56 -0700 · Matt Van Horn
Go module, Cobra CLI with generate/version commands, spec parser
structs, and generator stub. Compiles and runs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files touched
A cmd/printing-press/main.goA go.modA go.sumA internal/cli/root.goA internal/generator/generator.goA internal/spec/spec.go
Diff
commit 454739bd04333318247c1fb4c4d57cf47237b072
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Mon Mar 23 09:01:56 2026 -0700
feat(scaffold): initial project structure with CLI skeleton
Go module, Cobra CLI with generate/version commands, spec parser
structs, and generator stub. Compiles and runs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
cmd/printing-press/main.go | 15 +++++
go.mod | 13 +++++
go.sum | 13 +++++
internal/cli/root.go | 81 +++++++++++++++++++++++++++
internal/generator/generator.go | 19 +++++++
internal/spec/spec.go | 120 ++++++++++++++++++++++++++++++++++++++++
6 files changed, 261 insertions(+)
diff --git a/cmd/printing-press/main.go b/cmd/printing-press/main.go
new file mode 100644
index 00000000..06d27333
--- /dev/null
+++ b/cmd/printing-press/main.go
@@ -0,0 +1,15 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/mvanhorn/cli-printing-press/internal/cli"
+)
+
+func main() {
+ if err := cli.Execute(); err != nil {
+ fmt.Fprintln(os.Stderr, err.Error())
+ os.Exit(1)
+ }
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 00000000..d46c2082
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,13 @@
+module github.com/mvanhorn/cli-printing-press
+
+go 1.26.1
+
+require (
+ github.com/spf13/cobra v1.10.2
+ gopkg.in/yaml.v3 v3.0.1
+)
+
+require (
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/spf13/pflag v1.0.9 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 00000000..47edb24d
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,13 @@
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
+github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/cli/root.go b/internal/cli/root.go
new file mode 100644
index 00000000..0b7c80e0
--- /dev/null
+++ b/internal/cli/root.go
@@ -0,0 +1,81 @@
+package cli
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/mvanhorn/cli-printing-press/internal/generator"
+ "github.com/mvanhorn/cli-printing-press/internal/spec"
+ "github.com/spf13/cobra"
+)
+
+var version = "0.1.0"
+
+func Execute() error {
+ rootCmd := &cobra.Command{
+ Use: "printing-press",
+ Short: "Describe your API. Get a production CLI.",
+ SilenceUsage: true,
+ SilenceErrors: true,
+ Version: version,
+ }
+ rootCmd.SetVersionTemplate("printing-press {{.Version}}\n")
+
+ rootCmd.AddCommand(newGenerateCmd())
+ rootCmd.AddCommand(newVersionCmd())
+
+ return rootCmd.Execute()
+}
+
+func newGenerateCmd() *cobra.Command {
+ var specFile string
+ var outputDir string
+
+ cmd := &cobra.Command{
+ Use: "generate",
+ Short: "Generate a Go CLI project from an API spec",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if specFile == "" {
+ return fmt.Errorf("--spec is required")
+ }
+
+ apiSpec, err := spec.Parse(specFile)
+ if err != nil {
+ return fmt.Errorf("parsing spec: %w", err)
+ }
+
+ if outputDir == "" {
+ outputDir = apiSpec.Name + "-cli"
+ }
+
+ absOut, err := filepath.Abs(outputDir)
+ if err != nil {
+ return fmt.Errorf("resolving output path: %w", err)
+ }
+
+ gen := generator.New(apiSpec, absOut)
+ if err := gen.Generate(); err != nil {
+ return fmt.Errorf("generating project: %w", err)
+ }
+
+ fmt.Fprintf(os.Stderr, "Generated %s-cli at %s\n", apiSpec.Name, absOut)
+ return nil
+ },
+ }
+
+ cmd.Flags().StringVar(&specFile, "spec", "", "Path to API spec YAML file (required)")
+ cmd.Flags().StringVar(&outputDir, "output", "", "Output directory (default: <name>-cli)")
+
+ return cmd
+}
+
+func newVersionCmd() *cobra.Command {
+ return &cobra.Command{
+ Use: "version",
+ Short: "Print version",
+ Run: func(cmd *cobra.Command, args []string) {
+ fmt.Printf("printing-press %s\n", version)
+ },
+ }
+}
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
new file mode 100644
index 00000000..75aa5e7a
--- /dev/null
+++ b/internal/generator/generator.go
@@ -0,0 +1,19 @@
+package generator
+
+import (
+ "github.com/mvanhorn/cli-printing-press/internal/spec"
+)
+
+type Generator struct {
+ Spec *spec.APISpec
+ OutputDir string
+}
+
+func New(s *spec.APISpec, outputDir string) *Generator {
+ return &Generator{Spec: s, OutputDir: outputDir}
+}
+
+func (g *Generator) Generate() error {
+ // TODO: implement in Task 3+4
+ return nil
+}
diff --git a/internal/spec/spec.go b/internal/spec/spec.go
new file mode 100644
index 00000000..1d0f3188
--- /dev/null
+++ b/internal/spec/spec.go
@@ -0,0 +1,120 @@
+package spec
+
+import (
+ "fmt"
+ "os"
+
+ "gopkg.in/yaml.v3"
+)
+
+type APISpec struct {
+ Name string `yaml:"name"`
+ Description string `yaml:"description"`
+ Version string `yaml:"version"`
+ BaseURL string `yaml:"base_url"`
+ Auth AuthConfig `yaml:"auth"`
+ Config ConfigSpec `yaml:"config"`
+ Resources map[string]Resource `yaml:"resources"`
+ Types map[string]TypeDef `yaml:"types"`
+}
+
+type AuthConfig struct {
+ Type string `yaml:"type"` // api_key, oauth2, bearer_token, none
+ Header string `yaml:"header"`
+ Format string `yaml:"format"`
+ EnvVars []string `yaml:"env_vars"`
+}
+
+type ConfigSpec struct {
+ Format string `yaml:"format"` // toml, yaml
+ Path string `yaml:"path"`
+}
+
+type Resource struct {
+ Description string `yaml:"description"`
+ Endpoints map[string]Endpoint `yaml:"endpoints"`
+}
+
+type Endpoint struct {
+ Method string `yaml:"method"`
+ Path string `yaml:"path"`
+ Description string `yaml:"description"`
+ Params []Param `yaml:"params"`
+ Body []Param `yaml:"body"`
+ Response ResponseDef `yaml:"response"`
+ Pagination *Pagination `yaml:"pagination"`
+}
+
+type Param struct {
+ Name string `yaml:"name"`
+ Type string `yaml:"type"`
+ Required bool `yaml:"required"`
+ Positional bool `yaml:"positional"`
+ Default any `yaml:"default"`
+ Description string `yaml:"description"`
+ Fields []Param `yaml:"fields"` // for nested objects
+}
+
+type ResponseDef struct {
+ Type string `yaml:"type"` // object, array
+ Item string `yaml:"item"` // type name
+}
+
+type Pagination struct {
+ Type string `yaml:"type"` // cursor, offset, page_token
+ CursorField string `yaml:"cursor_field"`
+ HasMoreField string `yaml:"has_more_field"`
+}
+
+type TypeDef struct {
+ Fields []TypeField `yaml:"fields"`
+}
+
+type TypeField struct {
+ Name string `yaml:"name"`
+ Type string `yaml:"type"`
+}
+
+func Parse(path string) (*APISpec, error) {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("reading file: %w", err)
+ }
+
+ var s APISpec
+ if err := yaml.Unmarshal(data, &s); err != nil {
+ return nil, fmt.Errorf("parsing yaml: %w", err)
+ }
+
+ if err := s.Validate(); err != nil {
+ return nil, fmt.Errorf("validation: %w", err)
+ }
+
+ return &s, nil
+}
+
+func (s *APISpec) Validate() error {
+ if s.Name == "" {
+ return fmt.Errorf("name is required")
+ }
+ if s.BaseURL == "" {
+ return fmt.Errorf("base_url is required")
+ }
+ if len(s.Resources) == 0 {
+ return fmt.Errorf("at least one resource is required")
+ }
+ for name, r := range s.Resources {
+ if len(r.Endpoints) == 0 {
+ return fmt.Errorf("resource %q has no endpoints", name)
+ }
+ for eName, e := range r.Endpoints {
+ if e.Method == "" {
+ return fmt.Errorf("resource %q endpoint %q: method is required", name, eName)
+ }
+ if e.Path == "" {
+ return fmt.Errorf("resource %q endpoint %q: path is required", name, eName)
+ }
+ }
+ }
+ return nil
+}
(oldest)
·
back to Cli Printing Press
·
feat(spec): YAML spec parser with validation and Stytch test b4ab56ba →