[object Object]

← back to Cli Printing Press

feat(cli): add --dry-run flag to generate command

b494c3b88505448f6ac4b96de517a41c9c1cb750 · 2026-03-27 13:37:07 -0700 · Trevin Chow

Parses the spec and prints a summary (resource/endpoint counts, output
dir) without writing files, running go mod tidy, or quality gates.
Emits structured JSON to stdout for programmatic use. Rejects --docs
combined with --dry-run since doc scraping is inherently side-effectful.
Skips cache writes for remote specs in dry-run mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit b494c3b88505448f6ac4b96de517a41c9c1cb750
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Fri Mar 27 13:37:07 2026 -0700

    feat(cli): add --dry-run flag to generate command
    
    Parses the spec and prints a summary (resource/endpoint counts, output
    dir) without writing files, running go mod tidy, or quality gates.
    Emits structured JSON to stdout for programmatic use. Rejects --docs
    combined with --dry-run since doc scraping is inherently side-effectful.
    Skips cache writes for remote specs in dry-run mode.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
 internal/cli/root.go | 63 +++++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 53 insertions(+), 10 deletions(-)

diff --git a/internal/cli/root.go b/internal/cli/root.go
index 1a207e58..a3c445bd 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -57,11 +57,15 @@ func newGenerateCmd() *cobra.Command {
 	var docsURL string
 	var polish bool
 	var asJSON bool
+	var dryRun bool
 
 	cmd := &cobra.Command{
 		Use:   "generate",
 		Short: "Generate a Go CLI project from an API spec",
 		RunE: func(cmd *cobra.Command, args []string) error {
+			if dryRun && docsURL != "" {
+				return fmt.Errorf("--dry-run cannot be used with --docs (doc scraping has unavoidable side effects)")
+			}
 			if docsURL != "" {
 				apiName := cliName
 				if apiName == "" {
@@ -154,7 +158,7 @@ func newGenerateCmd() *cobra.Command {
 
 			var specs []*spec.APISpec
 			for _, specFile := range specFiles {
-				data, err := readSpec(specFile, refresh)
+				data, err := readSpec(specFile, refresh, dryRun)
 				if err != nil {
 					return fmt.Errorf("reading spec %s: %w", specFile, err)
 				}
@@ -196,6 +200,9 @@ func newGenerateCmd() *cobra.Command {
 			if err != nil {
 				return fmt.Errorf("resolving output path: %w", err)
 			}
+			if dryRun {
+				return printDryRun(apiSpec, absOut, specFiles)
+			}
 			if force {
 				if err := os.RemoveAll(absOut); err != nil {
 					return fmt.Errorf("removing existing output dir: %w", err)
@@ -254,13 +261,14 @@ func newGenerateCmd() *cobra.Command {
 	cmd.Flags().StringVar(&docsURL, "docs", "", "API documentation URL to generate spec from")
 	cmd.Flags().BoolVar(&polish, "polish", false, "Run LLM polish pass on generated CLI (requires claude or codex CLI)")
 	cmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON")
+	cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Parse spec and show what would be generated without writing files (remote specs are still fetched)")
 
 	return cmd
 }
 
-func readSpec(specFile string, refresh bool) ([]byte, error) {
+func readSpec(specFile string, refresh bool, skipCache bool) ([]byte, error) {
 	if strings.HasPrefix(specFile, "http://") || strings.HasPrefix(specFile, "https://") {
-		return fetchOrCacheSpec(specFile, refresh)
+		return fetchOrCacheSpec(specFile, refresh, skipCache)
 	}
 
 	return os.ReadFile(specFile)
@@ -311,7 +319,7 @@ func mergeSpecs(specs []*spec.APISpec, name string) *spec.APISpec {
 	return merged
 }
 
-func fetchOrCacheSpec(specURL string, refresh bool) ([]byte, error) {
+func fetchOrCacheSpec(specURL string, refresh bool, skipCache bool) ([]byte, error) {
 	sum := sha256.Sum256([]byte(specURL))
 	cacheKey := hex.EncodeToString(sum[:])
 
@@ -321,11 +329,9 @@ func fetchOrCacheSpec(specURL string, refresh bool) ([]byte, error) {
 	}
 
 	cacheDir := filepath.Join(homeDir, ".cache", "printing-press", "specs")
-	if err := os.MkdirAll(cacheDir, 0o755); err != nil {
-		return nil, fmt.Errorf("creating cache directory: %w", err)
-	}
-
 	cachePath := filepath.Join(cacheDir, cacheKey+".json")
+
+	// Read from existing cache even in dry-run mode (no writes needed)
 	if !refresh {
 		info, err := os.Stat(cachePath)
 		switch {
@@ -357,8 +363,13 @@ func fetchOrCacheSpec(specURL string, refresh bool) ([]byte, error) {
 		return nil, fmt.Errorf("reading response body: %w", err)
 	}
 
-	if err := os.WriteFile(cachePath, data, 0o644); err != nil {
-		return nil, fmt.Errorf("writing cached spec: %w", err)
+	if !skipCache {
+		if err := os.MkdirAll(cacheDir, 0o755); err != nil {
+			return nil, fmt.Errorf("creating cache directory: %w", err)
+		}
+		if err := os.WriteFile(cachePath, data, 0o644); err != nil {
+			return nil, fmt.Errorf("writing cached spec: %w", err)
+		}
 	}
 
 	return data, nil
@@ -435,3 +446,35 @@ func countCompletedPhases(state *pipeline.PipelineState) int {
 	}
 	return n
 }
+
+func printDryRun(apiSpec *spec.APISpec, absOut string, specFiles []string) error {
+	resourceCount := 0
+	endpointCount := 0
+	for _, r := range apiSpec.Resources {
+		resourceCount++
+		endpointCount += len(r.Endpoints)
+		for _, sub := range r.SubResources {
+			resourceCount++
+			endpointCount += len(sub.Endpoints)
+		}
+	}
+
+	fmt.Fprintf(os.Stderr, "Dry run — spec parsed, no files will be generated\n")
+	fmt.Fprintf(os.Stderr, "  Spec files: %s\n", strings.Join(specFiles, ", "))
+	fmt.Fprintf(os.Stderr, "  API name:   %s\n", apiSpec.Name)
+	fmt.Fprintf(os.Stderr, "  Output dir: %s\n", absOut)
+	fmt.Fprintf(os.Stderr, "  Resources:  %d\n", resourceCount)
+	fmt.Fprintf(os.Stderr, "  Endpoints:  %d\n", endpointCount)
+
+	summary := map[string]any{
+		"dry_run":        true,
+		"name":           apiSpec.Name,
+		"output_dir":     absOut,
+		"spec_files":     specFiles,
+		"resource_count": resourceCount,
+		"endpoint_count": endpointCount,
+	}
+	enc := json.NewEncoder(os.Stdout)
+	enc.SetIndent("", "  ")
+	return enc.Encode(summary)
+}

← 7f5c3520 feat(cli): add --json flag to generate, print, and vision co  ·  back to Cli Printing Press  ·  feat(cli): differentiate exit codes by failure type 538e65c4 →