[object Object]

← back to Cli Printing Press

fix(cli): preserve manifest fields during dogfood sync (#844)

2d9a304a509f427226d08cbf567f0a7aea5c5df3 · 2026-05-09 13:09:12 -0700 · Trevin Chow

Files touched

Diff

commit 2d9a304a509f427226d08cbf567f0a7aea5c5df3
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Sat May 9 13:09:12 2026 -0700

    fix(cli): preserve manifest fields during dogfood sync (#844)
---
 internal/pipeline/climanifest.go      | 127 +++++++++++++++++++++++++++++++++-
 internal/pipeline/climanifest_test.go |  63 +++++++++++++++++
 2 files changed, 189 insertions(+), 1 deletion(-)

diff --git a/internal/pipeline/climanifest.go b/internal/pipeline/climanifest.go
index 637fa33c..00473c5d 100644
--- a/internal/pipeline/climanifest.go
+++ b/internal/pipeline/climanifest.go
@@ -1,11 +1,13 @@
 package pipeline
 
 import (
+	"bytes"
 	"crypto/sha256"
 	"encoding/hex"
 	"encoding/json"
 	"errors"
 	"fmt"
+	"maps"
 	"os"
 	"path/filepath"
 	"reflect"
@@ -213,7 +215,130 @@ func SyncCLIManifestNovelFeatures(dir string, features []NovelFeature) (bool, er
 	}
 	m.NovelFeatures = updated
 
-	return true, WriteCLIManifest(dir, m)
+	var raw map[string]json.RawMessage
+	if err := json.Unmarshal(data, &raw); err != nil {
+		return false, fmt.Errorf("parsing CLI manifest for raw update: %w", err)
+	}
+	if raw == nil {
+		return false, fmt.Errorf("parsing CLI manifest for raw update: expected JSON object")
+	}
+	known, err := marshalCLIManifestFields(m)
+	if err != nil {
+		return false, err
+	}
+	maps.Copy(raw, known)
+	rendered, err := marshalCLIManifestObject(raw)
+	if err != nil {
+		return false, err
+	}
+	if err := os.WriteFile(manifestPath, rendered, 0o644); err != nil {
+		return false, fmt.Errorf("writing CLI manifest: %w", err)
+	}
+
+	return true, nil
+}
+
+func marshalCLIManifestFields(m CLIManifest) (map[string]json.RawMessage, error) {
+	data, err := json.Marshal(m)
+	if err != nil {
+		return nil, fmt.Errorf("marshaling CLI manifest fields: %w", err)
+	}
+	var raw map[string]json.RawMessage
+	if err := json.Unmarshal(data, &raw); err != nil {
+		return nil, fmt.Errorf("parsing CLI manifest fields: %w", err)
+	}
+	return raw, nil
+}
+
+func marshalCLIManifestObject(raw map[string]json.RawMessage) ([]byte, error) {
+	keys := orderedCLIManifestKeys(raw)
+	var b strings.Builder
+	b.WriteString("{\n")
+	for i, key := range keys {
+		name, err := json.Marshal(key)
+		if err != nil {
+			return nil, fmt.Errorf("marshaling CLI manifest key: %w", err)
+		}
+		value, err := formatRawJSONValue(raw[key])
+		if err != nil {
+			return nil, fmt.Errorf("formatting CLI manifest field %q: %w", key, err)
+		}
+		b.WriteString("  ")
+		b.WriteString(string(name))
+		b.WriteString(": ")
+		b.WriteString(value)
+		if i < len(keys)-1 {
+			b.WriteByte(',')
+		}
+		b.WriteByte('\n')
+	}
+	b.WriteString("}\n")
+	return []byte(b.String()), nil
+}
+
+func orderedCLIManifestKeys(raw map[string]json.RawMessage) []string {
+	known := []string{
+		"schema_version",
+		"generated_at",
+		"printing_press_version",
+		"api_name",
+		"display_name",
+		"cli_name",
+		"owner",
+		"printer",
+		"printer_name",
+		"spec_url",
+		"spec_path",
+		"spec_format",
+		"spec_checksum",
+		"run_id",
+		"catalog_entry",
+		"category",
+		"description",
+		"mcp_binary",
+		"mcp_tool_count",
+		"mcp_public_tool_count",
+		"mcp_ready",
+		"api_version",
+		"auth_type",
+		"auth_env_vars",
+		"auth_env_var_specs",
+		"endpoint_template_vars",
+		"auth_key_url",
+		"auth_title",
+		"auth_description",
+		"auth_optional",
+		"novel_features",
+	}
+
+	keys := make([]string, 0, len(raw))
+	seen := make(map[string]bool, len(raw))
+	for _, key := range known {
+		if _, ok := raw[key]; ok {
+			keys = append(keys, key)
+			seen[key] = true
+		}
+	}
+	var unknown []string
+	for key := range raw {
+		if !seen[key] {
+			unknown = append(unknown, key)
+		}
+	}
+	sort.Strings(unknown)
+	return append(keys, unknown...)
+}
+
+func formatRawJSONValue(raw json.RawMessage) (string, error) {
+	var buf bytes.Buffer
+	if err := json.Indent(&buf, raw, "", "  "); err != nil {
+		return "", err
+	}
+	lines := strings.Split(buf.String(), "\n")
+	for i := 1; i < len(lines); i++ {
+		lines[i] = "  " + lines[i]
+	}
+	return strings.Join(lines, "\n"), nil
 }
 
 // findArchivedSpec looks for a spec file archived alongside a generated CLI.
diff --git a/internal/pipeline/climanifest_test.go b/internal/pipeline/climanifest_test.go
index b15b1613..fce21ea7 100644
--- a/internal/pipeline/climanifest_test.go
+++ b/internal/pipeline/climanifest_test.go
@@ -114,6 +114,69 @@ func TestWriteCLIManifestNonexistentDir(t *testing.T) {
 	assert.Error(t, err)
 }
 
+func TestSyncCLIManifestNovelFeaturesPreservesManifestContract(t *testing.T) {
+	dir := t.TempDir()
+	manifest := []byte(`{
+  "schema_version": 1,
+  "generated_at": "2026-05-09T17:28:02Z",
+  "printing_press_version": "4.2.0",
+  "api_name": "openrouter",
+  "display_name": "OpenRouter",
+  "cli_name": "openrouter-pp-cli",
+  "printer": "rvdlaar",
+  "printer_name": "Rick van de Laar",
+  "spec_url": "https://example.com/openapi.json",
+  "category": "ai",
+  "description": "Access OpenRouter models.",
+  "x_future_manifest_field": {
+    "keep": true
+  },
+  "novel_features": [
+    {
+      "name": "Old",
+      "command": "old",
+      "description": "Old feature."
+    }
+  ]
+}
+`)
+	require.NoError(t, os.WriteFile(filepath.Join(dir, CLIManifestFilename), manifest, 0o644))
+
+	changed, err := SyncCLIManifestNovelFeatures(dir, []NovelFeature{
+		{Name: "Model finder", Command: "models find", Description: "Find a model for a prompt."},
+	})
+	require.NoError(t, err)
+	assert.True(t, changed)
+
+	data, err := os.ReadFile(filepath.Join(dir, CLIManifestFilename))
+	require.NoError(t, err)
+
+	var got map[string]any
+	require.NoError(t, json.Unmarshal(data, &got))
+	assert.Equal(t, float64(1), got["schema_version"])
+	assert.Equal(t, "4.2.0", got["printing_press_version"])
+	assert.Equal(t, "openrouter", got["api_name"])
+	assert.Equal(t, "openrouter-pp-cli", got["cli_name"])
+	assert.Equal(t, "rvdlaar", got["printer"])
+	assert.Equal(t, "Rick van de Laar", got["printer_name"])
+	assert.Equal(t, "https://example.com/openapi.json", got["spec_url"])
+	assert.Equal(t, "ai", got["category"])
+	assert.Equal(t, "Access OpenRouter models.", got["description"])
+
+	future, ok := got["x_future_manifest_field"].(map[string]any)
+	require.True(t, ok)
+	assert.Equal(t, true, future["keep"])
+
+	features, ok := got["novel_features"].([]any)
+	require.True(t, ok)
+	require.Len(t, features, 1)
+	feature, ok := features[0].(map[string]any)
+	require.True(t, ok)
+	assert.Equal(t, "Model finder", feature["name"])
+	assert.Equal(t, "models find", feature["command"])
+	assert.Equal(t, "Find a model for a prompt.", feature["description"])
+}
+
 func TestSpecChecksum(t *testing.T) {
 	dir := t.TempDir()
 	content := []byte(`{"openapi": "3.0.0"}`)

← 117544d0 Ignore .omx workspace folders (#841)  ·  back to Cli Printing Press  ·  docs(cli): add Cursor guide for printed CLIs (#833) f34645a8 →