← back to Cli Printing Press
feat(cli): add 'printing-press print' command with plan-per-phase pipeline
6a76cb26f76506081eeb9172c7e504a3d7d55468 · 2026-03-24 07:21:37 -0700 · Matt Van Horn
'printing-press print gmail' creates a pipeline directory with 6 plan.md
seeds (preflight, scaffold, enrich, regenerate, review, ship). Each plan
has frontmatter, acceptance criteria, and implementation units ready for
ce:work execution. State.json tracks phase status across sessions.
Files touched
M internal/cli/root.goA internal/pipeline/pipeline.goA internal/pipeline/seeds.go
Diff
commit 6a76cb26f76506081eeb9172c7e504a3d7d55468
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Tue Mar 24 07:21:37 2026 -0700
feat(cli): add 'printing-press print' command with plan-per-phase pipeline
'printing-press print gmail' creates a pipeline directory with 6 plan.md
seeds (preflight, scaffold, enrich, regenerate, review, ship). Each plan
has frontmatter, acceptance criteria, and implementation units ready for
ce:work execution. State.json tracks phase status across sessions.
---
internal/cli/root.go | 44 +++++++
internal/pipeline/pipeline.go | 78 ++++++++++++
internal/pipeline/seeds.go | 268 ++++++++++++++++++++++++++++++++++++++++++
3 files changed, 390 insertions(+)
diff --git a/internal/cli/root.go b/internal/cli/root.go
index 4ba34f49..39d81e04 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -13,6 +13,7 @@ import (
"github.com/mvanhorn/cli-printing-press/internal/generator"
"github.com/mvanhorn/cli-printing-press/internal/openapi"
+ "github.com/mvanhorn/cli-printing-press/internal/pipeline"
"github.com/mvanhorn/cli-printing-press/internal/spec"
"github.com/spf13/cobra"
)
@@ -31,6 +32,7 @@ func Execute() error {
rootCmd.AddCommand(newGenerateCmd())
rootCmd.AddCommand(newVersionCmd())
+ rootCmd.AddCommand(newPrintCmd())
return rootCmd.Execute()
}
@@ -228,3 +230,45 @@ func newVersionCmd() *cobra.Command {
},
}
}
+
+func newPrintCmd() *cobra.Command {
+ var outputDir string
+ var force bool
+ var resume bool
+
+ cmd := &cobra.Command{
+ Use: "print <api-name>",
+ Short: "Create an autonomous CLI generation pipeline",
+ Long: "Creates a pipeline directory with plan seeds for each phase. Use /ce:work on each plan to execute.",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ apiName := args[0]
+
+ state, err := pipeline.Init(apiName, pipeline.Options{
+ OutputDir: outputDir,
+ Force: force,
+ Resume: resume,
+ })
+ if err != nil {
+ return err
+ }
+
+ fmt.Fprintf(os.Stderr, "Pipeline created for %s\n", apiName)
+ fmt.Fprintf(os.Stderr, " Spec: %s\n", state.SpecURL)
+ fmt.Fprintf(os.Stderr, " Output: %s\n", state.OutputDir)
+ fmt.Fprintf(os.Stderr, " Plans:\n")
+ for i, phase := range pipeline.PhaseOrder {
+ fmt.Fprintf(os.Stderr, " %d. %s\n", i, state.PlanPath(phase))
+ }
+ fmt.Fprintf(os.Stderr, "\nStart with: /ce:work %s\n", state.PlanPath(pipeline.PhasePreflight))
+
+ return nil
+ },
+ }
+
+ cmd.Flags().StringVar(&outputDir, "output", "", "Output directory (default: ./<api-name>-cli)")
+ cmd.Flags().BoolVar(&force, "force", false, "Overwrite existing pipeline")
+ cmd.Flags().BoolVar(&resume, "resume", false, "Resume from existing checkpoint")
+
+ return cmd
+}
diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go
new file mode 100644
index 00000000..9fcfbb97
--- /dev/null
+++ b/internal/pipeline/pipeline.go
@@ -0,0 +1,78 @@
+package pipeline
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+)
+
+// Options configures a pipeline run.
+type Options struct {
+ OutputDir string
+ Force bool
+ Resume bool
+ Phase string
+}
+
+// Init creates the pipeline directory, state file, and plan seeds.
+// It does NOT execute any phases.
+func Init(apiName string, opts Options) (*PipelineState, error) {
+ if opts.Resume && StateExists(apiName) {
+ return LoadState(apiName)
+ }
+
+ outputDir := opts.OutputDir
+ if outputDir == "" {
+ outputDir = "./" + apiName + "-cli"
+ }
+
+ absOutputDir, err := filepath.Abs(outputDir)
+ if err != nil {
+ return nil, fmt.Errorf("resolving output dir: %w", err)
+ }
+
+ pipeDir := PipelineDir(apiName)
+ if StateExists(apiName) && !opts.Force {
+ return nil, fmt.Errorf("pipeline for %q already exists at %s (use --force to overwrite or --resume to continue)", apiName, pipeDir)
+ }
+
+ specURL, specSource, err := DiscoverSpec(apiName)
+ if err != nil {
+ return nil, fmt.Errorf("discovering spec: %w", err)
+ }
+
+ state := NewState(apiName, absOutputDir)
+ state.SpecURL = specURL
+
+ if err := os.MkdirAll(pipeDir, 0o755); err != nil {
+ return nil, fmt.Errorf("creating pipeline dir: %w", err)
+ }
+
+ seedData := SeedData{
+ APIName: apiName,
+ OutputDir: absOutputDir,
+ SpecURL: specURL,
+ SpecSource: specSource,
+ PipelineDir: pipeDir,
+ }
+
+ for _, phase := range PhaseOrder {
+ content, err := RenderSeed(phase, seedData)
+ if err != nil {
+ return nil, fmt.Errorf("rendering seed for %s: %w", phase, err)
+ }
+
+ planPath := state.PlanPath(phase)
+ if err := os.WriteFile(planPath, []byte(content), 0o644); err != nil {
+ return nil, fmt.Errorf("writing plan seed for %s: %w", phase, err)
+ }
+
+ state.MarkPlanned(phase)
+ }
+
+ if err := state.Save(); err != nil {
+ return nil, fmt.Errorf("saving state: %w", err)
+ }
+
+ return state, nil
+}
diff --git a/internal/pipeline/seeds.go b/internal/pipeline/seeds.go
new file mode 100644
index 00000000..6fd1fdfe
--- /dev/null
+++ b/internal/pipeline/seeds.go
@@ -0,0 +1,268 @@
+package pipeline
+
+import (
+ "bytes"
+ "fmt"
+ "text/template"
+ "time"
+)
+
+// SeedData holds the context for rendering plan seeds.
+type SeedData struct {
+ APIName string
+ OutputDir string
+ SpecURL string
+ SpecSource string
+ PipelineDir string
+}
+
+var seedTemplates = map[string]string{
+ PhasePreflight: `---
+title: "{{.APIName}} CLI Pipeline - Phase 0: Preflight"
+type: feat
+status: active
+date: {{now}}
+---
+
+# Preflight: {{.APIName}} CLI
+
+## Goal
+Validate the environment and discover the OpenAPI spec for {{.APIName}}.
+
+## Acceptance Criteria
+- [ ] Go is installed and working (go version succeeds)
+- [ ] printing-press binary compiles (go build ./cmd/printing-press)
+- [ ] OpenAPI spec downloaded and validated
+- [ ] Spec has 3+ endpoints and a base URL
+- [ ] Conventions cache written with auth type, resource count, pagination patterns
+
+## Implementation Units
+
+### Unit 1: Environment Check
+- Run ` + "`go version`" + ` - verify Go 1.23+
+- Run ` + "`go build -o /tmp/pp-check ./cmd/printing-press`" + ` - verify press compiles
+- Check output dir {{.OutputDir}} doesn't exist (or --force was used)
+
+### Unit 2: Spec Discovery
+- Spec URL: {{.SpecURL}} (source: {{.SpecSource}})
+- Download with: ` + "`printing-press generate --spec {{.SpecURL}} --output /dev/null 2>&1 | head -5`" + ` to test parsing
+- If parse fails: try alternative sources
+
+### Unit 3: Write Conventions Cache
+- After successful parse, write conventions.json to {{.PipelineDir}}/
+- Include: auth type, endpoint count, resource names, pagination patterns detected, global params found
+
+## Context
+Pipeline directory: {{.PipelineDir}}
+`,
+ PhaseScaffold: `---
+title: "{{.APIName}} CLI Pipeline - Phase 1: Scaffold"
+type: feat
+status: active
+date: {{now}}
+---
+
+# Scaffold: {{.APIName}} CLI
+
+## Goal
+Generate the initial CLI from the discovered OpenAPI spec.
+
+## Acceptance Criteria
+- [ ] CLI generated at {{.OutputDir}}
+- [ ] All 7 quality gates pass (go mod tidy, go vet, go build, binary, --help, version, doctor)
+- [ ] CLI compiles to a working binary
+
+## Implementation Units
+
+### Unit 1: Generate CLI
+Run: ` + "`printing-press generate --spec <spec-path-from-preflight> --output {{.OutputDir}}`" + `
+
+### Unit 2: Validate
+- Build the binary: ` + "`cd {{.OutputDir}} && go build -o ./{{.APIName}}-cli ./cmd/{{.APIName}}-cli`" + `
+- Run: ` + "`./{{.APIName}}-cli --help`" + `
+- Run: ` + "`./{{.APIName}}-cli doctor`" + `
+- Count resources and endpoints
+
+### Unit 3: Document
+- List all top-level resources
+- List total endpoint count
+- Note any warnings from generation
+`,
+ PhaseEnrich: `---
+title: "{{.APIName}} CLI Pipeline - Phase 2: Enrich"
+type: feat
+status: active
+date: {{now}}
+---
+
+# Enrich: {{.APIName}} CLI
+
+## Goal
+Deep-read the original spec for hints the parser missed. Research API docs. Produce a spec overlay.
+
+## Acceptance Criteria
+- [ ] overlay.yaml written to {{.PipelineDir}}/
+- [ ] At least 1 enrichment found
+- [ ] Overlay is valid YAML
+
+## Implementation Units
+
+### Unit 1: Description Hints
+- Read every parameter description in the spec
+- Find default value hints (e.g., "The special value 'me' can be used")
+- Extract as ParamPatch with Default field
+
+### Unit 2: Upload Detection
+- Scan for mediaUpload fields, x-google extensions, multipart content types
+- Flag endpoints that support file upload (note in overlay comments)
+
+### Unit 3: Sync Token Patterns
+- Scan response schemas for historyId, syncToken, nextSyncToken fields
+- Note resources that support incremental sync
+
+### Unit 4: Better Descriptions
+- Find endpoints with empty or generic descriptions
+- WebSearch for "{{.APIName}} API" to find better descriptions
+
+### Unit 5: Write Overlay
+- Compile all enrichments into overlay.yaml at {{.PipelineDir}}/overlay.yaml
+`,
+ PhaseRegenerate: `---
+title: "{{.APIName}} CLI Pipeline - Phase 3: Regenerate"
+type: feat
+status: active
+date: {{now}}
+---
+
+# Regenerate: {{.APIName}} CLI with Enrichments
+
+## Goal
+Merge the spec overlay with the original spec and re-generate the CLI.
+
+## Acceptance Criteria
+- [ ] Overlay merged with original spec
+- [ ] CLI re-generated at {{.OutputDir}}
+- [ ] All 7 quality gates still pass
+- [ ] Enrichments visible in CLI help output
+
+## Implementation Units
+
+### Unit 1: Load and Merge
+- Load original spec
+- Load overlay.yaml from {{.PipelineDir}}/
+- Apply overlay using MergeOverlay function
+- Write merged spec to {{.PipelineDir}}/merged-spec.yaml
+
+### Unit 2: Regenerate
+- Run ` + "`printing-press generate --spec {{.PipelineDir}}/merged-spec.yaml --output {{.OutputDir}}`" + `
+- If quality gates fail, fall back to original spec
+
+### Unit 3: Verify Enrichments
+- Check CLI help for enriched defaults (e.g., userId now shows default)
+- Compare before/after help output
+`,
+ PhaseReview: `---
+title: "{{.APIName}} CLI Pipeline - Phase 4: Review"
+type: feat
+status: active
+date: {{now}}
+---
+
+# Review: {{.APIName}} CLI Quality
+
+## Goal
+Static quality analysis of the generated CLI. No API calls.
+
+## Acceptance Criteria
+- [ ] Quality score calculated (0-100)
+- [ ] All critical checks pass
+- [ ] Issue list written to review.md
+
+## Implementation Units
+
+### Unit 1: Help Completeness
+- Run ` + "`{{.APIName}}-cli --help`" + ` and check exit code
+- Run ` + "`{{.APIName}}-cli <resource> --help`" + ` for every top-level resource
+- Verify non-empty output for each
+
+### Unit 2: Name Quality
+- No command name > 40 characters
+- No raw operationId passthrough (no dots or underscores in command names)
+- No duplicate command names
+
+### Unit 3: Description Quality
+- No empty descriptions on top-level resources
+- No descriptions that just repeat the command name
+
+### Unit 4: Scoring
+- +20 points for compiles cleanly
+- +20 points for all help commands work
+- +20 points for no name quality issues
+- +20 points for no empty descriptions
+- +10 points for doctor works
+- +10 points for binary < 50MB
+- Write score and issues to {{.PipelineDir}}/review.md
+`,
+ PhaseShip: `---
+title: "{{.APIName}} CLI Pipeline - Phase 5: Ship"
+type: feat
+status: active
+date: {{now}}
+---
+
+# Ship: {{.APIName}} CLI
+
+## Goal
+Finalize the CLI for human use.
+
+## Acceptance Criteria
+- [ ] Git repo initialized in {{.OutputDir}}
+- [ ] Initial commit created
+- [ ] Morning report written to {{.PipelineDir}}/report.md
+
+## Implementation Units
+
+### Unit 1: Git Init
+- Run ` + "`cd {{.OutputDir}} && git init && git add -A && git commit -m 'Initial CLI generated by printing-press'`" + `
+
+### Unit 2: Morning Report
+Write {{.PipelineDir}}/report.md with:
+- API name and spec source
+- Resource and endpoint count
+- Quality score from review phase
+- Enrichments applied
+- Time per phase
+- Next steps: configure auth, test against real API, publish
+
+### Unit 3: Summary
+Print to stderr:
+- "{{.APIName}}-cli ready at {{.OutputDir}}/"
+- Resource list
+- Auth type
+- Total pipeline time
+`,
+}
+
+// RenderSeed renders a plan seed template for the given phase.
+func RenderSeed(phase string, data SeedData) (string, error) {
+ tmplStr, ok := seedTemplates[phase]
+ if !ok {
+ return "", fmt.Errorf("no seed template for phase %q", phase)
+ }
+
+ tmpl, err := template.New(phase).Funcs(template.FuncMap{
+ "now": func() string {
+ return time.Now().Format("2006-01-02")
+ },
+ }).Parse(tmplStr)
+ if err != nil {
+ return "", fmt.Errorf("parsing seed template for %s: %w", phase, err)
+ }
+
+ var buf bytes.Buffer
+ if err := tmpl.Execute(&buf, data); err != nil {
+ return "", fmt.Errorf("rendering seed template for %s: %w", phase, err)
+ }
+
+ return buf.String(), nil
+}
← 5d456c67 feat(pipeline): spec discovery registry and overlay merge ty
·
back to Cli Printing Press
·
docs(plans): update pipeline plan with plan-first architectu cd1cea5a →