[object Object]

← back to Cli Printing Press

fix(cli): align generated install and rename metadata (#749)

ca580efc6ad47f0f4678b9a54fb1773c7efb33b7 · 2026-05-08 14:49:15 -0700 · Trevin Chow

Files touched

Diff

commit ca580efc6ad47f0f4678b9a54fb1773c7efb33b7
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Fri May 8 14:49:15 2026 -0700

    fix(cli): align generated install and rename metadata (#749)
---
 internal/cli/publish.go                            |  17 ++--
 internal/cli/publish_test.go                       |  11 ++-
 internal/generator/generator_test.go               |   3 +-
 internal/generator/install_section.go              |  37 +++++---
 internal/generator/install_section_test.go         |  23 +++++
 internal/generator/skill_test.go                   |  13 +--
 internal/generator/templates/readme.md.tmpl        |  20 +++-
 internal/generator/templates/skill.md.tmpl         |  25 +++--
 internal/pipeline/renamecli.go                     |  78 ++++++++++++++--
 internal/pipeline/renamecli_test.go                | 101 ++++++++++++++++++++-
 skills/printing-press-publish/SKILL.md             |   1 -
 .../printing-press-rich-auth/README.md             |  19 ++--
 .../printing-press-rich-auth/SKILL.md              |  26 ++----
 .../printing-press-golden/README.md                |  19 ++--
 .../printing-press-golden/SKILL.md                 |  26 ++----
 .../public-param-golden/README.md                  |  19 ++--
 .../public-param-golden/SKILL.md                   |  26 ++----
 17 files changed, 312 insertions(+), 152 deletions(-)

diff --git a/internal/cli/publish.go b/internal/cli/publish.go
index cfc40deb..fa92d50e 100644
--- a/internal/cli/publish.go
+++ b/internal/cli/publish.go
@@ -84,14 +84,13 @@ func newPublishRenameCmd() *cobra.Command {
 	var dir string
 	var oldName string
 	var newName string
-	var apiName string
+	var legacyAPIName string
 	var asJSON bool
 
 	cmd := &cobra.Command{
-		Use:   "rename",
-		Short: "Rename a staged CLI (for name collision resolution)",
-		Example: `  printing-press publish rename --dir /tmp/staging/library/ai/notion --old-name notion-pp-cli --new-name notion-alt-pp-cli --json
-  printing-press publish rename --dir /tmp/staging/library/ai/notion --old-name notion-pp-cli --new-name notion-2-pp-cli --api-name notion --json`,
+		Use:     "rename",
+		Short:   "Rename a staged CLI (for name collision resolution)",
+		Example: `  printing-press publish rename --dir /tmp/staging/library/ai/notion --old-name notion-pp-cli --new-name notion-alt-pp-cli --json`,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			if dir == "" {
 				return &ExitError{Code: ExitInputError, Err: fmt.Errorf("--dir is required")}
@@ -102,11 +101,7 @@ func newPublishRenameCmd() *cobra.Command {
 			if newName == "" {
 				return &ExitError{Code: ExitInputError, Err: fmt.Errorf("--new-name is required")}
 			}
-			if apiName == "" {
-				apiName = naming.TrimCLISuffix(oldName)
-			}
-
-			filesModified, err := pipeline.RenameCLI(dir, oldName, newName, apiName)
+			filesModified, err := pipeline.RenameCLI(dir, oldName, newName, legacyAPIName)
 
 			if asJSON {
 				result := RenameResult{
@@ -144,7 +139,7 @@ func newPublishRenameCmd() *cobra.Command {
 	cmd.Flags().StringVar(&dir, "dir", "", "Staged CLI directory to rename (required)")
 	cmd.Flags().StringVar(&oldName, "old-name", "", "Current CLI name (required)")
 	cmd.Flags().StringVar(&newName, "new-name", "", "New CLI name (required)")
-	cmd.Flags().StringVar(&apiName, "api-name", "", "Original API name (defaults to TrimCLISuffix of old-name)")
+	cmd.Flags().StringVar(&legacyAPIName, "api-name", "", "Deprecated no-op; api_name now follows --new-name")
 	cmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON")
 
 	return cmd
diff --git a/internal/cli/publish_test.go b/internal/cli/publish_test.go
index b97bdc08..4314b5b7 100644
--- a/internal/cli/publish_test.go
+++ b/internal/cli/publish_test.go
@@ -780,7 +780,7 @@ func main() {}
 	assert.Greater(t, result.FilesModified, 0)
 }
 
-func TestPublishRenameAPINameFallback(t *testing.T) {
+func TestPublishRenameAPINameTracksNewName(t *testing.T) {
 	root := t.TempDir()
 	oldName := "test-pp-cli"
 	newName := "test-alt-pp-cli"
@@ -794,8 +794,9 @@ func TestPublishRenameAPINameFallback(t *testing.T) {
 	})
 
 	cmd := newPublishCmd()
-	// No --api-name flag — should fall back to TrimCLISuffix("test-pp-cli") = "test"
-	cmd.SetArgs([]string{"rename", "--dir", cliDir, "--old-name", oldName, "--new-name", newName, "--json"})
+	// The legacy flag is accepted for old callers but no longer controls
+	// metadata; the final public slug follows --new-name.
+	cmd.SetArgs([]string{"rename", "--dir", cliDir, "--old-name", oldName, "--new-name", newName, "--api-name", "test", "--json"})
 
 	output, err := runWithCapturedStdout(t, cmd.Execute)
 	require.NoError(t, err)
@@ -804,13 +805,13 @@ func TestPublishRenameAPINameFallback(t *testing.T) {
 	require.NoError(t, json.Unmarshal([]byte(output), &result))
 	assert.True(t, result.Success)
 
-	// Verify manifest has correct api_name from fallback
+	// Verify manifest has the final public API slug after rename.
 	newDir := filepath.Join(root, naming.LibraryDirName(newName))
 	mData, err := os.ReadFile(filepath.Join(newDir, pipeline.CLIManifestFilename))
 	require.NoError(t, err)
 	var m pipeline.CLIManifest
 	require.NoError(t, json.Unmarshal(mData, &m))
-	assert.Equal(t, "test", m.APIName, "api_name should come from TrimCLISuffix fallback")
+	assert.Equal(t, "test-alt", m.APIName, "api_name should track the final public slug")
 	assert.Equal(t, newName, m.CLIName)
 }
 
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 16d4ba34..a2723862 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -1888,7 +1888,8 @@ func TestGenerateWithOwnerField(t *testing.T) {
 	assert.Contains(t, string(mainGo), "testowner")
 	readme, err := os.ReadFile(filepath.Join(outputDir, "README.md"))
 	require.NoError(t, err)
-	assert.Contains(t, string(readme), "go install github.com/mvanhorn/printing-press-library/library/other/owned/cmd/owned-pp-cli@latest")
+	assert.Contains(t, string(readme), "npx -y @mvanhorn/printing-press install owned")
+	assert.NotContains(t, string(readme), "library/other/owned")
 }
 
 func TestGenerateWithEmptyOwner(t *testing.T) {
diff --git a/internal/generator/install_section.go b/internal/generator/install_section.go
index b8c223aa..f3d67d99 100644
--- a/internal/generator/install_section.go
+++ b/internal/generator/install_section.go
@@ -16,15 +16,14 @@ const SkillInstallSectionStartHeading = "## Prerequisites: Install the CLI"
 // the template and is not enforced by the canonical-sections check.
 const SkillInstallSectionEndSubstr = "Do not proceed with skill commands until verification succeeds."
 
-// canonicalSkillInstallSectionFormat is the literal text the generator
-// emits into a printed CLI's SKILL.md install section. Indexed verbs:
+// canonicalSkillInstallSectionStartFormat is the literal text the generator
+// emits at the start of a printed CLI's SKILL.md install section. Indexed verb:
 //
 //	%[1]s — CLI slug (e.g. "linear" — produces linear-pp-cli)
-//	%[2]s — catalog category (or "other" when empty)
 //
 // Stays in lockstep with internal/generator/templates/skill.md.tmpl via
 // TestCanonicalSkillInstallSectionMatchesTemplate.
-const canonicalSkillInstallSectionFormat = "## Prerequisites: Install the CLI\n" +
+const canonicalSkillInstallSectionStartFormat = "## Prerequisites: Install the CLI\n" +
 	"\n" +
 	"This skill drives the `%[1]s-pp-cli` binary. **You must verify the CLI is installed before invoking any command from this skill.** If it is missing, install it first:\n" +
 	"\n" +
@@ -34,28 +33,42 @@ const canonicalSkillInstallSectionFormat = "## Prerequisites: Install the CLI\n"
 	"   ```\n" +
 	"2. Verify: `%[1]s-pp-cli --version`\n" +
 	"3. Ensure `$GOPATH/bin` (or `$HOME/go/bin`) is on `$PATH`.\n" +
-	"\n" +
-	"If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.3 or newer):\n" +
+	"\n"
+
+// canonicalSkillInstallSectionGoFallbackFormat is appended only once the
+// catalog category is known. Before publish, the category-agnostic installer is
+// the only canonical path; emitting library/other/<slug> creates drift.
+const canonicalSkillInstallSectionGoFallbackFormat = "If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.3 or newer):\n" +
 	"\n" +
 	"```bash\n" +
 	"go install github.com/mvanhorn/printing-press-library/library/%[2]s/%[1]s/cmd/%[1]s-pp-cli@latest\n" +
 	"```\n" +
-	"\n" +
-	"If `--version` reports \"command not found\" after install, the install step did not put the binary on `$PATH`. Do not proceed with skill commands until verification succeeds.\n"
+	"\n"
+
+const canonicalSkillInstallSectionPrepublishFallback = "If the `npx` install fails before this CLI has a public-library category, install Node or use the category-specific Go fallback after publish.\n" +
+	"\n"
+
+const canonicalSkillInstallSectionEnd = "If `--version` reports \"command not found\" after install, the install step did not put the binary on `$PATH`. Do not proceed with skill commands until verification succeeds.\n"
 
 // CanonicalSkillInstallSection returns the exact text of the install/
 // prerequisites section that the generator emits into a printed CLI's
-// SKILL.md, given the CLI slug and catalog category (empty -> "other").
+// SKILL.md, given the CLI slug and catalog category. A blank category emits
+// only the category-agnostic installer path so generate-time output does not
+// bake in the publish-time placeholder category.
 //
 // The verify-skill canonical-sections check uses this function to detect
 // post-publish edits to the install instructions. The function is the
 // authoritative source post-generation; the template stays in sync via
 // TestCanonicalSkillInstallSectionMatchesTemplate.
 func CanonicalSkillInstallSection(name, category string) string {
-	if category == "" {
-		category = "other"
+	section := fmt.Sprintf(canonicalSkillInstallSectionStartFormat, name)
+	if category != "" {
+		section += fmt.Sprintf(canonicalSkillInstallSectionGoFallbackFormat, name, category)
+	} else {
+		section += canonicalSkillInstallSectionPrepublishFallback
 	}
-	return fmt.Sprintf(canonicalSkillInstallSectionFormat, name, category)
+	section += canonicalSkillInstallSectionEnd
+	return section
 }
 
 // ExtractSkillInstallSection slices the install/prerequisites block out of
diff --git a/internal/generator/install_section_test.go b/internal/generator/install_section_test.go
index e2b80c18..348a8538 100644
--- a/internal/generator/install_section_test.go
+++ b/internal/generator/install_section_test.go
@@ -64,6 +64,29 @@ func TestCanonicalSkillInstallSectionMatchesTemplate(t *testing.T) {
 	}
 }
 
+func TestCategorylessInstallSectionsAvoidOtherLibraryPath(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("gohighlevel")
+	outputDir := filepath.Join(t.TempDir(), "gohighlevel-pp-cli")
+	require.NoError(t, New(apiSpec, outputDir).Generate())
+
+	for _, filename := range []string{"SKILL.md", "README.md"} {
+		t.Run(filename, func(t *testing.T) {
+			t.Parallel()
+
+			rendered, err := os.ReadFile(filepath.Join(outputDir, filename))
+			require.NoError(t, err)
+
+			content := string(rendered)
+			require.NotContains(t, content, "library/other/gohighlevel",
+				"category-less generation must not bake in a publish-time placeholder category")
+			require.Contains(t, content, "npx -y @mvanhorn/printing-press install gohighlevel",
+				"category-less generation should keep the category-agnostic installer path")
+		})
+	}
+}
+
 // TestExtractSkillInstallSectionMissingStart confirms the extractor
 // reports ok=false when the canonical heading is missing — the case
 // where an agent has rewritten the section into something unrecognizable.
diff --git a/internal/generator/skill_test.go b/internal/generator/skill_test.go
index ce775ab5..f21b35a6 100644
--- a/internal/generator/skill_test.go
+++ b/internal/generator/skill_test.go
@@ -600,9 +600,9 @@ func TestSkillFrontmatterOmitsAllEnvVarDeclarations(t *testing.T) {
 	}
 }
 
-// TestSkillFrontmatterMetadataDefaultsCategoryToOther asserts that when the
-// spec has no Category set, the install module path falls back to 'other'.
-func TestSkillFrontmatterMetadataDefaultsCategoryToOther(t *testing.T) {
+// TestSkillFrontmatterMetadataOmitsUnknownCategoryInstall asserts that when the
+// spec has no Category set, generated install metadata stays category-agnostic.
+func TestSkillFrontmatterMetadataOmitsUnknownCategoryInstall(t *testing.T) {
 	t.Parallel()
 
 	apiSpec := minimalSpec("uncategorized")
@@ -615,9 +615,10 @@ func TestSkillFrontmatterMetadataDefaultsCategoryToOther(t *testing.T) {
 	require.NoError(t, err)
 	content := string(skill)
 
-	assert.Contains(t, content,
-		"module: github.com/mvanhorn/printing-press-library/library/other/uncategorized/cmd/uncategorized-pp-cli",
-		"empty Category should default to 'other' in install module path")
+	assert.NotContains(t, content, "library/other/uncategorized",
+		"empty Category should not bake a placeholder category into install metadata")
+	assert.Contains(t, content, "npx -y @mvanhorn/printing-press install uncategorized --cli-only",
+		"empty Category should keep the category-agnostic installer path")
 }
 
 // TestSkillNoExtraCommandsIsBackwardCompatible asserts the template emits
diff --git a/internal/generator/templates/readme.md.tmpl b/internal/generator/templates/readme.md.tmpl
index b246711f..ed0376ef 100644
--- a/internal/generator/templates/readme.md.tmpl
+++ b/internal/generator/templates/readme.md.tmpl
@@ -30,15 +30,21 @@ For CLI only (no skill):
 npx -y @mvanhorn/printing-press install {{.Name}} --cli-only
 ```
 
+{{ if .Category}}
 ### Without Node (Go fallback)
 
 If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.26.3 or newer):
 
 ```bash
-go install github.com/mvanhorn/printing-press-library/library/{{if .Category}}{{.Category}}{{else}}other{{end}}/{{.Name}}/cmd/{{.Name}}-pp-cli@latest
+go install github.com/mvanhorn/printing-press-library/library/{{.Category}}/{{.Name}}/cmd/{{.Name}}-pp-cli@latest
 ```
 
 This installs the CLI only — no skill.
+{{- else}}
+### Without Node
+
+The generated install path is category-agnostic until this CLI is published. If `npx` is not available before publish, install Node or use the category-specific Go fallback from the public-library entry after publish.
+{{- end}}
 
 ### Pre-built binary
 
@@ -348,9 +354,13 @@ Then invoke `/pp-{{.Name}} <query>` in Claude Code. The skill is the most effici
 
 If you'd rather register this CLI as an MCP server in Claude Code, install the MCP binary first:
 
+{{ if .Category}}
 ```bash
-go install github.com/mvanhorn/printing-press-library/library/{{if .Category}}{{.Category}}{{else}}other{{end}}/{{.Name}}/cmd/{{.Name}}-pp-mcp@latest
+go install github.com/mvanhorn/printing-press-library/library/{{.Category}}/{{.Name}}/cmd/{{.Name}}-pp-mcp@latest
 ```
+{{- else}}
+Install the MCP binary from this CLI's published public-library entry or pre-built release.
+{{- end}}
 
 Then register it:
 {{- if and (eq .Auth.Type "api_key") $canonicalEnvVar}}
@@ -437,9 +447,13 @@ Requires Claude Desktop 1.0.0 or later. Pre-built bundles ship for macOS Apple S
 
 If you can't use the MCPB bundle (older Claude Desktop, unsupported platform), install the MCP binary and configure it manually.
 
+{{ if .Category}}
 ```bash
-go install github.com/mvanhorn/printing-press-library/library/{{if .Category}}{{.Category}}{{else}}other{{end}}/{{.Name}}/cmd/{{.Name}}-pp-mcp@latest
+go install github.com/mvanhorn/printing-press-library/library/{{.Category}}/{{.Name}}/cmd/{{.Name}}-pp-mcp@latest
 ```
+{{- else}}
+Install the MCP binary from this CLI's published public-library entry or pre-built release.
+{{- end}}
 
 Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
 
diff --git a/internal/generator/templates/skill.md.tmpl b/internal/generator/templates/skill.md.tmpl
index 92d52654..2bd2e9c2 100644
--- a/internal/generator/templates/skill.md.tmpl
+++ b/internal/generator/templates/skill.md.tmpl
@@ -10,10 +10,12 @@ metadata:
     requires:
       bins:
         - {{.Name}}-pp-cli
+{{- if .Category}}
     install:
       - kind: go
         bins: [{{.Name}}-pp-cli]
-        module: github.com/mvanhorn/printing-press-library/library/{{if .Category}}{{.Category}}{{else}}other{{end}}/{{.Name}}/cmd/{{.Name}}-pp-cli
+        module: github.com/mvanhorn/printing-press-library/library/{{.Category}}/{{.Name}}/cmd/{{.Name}}-pp-cli
+{{- end}}
 ---
 
 # {{.ProseName}} — Printing Press CLI
@@ -28,13 +30,15 @@ This skill drives the `{{.Name}}-pp-cli` binary. **You must verify the CLI is in
    ```
 2. Verify: `{{.Name}}-pp-cli --version`
 3. Ensure `$GOPATH/bin` (or `$HOME/go/bin`) is on `$PATH`.
-
+{{ if .Category}}
 If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.3 or newer):
 
 ```bash
-go install github.com/mvanhorn/printing-press-library/library/{{if .Category}}{{.Category}}{{else}}other{{end}}/{{.Name}}/cmd/{{.Name}}-pp-cli@latest
+go install github.com/mvanhorn/printing-press-library/library/{{.Category}}/{{.Name}}/cmd/{{.Name}}-pp-cli@latest
 ```
-
+{{ else}}
+If the `npx` install fails before this CLI has a public-library category, install Node or use the category-specific Go fallback after publish.
+{{ end}}
 If `--version` reports "command not found" after install, the install step did not put the binary on `$PATH`. Do not proceed with skill commands until verification succeeds.
 {{- if and .Narrative .Narrative.ValueProp}}
 
@@ -369,16 +373,25 @@ Parse `$ARGUMENTS`:
 3. **Anything else** → Direct Use (execute as CLI command with `--agent`)
 
 ## MCP Server Installation
-
+{{ if .Category}}
 1. Install the MCP server:
    ```bash
-   go install github.com/mvanhorn/printing-press-library/library/{{if .Category}}{{.Category}}{{else}}other{{end}}/{{.Name}}/cmd/{{.Name}}-pp-mcp@latest
+   go install github.com/mvanhorn/printing-press-library/library/{{.Category}}/{{.Name}}/cmd/{{.Name}}-pp-mcp@latest
    ```
 2. Register with Claude Code:
    ```bash
    claude mcp add {{.Name}}-pp-mcp -- {{.Name}}-pp-mcp
    ```
 3. Verify: `claude mcp list`
+{{- else}}
+Install the MCP binary from this CLI's published public-library entry or pre-built release, then register it:
+
+```bash
+claude mcp add {{.Name}}-pp-mcp -- {{.Name}}-pp-mcp
+```
+
+Verify: `claude mcp list`
+{{- end}}
 
 ## Direct Use
 
diff --git a/internal/pipeline/renamecli.go b/internal/pipeline/renamecli.go
index ab6a670d..731aeb76 100644
--- a/internal/pipeline/renamecli.go
+++ b/internal/pipeline/renamecli.go
@@ -20,18 +20,20 @@ var renameExtensions = []string{".go", ".yaml", ".yml", ".md"}
 //     from newCLIName, and cmd/oldCLIName/ → cmd/newCLIName/
 //   - File content: replaces occurrences of oldCLIName with newCLIName in
 //     .go, .yaml, .yml, .md files and Makefiles (skips .manuscripts/)
-//   - Manifest: updates cli_name to newCLIName, preserves api_name as
-//     originalAPIName
+//   - Metadata: updates .printing-press.json, manifest.json, and
+//     tools-manifest.json to the final public slug/binary names
 //
 // This function does NOT call RewriteModulePath — that handles import
 // paths and is run separately during packaging. RenameCLI handles exactly
 // the user-visible references that RewriteModulePath intentionally skips.
-func RenameCLI(dir, oldCLIName, newCLIName, originalAPIName string) (int, error) {
+func RenameCLI(dir, oldCLIName, newCLIName, _ string) (int, error) {
 	if err := validateRenameInputs(oldCLIName, newCLIName); err != nil {
 		return 0, err
 	}
-	oldMCPName := naming.MCP(naming.TrimCLISuffix(oldCLIName))
-	newMCPName := naming.MCP(naming.TrimCLISuffix(newCLIName))
+	oldSlug := naming.TrimCLISuffix(oldCLIName)
+	newSlug := naming.TrimCLISuffix(newCLIName)
+	oldMCPName := naming.MCP(oldSlug)
+	newMCPName := naming.MCP(newSlug)
 
 	// Path traversal protection: verify the directory and new name resolve
 	// within the expected parent.
@@ -82,8 +84,10 @@ func RenameCLI(dir, oldCLIName, newCLIName, originalAPIName string) (int, error)
 			return fmt.Errorf("reading %s: %w", path, err)
 		}
 
-		result := strings.ReplaceAll(string(content), oldCLIName, newCLIName)
-		result = strings.ReplaceAll(result, oldMCPName, newMCPName)
+		result := renameCLIContent(string(content), oldCLIName, newCLIName, oldMCPName, newMCPName, oldSlug, newSlug)
+		if filepath.Base(path) == ".gitignore" {
+			result = anchorRenamedGitignorePatterns(result, newCLIName, newMCPName)
+		}
 		if result == string(content) {
 			return nil
 		}
@@ -98,22 +102,30 @@ func RenameCLI(dir, oldCLIName, newCLIName, originalAPIName string) (int, error)
 		return filesModified, fmt.Errorf("walking directory: %w", err)
 	}
 
-	// 2. Update manifest: set cli_name, preserve api_name.
+	// 2. Update metadata files from the final public slug/binary names.
 	manifestPath := filepath.Join(absDir, CLIManifestFilename)
 	if manifestData, readErr := os.ReadFile(manifestPath); readErr == nil {
 		var m CLIManifest
 		if jsonErr := json.Unmarshal(manifestData, &m); jsonErr == nil {
 			m.CLIName = newCLIName
-			m.APIName = originalAPIName
+			m.APIName = newSlug
 			if m.MCPBinary != "" {
 				m.MCPBinary = newMCPName
 			}
 			if writeErr := WriteCLIManifest(absDir, m); writeErr != nil {
 				return filesModified, fmt.Errorf("updating manifest: %w", writeErr)
 			}
+			if writeErr := WriteMCPBManifestFromStruct(absDir, m); writeErr != nil {
+				return filesModified, fmt.Errorf("updating MCPB manifest: %w", writeErr)
+			}
 			filesModified++
 		}
 	}
+	if modified, err := updateToolsManifestAPIName(absDir, newSlug); err != nil {
+		return filesModified, err
+	} else if modified {
+		filesModified++
+	}
 
 	// 3. Rename cmd/ subdirectory if it exists.
 	oldCmdDir := filepath.Join(absDir, "cmd", oldCLIName)
@@ -141,11 +153,57 @@ func RenameCLI(dir, oldCLIName, newCLIName, originalAPIName string) (int, error)
 	return filesModified, nil
 }
 
+func renameCLIContent(content, oldCLIName, newCLIName, oldMCPName, newMCPName, oldSlug, newSlug string) string {
+	result := strings.ReplaceAll(content, oldCLIName, newCLIName)
+	result = strings.ReplaceAll(result, oldMCPName, newMCPName)
+	result = strings.ReplaceAll(result, "pp-"+oldSlug, "pp-"+newSlug)
+	result = strings.ReplaceAll(result, "/"+oldSlug+"/cmd/", "/"+newSlug+"/cmd/")
+	return result
+}
+
+func anchorRenamedGitignorePatterns(content, cliName, mcpName string) string {
+	lines := strings.Split(content, "\n")
+	for i, line := range lines {
+		if line == cliName || line == mcpName {
+			lines[i] = "/" + line
+		}
+	}
+	return strings.Join(lines, "\n")
+}
+
+func updateToolsManifestAPIName(dir, apiName string) (bool, error) {
+	path := filepath.Join(dir, ToolsManifestFilename)
+	data, err := os.ReadFile(path)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return false, nil
+		}
+		return false, fmt.Errorf("reading tools manifest: %w", err)
+	}
+	var m ToolsManifest
+	if err := json.Unmarshal(data, &m); err != nil {
+		return false, fmt.Errorf("parsing tools manifest: %w", err)
+	}
+	if m.APIName == apiName {
+		return false, nil
+	}
+	m.APIName = apiName
+	updated, err := json.MarshalIndent(m, "", "  ")
+	if err != nil {
+		return false, fmt.Errorf("marshaling tools manifest: %w", err)
+	}
+	updated = append(updated, '\n')
+	if err := os.WriteFile(path, updated, 0o644); err != nil {
+		return false, fmt.Errorf("writing tools manifest: %w", err)
+	}
+	return true, nil
+}
+
 // shouldRenameFile returns true if a file should be processed during rename.
 // Checks extension (.go, .yaml, .yml, .md) and base name (Makefile).
 func shouldRenameFile(path string) bool {
 	base := filepath.Base(path)
-	if base == "Makefile" {
+	if base == "Makefile" || base == ".gitignore" {
 		return true
 	}
 	for _, ext := range renameExtensions {
diff --git a/internal/pipeline/renamecli_test.go b/internal/pipeline/renamecli_test.go
index ce8b786c..de05cb26 100644
--- a/internal/pipeline/renamecli_test.go
+++ b/internal/pipeline/renamecli_test.go
@@ -111,6 +111,29 @@ CLI for the `+apiName+` API.
 `+"```"+`
 claude mcp add `+apiName+` `+mcpName+`
 `+"```"+`
+
+Install the skill from `+"`cli-skills/pp-"+apiName+"`"+` and install with:
+
+`+"```"+`
+go install github.com/mvanhorn/printing-press-library/library/other/`+apiName+`/cmd/`+cliName+`@latest
+`+"```"+`
+`), 0o644))
+
+	// SKILL.md
+	require.NoError(t, os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte(`---
+name: pp-`+apiName+`
+metadata:
+  openclaw:
+    install:
+      - kind: go
+        module: github.com/mvanhorn/printing-press-library/library/other/`+apiName+`/cmd/`+cliName+`
+---
+
+# `+apiName+`
+
+`+"```bash"+`
+go install github.com/mvanhorn/printing-press-library/library/other/`+apiName+`/cmd/`+cliName+`@latest
+`+"```"+`
 `), 0o644))
 
 	// go.mod (module path uses bare CLI name, as generated CLIs do)
@@ -134,6 +157,39 @@ go 1.24
 	}
 	data, _ := json.MarshalIndent(m, "", "  ")
 	require.NoError(t, os.WriteFile(filepath.Join(dir, CLIManifestFilename), data, 0o644))
+
+	mcpb := MCPBManifest{
+		ManifestVersion: MCPBManifestVersion,
+		Name:            mcpName,
+		Version:         "1.0.0",
+		Description:     apiName + " API surface as MCP tools.",
+		Author:          MCPBAuthor{Name: "CLI Printing Press"},
+		Server: MCPBServer{
+			Type:       "binary",
+			EntryPoint: "bin/" + mcpName,
+			MCPConfig: MCPBLaunchSpec{
+				Command: "${__dirname}/bin/" + mcpName,
+				Args:    []string{},
+			},
+		},
+	}
+	mcpbData, err := json.MarshalIndent(mcpb, "", "  ")
+	require.NoError(t, err)
+	require.NoError(t, os.WriteFile(filepath.Join(dir, MCPBManifestFilename), mcpbData, 0o644))
+
+	toolsManifest := ToolsManifest{
+		APIName:     apiName,
+		BaseURL:     "https://example.com",
+		Description: "tools for " + apiName,
+		MCPReady:    "full",
+		Auth:        ManifestAuth{Type: "none"},
+		Tools:       []ManifestTool{},
+	}
+	toolsData, err := json.MarshalIndent(toolsManifest, "", "  ")
+	require.NoError(t, err)
+	require.NoError(t, os.WriteFile(filepath.Join(dir, ToolsManifestFilename), toolsData, 0o644))
+
+	require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte(cliName+"\n"+mcpName+"\n"), 0o644))
 }
 
 func TestRenameCLI(t *testing.T) {
@@ -212,15 +268,52 @@ func TestRenameCLI(t *testing.T) {
 		assert.NotContains(t, string(readme), oldName)
 		assert.Contains(t, string(readme), newMCPName)
 		assert.NotContains(t, string(readme), oldMCPName)
+		assert.Contains(t, string(readme), "cli-skills/pp-"+naming.TrimCLISuffix(newName))
+		assert.NotContains(t, string(readme), "`cli-skills/pp-"+apiName+"`")
+		assert.Contains(t, string(readme), "library/other/"+naming.TrimCLISuffix(newName)+"/cmd/"+newName)
+		assert.NotContains(t, string(readme), "library/other/"+apiName+"/cmd/"+newName)
+
+		// SKILL should have new skill identity, install metadata, and Go fallback path.
+		skill, err := os.ReadFile(filepath.Join(newDir, "SKILL.md"))
+		require.NoError(t, err)
+		assert.Contains(t, string(skill), "name: pp-"+naming.TrimCLISuffix(newName))
+		assert.NotContains(t, string(skill), "name: pp-"+apiName+"\n")
+		assert.Contains(t, string(skill), "library/other/"+naming.TrimCLISuffix(newName)+"/cmd/"+newName)
+		assert.NotContains(t, string(skill), "library/other/"+apiName+"/cmd/"+newName)
 
-		// Manifest should have new cli_name, original api_name, and new MCP binary
+		// Manifest should have the final public slug, CLI name, and MCP binary.
 		mData, err := os.ReadFile(filepath.Join(newDir, CLIManifestFilename))
 		require.NoError(t, err)
 		var m CLIManifest
 		require.NoError(t, json.Unmarshal(mData, &m))
 		assert.Equal(t, newName, m.CLIName)
-		assert.Equal(t, apiName, m.APIName)
+		assert.Equal(t, naming.TrimCLISuffix(newName), m.APIName)
 		assert.Equal(t, newMCPName, m.MCPBinary)
+
+		// MCPB manifest should launch the renamed MCP binary.
+		mcpbData, err := os.ReadFile(filepath.Join(newDir, MCPBManifestFilename))
+		require.NoError(t, err)
+		var mcpb MCPBManifest
+		require.NoError(t, json.Unmarshal(mcpbData, &mcpb))
+		assert.Equal(t, newMCPName, mcpb.Name)
+		assert.Equal(t, "bin/"+newMCPName, mcpb.Server.EntryPoint)
+		assert.Equal(t, "${__dirname}/bin/"+newMCPName, mcpb.Server.MCPConfig.Command)
+		assert.NotContains(t, string(mcpbData), oldMCPName)
+
+		// Tools manifest should use the post-rename public API slug.
+		toolsData, err := os.ReadFile(filepath.Join(newDir, ToolsManifestFilename))
+		require.NoError(t, err)
+		var tools ToolsManifest
+		require.NoError(t, json.Unmarshal(toolsData, &tools))
+		assert.Equal(t, naming.TrimCLISuffix(newName), tools.APIName)
+
+		// Bare binary ignore patterns must be root-anchored so cmd/<binary> is tracked.
+		gitignore, err := os.ReadFile(filepath.Join(newDir, ".gitignore"))
+		require.NoError(t, err)
+		assert.Contains(t, string(gitignore), "/"+newName)
+		assert.Contains(t, string(gitignore), "/"+newMCPName)
+		assert.NotContains(t, string(gitignore), "\n"+newName+"\n")
+		assert.NotContains(t, string(gitignore), "\n"+newMCPName+"\n")
 	})
 
 	t.Run("numeric qualifier renames correctly", func(t *testing.T) {
@@ -386,13 +479,13 @@ func main() {}
 		assert.Contains(t, string(rootGo), `Use:   "`+newName+`"`)
 		assert.NotContains(t, string(rootGo), oldName)
 
-		// Manifest should have new cli_name, original api_name
+		// Manifest should have the final public slug and CLI name.
 		mData, err := os.ReadFile(filepath.Join(newDir, CLIManifestFilename))
 		require.NoError(t, err)
 		var m CLIManifest
 		require.NoError(t, json.Unmarshal(mData, &m))
 		assert.Equal(t, newName, m.CLIName)
-		assert.Equal(t, apiName, m.APIName)
+		assert.Equal(t, naming.TrimCLISuffix(newName), m.APIName)
 	})
 
 	t.Run("skips non-target file extensions", func(t *testing.T) {
diff --git a/skills/printing-press-publish/SKILL.md b/skills/printing-press-publish/SKILL.md
index f7e15aa3..6589ec0d 100644
--- a/skills/printing-press-publish/SKILL.md
+++ b/skills/printing-press-publish/SKILL.md
@@ -581,7 +581,6 @@ printing-press publish rename \
   --dir "$PUBLISH_REPO_DIR/library/<category>/<api-slug>" \
   --old-name <old-cli-name> \
   --new-name "$NEW_CLI_NAME" \
-  --api-name "$ORIGINAL_API_SLUG" \
   --json
 ```
 
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/README.md b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/README.md
index 9d1e5277..c40c6b35 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/README.md
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/README.md
@@ -16,15 +16,10 @@ For CLI only (no skill):
 npx -y @mvanhorn/printing-press install printing-press-rich --cli-only
 ```
 
-### Without Node (Go fallback)
 
-If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.26.3 or newer):
+### Without Node
 
-```bash
-go install github.com/mvanhorn/printing-press-library/library/other/printing-press-rich/cmd/printing-press-rich-pp-cli@latest
-```
-
-This installs the CLI only — no skill.
+The generated install path is category-agnostic until this CLI is published. If `npx` is not available before publish, install Node or use the category-specific Go fallback from the public-library entry after publish.
 
 ### Pre-built binary
 
@@ -144,9 +139,8 @@ Then invoke `/pp-printing-press-rich <query>` in Claude Code. The skill is the m
 
 If you'd rather register this CLI as an MCP server in Claude Code, install the MCP binary first:
 
-```bash
-go install github.com/mvanhorn/printing-press-library/library/other/printing-press-rich/cmd/printing-press-rich-pp-mcp@latest
-```
+
+Install the MCP binary from this CLI's published public-library entry or pre-built release.
 
 Then register it:
 
@@ -173,9 +167,8 @@ Requires Claude Desktop 1.0.0 or later. Pre-built bundles ship for macOS Apple S
 
 If you can't use the MCPB bundle (older Claude Desktop, unsupported platform), install the MCP binary and configure it manually.
 
-```bash
-go install github.com/mvanhorn/printing-press-library/library/other/printing-press-rich/cmd/printing-press-rich-pp-mcp@latest
-```
+
+Install the MCP binary from this CLI's published public-library entry or pre-built release.
 
 Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
 
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.md b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.md
index d7dececc..855f1005 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.md
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.md
@@ -10,10 +10,6 @@ metadata:
     requires:
       bins:
         - printing-press-rich-pp-cli
-    install:
-      - kind: go
-        bins: [printing-press-rich-pp-cli]
-        module: github.com/mvanhorn/printing-press-library/library/other/printing-press-rich/cmd/printing-press-rich-pp-cli
 ---
 
 # Printing Press Rich — Printing Press CLI
@@ -29,11 +25,7 @@ This skill drives the `printing-press-rich-pp-cli` binary. **You must verify the
 2. Verify: `printing-press-rich-pp-cli --version`
 3. Ensure `$GOPATH/bin` (or `$HOME/go/bin`) is on `$PATH`.
 
-If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.3 or newer):
-
-```bash
-go install github.com/mvanhorn/printing-press-library/library/other/printing-press-rich/cmd/printing-press-rich-pp-cli@latest
-```
+If the `npx` install fails before this CLI has a public-library category, install Node or use the category-specific Go fallback after publish.
 
 If `--version` reports "command not found" after install, the install step did not put the binary on `$PATH`. Do not proceed with skill commands until verification succeeds.
 
@@ -161,15 +153,13 @@ Parse `$ARGUMENTS`:
 
 ## MCP Server Installation
 
-1. Install the MCP server:
-   ```bash
-   go install github.com/mvanhorn/printing-press-library/library/other/printing-press-rich/cmd/printing-press-rich-pp-mcp@latest
-   ```
-2. Register with Claude Code:
-   ```bash
-   claude mcp add printing-press-rich-pp-mcp -- printing-press-rich-pp-mcp
-   ```
-3. Verify: `claude mcp list`
+Install the MCP binary from this CLI's published public-library entry or pre-built release, then register it:
+
+```bash
+claude mcp add printing-press-rich-pp-mcp -- printing-press-rich-pp-mcp
+```
+
+Verify: `claude mcp list`
 
 ## Direct Use
 
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md b/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md
index fde3b361..9d5bdaf8 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md
@@ -16,15 +16,10 @@ For CLI only (no skill):
 npx -y @mvanhorn/printing-press install printing-press-golden --cli-only
 ```
 
-### Without Node (Go fallback)
 
-If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.26.3 or newer):
+### Without Node
 
-```bash
-go install github.com/mvanhorn/printing-press-library/library/other/printing-press-golden/cmd/printing-press-golden-pp-cli@latest
-```
-
-This installs the CLI only — no skill.
+The generated install path is category-agnostic until this CLI is published. If `npx` is not available before publish, install Node or use the category-specific Go fallback from the public-library entry after publish.
 
 ### Pre-built binary
 
@@ -165,9 +160,8 @@ Then invoke `/pp-printing-press-golden <query>` in Claude Code. The skill is the
 
 If you'd rather register this CLI as an MCP server in Claude Code, install the MCP binary first:
 
-```bash
-go install github.com/mvanhorn/printing-press-library/library/other/printing-press-golden/cmd/printing-press-golden-pp-mcp@latest
-```
+
+Install the MCP binary from this CLI's published public-library entry or pre-built release.
 
 Then register it:
 
@@ -194,9 +188,8 @@ Requires Claude Desktop 1.0.0 or later. Pre-built bundles ship for macOS Apple S
 
 If you can't use the MCPB bundle (older Claude Desktop, unsupported platform), install the MCP binary and configure it manually.
 
-```bash
-go install github.com/mvanhorn/printing-press-library/library/other/printing-press-golden/cmd/printing-press-golden-pp-mcp@latest
-```
+
+Install the MCP binary from this CLI's published public-library entry or pre-built release.
 
 Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
 
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md b/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md
index bd0b2761..260a1e42 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md
@@ -10,10 +10,6 @@ metadata:
     requires:
       bins:
         - printing-press-golden-pp-cli
-    install:
-      - kind: go
-        bins: [printing-press-golden-pp-cli]
-        module: github.com/mvanhorn/printing-press-library/library/other/printing-press-golden/cmd/printing-press-golden-pp-cli
 ---
 
 # Printing Press Golden — Printing Press CLI
@@ -29,11 +25,7 @@ This skill drives the `printing-press-golden-pp-cli` binary. **You must verify t
 2. Verify: `printing-press-golden-pp-cli --version`
 3. Ensure `$GOPATH/bin` (or `$HOME/go/bin`) is on `$PATH`.
 
-If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.3 or newer):
-
-```bash
-go install github.com/mvanhorn/printing-press-library/library/other/printing-press-golden/cmd/printing-press-golden-pp-cli@latest
-```
+If the `npx` install fails before this CLI has a public-library category, install Node or use the category-specific Go fallback after publish.
 
 If `--version` reports "command not found" after install, the install step did not put the binary on `$PATH`. Do not proceed with skill commands until verification succeeds.
 
@@ -170,15 +162,13 @@ Parse `$ARGUMENTS`:
 
 ## MCP Server Installation
 
-1. Install the MCP server:
-   ```bash
-   go install github.com/mvanhorn/printing-press-library/library/other/printing-press-golden/cmd/printing-press-golden-pp-mcp@latest
-   ```
-2. Register with Claude Code:
-   ```bash
-   claude mcp add printing-press-golden-pp-mcp -- printing-press-golden-pp-mcp
-   ```
-3. Verify: `claude mcp list`
+Install the MCP binary from this CLI's published public-library entry or pre-built release, then register it:
+
+```bash
+claude mcp add printing-press-golden-pp-mcp -- printing-press-golden-pp-mcp
+```
+
+Verify: `claude mcp list`
 
 ## Direct Use
 
diff --git a/testdata/golden/expected/generate-public-param-names/public-param-golden/README.md b/testdata/golden/expected/generate-public-param-names/public-param-golden/README.md
index 60acc282..ec2b4076 100644
--- a/testdata/golden/expected/generate-public-param-names/public-param-golden/README.md
+++ b/testdata/golden/expected/generate-public-param-names/public-param-golden/README.md
@@ -16,15 +16,10 @@ For CLI only (no skill):
 npx -y @mvanhorn/printing-press install public-param-golden --cli-only
 ```
 
-### Without Node (Go fallback)
 
-If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.26.3 or newer):
+### Without Node
 
-```bash
-go install github.com/mvanhorn/printing-press-library/library/other/public-param-golden/cmd/public-param-golden-pp-cli@latest
-```
-
-This installs the CLI only — no skill.
+The generated install path is category-agnostic until this CLI is published. If `npx` is not available before publish, install Node or use the category-specific Go fallback from the public-library entry after publish.
 
 ### Pre-built binary
 
@@ -136,9 +131,8 @@ Then invoke `/pp-public-param-golden <query>` in Claude Code. The skill is the m
 
 If you'd rather register this CLI as an MCP server in Claude Code, install the MCP binary first:
 
-```bash
-go install github.com/mvanhorn/printing-press-library/library/other/public-param-golden/cmd/public-param-golden-pp-mcp@latest
-```
+
+Install the MCP binary from this CLI's published public-library entry or pre-built release.
 
 Then register it:
 
@@ -164,9 +158,8 @@ Requires Claude Desktop 1.0.0 or later. Pre-built bundles ship for macOS Apple S
 
 If you can't use the MCPB bundle (older Claude Desktop, unsupported platform), install the MCP binary and configure it manually.
 
-```bash
-go install github.com/mvanhorn/printing-press-library/library/other/public-param-golden/cmd/public-param-golden-pp-mcp@latest
-```
+
+Install the MCP binary from this CLI's published public-library entry or pre-built release.
 
 Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
 
diff --git a/testdata/golden/expected/generate-public-param-names/public-param-golden/SKILL.md b/testdata/golden/expected/generate-public-param-names/public-param-golden/SKILL.md
index cfa65c8c..9fd42252 100644
--- a/testdata/golden/expected/generate-public-param-names/public-param-golden/SKILL.md
+++ b/testdata/golden/expected/generate-public-param-names/public-param-golden/SKILL.md
@@ -10,10 +10,6 @@ metadata:
     requires:
       bins:
         - public-param-golden-pp-cli
-    install:
-      - kind: go
-        bins: [public-param-golden-pp-cli]
-        module: github.com/mvanhorn/printing-press-library/library/other/public-param-golden/cmd/public-param-golden-pp-cli
 ---
 
 # Public Param Golden — Printing Press CLI
@@ -29,11 +25,7 @@ This skill drives the `public-param-golden-pp-cli` binary. **You must verify the
 2. Verify: `public-param-golden-pp-cli --version`
 3. Ensure `$GOPATH/bin` (or `$HOME/go/bin`) is on `$PATH`.
 
-If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.3 or newer):
-
-```bash
-go install github.com/mvanhorn/printing-press-library/library/other/public-param-golden/cmd/public-param-golden-pp-cli@latest
-```
+If the `npx` install fails before this CLI has a public-library category, install Node or use the category-specific Go fallback after publish.
 
 If `--version` reports "command not found" after install, the install step did not put the binary on `$PATH`. Do not proceed with skill commands until verification succeeds.
 
@@ -138,15 +130,13 @@ Parse `$ARGUMENTS`:
 
 ## MCP Server Installation
 
-1. Install the MCP server:
-   ```bash
-   go install github.com/mvanhorn/printing-press-library/library/other/public-param-golden/cmd/public-param-golden-pp-mcp@latest
-   ```
-2. Register with Claude Code:
-   ```bash
-   claude mcp add public-param-golden-pp-mcp -- public-param-golden-pp-mcp
-   ```
-3. Verify: `claude mcp list`
+Install the MCP binary from this CLI's published public-library entry or pre-built release, then register it:
+
+```bash
+claude mcp add public-param-golden-pp-mcp -- public-param-golden-pp-mcp
+```
+
+Verify: `claude mcp list`
 
 ## Direct Use
 

← 9675f8b2 fix(cli): preserve novel cli files on force generate (#747)  ·  back to Cli Printing Press  ·  feat(cli): credit original printer in per-CLI README (closes ee8f6ad9 →