[object Object]

← back to Cli Printing Press

fix(cli): decouple CLI version from API version

68bc76f3ade620d696c08f60f9b8e738e7d9af65 · 2026-04-11 12:52:01 -0700 · Trevin Chow

Generated CLIs now always start at version 1.0.0 regardless of the
API spec's info.version. The API version is stored in the manifest's
api_version field for provenance only.

Previously the generator copied the spec version into the CLI's
--version output, causing confusion: a CLI at "4.4" looked mature
but was actually a fresh generation that inherited the API's version.

- Templates: hardcode `var version = "1.0.0"` instead of `{{.Version}}`
- Manifest: new `api_version` field preserves the spec version
- Validate: removed version normalization (no longer needed)
- Tests: updated to reflect pass-through behavior

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

Files touched

Diff

commit 68bc76f3ade620d696c08f60f9b8e738e7d9af65
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Sat Apr 11 12:52:01 2026 -0700

    fix(cli): decouple CLI version from API version
    
    Generated CLIs now always start at version 1.0.0 regardless of the
    API spec's info.version. The API version is stored in the manifest's
    api_version field for provenance only.
    
    Previously the generator copied the spec version into the CLI's
    --version output, causing confusion: a CLI at "4.4" looked mature
    but was actually a fresh generation that inherited the API's version.
    
    - Templates: hardcode `var version = "1.0.0"` instead of `{{.Version}}`
    - Manifest: new `api_version` field preserves the spec version
    - Validate: removed version normalization (no longer needed)
    - Tests: updated to reflect pass-through behavior
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
 internal/generator/templates/plan_root.go.tmpl |  2 +-
 internal/generator/templates/root.go.tmpl      |  2 +-
 internal/pipeline/climanifest.go               |  6 ++++++
 internal/spec/spec.go                          | 13 +++----------
 internal/spec/spec_test.go                     | 15 ++++++++-------
 5 files changed, 19 insertions(+), 19 deletions(-)

diff --git a/internal/generator/templates/plan_root.go.tmpl b/internal/generator/templates/plan_root.go.tmpl
index b37a3da5..10b2889b 100644
--- a/internal/generator/templates/plan_root.go.tmpl
+++ b/internal/generator/templates/plan_root.go.tmpl
@@ -10,7 +10,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
-var version = "{{.Version}}"
+var version = "1.0.0"
 
 // Execute runs the CLI.
 func Execute() error {
diff --git a/internal/generator/templates/root.go.tmpl b/internal/generator/templates/root.go.tmpl
index 7ee7dfa4..508ddbec 100644
--- a/internal/generator/templates/root.go.tmpl
+++ b/internal/generator/templates/root.go.tmpl
@@ -15,7 +15,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
-var version = "{{.Version}}"
+var version = "1.0.0"
 
 type rootFlags struct {
 	asJSON       bool
diff --git a/internal/pipeline/climanifest.go b/internal/pipeline/climanifest.go
index 61bac45a..1e8bd9dc 100644
--- a/internal/pipeline/climanifest.go
+++ b/internal/pipeline/climanifest.go
@@ -48,6 +48,7 @@ type CLIManifest struct {
 	MCPToolCount       int                    `json:"mcp_tool_count,omitempty"`
 	MCPPublicToolCount int                    `json:"mcp_public_tool_count,omitempty"`
 	MCPReady           string                 `json:"mcp_ready,omitempty"`
+	APIVersion         string                 `json:"api_version,omitempty"` // from the spec's info.version — provenance only, not the CLI version
 	AuthType           string                 `json:"auth_type,omitempty"`
 	AuthEnvVars        []string               `json:"auth_env_vars,omitempty"`
 	NovelFeatures      []NovelFeatureManifest `json:"novel_features,omitempty"`
@@ -198,6 +199,11 @@ func WriteManifestForGenerate(p GenerateManifestParams) error {
 		m.Description = entry.Description
 	}
 
+	// Record the API version from the spec for provenance (not the CLI version).
+	if p.Spec != nil && p.Spec.Version != "" {
+		m.APIVersion = p.Spec.Version
+	}
+
 	// Populate MCP metadata from the parsed spec.
 	if p.Spec != nil {
 		populateMCPMetadata(&m, p.Spec)
diff --git a/internal/spec/spec.go b/internal/spec/spec.go
index e7abde8b..b9347eb3 100644
--- a/internal/spec/spec.go
+++ b/internal/spec/spec.go
@@ -274,16 +274,9 @@ func (s *APISpec) Validate() error {
 	if s.Name == "" {
 		return fmt.Errorf("name is required")
 	}
-	// Default version to 1.0.0 if missing, and normalize to semver.
-	if s.Version == "" {
-		s.Version = "1.0.0"
-	} else if !strings.Contains(s.Version, ".") {
-		// Bare major: "4" → "4.0.0"
-		s.Version = s.Version + ".0.0"
-	} else if strings.Count(s.Version, ".") == 1 {
-		// Major.minor only: "4.4" → "4.4.0"
-		s.Version = s.Version + ".0"
-	}
+	// Note: s.Version holds the API version from the spec (for provenance).
+	// The CLI version is always hardcoded to "1.0.0" in the generated root.go
+	// template — it is independent of the API version.
 	// Parser fallback may supply a placeholder base_url when the source spec omits servers.
 	if s.BaseURL == "" && s.BasePath == "" {
 		return fmt.Errorf("base_url is required")
diff --git a/internal/spec/spec_test.go b/internal/spec/spec_test.go
index 693320de..95537e7d 100644
--- a/internal/spec/spec_test.go
+++ b/internal/spec/spec_test.go
@@ -87,7 +87,7 @@ func TestValidation(t *testing.T) {
 	}
 }
 
-func TestVersionDefaultAndNormalize(t *testing.T) {
+func TestVersionPassedThrough(t *testing.T) {
 	base := func(v string) APISpec {
 		return APISpec{
 			Name:      "x",
@@ -97,16 +97,17 @@ func TestVersionDefaultAndNormalize(t *testing.T) {
 		}
 	}
 
+	// Version is the API version (provenance only). It passes through as-is.
+	// The CLI version is hardcoded to "1.0.0" in the generated root.go template.
 	tests := []struct {
 		input    string
 		expected string
 	}{
-		{"", "1.0.0"},        // empty → default
-		{"1.0.0", "1.0.0"},   // already semver
-		{"4", "4.0.0"},       // bare major
-		{"4.4", "4.4.0"},     // major.minor only
-		{"4.17.1", "4.17.1"}, // already semver
-		{"0.1.0", "0.1.0"},   // already semver
+		{"", ""},             // empty stays empty
+		{"1.0.0", "1.0.0"},   // semver preserved
+		{"4", "4"},           // non-semver API versions preserved
+		{"4.4", "4.4"},       // major.minor preserved
+		{"4.17.1", "4.17.1"}, // full semver preserved
 	}
 
 	for _, tt := range tests {

← f07a795d fix(cli): default empty version to 1.0.0 and normalize to se  ·  back to Cli Printing Press  ·  fix(ci): auto-sync go install when installed binary exists 2e21807d →