[object Object]

← back to Cli Printing Press

feat(cli): accept URLs for --spec with local caching

2e70bc46b259e0b799d5052b306bb52cd489c745 · 2026-03-23 23:55:19 -0700 · Matt Van Horn

printing-press generate --spec https://example.com/openapi.json now works.
Specs are cached at ~/.cache/printing-press/specs/<sha256>.json for 24h.
--refresh flag forces re-download.

Files touched

Diff

commit 2e70bc46b259e0b799d5052b306bb52cd489c745
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date:   Mon Mar 23 23:55:19 2026 -0700

    feat(cli): accept URLs for --spec with local caching
    
    printing-press generate --spec https://example.com/openapi.json now works.
    Specs are cached at ~/.cache/printing-press/specs/<sha256>.json for 24h.
    --refresh flag forces re-download.
---
 internal/cli/root.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 75 insertions(+), 3 deletions(-)

diff --git a/internal/cli/root.go b/internal/cli/root.go
index 2bb2f109..66d66767 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -1,9 +1,15 @@
 package cli
 
 import (
+	"crypto/sha256"
+	"encoding/hex"
 	"fmt"
+	"io"
+	"net/http"
 	"os"
 	"path/filepath"
+	"strings"
+	"time"
 
 	"github.com/mvanhorn/cli-printing-press/internal/generator"
 	"github.com/mvanhorn/cli-printing-press/internal/openapi"
@@ -33,6 +39,7 @@ func newGenerateCmd() *cobra.Command {
 	var specFile string
 	var outputDir string
 	var validate bool
+	var refresh bool
 
 	cmd := &cobra.Command{
 		Use:   "generate",
@@ -42,9 +49,20 @@ func newGenerateCmd() *cobra.Command {
 				return fmt.Errorf("--spec is required")
 			}
 
-			data, err := os.ReadFile(specFile)
-			if err != nil {
-				return fmt.Errorf("reading spec file: %w", err)
+			var (
+				data []byte
+				err  error
+			)
+			if strings.HasPrefix(specFile, "http://") || strings.HasPrefix(specFile, "https://") {
+				data, err = fetchOrCacheSpec(specFile, refresh)
+				if err != nil {
+					return fmt.Errorf("fetching spec from URL: %w", err)
+				}
+			} else {
+				data, err = os.ReadFile(specFile)
+				if err != nil {
+					return fmt.Errorf("reading spec file: %w", err)
+				}
 			}
 
 			var apiSpec *spec.APISpec
@@ -84,10 +102,64 @@ func newGenerateCmd() *cobra.Command {
 	cmd.Flags().StringVar(&specFile, "spec", "", "Path to API spec (internal YAML or OpenAPI 3.0+)")
 	cmd.Flags().StringVar(&outputDir, "output", "", "Output directory (default: <name>-cli)")
 	cmd.Flags().BoolVar(&validate, "validate", true, "Run quality gates on the generated project")
+	cmd.Flags().BoolVar(&refresh, "refresh", false, "Refresh cached remote spec before generating")
 
 	return cmd
 }
 
+func fetchOrCacheSpec(specURL string, refresh bool) ([]byte, error) {
+	sum := sha256.Sum256([]byte(specURL))
+	cacheKey := hex.EncodeToString(sum[:])
+
+	homeDir, err := os.UserHomeDir()
+	if err != nil {
+		return nil, fmt.Errorf("finding user home directory: %w", err)
+	}
+
+	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")
+	if !refresh {
+		info, err := os.Stat(cachePath)
+		switch {
+		case err == nil && time.Since(info.ModTime()) < 24*time.Hour:
+			fmt.Fprintf(os.Stderr, "Using cached spec for %s\n", specURL)
+			data, readErr := os.ReadFile(cachePath)
+			if readErr != nil {
+				return nil, fmt.Errorf("reading cached spec: %w", readErr)
+			}
+			return data, nil
+		case err != nil && !os.IsNotExist(err):
+			return nil, fmt.Errorf("checking cached spec: %w", err)
+		}
+	}
+
+	fmt.Fprintf(os.Stderr, "Fetching spec from %s...\n", specURL)
+	resp, err := http.Get(specURL)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
+		return nil, fmt.Errorf("unexpected response status: %s", resp.Status)
+	}
+
+	data, err := io.ReadAll(resp.Body)
+	if err != nil {
+		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)
+	}
+
+	return data, nil
+}
+
 func newVersionCmd() *cobra.Command {
 	return &cobra.Command{
 		Use:   "version",

← 3cb6e869 feat(generator): add color and TTY detection to generated CL  ·  back to Cli Printing Press  ·  feat(generator): OAuth2 auth flow for generated CLIs dacda31f →