[object Object]

← back to Cli Printing Press

fix(cli): drop unused client import from param-less endpoint command files (#911)

72adfefd408b12f5d02b8e6aeef2ba85183b0859 · 2026-05-15 15:50:20 -0400 · Kyle Kirkland

* fix(cli): drop unused client import from param-less endpoint command files

The per-endpoint command template emits internal/client whenever the
spec is GraphQL-shaped and the endpoint sits at /graphql with name
list or get. The corresponding `client.X` reference inside the rendered
body only lands in the GET branch, so any GraphQL spec recording its
list/get endpoints with method POST (the wire-correct method, since
GraphQL queries POST to /graphql) ships an unused import. Go's strict
unused-import rule fires and `go build` fails on every such CLI.

A post-render fixup over command_endpoint.go.tmpl output drops the
`<module>/internal/client` import line when the rendered body never
references the `client` package as a qualifier. Endpoints that DO
reference `client.X` (current GraphQL GET branches) keep the import.

Closes #908

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: comments

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* test(cli): add golden fixture for GraphQL shared-endpoint client-import pruning

Locks the import-block shape for the POST /graphql list/get branch so
future template edits cannot silently re-introduce the unused
internal/client import that this PR's pruneUnusedClientImport addresses.
TestEndpointCommandBuildsPostFixup catches compile-time regressions, but
without a golden the rendered import block is unwitnessed and a future
refactor could re-add the line under a different code path that still
compiles for some unrelated reason.

Addresses the second Greptile review comment on #911 per the AGENTS.md
rule: "When adding a new deterministic CLI behavior or generated
artifact contract, explicitly decide whether the golden suite needs a
new or expanded fixture."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(cli): refresh generate-graphql-shared-endpoint goldens against current main

The fixture introduced by this PR was captured before main added:
- gofmt of rendered .go templates in normalizeRendered (alters struct
  field alignment, multi-line struct literals)
- expanded terminal-detection guard for table output that takes --csv /
  --plain / --quiet into account

Behavior of the import-prune change in this PR is unchanged — the
fixture diff is purely the downstream of main's progression.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Trevin Chow <trevin@trevinchow.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

Files touched

Diff

commit 72adfefd408b12f5d02b8e6aeef2ba85183b0859
Author: Kyle Kirkland <Kyle.kirkland0528@gmail.com>
Date:   Fri May 15 15:50:20 2026 -0400

    fix(cli): drop unused client import from param-less endpoint command files (#911)
    
    * fix(cli): drop unused client import from param-less endpoint command files
    
    The per-endpoint command template emits internal/client whenever the
    spec is GraphQL-shaped and the endpoint sits at /graphql with name
    list or get. The corresponding `client.X` reference inside the rendered
    body only lands in the GET branch, so any GraphQL spec recording its
    list/get endpoints with method POST (the wire-correct method, since
    GraphQL queries POST to /graphql) ships an unused import. Go's strict
    unused-import rule fires and `go build` fails on every such CLI.
    
    A post-render fixup over command_endpoint.go.tmpl output drops the
    `<module>/internal/client` import line when the rendered body never
    references the `client` package as a qualifier. Endpoints that DO
    reference `client.X` (current GraphQL GET branches) keep the import.
    
    Closes #908
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * fix: comments
    
    Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
    
    * test(cli): add golden fixture for GraphQL shared-endpoint client-import pruning
    
    Locks the import-block shape for the POST /graphql list/get branch so
    future template edits cannot silently re-introduce the unused
    internal/client import that this PR's pruneUnusedClientImport addresses.
    TestEndpointCommandBuildsPostFixup catches compile-time regressions, but
    without a golden the rendered import block is unwitnessed and a future
    refactor could re-add the line under a different code path that still
    compiles for some unrelated reason.
    
    Addresses the second Greptile review comment on #911 per the AGENTS.md
    rule: "When adding a new deterministic CLI behavior or generated
    artifact contract, explicitly decide whether the golden suite needs a
    new or expanded fixture."
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * test(cli): refresh generate-graphql-shared-endpoint goldens against current main
    
    The fixture introduced by this PR was captured before main added:
    - gofmt of rendered .go templates in normalizeRendered (alters struct
      field alignment, multi-line struct literals)
    - expanded terminal-detection guard for table output that takes --csv /
      --plain / --quiet into account
    
    Behavior of the import-prune change in this PR is unchanged — the
    fixture diff is purely the downstream of main's progression.
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    Co-authored-by: Trevin Chow <trevin@trevinchow.com>
    Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
---
 internal/generator/generator.go                    |  65 +++++++++-
 internal/generator/plan_generate.go                |   2 +-
 internal/generator/unused_client_import_test.go    | 138 +++++++++++++++++++++
 .../generate-graphql-shared-endpoint/artifacts.txt |   2 +
 .../generate-graphql-shared-endpoint/command.txt   |   2 +
 .../generate-graphql-shared-endpoint/exit.txt      |   1 +
 .../internal/cli/things_get.go                     | 120 ++++++++++++++++++
 .../internal/cli/things_list.go                    | 117 +++++++++++++++++
 .../generate-graphql-shared-endpoint/stderr.txt    |   2 +
 .../generate-graphql-shared-endpoint/stdout.txt    |   0
 .../golden/fixtures/graphql-shared-endpoint.yaml   |  33 +++++
 11 files changed, 479 insertions(+), 3 deletions(-)

diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index fa1e29de..ea9a3c79 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -2770,7 +2770,7 @@ func (g *Generator) renderTemplate(tmplName, outPath string, data any) error {
 		return err
 	}
 
-	return os.WriteFile(fullPath, normalizeRendered(buf.Bytes(), outPath), 0o644)
+	return os.WriteFile(fullPath, normalizeRendered(buf.Bytes(), tmplName, outPath), 0o644)
 }
 
 // normalizeRendered prepares template-rendered bytes for disk: trims trailing
@@ -2780,9 +2780,12 @@ func (g *Generator) renderTemplate(tmplName, outPath string, data any) error {
 // print surfaces hundreds of phantom diffs on the first `gofmt -w`. Falls
 // through with a stderr warning rather than fail-hard so a malformed template
 // surfaces as a compile error downstream instead of an opaque emit failure.
-func normalizeRendered(raw []byte, outPath string) []byte {
+func normalizeRendered(raw []byte, tmplName, outPath string) []byte {
 	rendered := bytes.TrimRight(raw, " \t\r\n")
 	rendered = append(rendered, '\n')
+	if tmplName == "command_endpoint.go.tmpl" {
+		rendered = pruneUnusedClientImport(rendered)
+	}
 	if filepath.Ext(outPath) != ".go" {
 		return rendered
 	}
@@ -2794,6 +2797,64 @@ func normalizeRendered(raw []byte, outPath string) []byte {
 	return formatted
 }
 
+// pruneUnusedClientImport drops the `<module>/internal/client` import line
+// from a rendered command_endpoint.go file when the body never references
+// the `client` package as a qualifier. The endpoint template emits this
+// import for the GraphQL list/get path, but other branches (notably
+// POST /graphql for GraphQL specs whose author wrote method: POST) reach
+// the rendered file without producing a `client.X` reference, leaving an
+// unused import that breaks `go build`.
+func pruneUnusedClientImport(src []byte) []byte {
+	// Find the import block.
+	importStart := bytes.Index(src, []byte("\nimport (\n"))
+	if importStart < 0 {
+		return src
+	}
+	importBlockStart := importStart + len("\nimport (\n")
+	importEnd := bytes.Index(src[importBlockStart:], []byte("\n)"))
+	if importEnd < 0 {
+		return src
+	}
+	importEnd += importBlockStart
+
+	// Body is everything after the closing `)` of the import block.
+	body := src[importEnd:]
+	// A `client.X` reference is the only way the import is used inside the
+	// rendered body.
+	if bytes.Contains(body, []byte("client.")) {
+		return src
+	}
+
+	// Locate the line `"<...>/internal/client"` inside the import block and
+	// remove it (including its trailing newline). The literal suffix is
+	// stable across all module paths because it's emitted as
+	// `"{{modulePath}}/internal/client"` from the template.
+	importBlock := src[importBlockStart:importEnd]
+	suffix := []byte(`/internal/client"`)
+	rel := bytes.Index(importBlock, suffix)
+	if rel < 0 {
+		return src
+	}
+	abs := importBlockStart + rel
+	// Walk back to the start of the line.
+	lineStart := abs
+	for lineStart > 0 && src[lineStart-1] != '\n' {
+		lineStart--
+	}
+	// Walk forward to the end of the line, including the trailing newline.
+	lineEnd := abs
+	for lineEnd < len(src) && src[lineEnd] != '\n' {
+		lineEnd++
+	}
+	if lineEnd < len(src) {
+		lineEnd++
+	}
+	out := make([]byte, 0, len(src)-(lineEnd-lineStart))
+	out = append(out, src[:lineStart]...)
+	out = append(out, src[lineEnd:]...)
+	return out
+}
+
 func validateRenderedArtifact(outPath, content string) error {
 	switch filepath.Base(outPath) {
 	case "README.md", "SKILL.md":
diff --git a/internal/generator/plan_generate.go b/internal/generator/plan_generate.go
index 42d1bdf2..58f9d639 100644
--- a/internal/generator/plan_generate.go
+++ b/internal/generator/plan_generate.go
@@ -105,7 +105,7 @@ func GenerateFromPlan(planSpec *PlanSpec, outputDir string) error {
 			return fmt.Errorf("executing template %s: %w", tmplName, err)
 		}
 		fullPath := filepath.Join(outputDir, outPath)
-		return os.WriteFile(fullPath, normalizeRendered(buf.Bytes(), outPath), 0o644)
+		return os.WriteFile(fullPath, normalizeRendered(buf.Bytes(), tmplName, outPath), 0o644)
 	}
 
 	// Partition commands into top-level and subcommands
diff --git a/internal/generator/unused_client_import_test.go b/internal/generator/unused_client_import_test.go
new file mode 100644
index 00000000..3a4993a5
--- /dev/null
+++ b/internal/generator/unused_client_import_test.go
@@ -0,0 +1,138 @@
+package generator
+
+import (
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/mvanhorn/cli-printing-press/v4/internal/spec"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// TestEndpointCommandDropsUnusedClientImport pins the post-render fixup that
+// strips `<module>/internal/client` from per-endpoint command files when the
+// rendered body never references the `client` package.
+//
+// The endpoint template emits the import for the GraphQL list/get path, but
+// other branches reach the rendered file without producing a `client.X`
+// reference — most prominently when a GraphQL spec records list/get endpoints
+// with method POST (which is the wire-correct method, since GraphQL queries
+// POST to /graphql). Without the fixup, Go's strict unused-import rule fires
+// and `go build` fails on every such CLI.
+func TestEndpointCommandDropsUnusedClientImport(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name            string
+		method          string
+		expectImport    bool // whether the generated endpoint command keeps the client import
+		expectClientUse bool
+	}{
+		{
+			name:            "graphql_post_drops_unused_import",
+			method:          "POST",
+			expectImport:    false,
+			expectClientUse: false,
+		},
+		{
+			name:            "graphql_get_keeps_used_import",
+			method:          "GET",
+			expectImport:    true,
+			expectClientUse: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			apiSpec := &spec.APISpec{
+				Name:    tt.name,
+				Version: "0.1.0",
+				BaseURL: "https://api.example.com/graphql",
+				Owner:   "test-owner",
+				Auth: spec.AuthConfig{
+					Type:    "api_key",
+					Header:  "Authorization",
+					Format:  "Bearer {token}",
+					EnvVars: []string{"X_TOKEN"},
+				},
+				Resources: map[string]spec.Resource{
+					"accounts": {
+						Description: "Accounts",
+						Endpoints: map[string]spec.Endpoint{
+							"list": {Method: tt.method, Path: "/graphql", Description: "List accounts"},
+							"get": {Method: tt.method, Path: "/graphql", Description: "Get account",
+								Params: []spec.Param{{Name: "id", Type: "string", Required: true, Positional: true, Description: "Account ID"}},
+							},
+						},
+					},
+				},
+			}
+
+			outputDir := filepath.Join(t.TempDir(), tt.name+"-pp-cli")
+			gen := New(apiSpec, outputDir)
+			require.NoError(t, gen.Generate())
+
+			for _, fname := range []string{"accounts_list.go", "accounts_get.go"} {
+				src := readGeneratedFile(t, outputDir, "internal", "cli", fname)
+				hasImport := strings.Contains(src, `/internal/client"`)
+				hasUsage := strings.Contains(src, "client.")
+				assert.Equal(t, tt.expectImport, hasImport, "%s import presence", fname)
+				assert.Equal(t, tt.expectClientUse, hasUsage, "%s client.X usage", fname)
+			}
+		})
+	}
+}
+
+// TestEndpointCommandBuildsPostFixup runs `go build` on a generated module
+// whose endpoints would have shipped an unused client import before the
+// fixup landed. Catches future regressions of the underlying compile error
+// even if the import-vs-usage assertions in the unit test above drift.
+func TestEndpointCommandBuildsPostFixup(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping go build smoke test in short mode")
+	}
+	t.Parallel()
+
+	apiSpec := &spec.APISpec{
+		Name:    "buildcheck",
+		Version: "0.1.0",
+		BaseURL: "https://api.example.com/graphql",
+		Owner:   "test-owner",
+		Auth: spec.AuthConfig{
+			Type:    "api_key",
+			Header:  "Authorization",
+			Format:  "Bearer {token}",
+			EnvVars: []string{"X_TOKEN"},
+		},
+		Resources: map[string]spec.Resource{
+			"accounts": {
+				Description: "Accounts",
+				Endpoints: map[string]spec.Endpoint{
+					"list": {Method: "POST", Path: "/graphql", Description: "List accounts"},
+					"get": {Method: "POST", Path: "/graphql", Description: "Get account",
+						Params: []spec.Param{{Name: "id", Type: "string", Required: true, Positional: true, Description: "Account ID"}},
+					},
+				},
+			},
+		},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), "buildcheck-pp-cli")
+	gen := New(apiSpec, outputDir)
+	require.NoError(t, gen.Generate())
+
+	tidy := exec.Command("go", "mod", "tidy")
+	tidy.Dir = outputDir
+	if out, err := tidy.CombinedOutput(); err != nil {
+		t.Fatalf("go mod tidy failed:\n%s", out)
+	}
+
+	build := exec.Command("go", "build", "./...")
+	build.Dir = outputDir
+	out, err := build.CombinedOutput()
+	require.NoError(t, err, "go build failed:\n%s", out)
+}
diff --git a/testdata/golden/cases/generate-graphql-shared-endpoint/artifacts.txt b/testdata/golden/cases/generate-graphql-shared-endpoint/artifacts.txt
new file mode 100644
index 00000000..f920f649
--- /dev/null
+++ b/testdata/golden/cases/generate-graphql-shared-endpoint/artifacts.txt
@@ -0,0 +1,2 @@
+graphql-shared-golden/internal/cli/things_list.go
+graphql-shared-golden/internal/cli/things_get.go
diff --git a/testdata/golden/cases/generate-graphql-shared-endpoint/command.txt b/testdata/golden/cases/generate-graphql-shared-endpoint/command.txt
new file mode 100644
index 00000000..ba0c6473
--- /dev/null
+++ b/testdata/golden/cases/generate-graphql-shared-endpoint/command.txt
@@ -0,0 +1,2 @@
+set -euo pipefail
+"$BINARY" generate --spec testdata/golden/fixtures/graphql-shared-endpoint.yaml --output "$CASE_ACTUAL_DIR/graphql-shared-golden" --force --validate=false
diff --git a/testdata/golden/expected/generate-graphql-shared-endpoint/exit.txt b/testdata/golden/expected/generate-graphql-shared-endpoint/exit.txt
new file mode 100644
index 00000000..573541ac
--- /dev/null
+++ b/testdata/golden/expected/generate-graphql-shared-endpoint/exit.txt
@@ -0,0 +1 @@
+0
diff --git a/testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_get.go b/testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_get.go
new file mode 100644
index 00000000..faed9b13
--- /dev/null
+++ b/testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_get.go
@@ -0,0 +1,120 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+func newThingsGetCmd(flags *rootFlags) *cobra.Command {
+	var stdinBody bool
+
+	cmd := &cobra.Command{
+		Use:         "get <thing_id>",
+		Short:       "Get a thing via GraphQL",
+		Example:     "  graphql-shared-golden-pp-cli things get 550e8400-e29b-41d4-a716-446655440000",
+		Annotations: map[string]string{"pp:endpoint": "things.get", "pp:method": "POST", "pp:path": "/graphql", "mcp:read-only": "true"},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			if len(args) == 0 {
+				return cmd.Help()
+			}
+			if !stdinBody {
+			}
+			c, err := flags.newClient()
+			if err != nil {
+				return err
+			}
+
+			path := "/graphql"
+			_ = path
+			var body map[string]any
+			if stdinBody {
+				stdinData, err := io.ReadAll(os.Stdin)
+				if err != nil {
+					return fmt.Errorf("reading stdin: %w", err)
+				}
+				var jsonBody map[string]any
+				if err := json.Unmarshal(stdinData, &jsonBody); err != nil {
+					return fmt.Errorf("parsing stdin JSON: %w", err)
+				}
+				body = jsonBody
+			} else {
+				body = map[string]any{}
+			}
+			data, statusCode, err := c.Post(path, body)
+			if err != nil {
+				return classifyAPIError(err, flags)
+			}
+			if wantsHumanTable(cmd.OutOrStdout(), flags) {
+				// Check if response contains an array (directly or wrapped in "data")
+				var items []map[string]any
+				if json.Unmarshal(data, &items) == nil && len(items) > 0 {
+					if err := printAutoTable(cmd.OutOrStdout(), items); err != nil {
+						fmt.Fprintf(os.Stderr, "warning: table rendering failed, falling back to JSON: %v\n", err)
+					} else {
+						return nil
+					}
+				} else {
+					var wrapped struct {
+						Data []map[string]any `json:"data"`
+					}
+					if json.Unmarshal(data, &wrapped) == nil && len(wrapped.Data) > 0 {
+						if err := printAutoTable(cmd.OutOrStdout(), wrapped.Data); err != nil {
+							fmt.Fprintf(os.Stderr, "warning: table rendering failed, falling back to JSON: %v\n", err)
+						} else {
+							return nil
+						}
+					}
+				}
+			}
+			if flags.asJSON || (!isTerminal(cmd.OutOrStdout()) && !flags.csv && !flags.quiet && !flags.plain) {
+				if flags.quiet {
+					return nil
+				}
+				// Apply --compact and --select to the API response before wrapping.
+				// --select wins when both are set: explicit field choice trumps the
+				// generic high-gravity allow-list. Otherwise --compact still applies
+				// when --agent is on but the user did not name fields.
+				filtered := data
+				if flags.selectFields != "" {
+					filtered = filterFields(filtered, flags.selectFields)
+				} else if flags.compact {
+					filtered = compactFields(filtered)
+				}
+				envelope := map[string]any{
+					"action":   "post",
+					"resource": "things",
+					"path":     path,
+					"status":   statusCode,
+					"success":  statusCode >= 200 && statusCode < 300,
+				}
+				if flags.dryRun {
+					envelope["dry_run"] = true
+					envelope["status"] = 0
+					envelope["success"] = false
+				}
+				if len(filtered) > 0 {
+					var parsed any
+					if err := json.Unmarshal(filtered, &parsed); err == nil {
+						envelope["data"] = parsed
+					}
+				}
+				envelopeJSON, err := json.Marshal(envelope)
+				if err != nil {
+					return err
+				}
+				return printOutput(cmd.OutOrStdout(), json.RawMessage(envelopeJSON), true)
+			}
+			return printOutputWithFlags(cmd.OutOrStdout(), data, flags)
+		},
+	}
+	cmd.Flags().BoolVar(&stdinBody, "stdin", false, "Read request body as JSON from stdin")
+
+	return cmd
+}
diff --git a/testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_list.go b/testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_list.go
new file mode 100644
index 00000000..5263ff0d
--- /dev/null
+++ b/testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_list.go
@@ -0,0 +1,117 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+func newThingsListCmd(flags *rootFlags) *cobra.Command {
+	var stdinBody bool
+
+	cmd := &cobra.Command{
+		Use:         "list",
+		Short:       "List things via GraphQL",
+		Example:     "  graphql-shared-golden-pp-cli things list",
+		Annotations: map[string]string{"pp:endpoint": "things.list", "pp:method": "POST", "pp:path": "/graphql", "mcp:read-only": "true"},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			if !stdinBody {
+			}
+			c, err := flags.newClient()
+			if err != nil {
+				return err
+			}
+
+			path := "/graphql"
+			_ = path
+			var body map[string]any
+			if stdinBody {
+				stdinData, err := io.ReadAll(os.Stdin)
+				if err != nil {
+					return fmt.Errorf("reading stdin: %w", err)
+				}
+				var jsonBody map[string]any
+				if err := json.Unmarshal(stdinData, &jsonBody); err != nil {
+					return fmt.Errorf("parsing stdin JSON: %w", err)
+				}
+				body = jsonBody
+			} else {
+				body = map[string]any{}
+			}
+			data, statusCode, err := c.Post(path, body)
+			if err != nil {
+				return classifyAPIError(err, flags)
+			}
+			if wantsHumanTable(cmd.OutOrStdout(), flags) {
+				// Check if response contains an array (directly or wrapped in "data")
+				var items []map[string]any
+				if json.Unmarshal(data, &items) == nil && len(items) > 0 {
+					if err := printAutoTable(cmd.OutOrStdout(), items); err != nil {
+						fmt.Fprintf(os.Stderr, "warning: table rendering failed, falling back to JSON: %v\n", err)
+					} else {
+						return nil
+					}
+				} else {
+					var wrapped struct {
+						Data []map[string]any `json:"data"`
+					}
+					if json.Unmarshal(data, &wrapped) == nil && len(wrapped.Data) > 0 {
+						if err := printAutoTable(cmd.OutOrStdout(), wrapped.Data); err != nil {
+							fmt.Fprintf(os.Stderr, "warning: table rendering failed, falling back to JSON: %v\n", err)
+						} else {
+							return nil
+						}
+					}
+				}
+			}
+			if flags.asJSON || (!isTerminal(cmd.OutOrStdout()) && !flags.csv && !flags.quiet && !flags.plain) {
+				if flags.quiet {
+					return nil
+				}
+				// Apply --compact and --select to the API response before wrapping.
+				// --select wins when both are set: explicit field choice trumps the
+				// generic high-gravity allow-list. Otherwise --compact still applies
+				// when --agent is on but the user did not name fields.
+				filtered := data
+				if flags.selectFields != "" {
+					filtered = filterFields(filtered, flags.selectFields)
+				} else if flags.compact {
+					filtered = compactFields(filtered)
+				}
+				envelope := map[string]any{
+					"action":   "post",
+					"resource": "things",
+					"path":     path,
+					"status":   statusCode,
+					"success":  statusCode >= 200 && statusCode < 300,
+				}
+				if flags.dryRun {
+					envelope["dry_run"] = true
+					envelope["status"] = 0
+					envelope["success"] = false
+				}
+				if len(filtered) > 0 {
+					var parsed any
+					if err := json.Unmarshal(filtered, &parsed); err == nil {
+						envelope["data"] = parsed
+					}
+				}
+				envelopeJSON, err := json.Marshal(envelope)
+				if err != nil {
+					return err
+				}
+				return printOutput(cmd.OutOrStdout(), json.RawMessage(envelopeJSON), true)
+			}
+			return printOutputWithFlags(cmd.OutOrStdout(), data, flags)
+		},
+	}
+	cmd.Flags().BoolVar(&stdinBody, "stdin", false, "Read request body as JSON from stdin")
+
+	return cmd
+}
diff --git a/testdata/golden/expected/generate-graphql-shared-endpoint/stderr.txt b/testdata/golden/expected/generate-graphql-shared-endpoint/stderr.txt
new file mode 100644
index 00000000..475570e1
--- /dev/null
+++ b/testdata/golden/expected/generate-graphql-shared-endpoint/stderr.txt
@@ -0,0 +1,2 @@
+warning: could not derive run_id from --research-dir; phase5 dogfood acceptance will refuse to write without it
+Generated graphql-shared-golden at <ARTIFACT_DIR>/generate-graphql-shared-endpoint/graphql-shared-golden
diff --git a/testdata/golden/expected/generate-graphql-shared-endpoint/stdout.txt b/testdata/golden/expected/generate-graphql-shared-endpoint/stdout.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/golden/fixtures/graphql-shared-endpoint.yaml b/testdata/golden/fixtures/graphql-shared-endpoint.yaml
new file mode 100644
index 00000000..3b07cf15
--- /dev/null
+++ b/testdata/golden/fixtures/graphql-shared-endpoint.yaml
@@ -0,0 +1,33 @@
+name: graphql-shared-golden
+description: Golden fixture for GraphQL specs where every endpoint POSTs to a single shared /graphql path. Locks the import-block shape for the GraphQL POST branch so future template edits cannot silently re-introduce an unused `internal/client` import that breaks `go build` of the generated CLI.
+version: "0.1.0"
+base_url: https://api.graphql-shared.example
+auth:
+  type: bearer_token
+  header: Authorization
+  env_vars:
+    - GRAPHQL_SHARED_GOLDEN_TOKEN
+resources:
+  things:
+    description: GraphQL-backed things
+    endpoints:
+      list:
+        method: POST
+        path: /graphql
+        description: List things via GraphQL
+        response:
+          type: array
+          item: Thing
+      get:
+        method: POST
+        path: /graphql
+        description: Get a thing via GraphQL
+        params:
+          - name: thing_id
+            type: string
+            required: true
+            positional: true
+            description: Thing ID
+        response:
+          type: object
+          item: Thing

← 35e515c1 fix(cli): query generic resources_fts from search empty-type  ·  back to Cli Printing Press  ·  fix(cli): pick gid/sid/uid before name in PK heuristic (#146 01c816da →