← back to Cli Printing Press
fix(cli): research-dir state.json api_name overrides info.title-derived name (#1476)
a9ffa08f0d373b28406feb94acd6b1aacf11f819 · 2026-05-15 13:29:46 -0700 · Trevin Chow
When `generate --research-dir <dir>` ran with a state.json that recorded
`api_name: canvas`, the generator still derived the CLI/module name
from `cleanSpecName(info.title)` ("Canvas LMS API" -> `canvas-lms`).
The mismatch made `publish validate --help/--version` fail because the
manifest's `cli_name: canvas-pp-cli` pointed at a `cmd/canvas-pp-cli/`
directory that didn't exist; only `cmd/canvas-lms-pp-cli/` did.
Add `pipeline.LoadAPINameFromResearchDir(dir)` that reads
`<dir>/state.json` and returns the recorded `api_name`. Wire it into the
generate command's single-spec branch as an implicit `--name` override
that yields to an explicit `--name` flag. Behavior without
`--research-dir` is unchanged.
Closes #1375
Files touched
M internal/cli/root.goM internal/pipeline/climanifest.goM internal/pipeline/climanifest_test.go
Diff
commit a9ffa08f0d373b28406feb94acd6b1aacf11f819
Author: Trevin Chow <trevin@trevinchow.com>
Date: Fri May 15 13:29:46 2026 -0700
fix(cli): research-dir state.json api_name overrides info.title-derived name (#1476)
When `generate --research-dir <dir>` ran with a state.json that recorded
`api_name: canvas`, the generator still derived the CLI/module name
from `cleanSpecName(info.title)` ("Canvas LMS API" -> `canvas-lms`).
The mismatch made `publish validate --help/--version` fail because the
manifest's `cli_name: canvas-pp-cli` pointed at a `cmd/canvas-pp-cli/`
directory that didn't exist; only `cmd/canvas-lms-pp-cli/` did.
Add `pipeline.LoadAPINameFromResearchDir(dir)` that reads
`<dir>/state.json` and returns the recorded `api_name`. Wire it into the
generate command's single-spec branch as an implicit `--name` override
that yields to an explicit `--name` flag. Behavior without
`--research-dir` is unchanged.
Closes #1375
---
internal/cli/root.go | 10 ++++++-
internal/pipeline/climanifest.go | 26 ++++++++++++++++++
internal/pipeline/climanifest_test.go | 50 +++++++++++++++++++++++++++++++++++
3 files changed, 85 insertions(+), 1 deletion(-)
diff --git a/internal/cli/root.go b/internal/cli/root.go
index 1945a8e4..9debd614 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -304,9 +304,17 @@ func newGenerateCmd() *cobra.Command {
var apiSpec *spec.APISpec
if len(specs) == 1 {
apiSpec = specs[0]
- // Override spec-derived name when --name is explicitly provided
+ // Override spec-derived name when --name is explicitly provided.
+ // When --name is empty but --research-dir points at a state.json
+ // whose api_name slug differs from the title-derived name (e.g.
+ // "Canvas LMS API" → `canvas-lms` vs the user's intended
+ // `canvas`), prefer the state.json slug so the generated
+ // cmd/<slug>-pp-cli matches what manifest/publish-validate look
+ // for. Explicit --name still wins.
if cliName != "" {
apiSpec.Name = cliName
+ } else if researchName := pipeline.LoadAPINameFromResearchDir(researchDir); researchName != "" {
+ apiSpec.Name = researchName
}
} else {
if cliName == "" {
diff --git a/internal/pipeline/climanifest.go b/internal/pipeline/climanifest.go
index 56fc218d..1780139d 100644
--- a/internal/pipeline/climanifest.go
+++ b/internal/pipeline/climanifest.go
@@ -560,6 +560,32 @@ func DeriveRunIDFromResearchDir(researchDir string) string {
return ""
}
+// LoadAPINameFromResearchDir reads `<researchDir>/state.json` and returns the
+// recorded api_name slug, or "" when the file is absent, unreadable, malformed,
+// or has no api_name. The generate command uses this as an implicit --name
+// override so a spec whose `info.title` derives to something different from
+// the user's intended slug (e.g. "Canvas LMS API" vs `canvas`) still produces
+// the slug-keyed cmd/ directory the rest of the pipeline expects. Explicit
+// --name wins over this; an absent or unreadable state.json silently yields
+// to the title-derived default.
+func LoadAPINameFromResearchDir(researchDir string) string {
+ if researchDir == "" {
+ return ""
+ }
+ statePath := filepath.Join(researchDir, "state.json")
+ data, err := os.ReadFile(statePath)
+ if err != nil {
+ return ""
+ }
+ var probe struct {
+ APIName string `json:"api_name"`
+ }
+ if err := json.Unmarshal(data, &probe); err != nil {
+ return ""
+ }
+ return strings.TrimSpace(probe.APIName)
+}
+
// WriteManifestForGenerate writes a .printing-press.json manifest into the
// generated CLI directory. This is the generate-command counterpart of
// writeCLIManifestForPublish (which operates on PipelineState).
diff --git a/internal/pipeline/climanifest_test.go b/internal/pipeline/climanifest_test.go
index d577ec25..a79ca9ea 100644
--- a/internal/pipeline/climanifest_test.go
+++ b/internal/pipeline/climanifest_test.go
@@ -637,6 +637,56 @@ func TestDeriveRunIDFromResearchDir(t *testing.T) {
}
}
+func TestLoadAPINameFromResearchDir(t *testing.T) {
+ t.Parallel()
+
+ t.Run("returns api_name when state.json present", func(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ require.NoError(t, os.WriteFile(
+ filepath.Join(dir, "state.json"),
+ []byte(`{"api_name":"canvas","run_id":"20260514-070718"}`),
+ 0o644,
+ ))
+ assert.Equal(t, "canvas", LoadAPINameFromResearchDir(dir))
+ })
+
+ t.Run("trims whitespace from api_name", func(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ require.NoError(t, os.WriteFile(
+ filepath.Join(dir, "state.json"),
+ []byte(`{"api_name":" canvas "}`),
+ 0o644,
+ ))
+ assert.Equal(t, "canvas", LoadAPINameFromResearchDir(dir))
+ })
+
+ t.Run("empty string when researchDir empty", func(t *testing.T) {
+ t.Parallel()
+ assert.Equal(t, "", LoadAPINameFromResearchDir(""))
+ })
+
+ t.Run("empty string when state.json absent", func(t *testing.T) {
+ t.Parallel()
+ assert.Equal(t, "", LoadAPINameFromResearchDir(t.TempDir()))
+ })
+
+ t.Run("empty string when state.json malformed", func(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "state.json"), []byte(`not json`), 0o644))
+ assert.Equal(t, "", LoadAPINameFromResearchDir(dir))
+ })
+
+ t.Run("empty string when api_name field missing", func(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "state.json"), []byte(`{"run_id":"20260514-070718"}`), 0o644))
+ assert.Equal(t, "", LoadAPINameFromResearchDir(dir))
+ })
+}
+
func TestArchiveRunArtifactsCopiesDiscovery(t *testing.T) {
home := setPressTestEnv(t)
← 272d8e83 fix(cli): preserve OpenAPI param casing in detected paginati
·
back to Cli Printing Press
·
docs(cli): canonicalize README install block + cross-repo sw 77b91be6 →