← 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 →