← back to Cli Printing Press
fix(cli): resolve shipcheck CLI binary name from .printing-press.json (#1700)
9eff1e208a5b0238d990f1e0774ae40afc8d5ceb · 2026-05-19 19:01:38 -0700 · Trevin Chow
* fix(cli): resolve shipcheck CLI binary name from .printing-press.json
shipcheckCLIPath and shipcheckCLIPathForGOOS derived the binary name from
filepath.Base(o.dir), which is wrong for slug-keyed library dirs. For
~/printing-press/library/notion, basename is "notion" but the binary is
"notion-pp-cli", so validate-narrative was invoked with a binary path
that does not exist on disk.
Resolve the binary name from .printing-press.json's cli_name via
pipeline.ReadCLIBinaryName, and fall back to filepath.Base(dir) when the
manifest is absent or unparseable (preserves legacy behavior).
Clawpatch finding: fnd_sig-feat-cli-command-e17f734225-_8ca7979519
* test(cli): cover unparseable-manifest fallback in shipcheckBinaryName
Adds TestShipcheckCLIPath_FallsBackOnUnparseableManifest to pin the
parse-error branch of pipeline.ReadCLIBinaryName: when
.printing-press.json exists but is malformed JSON, the binary name
must fall back to filepath.Base(dir) rather than propagate the parse
error or yield an empty name.
Greptile P2 on #1700 — the godoc promised the fallback for the
"absent or unparseable" case but only the absent path was tested.
Files touched
M internal/cli/shipcheck.goM internal/cli/shipcheck_test.go
Diff
commit 9eff1e208a5b0238d990f1e0774ae40afc8d5ceb
Author: Trevin Chow <trevin@trevinchow.com>
Date: Tue May 19 19:01:38 2026 -0700
fix(cli): resolve shipcheck CLI binary name from .printing-press.json (#1700)
* fix(cli): resolve shipcheck CLI binary name from .printing-press.json
shipcheckCLIPath and shipcheckCLIPathForGOOS derived the binary name from
filepath.Base(o.dir), which is wrong for slug-keyed library dirs. For
~/printing-press/library/notion, basename is "notion" but the binary is
"notion-pp-cli", so validate-narrative was invoked with a binary path
that does not exist on disk.
Resolve the binary name from .printing-press.json's cli_name via
pipeline.ReadCLIBinaryName, and fall back to filepath.Base(dir) when the
manifest is absent or unparseable (preserves legacy behavior).
Clawpatch finding: fnd_sig-feat-cli-command-e17f734225-_8ca7979519
* test(cli): cover unparseable-manifest fallback in shipcheckBinaryName
Adds TestShipcheckCLIPath_FallsBackOnUnparseableManifest to pin the
parse-error branch of pipeline.ReadCLIBinaryName: when
.printing-press.json exists but is malformed JSON, the binary name
must fall back to filepath.Base(dir) rather than propagate the parse
error or yield an empty name.
Greptile P2 on #1700 — the godoc promised the fallback for the
"absent or unparseable" case but only the absent path was tested.
---
internal/cli/shipcheck.go | 15 +++++++--
internal/cli/shipcheck_test.go | 70 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 83 insertions(+), 2 deletions(-)
diff --git a/internal/cli/shipcheck.go b/internal/cli/shipcheck.go
index 3c5a577b..8a5dfbad 100644
--- a/internal/cli/shipcheck.go
+++ b/internal/cli/shipcheck.go
@@ -11,6 +11,7 @@ import (
"strings"
"time"
+ "github.com/mvanhorn/cli-printing-press/v4/internal/pipeline"
"github.com/mvanhorn/cli-printing-press/v4/internal/platform"
"github.com/spf13/cobra"
)
@@ -156,12 +157,22 @@ func shipcheckResearchPath(o *shipcheckOpts) string {
return filepath.Join(dir, "research.json")
}
+// shipcheckBinaryName resolves the CLI binary name for a generated CLI directory.
+// Prefers .printing-press.json's cli_name (the canonical "<api-slug>-pp-cli" form)
+// and falls back to the directory's basename for legacy/manifest-less dirs.
+func shipcheckBinaryName(dir string) string {
+ if name := pipeline.ReadCLIBinaryName(dir); name != "" {
+ return name
+ }
+ return filepath.Base(dir)
+}
+
func shipcheckCLIPath(o *shipcheckOpts) string {
- return platform.ExecutablePath(filepath.Join(o.dir, filepath.Base(o.dir)))
+ return platform.ExecutablePath(filepath.Join(o.dir, shipcheckBinaryName(o.dir)))
}
func shipcheckCLIPathForGOOS(o *shipcheckOpts, goos string) string {
- return platform.ExecutablePathForGOOS(filepath.Join(o.dir, filepath.Base(o.dir)), goos)
+ return platform.ExecutablePathForGOOS(filepath.Join(o.dir, shipcheckBinaryName(o.dir)), goos)
}
// shipcheckLegResult is the per-leg outcome of one umbrella run.
diff --git a/internal/cli/shipcheck_test.go b/internal/cli/shipcheck_test.go
index db13d6f3..910b9a0b 100644
--- a/internal/cli/shipcheck_test.go
+++ b/internal/cli/shipcheck_test.go
@@ -64,6 +64,76 @@ func TestShipcheckCLIPathForGOOS(t *testing.T) {
}
}
+// TestShipcheckCLIPath_ManifestOverridesBasename covers the slug-keyed
+// library layout: dir basename ("notion") differs from the actual binary
+// name ("notion-pp-cli"), so the binary path must be resolved from
+// .printing-press.json's cli_name rather than the directory's basename.
+func TestShipcheckCLIPath_ManifestOverridesBasename(t *testing.T) {
+ t.Parallel()
+
+ dir := filepath.Join(t.TempDir(), "notion")
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ t.Fatalf("creating slug dir: %v", err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, ".printing-press.json"), []byte(`{"cli_name":"notion-pp-cli"}`), 0o644); err != nil {
+ t.Fatalf("writing manifest: %v", err)
+ }
+ opts := &shipcheckOpts{dir: dir}
+
+ if got, want := shipcheckCLIPathForGOOS(opts, "linux"), filepath.Join(dir, "notion-pp-cli"); got != want {
+ t.Fatalf("linux path = %q, want %q", got, want)
+ }
+ if got, want := shipcheckCLIPathForGOOS(opts, "windows"), filepath.Join(dir, "notion-pp-cli.exe"); got != want {
+ t.Fatalf("windows path = %q, want %q", got, want)
+ }
+}
+
+// TestShipcheckCLIPath_FallsBackToBasename covers the manifest-less case:
+// when .printing-press.json is missing or unparseable, the binary name
+// falls back to the directory basename (preserves legacy behavior).
+func TestShipcheckCLIPath_FallsBackToBasename(t *testing.T) {
+ t.Parallel()
+
+ dir := filepath.Join(t.TempDir(), "sample-cli")
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ t.Fatalf("creating dir: %v", err)
+ }
+ opts := &shipcheckOpts{dir: dir}
+
+ if got, want := shipcheckCLIPathForGOOS(opts, "linux"), filepath.Join(dir, "sample-cli"); got != want {
+ t.Fatalf("linux path = %q, want %q", got, want)
+ }
+ if got, want := shipcheckCLIPathForGOOS(opts, "windows"), filepath.Join(dir, "sample-cli.exe"); got != want {
+ t.Fatalf("windows path = %q, want %q", got, want)
+ }
+}
+
+// TestShipcheckCLIPath_FallsBackOnUnparseableManifest pins the
+// unparseable-error branch of pipeline.ReadCLIBinaryName: a
+// .printing-press.json that exists but contains malformed JSON must
+// fall back to the directory basename rather than propagating a parse
+// error or producing an empty binary name.
+func TestShipcheckCLIPath_FallsBackOnUnparseableManifest(t *testing.T) {
+ t.Parallel()
+
+ dir := filepath.Join(t.TempDir(), "notion")
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ t.Fatalf("creating slug dir: %v", err)
+ }
+ // Truncated JSON: opening brace, key, colon, no value, no close.
+ if err := os.WriteFile(filepath.Join(dir, ".printing-press.json"), []byte(`{"cli_name":`), 0o644); err != nil {
+ t.Fatalf("writing malformed manifest: %v", err)
+ }
+ opts := &shipcheckOpts{dir: dir}
+
+ if got, want := shipcheckCLIPathForGOOS(opts, "linux"), filepath.Join(dir, "notion"); got != want {
+ t.Fatalf("linux path = %q, want %q", got, want)
+ }
+ if got, want := shipcheckCLIPathForGOOS(opts, "windows"), filepath.Join(dir, "notion.exe"); got != want {
+ t.Fatalf("windows path = %q, want %q", got, want)
+ }
+}
+
type shipcheckHarness struct {
dir string
logFile string
← b534884f feat(cli): GraphQL credential probe in doctor + actionable c
·
back to Cli Printing Press
·
fix(cli): harden manifest-gen remote spec loader against han aecf495a →