← back to Cli Printing Press
feat(cli): use local module path at generation, rewrite at publish (#63)
244c4845521378fa1e248ec34d3482469367f02d · 2026-03-29 21:18:00 -0700 · Trevin Chow
* feat(cli): use local module path at generation, rewrite at publish
Generated CLIs now use a bare module path (e.g., `notion-pp-cli`) instead
of `github.com/user/notion-pp-cli`. This avoids broken `go install` URLs
and defers the real module path to publish time.
- Add `modulePath` template function returning the CLI name
- Update all templates to use `{{modulePath}}` for Go import paths
- Add `--module-path` flag to `publish package` for rewriting go.mod
and import paths to the configured library repo URL
- Add `RewriteModulePath` to pipeline package with tests
- Update publish skill with configurable `module_path_base` setting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): restrict module path rewrite to imports and ldflags only
RewriteModulePath was using strings.ReplaceAll on the bare CLI name,
which corrupted non-import strings like command Use fields, User-Agent
headers, and config paths. Now only replaces oldPath/internal/ patterns,
which precisely targets Go import paths and goreleaser ldflags.
Also extends file walk to .yaml/.yml files so .goreleaser.yaml ldflags
get the correct module path for release builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): rewrite README go install path at publish time
README template now uses {{modulePath}}/cmd/... for the go install line
instead of hardcoding github.com/{{.Owner}}/... which was inconsistent
with the staged module path after publish packaging.
RewriteModulePath now also replaces oldPath/cmd/ patterns and walks .md
files, so the README install instruction gets the correct published path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): rewrite published README and repo paths
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files touched
M internal/cli/publish.goM internal/generator/generator.goM internal/generator/generator_test.goM internal/generator/templates/analytics.go.tmplM internal/generator/templates/auth.go.tmplM internal/generator/templates/auth_simple.go.tmplM internal/generator/templates/channel_workflow.go.tmplM internal/generator/templates/client.go.tmplM internal/generator/templates/doctor.go.tmplM internal/generator/templates/go.mod.tmplM internal/generator/templates/goreleaser.yaml.tmplM internal/generator/templates/insights/health_score.go.tmplM internal/generator/templates/insights/similar.go.tmplM internal/generator/templates/main.go.tmplM internal/generator/templates/main_mcp.go.tmplM internal/generator/templates/mcp_tools.go.tmplM internal/generator/templates/root.go.tmplM internal/generator/templates/search.go.tmplM internal/generator/templates/sync.go.tmplM internal/generator/templates/workflows/pm_load.go.tmplM internal/generator/templates/workflows/pm_orphans.go.tmplM internal/generator/templates/workflows/pm_stale.go.tmplA internal/pipeline/modulepath.goA internal/pipeline/modulepath_test.goM skills/printing-press-publish/SKILL.md
Diff
commit 244c4845521378fa1e248ec34d3482469367f02d
Author: Trevin Chow <trevin@trevinchow.com>
Date: Sun Mar 29 21:18:00 2026 -0700
feat(cli): use local module path at generation, rewrite at publish (#63)
* feat(cli): use local module path at generation, rewrite at publish
Generated CLIs now use a bare module path (e.g., `notion-pp-cli`) instead
of `github.com/user/notion-pp-cli`. This avoids broken `go install` URLs
and defers the real module path to publish time.
- Add `modulePath` template function returning the CLI name
- Update all templates to use `{{modulePath}}` for Go import paths
- Add `--module-path` flag to `publish package` for rewriting go.mod
and import paths to the configured library repo URL
- Add `RewriteModulePath` to pipeline package with tests
- Update publish skill with configurable `module_path_base` setting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): restrict module path rewrite to imports and ldflags only
RewriteModulePath was using strings.ReplaceAll on the bare CLI name,
which corrupted non-import strings like command Use fields, User-Agent
headers, and config paths. Now only replaces oldPath/internal/ patterns,
which precisely targets Go import paths and goreleaser ldflags.
Also extends file walk to .yaml/.yml files so .goreleaser.yaml ldflags
get the correct module path for release builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): rewrite README go install path at publish time
README template now uses {{modulePath}}/cmd/... for the go install line
instead of hardcoding github.com/{{.Owner}}/... which was inconsistent
with the staged module path after publish packaging.
RewriteModulePath now also replaces oldPath/cmd/ patterns and walks .md
files, so the README install instruction gets the correct published path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): rewrite published README and repo paths
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
internal/cli/publish.go | 21 ++-
internal/generator/generator.go | 1 +
internal/generator/generator_test.go | 15 +-
internal/generator/templates/analytics.go.tmpl | 2 +-
internal/generator/templates/auth.go.tmpl | 2 +-
internal/generator/templates/auth_simple.go.tmpl | 2 +-
.../generator/templates/channel_workflow.go.tmpl | 2 +-
internal/generator/templates/client.go.tmpl | 2 +-
internal/generator/templates/doctor.go.tmpl | 2 +-
internal/generator/templates/go.mod.tmpl | 2 +-
internal/generator/templates/goreleaser.yaml.tmpl | 2 +-
.../templates/insights/health_score.go.tmpl | 2 +-
.../generator/templates/insights/similar.go.tmpl | 2 +-
internal/generator/templates/main.go.tmpl | 2 +-
internal/generator/templates/main_mcp.go.tmpl | 2 +-
internal/generator/templates/mcp_tools.go.tmpl | 6 +-
internal/generator/templates/root.go.tmpl | 4 +-
internal/generator/templates/search.go.tmpl | 2 +-
internal/generator/templates/sync.go.tmpl | 2 +-
.../generator/templates/workflows/pm_load.go.tmpl | 2 +-
.../templates/workflows/pm_orphans.go.tmpl | 2 +-
.../generator/templates/workflows/pm_stale.go.tmpl | 2 +-
internal/pipeline/modulepath.go | 116 ++++++++++++
internal/pipeline/modulepath_test.go | 199 +++++++++++++++++++++
skills/printing-press-publish/SKILL.md | 30 +++-
25 files changed, 396 insertions(+), 30 deletions(-)
diff --git a/internal/cli/publish.go b/internal/cli/publish.go
index 0d016886..39031ba7 100644
--- a/internal/cli/publish.go
+++ b/internal/cli/publish.go
@@ -62,6 +62,7 @@ type PackageResult struct {
CLIName string `json:"cli_name"`
APIName string `json:"api_name"`
Category string `json:"category"`
+ ModulePath string `json:"module_path,omitempty"`
ManuscriptsIncluded bool `json:"manuscripts_included"`
RunID string `json:"run_id,omitempty"`
}
@@ -131,6 +132,7 @@ func newPublishPackageCmd() *cobra.Command {
var dir string
var category string
var target string
+ var modulePath string
var asJSON bool
cmd := &cobra.Command{
@@ -205,12 +207,22 @@ func newPublishPackageCmd() *cobra.Command {
return &ExitError{Code: ExitPublishError, Err: fmt.Errorf("copying CLI: %w", err)}
}
+ // Rewrite go.mod module path if --module-path is set
+ if modulePath != "" {
+ oldModPath := cliName // generated CLIs use bare CLI name as module path
+ if err := pipeline.RewriteModulePath(stagingCLIDir, oldModPath, modulePath); err != nil {
+ cleanupTarget()
+ return &ExitError{Code: ExitPublishError, Err: fmt.Errorf("rewriting module path: %w", err)}
+ }
+ }
+
// Resolve and copy manuscripts
result := PackageResult{
- StagedDir: stagingCLIDir,
- CLIName: cliName,
- APIName: vResult.APIName,
- Category: category,
+ StagedDir: stagingCLIDir,
+ CLIName: cliName,
+ APIName: vResult.APIName,
+ Category: category,
+ ModulePath: modulePath,
}
apiName := vResult.APIName
@@ -254,6 +266,7 @@ func newPublishPackageCmd() *cobra.Command {
cmd.Flags().StringVar(&dir, "dir", "", "CLI directory to package (required)")
cmd.Flags().StringVar(&category, "category", "", "Category for the CLI (required)")
cmd.Flags().StringVar(&target, "target", "", "Staging directory to create (required)")
+ cmd.Flags().StringVar(&modulePath, "module-path", "", "Go module path to set (e.g., github.com/org/repo/library/category/cli-name)")
cmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON")
return cmd
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 6fa29301..5e1d245a 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -84,6 +84,7 @@ func New(s *spec.APISpec, outputDir string) *Generator {
"safeTypeName": safeTypeName,
"exampleLine": g.exampleLine,
"currentYear": func() string { return strconv.Itoa(time.Now().Year()) },
+ "modulePath": func() string { return naming.CLI(s.Name) },
}
return g
}
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index ed60b89d..f3cbf4ef 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -187,7 +187,15 @@ func TestGenerateWithOwnerField(t *testing.T) {
gomod, err := os.ReadFile(filepath.Join(outputDir, "go.mod"))
require.NoError(t, err)
- assert.Contains(t, string(gomod), "github.com/testowner/")
+ // Module path uses bare CLI name (no github.com/owner prefix)
+ assert.Contains(t, string(gomod), "module owned-pp-cli")
+ // Owner is still used for copyright
+ mainGo, err := os.ReadFile(filepath.Join(outputDir, "cmd", "owned-pp-cli", "main.go"))
+ require.NoError(t, err)
+ 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/testowner/owned-pp-cli/cmd/owned-pp-cli@latest")
}
func TestGenerateWithEmptyOwner(t *testing.T) {
@@ -228,7 +236,10 @@ func TestGenerateWithEmptyOwner(t *testing.T) {
gomod, err := os.ReadFile(filepath.Join(outputDir, "go.mod"))
require.NoError(t, err)
- assert.Contains(t, string(gomod), "github.com/")
+ // Module path uses bare CLI name regardless of Owner
+ assert.Contains(t, string(gomod), "module unowned-pp-cli")
+ // Module line should not have a github.com prefix
+ assert.NotContains(t, string(gomod), "module github.com/")
}
// --- Unit 7: Feature Verification Tests ---
diff --git a/internal/generator/templates/analytics.go.tmpl b/internal/generator/templates/analytics.go.tmpl
index c5e70716..5e18a2bb 100644
--- a/internal/generator/templates/analytics.go.tmpl
+++ b/internal/generator/templates/analytics.go.tmpl
@@ -10,7 +10,7 @@ import (
"path/filepath"
"sort"
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/store"
+ "{{modulePath}}/internal/store"
"github.com/spf13/cobra"
)
diff --git a/internal/generator/templates/auth.go.tmpl b/internal/generator/templates/auth.go.tmpl
index 76a28359..21ec0b02 100644
--- a/internal/generator/templates/auth.go.tmpl
+++ b/internal/generator/templates/auth.go.tmpl
@@ -18,7 +18,7 @@ import (
"strings"
"time"
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/config"
+ "{{modulePath}}/internal/config"
"github.com/spf13/cobra"
)
diff --git a/internal/generator/templates/auth_simple.go.tmpl b/internal/generator/templates/auth_simple.go.tmpl
index 4c5bef36..4d1decc6 100644
--- a/internal/generator/templates/auth_simple.go.tmpl
+++ b/internal/generator/templates/auth_simple.go.tmpl
@@ -9,7 +9,7 @@ import (
"os"
{{- end}}
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/config"
+ "{{modulePath}}/internal/config"
"github.com/spf13/cobra"
)
diff --git a/internal/generator/templates/channel_workflow.go.tmpl b/internal/generator/templates/channel_workflow.go.tmpl
index d24caa7b..d8619527 100644
--- a/internal/generator/templates/channel_workflow.go.tmpl
+++ b/internal/generator/templates/channel_workflow.go.tmpl
@@ -10,7 +10,7 @@ import (
"path/filepath"
"time"
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/store"
+ "{{modulePath}}/internal/store"
"github.com/spf13/cobra"
)
diff --git a/internal/generator/templates/client.go.tmpl b/internal/generator/templates/client.go.tmpl
index f10cd9a7..98c8e12e 100644
--- a/internal/generator/templates/client.go.tmpl
+++ b/internal/generator/templates/client.go.tmpl
@@ -19,7 +19,7 @@ import (
"sync"
"time"
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/config"
+ "{{modulePath}}/internal/config"
)
type Client struct {
diff --git a/internal/generator/templates/doctor.go.tmpl b/internal/generator/templates/doctor.go.tmpl
index 8f28faa0..d89e7434 100644
--- a/internal/generator/templates/doctor.go.tmpl
+++ b/internal/generator/templates/doctor.go.tmpl
@@ -12,7 +12,7 @@ import (
"strings"
"time"
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/config"
+ "{{modulePath}}/internal/config"
"github.com/spf13/cobra"
)
diff --git a/internal/generator/templates/go.mod.tmpl b/internal/generator/templates/go.mod.tmpl
index fcd2421c..0f975573 100644
--- a/internal/generator/templates/go.mod.tmpl
+++ b/internal/generator/templates/go.mod.tmpl
@@ -1,4 +1,4 @@
-module github.com/{{.Owner}}/{{.Name}}-pp-cli
+module {{modulePath}}
go 1.23
diff --git a/internal/generator/templates/goreleaser.yaml.tmpl b/internal/generator/templates/goreleaser.yaml.tmpl
index b1526a8d..5c02a392 100644
--- a/internal/generator/templates/goreleaser.yaml.tmpl
+++ b/internal/generator/templates/goreleaser.yaml.tmpl
@@ -9,7 +9,7 @@ builds:
env:
- CGO_ENABLED=0
ldflags:
- - -s -w -X github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/cli.version={{"{{"}} .Version {{"}}"}}
+ - -s -w -X {{modulePath}}/internal/cli.version={{"{{"}} .Version {{"}}"}}
targets:
- darwin_amd64
- darwin_arm64
diff --git a/internal/generator/templates/insights/health_score.go.tmpl b/internal/generator/templates/insights/health_score.go.tmpl
index 47c4eb24..2aa76dbb 100644
--- a/internal/generator/templates/insights/health_score.go.tmpl
+++ b/internal/generator/templates/insights/health_score.go.tmpl
@@ -10,7 +10,7 @@ import (
"path/filepath"
"time"
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/store"
+ "{{modulePath}}/internal/store"
"github.com/spf13/cobra"
)
diff --git a/internal/generator/templates/insights/similar.go.tmpl b/internal/generator/templates/insights/similar.go.tmpl
index f520da99..b31468aa 100644
--- a/internal/generator/templates/insights/similar.go.tmpl
+++ b/internal/generator/templates/insights/similar.go.tmpl
@@ -9,7 +9,7 @@ import (
"os"
"path/filepath"
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/store"
+ "{{modulePath}}/internal/store"
"github.com/spf13/cobra"
)
diff --git a/internal/generator/templates/main.go.tmpl b/internal/generator/templates/main.go.tmpl
index 49527b8e..8cef8696 100644
--- a/internal/generator/templates/main.go.tmpl
+++ b/internal/generator/templates/main.go.tmpl
@@ -7,7 +7,7 @@ import (
"fmt"
"os"
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/cli"
+ "{{modulePath}}/internal/cli"
)
func main() {
diff --git a/internal/generator/templates/main_mcp.go.tmpl b/internal/generator/templates/main_mcp.go.tmpl
index 6ee51c47..80161b23 100644
--- a/internal/generator/templates/main_mcp.go.tmpl
+++ b/internal/generator/templates/main_mcp.go.tmpl
@@ -8,7 +8,7 @@ import (
"os"
"github.com/mark3labs/mcp-go/server"
- mcptools "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/mcp"
+ mcptools "{{modulePath}}/internal/mcp"
)
func main() {
diff --git a/internal/generator/templates/mcp_tools.go.tmpl b/internal/generator/templates/mcp_tools.go.tmpl
index 3dd92747..4defc890 100644
--- a/internal/generator/templates/mcp_tools.go.tmpl
+++ b/internal/generator/templates/mcp_tools.go.tmpl
@@ -14,10 +14,10 @@ import (
mcplib "github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/client"
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/config"
+ "{{modulePath}}/internal/client"
+ "{{modulePath}}/internal/config"
{{- if .VisionSet.Store}}
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/store"
+ "{{modulePath}}/internal/store"
{{- end}}
)
diff --git a/internal/generator/templates/root.go.tmpl b/internal/generator/templates/root.go.tmpl
index 7dc403ea..29dfbd67 100644
--- a/internal/generator/templates/root.go.tmpl
+++ b/internal/generator/templates/root.go.tmpl
@@ -10,8 +10,8 @@ import (
"text/tabwriter"
"time"
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/client"
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/config"
+ "{{modulePath}}/internal/client"
+ "{{modulePath}}/internal/config"
"github.com/spf13/cobra"
)
diff --git a/internal/generator/templates/search.go.tmpl b/internal/generator/templates/search.go.tmpl
index 3d080c32..65626478 100644
--- a/internal/generator/templates/search.go.tmpl
+++ b/internal/generator/templates/search.go.tmpl
@@ -10,7 +10,7 @@ import (
"path/filepath"
"strings"
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/store"
+ "{{modulePath}}/internal/store"
"github.com/spf13/cobra"
)
diff --git a/internal/generator/templates/sync.go.tmpl b/internal/generator/templates/sync.go.tmpl
index cac31a91..b5802f1d 100644
--- a/internal/generator/templates/sync.go.tmpl
+++ b/internal/generator/templates/sync.go.tmpl
@@ -15,7 +15,7 @@ import (
"sync/atomic"
"time"
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/store"
+ "{{modulePath}}/internal/store"
"github.com/spf13/cobra"
)
diff --git a/internal/generator/templates/workflows/pm_load.go.tmpl b/internal/generator/templates/workflows/pm_load.go.tmpl
index 18bbfe5f..c2c2b18e 100644
--- a/internal/generator/templates/workflows/pm_load.go.tmpl
+++ b/internal/generator/templates/workflows/pm_load.go.tmpl
@@ -10,7 +10,7 @@ import (
"path/filepath"
"sort"
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/store"
+ "{{modulePath}}/internal/store"
"github.com/spf13/cobra"
)
diff --git a/internal/generator/templates/workflows/pm_orphans.go.tmpl b/internal/generator/templates/workflows/pm_orphans.go.tmpl
index fefe3b18..16e9b5bc 100644
--- a/internal/generator/templates/workflows/pm_orphans.go.tmpl
+++ b/internal/generator/templates/workflows/pm_orphans.go.tmpl
@@ -10,7 +10,7 @@ import (
"path/filepath"
"strings"
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/store"
+ "{{modulePath}}/internal/store"
"github.com/spf13/cobra"
)
diff --git a/internal/generator/templates/workflows/pm_stale.go.tmpl b/internal/generator/templates/workflows/pm_stale.go.tmpl
index c6850e0c..24ea4ea6 100644
--- a/internal/generator/templates/workflows/pm_stale.go.tmpl
+++ b/internal/generator/templates/workflows/pm_stale.go.tmpl
@@ -10,7 +10,7 @@ import (
"path/filepath"
"time"
- "github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/store"
+ "{{modulePath}}/internal/store"
"github.com/spf13/cobra"
)
diff --git a/internal/pipeline/modulepath.go b/internal/pipeline/modulepath.go
new file mode 100644
index 00000000..9d4daa8e
--- /dev/null
+++ b/internal/pipeline/modulepath.go
@@ -0,0 +1,116 @@
+package pipeline
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+)
+
+// rewriteExtensions lists file extensions that may contain Go import paths,
+// module-path references (e.g., goreleaser ldflags), or install instructions.
+var rewriteExtensions = []string{".go", ".yaml", ".yml", ".md"}
+
+// RewriteModulePath replaces the Go module path in a CLI directory.
+// It rewrites the module declaration in go.mod and import paths
+// (oldPath/internal/...) in .go and .yaml files from oldPath to newPath.
+//
+// Only import-style references (oldPath + "/internal/") are replaced in
+// source files. Bare CLI name occurrences in command strings, User-Agent
+// headers, and config paths are intentionally left untouched.
+func RewriteModulePath(dir, oldPath, newPath string) error {
+ if oldPath == newPath {
+ return nil
+ }
+
+ // Rewrite go.mod module line
+ gomodPath := filepath.Join(dir, "go.mod")
+ gomod, err := os.ReadFile(gomodPath)
+ if err != nil {
+ return fmt.Errorf("reading go.mod: %w", err)
+ }
+
+ oldModule := "module " + oldPath
+ newModule := "module " + newPath
+ updated := strings.Replace(string(gomod), oldModule, newModule, 1)
+ if updated == string(gomod) {
+ return fmt.Errorf("go.mod does not contain expected module path %q", oldPath)
+ }
+ if err := os.WriteFile(gomodPath, []byte(updated), 0o644); err != nil {
+ return fmt.Errorf("writing go.mod: %w", err)
+ }
+
+ // Only replace subpath references: oldPath/internal/... and oldPath/cmd/...
+ // This avoids corrupting command Use strings, User-Agent headers,
+ // config paths, and other runtime literals that contain the CLI name.
+ replacements := []struct{ old, new string }{
+ {oldPath + "/internal/", newPath + "/internal/"}, // Go imports, goreleaser ldflags
+ }
+
+ return filepath.WalkDir(dir, func(path string, d os.DirEntry, walkErr error) error {
+ if walkErr != nil {
+ return walkErr
+ }
+ if d.IsDir() {
+ return nil
+ }
+
+ if !hasRewriteExtension(path) {
+ return nil
+ }
+
+ content, err := os.ReadFile(path)
+ if err != nil {
+ return fmt.Errorf("reading %s: %w", path, err)
+ }
+
+ result := string(content)
+ for _, r := range replacements {
+ result = strings.ReplaceAll(result, r.old, r.new)
+ }
+ result = rewriteModuleInstallPaths(result, oldPath, newPath)
+ result = rewriteGitHubRepoURLs(result, oldPath, newPath)
+ if result == string(content) {
+ return nil // no changes needed
+ }
+
+ return os.WriteFile(path, []byte(result), 0o644)
+ })
+}
+
+func hasRewriteExtension(path string) bool {
+ for _, ext := range rewriteExtensions {
+ if strings.HasSuffix(path, ext) {
+ return true
+ }
+ }
+ return false
+}
+
+func rewriteModuleInstallPaths(content, oldPath, newPath string) string {
+ pattern := regexp.MustCompile(`(?:github\.com/[^/\s"]+/)?` + regexp.QuoteMeta(oldPath) + `/cmd/`)
+ return pattern.ReplaceAllString(content, newPath+"/cmd/")
+}
+
+func rewriteGitHubRepoURLs(content, oldPath, newPath string) string {
+ newRepoURL, ok := githubRepoURL(newPath)
+ if !ok {
+ return content
+ }
+
+ releasesPattern := regexp.MustCompile(`https://github\.com/[^/\s"]+/` + regexp.QuoteMeta(oldPath) + `/releases\b`)
+ content = releasesPattern.ReplaceAllString(content, newRepoURL+"/releases")
+
+ repoPattern := regexp.MustCompile(`https://github\.com/[^/\s"]+/` + regexp.QuoteMeta(oldPath) + `\b`)
+ return repoPattern.ReplaceAllString(content, newRepoURL)
+}
+
+func githubRepoURL(modulePath string) (string, bool) {
+ parts := strings.Split(modulePath, "/")
+ if len(parts) < 3 || parts[0] != "github.com" {
+ return "", false
+ }
+
+ return "https://" + strings.Join(parts[:3], "/"), true
+}
diff --git a/internal/pipeline/modulepath_test.go b/internal/pipeline/modulepath_test.go
new file mode 100644
index 00000000..9877c7bf
--- /dev/null
+++ b/internal/pipeline/modulepath_test.go
@@ -0,0 +1,199 @@
+package pipeline
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRewriteModulePath(t *testing.T) {
+ t.Parallel()
+
+ t.Run("rewrites go.mod and go imports", func(t *testing.T) {
+ dir := t.TempDir()
+
+ // Write a go.mod with the old module path
+ gomod := "module notion-pp-cli\n\ngo 1.23\n"
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0o644))
+
+ // Write a .go file with import paths
+ goFile := `package main
+
+import (
+ "notion-pp-cli/internal/cli"
+ "notion-pp-cli/internal/config"
+)
+
+func main() {}
+`
+ require.NoError(t, os.MkdirAll(filepath.Join(dir, "cmd", "notion-pp-cli"), 0o755))
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "cmd", "notion-pp-cli", "main.go"), []byte(goFile), 0o644))
+
+ err := RewriteModulePath(dir, "notion-pp-cli", "github.com/mvanhorn/printing-press-library/library/productivity/notion-pp-cli")
+ require.NoError(t, err)
+
+ // Check go.mod
+ updatedMod, err := os.ReadFile(filepath.Join(dir, "go.mod"))
+ require.NoError(t, err)
+ assert.Contains(t, string(updatedMod), "module github.com/mvanhorn/printing-press-library/library/productivity/notion-pp-cli")
+ assert.NotContains(t, string(updatedMod), "module notion-pp-cli\n")
+
+ // Check .go file imports
+ updatedGo, err := os.ReadFile(filepath.Join(dir, "cmd", "notion-pp-cli", "main.go"))
+ require.NoError(t, err)
+ assert.Contains(t, string(updatedGo), `"github.com/mvanhorn/printing-press-library/library/productivity/notion-pp-cli/internal/cli"`)
+ assert.Contains(t, string(updatedGo), `"github.com/mvanhorn/printing-press-library/library/productivity/notion-pp-cli/internal/config"`)
+ })
+
+ t.Run("does not corrupt non-import strings", func(t *testing.T) {
+ dir := t.TempDir()
+
+ gomod := "module notion-pp-cli\n\ngo 1.23\n"
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0o644))
+
+ // Simulate a generated root.go with both imports AND runtime literals
+ goFile := `package cli
+
+import (
+ "notion-pp-cli/internal/client"
+ "notion-pp-cli/internal/config"
+)
+
+var version = "0.1.0"
+
+func Execute() {
+ rootCmd := &cobra.Command{
+ Use: "notion-pp-cli",
+ Short: "CLI for Notion API",
+ }
+ rootCmd.SetVersionTemplate("notion-pp-cli {{ .Version }}\n")
+}
+`
+ require.NoError(t, os.MkdirAll(filepath.Join(dir, "internal", "cli"), 0o755))
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "internal", "cli", "root.go"), []byte(goFile), 0o644))
+
+ // Simulate a client.go with User-Agent
+ clientFile := `package client
+
+func (c *Client) do() {
+ req.Header.Set("User-Agent", "notion-pp-cli/0.1.0")
+}
+`
+ require.NoError(t, os.MkdirAll(filepath.Join(dir, "internal", "client"), 0o755))
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "internal", "client", "client.go"), []byte(clientFile), 0o644))
+
+ newPath := "github.com/acme/library/notion-pp-cli"
+ err := RewriteModulePath(dir, "notion-pp-cli", newPath)
+ require.NoError(t, err)
+
+ // Imports should be rewritten
+ updatedRoot, err := os.ReadFile(filepath.Join(dir, "internal", "cli", "root.go"))
+ require.NoError(t, err)
+ rootContent := string(updatedRoot)
+ assert.Contains(t, rootContent, `"github.com/acme/library/notion-pp-cli/internal/client"`)
+ assert.Contains(t, rootContent, `"github.com/acme/library/notion-pp-cli/internal/config"`)
+
+ // Command Use string must NOT be rewritten
+ assert.Contains(t, rootContent, `Use: "notion-pp-cli"`)
+ assert.NotContains(t, rootContent, `Use: "github.com/acme/library/notion-pp-cli"`)
+
+ // Version template must NOT be rewritten
+ assert.Contains(t, rootContent, `"notion-pp-cli {{ .Version }}\n"`)
+
+ // User-Agent must NOT be rewritten
+ updatedClient, err := os.ReadFile(filepath.Join(dir, "internal", "client", "client.go"))
+ require.NoError(t, err)
+ assert.Contains(t, string(updatedClient), `"notion-pp-cli/0.1.0"`)
+ })
+
+ t.Run("rewrites goreleaser ldflags", func(t *testing.T) {
+ dir := t.TempDir()
+
+ gomod := "module notion-pp-cli\n\ngo 1.23\n"
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0o644))
+
+ goreleaser := `version: 2
+builds:
+ - ldflags:
+ - -s -w -X notion-pp-cli/internal/cli.version={{ .Version }}
+`
+ require.NoError(t, os.WriteFile(filepath.Join(dir, ".goreleaser.yaml"), []byte(goreleaser), 0o644))
+
+ err := RewriteModulePath(dir, "notion-pp-cli", "github.com/acme/library/notion-pp-cli")
+ require.NoError(t, err)
+
+ updatedGR, err := os.ReadFile(filepath.Join(dir, ".goreleaser.yaml"))
+ require.NoError(t, err)
+ assert.Contains(t, string(updatedGR), "-X github.com/acme/library/notion-pp-cli/internal/cli.version=")
+ assert.NotContains(t, string(updatedGR), "-X notion-pp-cli/internal/cli.version=")
+ })
+
+ t.Run("rewrites README go install path", func(t *testing.T) {
+ dir := t.TempDir()
+
+ gomod := "module notion-pp-cli\n\ngo 1.23\n"
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0o644))
+
+ readme := "# notion-pp-cli\n\n## Install\n\n```\ngo install github.com/testowner/notion-pp-cli/cmd/notion-pp-cli@latest\n```\n\n### Binary\n\nDownload from [Releases](https://github.com/testowner/notion-pp-cli/releases).\n\n```bash\nnotion-pp-cli doctor\nnotion-pp-cli users list\n```\n"
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "README.md"), []byte(readme), 0o644))
+
+ err := RewriteModulePath(dir, "notion-pp-cli", "github.com/acme/library/notion-pp-cli")
+ require.NoError(t, err)
+
+ updatedReadme, err := os.ReadFile(filepath.Join(dir, "README.md"))
+ require.NoError(t, err)
+ content := string(updatedReadme)
+
+ // go install path should be rewritten
+ assert.Contains(t, content, "go install github.com/acme/library/notion-pp-cli/cmd/notion-pp-cli@latest")
+ assert.Contains(t, content, "https://github.com/acme/library/releases")
+
+ // Bare CLI name in usage examples must NOT be rewritten
+ assert.Contains(t, content, "notion-pp-cli doctor")
+ assert.Contains(t, content, "notion-pp-cli users list")
+ assert.Contains(t, content, "# notion-pp-cli")
+ })
+
+ t.Run("rewrites goreleaser homepage to published repo", func(t *testing.T) {
+ dir := t.TempDir()
+
+ gomod := "module notion-pp-cli\n\ngo 1.23\n"
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0o644))
+
+ goreleaser := `version: 2
+brews:
+ - homepage: "https://github.com/testowner/notion-pp-cli"
+`
+ require.NoError(t, os.WriteFile(filepath.Join(dir, ".goreleaser.yaml"), []byte(goreleaser), 0o644))
+
+ err := RewriteModulePath(dir, "notion-pp-cli", "github.com/acme/library/notion-pp-cli")
+ require.NoError(t, err)
+
+ updatedGR, err := os.ReadFile(filepath.Join(dir, ".goreleaser.yaml"))
+ require.NoError(t, err)
+ assert.Contains(t, string(updatedGR), `homepage: "https://github.com/acme/library"`)
+ assert.NotContains(t, string(updatedGR), `homepage: "https://github.com/testowner/notion-pp-cli"`)
+ })
+
+ t.Run("noop when paths are equal", func(t *testing.T) {
+ dir := t.TempDir()
+ gomod := "module notion-pp-cli\n\ngo 1.23\n"
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0o644))
+
+ err := RewriteModulePath(dir, "notion-pp-cli", "notion-pp-cli")
+ require.NoError(t, err)
+ })
+
+ t.Run("error when go.mod missing old path", func(t *testing.T) {
+ dir := t.TempDir()
+ gomod := "module other-cli\n\ngo 1.23\n"
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0o644))
+
+ err := RewriteModulePath(dir, "notion-pp-cli", "github.com/org/repo/notion-pp-cli")
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "does not contain expected module path")
+ })
+}
diff --git a/skills/printing-press-publish/SKILL.md b/skills/printing-press-publish/SKILL.md
index c92a1d54..76e7049d 100644
--- a/skills/printing-press-publish/SKILL.md
+++ b/skills/printing-press-publish/SKILL.md
@@ -71,6 +71,22 @@ PUBLISH_REPO_DIR="$PRESS_HOME/.publish-repo"
PUBLISH_CONFIG="$PRESS_HOME/.publish-config.json"
```
+### Publish config
+
+`$PUBLISH_CONFIG` stores persistent publish settings as JSON. On first publish, create it with defaults. The user can edit it to change the library repo or module path base.
+
+```json
+{
+ "repo_url": "https://github.com/mvanhorn/printing-press-library",
+ "access": "push",
+ "protocol": "ssh",
+ "clone_path": "~/printing-press/.publish-repo",
+ "module_path_base": "github.com/mvanhorn/printing-press-library/library"
+}
+```
+
+The `module_path_base` field sets the Go module path prefix for published CLIs. During packaging, the full module path is constructed as `<module_path_base>/<category>/<cli-name>`. If the user wants CLIs published to a different repo or path, they edit this field.
+
## Step 1: Prerequisites
Verify `gh` is authenticated:
@@ -149,6 +165,14 @@ Save the `help_output` field from the result — it's used in the PR description
## Step 5: Package
+Read `$PUBLISH_CONFIG` to get `module_path_base`. Construct the full module path:
+
+```
+MODULE_PATH="<module_path_base>/<category>/<cli-name>"
+```
+
+For example: `github.com/mvanhorn/printing-press-library/library/productivity/notion-pp-cli`
+
Create a temporary staging directory and run:
```bash
@@ -156,10 +180,11 @@ printing-press publish package \
--dir <cli-dir> \
--category <category> \
--target <staging-dir> \
+ --module-path "$MODULE_PATH" \
--json
```
-Parse the JSON result. Note the `staged_dir`, `manuscripts_included`, and `run_id`.
+Parse the JSON result. Note the `staged_dir`, `module_path`, `manuscripts_included`, and `run_id`. The `module_path` field confirms the Go module path that was set in the packaged CLI's `go.mod` and import paths.
## Step 6: Managed Clone
@@ -190,7 +215,8 @@ If `$PUBLISH_REPO_DIR` does not exist:
"repo_url": "https://github.com/mvanhorn/printing-press-library",
"access": "push",
"protocol": "ssh",
- "clone_path": "~/printing-press/.publish-repo"
+ "clone_path": "~/printing-press/.publish-repo",
+ "module_path_base": "github.com/mvanhorn/printing-press-library/library"
}
```
Write to `$PUBLISH_CONFIG`.
← bafe3563 fix(skills): require CLI description rewrite after generatio
·
back to Cli Printing Press
·
feat(cli): add proxy-envelope client pattern to generator (# f0bb0de2 →