← 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
M internal/generator/generator.goM internal/generator/plan_generate.goA internal/generator/unused_client_import_test.goA testdata/golden/cases/generate-graphql-shared-endpoint/artifacts.txtA testdata/golden/cases/generate-graphql-shared-endpoint/command.txtA testdata/golden/expected/generate-graphql-shared-endpoint/exit.txtA testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_get.goA testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_list.goA testdata/golden/expected/generate-graphql-shared-endpoint/stderr.txtA testdata/golden/expected/generate-graphql-shared-endpoint/stdout.txtA testdata/golden/fixtures/graphql-shared-endpoint.yaml
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 →