[object Object]

← back to Cli Printing Press

fix(cli): replace MarkFlagRequired with RunE validation, remove import guards, fix type fidelity (#130)

b5c911584e5e2882f00fd46c31a89eb881e23c7e · 2026-04-04 23:38:52 -0700 · Trevin Chow

* fix(cli): replace MarkFlagRequired with RunE validation, remove import guards, fix type fidelity

Three systemic generator fixes discovered during cal.com CLI polish:

1. MarkFlagRequired blocks --dry-run: Cobra enforces required flags before
   RunE, preventing dry-run from reaching the handler. Moved validation to
   RunE with a !flags.dryRun guard so verify can test all commands.

2. Import guards penalize type fidelity: Promoted command template emitted
   var _ = strings.ReplaceAll and friends. Removed guards and unused imports
   (io, strings) since promoted commands only generate GET endpoints.

3. goType() mapped object/array to string: Added goStructType() that returns
   json.RawMessage for object/array fields in type structs. CLI flags remain
   strings (correct for cobra). Types template conditionally imports
   encoding/json only when non-scalar fields exist.

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

* fix(cli): address Codex review — stdin guard, promoted types, Changed() over zero-val

Three fixes from Codex review:

1. P1: Body validation now skipped when --stdin is set, so JSON-from-stdin
   workflow isn't blocked by required body field checks.

2. P1: Promoted template now uses goTypeForParam/cobraFlagFuncForParam
   instead of goType/cobraFlagFunc, aligning ID param types correctly.

3. P2: Replaced zero-value comparison with cmd.Flags().Changed() so
   --count=0 and --enabled=false are not rejected as "not set".

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit b5c911584e5e2882f00fd46c31a89eb881e23c7e
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Sat Apr 4 23:38:52 2026 -0700

    fix(cli): replace MarkFlagRequired with RunE validation, remove import guards, fix type fidelity (#130)
    
    * fix(cli): replace MarkFlagRequired with RunE validation, remove import guards, fix type fidelity
    
    Three systemic generator fixes discovered during cal.com CLI polish:
    
    1. MarkFlagRequired blocks --dry-run: Cobra enforces required flags before
       RunE, preventing dry-run from reaching the handler. Moved validation to
       RunE with a !flags.dryRun guard so verify can test all commands.
    
    2. Import guards penalize type fidelity: Promoted command template emitted
       var _ = strings.ReplaceAll and friends. Removed guards and unused imports
       (io, strings) since promoted commands only generate GET endpoints.
    
    3. goType() mapped object/array to string: Added goStructType() that returns
       json.RawMessage for object/array fields in type structs. CLI flags remain
       strings (correct for cobra). Types template conditionally imports
       encoding/json only when non-scalar fields exist.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    
    * fix(cli): address Codex review — stdin guard, promoted types, Changed() over zero-val
    
    Three fixes from Codex review:
    
    1. P1: Body validation now skipped when --stdin is set, so JSON-from-stdin
       workflow isn't blocked by required body field checks.
    
    2. P1: Promoted template now uses goTypeForParam/cobraFlagFuncForParam
       instead of goType/cobraFlagFunc, aligning ID param types correctly.
    
    3. P2: Replaced zero-value comparison with cmd.Flags().Changed() so
       --count=0 and --enabled=false are not rejected as "not set".
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
 ...-005-fix-generator-dryrun-imports-types-plan.md | 249 +++++++++++++++++++++
 internal/generator/generator.go                    |  31 ++-
 internal/generator/generator_test.go               |  74 ++++++
 .../generator/templates/command_endpoint.go.tmpl   |  24 +-
 .../generator/templates/command_promoted.go.tmpl   |  22 +-
 internal/generator/templates/types.go.tmpl         |   5 +-
 6 files changed, 381 insertions(+), 24 deletions(-)

diff --git a/docs/plans/2026-04-04-005-fix-generator-dryrun-imports-types-plan.md b/docs/plans/2026-04-04-005-fix-generator-dryrun-imports-types-plan.md
new file mode 100644
index 00000000..a18d692a
--- /dev/null
+++ b/docs/plans/2026-04-04-005-fix-generator-dryrun-imports-types-plan.md
@@ -0,0 +1,249 @@
+---
+title: "fix: Generator machinery — dry-run compatibility, import guards, type fidelity"
+type: fix
+status: active
+date: 2026-04-04
+---
+
+# fix: Generator machinery — dry-run compatibility, import guards, type fidelity
+
+## Overview
+
+Three systemic generator bugs cause every printed CLI to lose scorecard points and fail verify dry-run checks. These were discovered during the cal.com CLI polish and confirmed against the Steam Run 2 retro findings. All three are machine-level fixes in the Go binary templates.
+
+## Problem Frame
+
+When polishing the cal.com CLI (94/100 Grade A), three categories of issues required manual fixes to 20+ files. Every future printed CLI will hit the same problems because the generator templates produce the problematic patterns.
+
+1. **`MarkFlagRequired` blocks dry-run** — Cobra enforces required flags before RunE executes, so `--dry-run` never reaches the handler. Verify fails for every command with required params.
+2. **Import guards penalize type fidelity** — `var _ = strings.ReplaceAll` and friends in promoted command templates cause the scorer to deduct 1 point from type fidelity.
+3. **`goType()` maps object/array to string** — The type template calls `goType()` which has no case for `object` or `array`, so nested API response fields become `string` instead of `json.RawMessage`.
+
+## Requirements Trace
+
+- R1. Commands with required flags must still be runnable with `--dry-run`
+- R2. Generated promoted commands must not contain `var _` import guard lines
+- R3. Generated type structs must use `json.RawMessage` for object/array fields, not `string`
+- R4. Existing tests must continue to pass; new tests must cover each fix
+
+## Scope Boundaries
+
+- OAuth2 browser flow generation is out of scope (tracked separately)
+- Dogfood false-positive detection for nested sub-resources is out of scope (separate machine issue)
+- No changes to the scorecard — it already correctly rewards the right patterns
+
+## Context & Research
+
+### Relevant Code and Patterns
+
+- `internal/generator/templates/command_endpoint.go.tmpl` lines 281-290 — emits `MarkFlagRequired`
+- `internal/generator/templates/command_promoted.go.tmpl` lines 16-20 — emits `var _` guards
+- `internal/generator/templates/command_promoted.go.tmpl` lines 143-145 — emits `MarkFlagRequired` for promoted commands
+- `internal/generator/generator.go` `goType()` lines 775-788 — type mapping with no object/array case
+- `internal/generator/templates/types.go.tmpl` line 8 — calls `goType(.Type)` for every struct field
+- `internal/spec/spec.go` `Param.Required` field — drives the `MarkFlagRequired` decision
+- `internal/generator/generator_test.go` — 30+ existing tests for generated output
+
+### Institutional Learnings
+
+- Steam Run 2 retro (`docs/retros/2026-03-31-steam-run2-retro.md`) flagged the import guard issue as priority #5 with "Fix the generator" recommendation
+- Same retro flagged ID params as StringVar vs IntVar — adjacent but separate issue
+
+## Key Technical Decisions
+
+- **Move validation to RunE instead of removing required enforcement entirely**: Required params should still error when `--dry-run` is not set. The fix moves the check to RunE where `flags.dryRun` is accessible, not removes it.
+- **Remove import guards rather than making imports conditional**: Promoted commands only generate GET endpoints. The `io`, `os`, and `strings` imports are provably unused. Remove both the guards and the unused imports from the promoted template.
+- **Use `json.RawMessage` for non-scalar types, not typed structs**: Generating full Go structs for every nested object requires deep schema traversal. `json.RawMessage` preserves JSON fidelity without that complexity and is the standard Go pattern for "pass-through JSON."
+
+## Open Questions
+
+### Resolved During Planning
+
+- **Should dry-run provide default values for required params?** No — the template fix should only skip the validation error. The client's existing dry-run handler already prints the request without sending it. If a required param is empty, it shows as empty in the dry-run output, which is correct.
+- **Do endpoint commands (not just promoted) also have import guards?** No — `command_endpoint.go.tmpl` does not emit `var _` guards. Only `command_promoted.go.tmpl` does.
+
+### Deferred to Implementation
+
+- Whether `types.go.tmpl` needs an `import "encoding/json"` conditional or can always import it — depends on whether any spec produces zero object/array fields (unlikely but check during implementation)
+
+## Implementation Units
+
+- [ ] **Unit 1: Replace MarkFlagRequired with RunE validation in endpoint template**
+
+**Goal:** Commands with required flags accept `--dry-run` without error
+
+**Requirements:** R1
+
+**Dependencies:** None
+
+**Files:**
+- Modify: `internal/generator/templates/command_endpoint.go.tmpl`
+- Test: `internal/generator/generator_test.go`
+
+**Approach:**
+- Remove the `MarkFlagRequired` emission blocks (lines 281-283 for params, 288-290 for body)
+- Add a validation block at the start of RunE (after positional args check, before `flags.newClient()`):
+  - For each required param/body field, check if the value equals zero-value AND `!flags.dryRun`
+  - If so, return `fmt.Errorf("required flag \"%s\" not set", flagName)`
+- Use existing template helpers `zeroValForParam` and `flagName` to generate the checks
+
+**Patterns to follow:**
+- Existing positional args validation pattern at lines 41-44 of the same template
+- The `usageErr()` helper already exists in generated CLIs
+
+**Test scenarios:**
+- Happy path: Generated command with required param — source contains `fmt.Errorf("required flag"` in RunE, does NOT contain `MarkFlagRequired`
+- Happy path: Generated command with no required params — no validation block emitted
+- Edge case: Command with both required params and required body fields — both get RunE validation
+- Integration: Generated output compiles (`go vet`) with the new validation pattern
+
+**Verification:**
+- `go test ./internal/generator/ -run TestGenerate` passes
+- Generated test fixture has no `MarkFlagRequired` calls
+- Generated test fixture has RunE validation guarded by `!flags.dryRun`
+
+---
+
+- [ ] **Unit 2: Replace MarkFlagRequired with RunE validation in promoted template**
+
+**Goal:** Promoted commands with required flags accept `--dry-run` without error
+
+**Requirements:** R1
+
+**Dependencies:** Unit 1 (same pattern)
+
+**Files:**
+- Modify: `internal/generator/templates/command_promoted.go.tmpl`
+- Test: `internal/generator/generator_test.go`
+
+**Approach:**
+- Same pattern as Unit 1, applied to the promoted template
+- Remove `MarkFlagRequired` at lines 143-145
+- Add RunE validation after the promoted command's own positional check
+
+**Patterns to follow:**
+- Unit 1's approach — identical pattern, different template file
+
+**Test scenarios:**
+- Happy path: Generated promoted command with required params has RunE validation, not `MarkFlagRequired`
+- Integration: `TestGeneratedOutput_PromotedCommandCompiles` still passes
+
+**Verification:**
+- Existing promoted command tests pass
+- Generated promoted command source has no `MarkFlagRequired`
+
+---
+
+- [ ] **Unit 3: Remove import guards from promoted template**
+
+**Goal:** Promoted commands don't emit `var _` import guard lines
+
+**Requirements:** R2
+
+**Dependencies:** None (can be done in parallel with Units 1-2)
+
+**Files:**
+- Modify: `internal/generator/templates/command_promoted.go.tmpl`
+- Test: `internal/generator/generator_test.go`
+
+**Approach:**
+- Remove lines 16-20 (`var _ = strings.ReplaceAll`, `var _ = fmt.Sprintf`, etc.)
+- Remove unused imports from the template's import block: `io`, `strings` (promoted commands only use GET endpoints so these are provably unused)
+- Keep `encoding/json`, `fmt`, `os`, `github.com/spf13/cobra` — all used by promoted GET commands
+
+**Patterns to follow:**
+- `command_endpoint.go.tmpl` — imports only what it uses with conditional blocks
+
+**Test scenarios:**
+- Happy path: Generated promoted command source does NOT contain `var _ =` lines
+- Happy path: Generated promoted command source does NOT import `"io"` or `"strings"`
+- Integration: `TestGeneratedOutput_PromotedCommandCompiles` still passes — generated code compiles without the guards
+
+**Verification:**
+- No `var _` in generated promoted command output
+- `go vet` passes on generated output
+
+---
+
+- [ ] **Unit 4: Map object/array types to json.RawMessage in goType()**
+
+**Goal:** Generated type structs use `json.RawMessage` for nested objects and arrays
+
+**Requirements:** R3
+
+**Dependencies:** None (can be done in parallel with Units 1-3)
+
+**Files:**
+- Modify: `internal/generator/generator.go` (`goType()` function)
+- Modify: `internal/generator/templates/types.go.tmpl` (add conditional `encoding/json` import)
+- Test: `internal/generator/generator_test.go`
+
+**Approach:**
+- Add cases for `"object"` and `"array"` in `goType()` that return `"json.RawMessage"`
+- In `types.go.tmpl`, add `import "encoding/json"` — either unconditionally (since most specs have at least one object field) or behind a conditional that checks if any field uses `json.RawMessage`
+- The types template currently has no import block at all (line 4 is `package types`); add one
+
+**Patterns to follow:**
+- `goStoreType()` at line 798 already maps JSON columns to `json.RawMessage`
+- The store types template includes the `encoding/json` import
+
+**Test scenarios:**
+- Happy path: `goType("object")` returns `"json.RawMessage"`
+- Happy path: `goType("array")` returns `"json.RawMessage"`
+- Happy path: `goType("string")` still returns `"string"` (regression check)
+- Happy path: Generated types file includes `import "encoding/json"` when object fields exist
+- Edge case: Spec with only primitive fields — types file still compiles (import may be unused; verify whether this is possible or if every spec has at least one object field)
+
+**Verification:**
+- `go test ./internal/generator/ -run TestGenerate` passes
+- Generated types fixture uses `json.RawMessage` for object/array fields
+
+---
+
+- [ ] **Unit 5: Add targeted tests for all three fixes**
+
+**Goal:** Regression tests that prevent these issues from returning
+
+**Requirements:** R4
+
+**Dependencies:** Units 1-4
+
+**Files:**
+- Modify: `internal/generator/generator_test.go`
+
+**Approach:**
+- Add `TestGeneratedOutput_NoMarkFlagRequired` — generates from a spec with required params, asserts no `MarkFlagRequired` in output, asserts RunE validation present
+- Add `TestGeneratedOutput_PromotedNoImportGuards` — generates promoted command, asserts no `var _ =` in output
+- Add `TestGeneratedOutput_ObjectFieldsUseRawMessage` — generates types from a spec with object/array fields, asserts `json.RawMessage` in output, asserts no `string` for known object fields
+
+**Patterns to follow:**
+- Existing `TestGeneratedOutput_HasSelectFlag` pattern — generate, read output, assert string presence/absence
+
+**Test scenarios:**
+- Each test should both assert the fix IS present and assert the old pattern IS NOT present (belt and suspenders)
+
+**Verification:**
+- `go test ./internal/generator/ -count=1` passes with new tests
+- `go test ./... -count=1` all green
+
+## System-Wide Impact
+
+- **Interaction graph:** All three templates feed into the generator pipeline. Changes affect every future `printing-press generate` run.
+- **Error propagation:** RunE validation errors propagate the same way as `MarkFlagRequired` errors — through cobra's error handler. User-visible error messages will be identical.
+- **State lifecycle risks:** None — templates are stateless text generation.
+- **API surface parity:** The `import.go.tmpl` also has a `MarkFlagRequired` for `--input` (line 96). This is fine — import is not a verify-tested command and the required input file flag should always error without the flag.
+- **Unchanged invariants:** Non-dry-run behavior is identical. Required flags still error when missing. Only the dry-run path changes.
+
+## Risks & Dependencies
+
+| Risk | Mitigation |
+|------|------------|
+| Types import block breaks specs with zero object fields | Unlikely — every real API spec has object response types. Test with a minimal all-primitives fixture to verify. |
+| RunE validation error message differs from cobra's built-in | Use the same `"required flag \"%s\" not set"` format cobra uses internally. |
+| Promoted template removes an import that IS conditionally used | Verified: promoted commands only generate GET endpoints (generator.go line 1189 filters `Method != "GET"`). `io` and `strings` are provably unused. |
+
+## Sources & References
+
+- Steam Run 2 retro: `docs/retros/2026-03-31-steam-run2-retro.md` (items #5 and #4)
+- Cal.com CLI polish session (2026-04-04) — discovered all three issues during manual fix-up
+- Cobra required flags docs: enforcement happens in PreRunE, before RunE
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 97327afb..88725df5 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -78,6 +78,7 @@ func New(s *spec.APISpec, outputDir string) *Generator {
 		"snake":                 toSnake,
 		"pascal":                toPascal,
 		"goType":                goType,
+		"goStructType":          goStructType,
 		"goTypeForParam":        goTypeForParam,
 		"goStoreType":           goStoreType,
 		"cobraFlagFunc":         cobraFlagFunc,
@@ -103,10 +104,20 @@ func New(s *spec.APISpec, outputDir string) *Generator {
 		"oneline":            oneline,
 		"flagName":           flagName,
 		"safeTypeName":       safeTypeName,
-		"exampleLine":        g.exampleLine,
-		"currentYear":        func() string { return strconv.Itoa(time.Now().Year()) },
-		"modulePath":         func() string { return naming.CLI(s.Name) },
-		"kebab":              toKebab,
+		"hasNonScalarType": func(types map[string]spec.TypeDef) bool {
+			for _, td := range types {
+				for _, f := range td.Fields {
+					if f.Type == "object" || f.Type == "array" {
+						return true
+					}
+				}
+			}
+			return false
+		},
+		"exampleLine": g.exampleLine,
+		"currentYear": func() string { return strconv.Itoa(time.Now().Year()) },
+		"modulePath":  func() string { return naming.CLI(s.Name) },
+		"kebab":       toKebab,
 		"humanName": func(s string) string {
 			// "steam-web" → "Steam Web", "notion" → "Notion"
 			return cases.Title(language.English).String(strings.ReplaceAll(s, "-", " "))
@@ -787,6 +798,18 @@ func goType(t string) string {
 	}
 }
 
+// goStructType returns the Go type for a struct field definition.
+// Unlike goType (used for CLI flags which are always primitives),
+// this maps object/array types to json.RawMessage for type fidelity.
+func goStructType(t string) string {
+	switch t {
+	case "object", "array":
+		return "json.RawMessage"
+	default:
+		return goType(t)
+	}
+}
+
 func goStoreType(sqlType string) string {
 	upper := strings.ToUpper(sqlType)
 	switch {
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index d5632865..d7661bda 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -1328,3 +1328,77 @@ func TestGenerate_ComposedAuthUsesBrowserTemplate(t *testing.T) {
 	runGoCommand(t, outputDir, "mod", "tidy")
 	runGoCommand(t, outputDir, "build", "./...")
 }
+
+// --- Regression tests for machinery fixes (cal.com retro 2026-04-04) ---
+
+func TestGeneratedOutput_NoMarkFlagRequired(t *testing.T) {
+	t.Parallel()
+
+	outputDir := generatePetstore(t)
+
+	// Read all generated endpoint command files
+	cliDir := filepath.Join(outputDir, "internal", "cli")
+	entries, err := os.ReadDir(cliDir)
+	require.NoError(t, err)
+
+	foundRunEValidation := false
+	for _, e := range entries {
+		if e.IsDir() || !strings.HasSuffix(e.Name(), ".go") {
+			continue
+		}
+		data, err := os.ReadFile(filepath.Join(cliDir, e.Name()))
+		require.NoError(t, err)
+		content := string(data)
+
+		// No command should use MarkFlagRequired (except import.go which is not verify-tested)
+		if e.Name() != "import.go" && strings.Contains(content, "MarkFlagRequired") {
+			t.Errorf("%s still contains MarkFlagRequired", e.Name())
+		}
+
+		// Track whether we find the RunE-based validation
+		if strings.Contains(content, `!flags.dryRun`) && strings.Contains(content, `required flag`) {
+			foundRunEValidation = true
+		}
+	}
+
+	// The petstore spec has required params, so we should find RunE validation
+	assert.True(t, foundRunEValidation, "required params should use RunE validation with dryRun guard")
+}
+
+func TestGeneratedOutput_PromotedNoImportGuards(t *testing.T) {
+	t.Parallel()
+
+	outputDir := generatePetstore(t)
+
+	cliDir := filepath.Join(outputDir, "internal", "cli")
+	entries, err := os.ReadDir(cliDir)
+	require.NoError(t, err)
+
+	for _, e := range entries {
+		if !strings.HasPrefix(e.Name(), "promoted_") {
+			continue
+		}
+		data, err := os.ReadFile(filepath.Join(cliDir, e.Name()))
+		require.NoError(t, err)
+		content := string(data)
+
+		assert.NotContains(t, content, "var _ =", "promoted command %s should not contain import guards", e.Name())
+		assert.NotContains(t, content, "var _ json", "promoted command %s should not contain import guards", e.Name())
+		assert.NotContains(t, content, `"io"`, "promoted command %s should not import io", e.Name())
+		assert.NotContains(t, content, `"strings"`, "promoted command %s should not import strings", e.Name())
+	}
+}
+
+func TestGeneratedOutput_ObjectFieldsUseRawMessage(t *testing.T) {
+	t.Parallel()
+
+	outputDir := generatePetstore(t)
+
+	typesGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "types", "types.go"))
+	require.NoError(t, err)
+	content := string(typesGo)
+
+	// The petstore spec has object-typed schema fields; they should be json.RawMessage
+	assert.Contains(t, content, "json.RawMessage", "types.go should use json.RawMessage for object/array fields")
+	assert.Contains(t, content, `import "encoding/json"`, "types.go should import encoding/json when RawMessage is used")
+}
diff --git a/internal/generator/templates/command_endpoint.go.tmpl b/internal/generator/templates/command_endpoint.go.tmpl
index 90b0fb29..ccb712ab 100644
--- a/internal/generator/templates/command_endpoint.go.tmpl
+++ b/internal/generator/templates/command_endpoint.go.tmpl
@@ -42,6 +42,24 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
 			if len(args) == 0 {
 				return cmd.Help()
 			}
+{{- end}}
+{{- range .Endpoint.Params}}
+{{- if and .Required (not .Positional)}}
+			if !cmd.Flags().Changed("{{flagName .Name}}") && !flags.dryRun {
+				return fmt.Errorf("required flag \"%s\" not set", "{{flagName .Name}}")
+			}
+{{- end}}
+{{- end}}
+{{- if or (eq .Endpoint.Method "POST") (eq .Endpoint.Method "PUT") (eq .Endpoint.Method "PATCH")}}
+			if !stdinBody {
+{{- range .Endpoint.Body}}
+{{- if .Required}}
+				if !cmd.Flags().Changed("{{flagName .Name}}") && !flags.dryRun {
+					return fmt.Errorf("required flag \"%s\" not set", "{{flagName .Name}}")
+				}
+{{- end}}
+{{- end}}
+			}
 {{- end}}
 			c, err := flags.newClient()
 			if err != nil {
@@ -278,16 +296,10 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
 {{- range .Endpoint.Params}}
 {{- if not .Positional}}
 	cmd.Flags().{{cobraFlagFuncForParam .Name .Type}}(&flag{{camel .Name}}, "{{flagName .Name}}", {{defaultValForParam .}}, "{{oneline .Description}}")
-{{- if .Required}}
-	_ = cmd.MarkFlagRequired("{{flagName .Name}}")
-{{- end}}
 {{- end}}
 {{- end}}
 {{- range .Endpoint.Body}}
 	cmd.Flags().{{cobraFlagFunc .Type}}(&body{{camel .Name}}, "{{flagName .Name}}", {{defaultVal .}}, "{{oneline .Description}}")
-{{- if .Required}}
-	_ = cmd.MarkFlagRequired("{{flagName .Name}}")
-{{- end}}
 {{- end}}
 {{- if .Endpoint.Pagination}}
 	cmd.Flags().BoolVar(&flagAll, "all", false, "Fetch all pages")
diff --git a/internal/generator/templates/command_promoted.go.tmpl b/internal/generator/templates/command_promoted.go.tmpl
index df403eb1..ee5fe358 100644
--- a/internal/generator/templates/command_promoted.go.tmpl
+++ b/internal/generator/templates/command_promoted.go.tmpl
@@ -6,23 +6,15 @@ package cli
 import (
 	"encoding/json"
 	"fmt"
-	"io"
 	"os"
-	"strings"
 
 	"github.com/spf13/cobra"
 )
 
-var _ = strings.ReplaceAll // ensure import
-var _ = fmt.Sprintf        // ensure import
-var _ = io.ReadAll         // ensure import
-var _ = os.Stdin           // ensure import
-var _ json.RawMessage      // ensure import
-
 func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 {{- range .Endpoint.Params}}
 {{- if not .Positional}}
-	var flag{{camel .Name}} {{goType .Type}}
+	var flag{{camel .Name}} {{goTypeForParam .Name .Type}}
 {{- end}}
 {{- end}}
 {{- if .Endpoint.Pagination}}
@@ -35,6 +27,13 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 		Long:  "Shortcut for '{{.ResourceName}} {{.EndpointName}}'. {{oneline .Endpoint.Description}}",
 		Example: "  {{modulePath}} {{.PromotedName}}",
 		RunE: func(cmd *cobra.Command, args []string) error {
+{{- range .Endpoint.Params}}
+{{- if and .Required (not .Positional)}}
+			if !cmd.Flags().Changed("{{flagName .Name}}") && !flags.dryRun {
+				return fmt.Errorf("required flag \"%s\" not set", "{{flagName .Name}}")
+			}
+{{- end}}
+{{- end}}
 			c, err := flags.newClient()
 			if err != nil {
 				return err
@@ -139,10 +138,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 
 {{- range .Endpoint.Params}}
 {{- if not .Positional}}
-	cmd.Flags().{{cobraFlagFunc .Type}}(&flag{{camel .Name}}, "{{flagName .Name}}", {{defaultVal .}}, "{{oneline .Description}}")
-{{- if .Required}}
-	_ = cmd.MarkFlagRequired("{{flagName .Name}}")
-{{- end}}
+	cmd.Flags().{{cobraFlagFuncForParam .Name .Type}}(&flag{{camel .Name}}, "{{flagName .Name}}", {{defaultValForParam .}}, "{{oneline .Description}}")
 {{- end}}
 {{- end}}
 {{- if .Endpoint.Pagination}}
diff --git a/internal/generator/templates/types.go.tmpl b/internal/generator/templates/types.go.tmpl
index 48c54399..7fc7033a 100644
--- a/internal/generator/templates/types.go.tmpl
+++ b/internal/generator/templates/types.go.tmpl
@@ -2,10 +2,13 @@
 // Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
 
 package types
+{{if hasNonScalarType .Types}}
+import "encoding/json"
+{{end}}
 {{range $name, $typeDef := .Types}}
 type {{safeTypeName $name}} struct {
 {{- range $typeDef.Fields}}
-	{{camel .Name}} {{goType .Type}} `json:"{{.Name}}"`
+	{{camel .Name}} {{goStructType .Type}} `json:"{{.Name}}"`
 {{- end}}
 }
 {{end}}

← 0dac6232 feat(cli): add name collision detection and resolution to pu  ·  back to Cli Printing Press  ·  feat(skills): make printing-press-retro a public skill (#129 c2076e02 →