Skip to content

refactor: auto-generate flag tables and replace --format with global --json#80

Merged
danielewood merged 8 commits intomainfrom
refactor/gendocs-and-json-flag
Feb 27, 2026
Merged

refactor: auto-generate flag tables and replace --format with global --json#80
danielewood merged 8 commits intomainfrom
refactor/gendocs-and-json-flag

Conversation

@danielewood
Copy link
Copy Markdown
Collaborator

@danielewood danielewood commented Feb 26, 2026

Summary

  • Add gendocs tool that auto-generates README flag tables from Cobra command definitions via go generate, with a CI check and pre-commit hook to keep them in sync
  • Extract ValidationError to dedicated errors.go file
  • Add global --json persistent flag — all commands now support JSON output; overrides per-command --format when both are set
  • Add JSON output to keygen, csr, sign, bundle, and convert commands (previously text-only)
  • Per-command --format text|json flags retained on display commands (inspect, verify, connect, scan, ocsp, crl)
  • Breaking: connect JSON sha256_fingerprint format changed from lowercase hex to colon-separated uppercase hex for consistency with inspect and sha1_fingerprint (CLI-4)
  • Breaking: Rename csr --cert to --from-cert for clarity
  • Fix backtick-quoted values in flag usage strings being consumed by pflag as type placeholders — all --format, --trust-store, --log-level, --algorithm, and --curve flags now display correctly in --help

Test plan

  • go build ./... passes
  • go test -race -count=1 ./... passes
  • go vet ./... clean
  • golangci-lint run — 0 issues
  • go generate ./... — README up to date
  • WASM build passes
  • All pre-commit hooks pass
  • Verify certkit inspect cert.pem --json works
  • Verify certkit bundle cert.pem --format p12 still works
  • Verify certkit convert cert.pem --to pem works
  • Verify --help output has no backtick artifacts on any command

🤖 Generated with Claude Code

…--json

Add gendocs tool that generates README flag tables from Cobra command
definitions via `go generate`, with CI check and pre-commit hook to
keep them in sync. Extract ValidationError to dedicated errors.go.

Replace per-command `--format text|json` flag on inspect, verify,
connect, scan, ocsp, and crl with a global `--json` persistent flag.
Rename `convert --to` to `convert --format` for consistency with
`bundle --format` (both specify container format).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 26, 2026 21:46
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces automated generation of CLI flag tables in README.md from Cobra command definitions, replacing the per-command --format text|json flags with a global --json boolean flag, and renaming convert --to to convert --format for consistency. It also extracts the ValidationError type to a dedicated errors.go file for better organization.

Changes:

  • Added gendocs tool with build tag that auto-generates markdown flag tables from Cobra commands via go generate, with CI and pre-commit enforcement
  • Replaced per-command --format flags with a global --json persistent flag on inspect, verify, connect, scan, ocsp, and crl commands
  • Renamed convert --to to convert --format for consistency with bundle --format

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated no comments.

Show a summary per file
File Description
doc.go Adds go:generate directive to run gendocs tool
cmd/certkit/main.go Adds build tag !gendocs to exclude from gendocs build
cmd/certkit/errors.go Extracts ValidationError type from main.go
cmd/certkit/gendocs.go New tool that generates markdown flag tables from Cobra command definitions
cmd/certkit/root.go Adds global --json persistent flag and updates flag descriptions with backticks
cmd/certkit/inspect.go Removes local --format flag and init function; uses global jsonOutput variable
cmd/certkit/verify.go Removes local --format flag; uses global jsonOutput; updates flag descriptions
cmd/certkit/connect.go Removes local --format flag; uses global jsonOutput
cmd/certkit/scan.go Removes local --format flag; uses global jsonOutput; reorders flag initialization
cmd/certkit/ocsp.go Removes local --format flag; uses global jsonOutput; updates example to use --json
cmd/certkit/crl.go Removes local --format flag; uses global jsonOutput; updates example to use --json
cmd/certkit/convert.go Renames --to flag to --format; updates all references and examples
cmd/certkit/bundle.go Updates flag descriptions with backticks; adds readme_default annotation
cmd/certkit/sign.go Reorders flags; adds readme_default annotations; updates flag descriptions
cmd/certkit/keygen.go Updates flag descriptions with backticks; adds readme_default annotation
cmd/certkit/csr.go Updates flag descriptions; adds readme_default annotation
README.md Auto-generated flag tables with markers; reflects all CLI flag changes
EXAMPLES.md Updates all examples to use --json instead of --format json and --format instead of --to
CLAUDE.md Updates CI check count to 11; adds G-8 requirement; updates CLI-3 and CLI-7 to reference --json
CHANGELOG.md Documents breaking changes and new auto-generation feature
.pre-commit-config.yaml Adds gendocs hook that runs on relevant file changes
.github/workflows/ci.yml Adds docs CI job that verifies flag tables are up to date

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

Keep per-command --format on display commands (inspect, verify, connect,
scan, ocsp, crl) while adding a global --json persistent flag that works
on ALL commands. --json overrides --format when both are set.

Add JSON output to keygen, csr, sign (self-signed and csr), bundle, and
convert commands. Revert convert --format back to --to. Fix CHANGELOG
refs and add missing PR references.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

- Fix connect JSON sha256_fingerprint to use colon-hex format matching
  inspect and sha1_fingerprint (CLI-4 consistency)
- Fix bundle --json to base64 encode binary p12/jks output instead of
  casting raw bytes to string (produces valid JSON)
- Fix sign --json + -o to write file AND output JSON (matching
  bundle/convert behavior; previously --json silently skipped file write)
- Simplify redundant conditionals in keygen/csr JSON output (omitempty
  handles empty strings)
- Move --json changelog entries from Changed to Added section (CL-2)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 27, 2026 01:16
- Add spliceMarkerInput struct for 3-parameter spliceMarker (CS-5)
- Replace fmt.Fprintf(os.Stderr) with slog calls for diagnostics (OBS-1)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@danielewood
Copy link
Copy Markdown
Collaborator Author

All 4 findings addressed in commits 5ac49bf and b5aed21:

  1. CS-5 violation (spliceMarker 3 params) — Fixed in b5aed21: added spliceMarkerInput struct, updated call site.
  2. CL-2 violation (new features under Changed) — Fixed in 5ac49bf: moved both --json entries to ### Added.
  3. Sign --json + -o bug — Fixed in 5ac49bf: file write now runs independently before JSON/raw output selection. Both runSignSelfSigned and runSignCSR updated.
  4. Bundle binary corruption — Fixed in 5ac49bf: binary formats (p12/jks) are now base64-encoded in JSON. When writing to file, emits metadata JSON (file, format, size) matching the convert pattern.

Additionally fixed in b5aed21:

  • OBS-1 violation in gendocs.go: replaced fmt.Fprintf(os.Stderr, ...) with slog.Error/slog.Info calls.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.

Comment thread cmd/certkit/convert.go Outdated
Comment thread cmd/certkit/connect.go
- Standardize --json override to use local variable copy instead of
  mutating package-level format variables (all 6 display commands)
- Decouple keygen/csr stderr file messages from --json (always print
  when -o is set, regardless of --json)
- Add file path fields to keygenJSON and csrJSON for script discovery
- Fix bundle --json -o with PEM format to emit file metadata instead
  of full PEM data (matches convert pattern)
- Add doc comment on connectCertJSON struct
- Add breaking change note for connect sha256_fingerprint format change

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

pflag's UnquoteUsage consumes backtick-quoted text as type placeholders,
corrupting --help output for --format, --trust-store, --log-level,
--algorithm, and --curve flags. Remove all backticks from usage strings.

Rename csr --cert to --from-cert for clarity — avoids confusion with
certificate file arguments used by other commands.

Also fix convert --json without -o missing the format field in output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 27, 2026 02:07
- github.com/spf13/pflag v1.0.9 → v1.0.10
- golang.org/x/exp → 2025-02-18
- modernc.org/libc v1.67.6 → v1.68.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 24 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

cmd/certkit/convert.go:55

  • PR description says convert --to was renamed to convert --format, but this change doesn't appear to be implemented: the flag is still named --to, help text/examples still refer to --to, and completion is registered for to. Either implement the rename (and update README/examples/completions) or adjust the PR description to avoid claiming this breaking change.
var convertCmd = &cobra.Command{
	Use:   "convert <file>",
	Short: "Convert certificates and keys between formats",
	Long: `Convert certificates and keys between PEM, DER, PKCS#12, JKS, and PKCS#7.

Input format is auto-detected. Use --to to specify the output format.
Binary formats (p12, jks) require -o to write to a file.

When --key is provided, convert matches keys to leaf certificates and builds
chain bundles. If multiple keys match different certs, JKS output creates a
multi-alias keystore. PKCS#12 supports only a single key entry.`,
	Example: `  certkit convert cert.der --to pem
  certkit convert cert.pem --to der -o cert.der
  certkit convert cert.pem --key key.pem --to p12 -o bundle.p12
  certkit convert bundle.p12 --to pem
  certkit convert cert.pem --to p7b -o certs.p7b
  certkit convert bundle.p12 --to jks -o keystore.jks`,
	Args: cobra.ExactArgs(1),
	RunE: runConvert,
}

func init() {
	convertCmd.Flags().StringVar(&convertTo, "to", "", "Output format: pem, der, p12, jks, p7b")
	convertCmd.Flags().StringVarP(&convertOutFile, "out-file", "o", "", "Output file (required for binary formats)")
	convertCmd.Flags().StringVar(&convertKeyPath, "key", "", "Private key file (PEM). Keys are matched to certificates automatically.")

	_ = convertCmd.MarkFlagRequired("to")

	convertCmd.Flags().Lookup("out-file").Annotations = map[string][]string{"readme_default": {"_(stdout for PEM)_"}}

	registerCompletion(convertCmd, completionInput{"to", fixedCompletion("pem", "der", "p12", "jks", "p7b")})
	registerCompletion(convertCmd, completionInput{"out-file", fileCompletion})
	registerCompletion(convertCmd, completionInput{"key", fileCompletion})
}

Comment thread cmd/certkit/gendocs.go Outdated
Comment thread cmd/certkit/root.go
spliceMarker error messages hardcoded "README.md" instead of using the
path argument. Include the path field in spliceMarkerInput so errors
reference the correct file when gendocs is run with a custom path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@danielewood danielewood merged commit 2ba8a32 into main Feb 27, 2026
20 checks passed
@danielewood danielewood deleted the refactor/gendocs-and-json-flag branch February 27, 2026 02:30
@claude
Copy link
Copy Markdown

claude bot commented Feb 27, 2026

OBS-1 (MUST): This diagnostic message uses fmt.Fprintf instead of slog, which violates OBS-1: "log/slog exclusively. Never log or fmt.Print for diagnostics."

All other diagnostic paths in this function correctly use slog:

  • Line 73: slog.Error("writing README", ...)
  • Line 76: slog.Info("updated README", ...)

This single line is inconsistent and breaks the exclusive-slog requirement.

fmt.Fprintf(os.Stderr, "%s is up to date\n", readmePath)
}
}
// generateFlagTable produces a markdown table for the given command's flags.

		slog.Info("README is up to date", "path", readmePath)

@claude
Copy link
Copy Markdown

claude bot commented Feb 27, 2026

Bug (CLI-4): The else branch (text PEM output with no output file) does not set out.Format, so certkit bundle cert.pem --json produces JSON without a format field.

The other two branches both set out.Format = bundleFormat:

  • Line 135: file-write path ✓
  • Line 139: binary no-file path ✓
  • Line 140–142: text no-file path ✗ — format is missing

The CHANGELOG entry for this PR explicitly documents the same fix for convert --json: "Fix convert --json without -o missing format field in JSON output." convert.go sets out.Format = convertTo in its equivalent else branch, but bundle.go does not.

out.Data = base64.StdEncoding.EncodeToString(output)
out.Format = bundleFormat
} else {
out.ChainPEM = string(output)
}
data, err := json.MarshalIndent(out, "", " ")

		} else {
			out.ChainPEM = string(output)
			out.Format = bundleFormat
		}

@claude
Copy link
Copy Markdown

claude bot commented Feb 27, 2026

OBS-1 (MUST): Use log/slog exclusively — never fmt.Print for diagnostics

File: cmd/certkit/gendocs.go Line: 77 (RIGHT side)

This fmt.Fprintf call is inconsistent with the rest of the function, which correctly uses slog. The immediately preceding branch uses slog.Info("updated README", "path", readmePath) — this else branch should match:

slog.Error("writing README", "path", readmePath, "err", err)
os.Exit(1)
}
slog.Info("updated README", "path", readmePath)
} else {
fmt.Fprintf(os.Stderr, "%s is up to date\n", readmePath)
}

From CLAUDE.md OBS-1: log/slog exclusively. Never log or fmt.Print for diagnostics.

		slog.Info("README is up to date", "path", readmePath)

@claude
Copy link
Copy Markdown

claude bot commented Feb 27, 2026

Bug: format field missing from JSON output when bundling to PEM stdout

File: cmd/certkit/bundle.go Line: 141 (RIGHT side)

The else branch (PEM output to stdout, no -o flag) never sets out.Format, so it is silently omitted from the JSON due to omitempty. The other two branches both set out.Format = bundleFormat correctly, and the analogous convert.go code (fixed in this same PR) also sets out.Format in its else branch.

out.Data = base64.StdEncoding.EncodeToString(output)
out.Format = bundleFormat
} else {
out.ChainPEM = string(output)
}
data, err := json.MarshalIndent(out, "", " ")

The CHANGELOG even notes fixing the same pattern in convert - but the equivalent fix was not applied here.

Suggested fix: Add out.Format = bundleFormat in the else branch, so the JSON output consistently includes the format field regardless of which output path is taken.

@claude
Copy link
Copy Markdown

claude bot commented Feb 27, 2026

Security: Private key emitted to stdout even when -o writes it to a file

File: cmd/certkit/keygen.go Line: 73 (RIGHT side)

When --json and -o are both specified, the full key_pem (private key) is always included in the JSON output to stdout, regardless of whether the key was already written to a file. Without --json, nothing is written to stdout when -o is used. The new --json flag unexpectedly creates a second output channel for the private key.

}
if jsonOutput {
out := keygenJSON{
KeyPEM: result.KeyPEM,
PublicKeyPEM: result.PubPEM,
CSRPEM: result.CSRPEM,
KeyFile: result.KeyFile,
PubFile: result.PubFile,
CSRFile: result.CSRFile,
}
data, err := json.MarshalIndent(out, "", " ")
if err != nil {
return fmt.Errorf("marshaling JSON: %w", err)
}
fmt.Println(string(data))
} else if keygenOutPath == "" {
fmt.Print(result.KeyPEM)

The same issue affects csr.go and sign.go (for sign self-signed).

Compare with the bundle command in this same PR, which explicitly uses the pattern "File was written - emit metadata only" when -o is specified - omitting the raw data and instead returning only File, Format, and Size. The keygen/csr/sign commands should follow the same pattern: when -o is specified, include only file path metadata in the JSON (not the PEM content).

From CLAUDE.md SEC-2: "Never log secrets (private keys, passwords)" - stdout in CI/CD environments is commonly captured in logs, making this an unintended exposure path.

@claude
Copy link
Copy Markdown

claude bot commented Feb 27, 2026

Bug: Binary guard blocks --json before the JSON path is reached; binary data would also be corrupted

File: cmd/certkit/convert.go Line: 129 (RIGHT side)

This guard runs unconditionally before the if jsonOutput block (line ~155), so "certkit convert cert.pem --to p12 --json" fails with an error even though --json could safely return base64-encoded binary data - exactly the behavior "bundle --json" provides.

isBinary := convertTo == "p12" || convertTo == "jks" || convertTo == "der" || convertTo == "p7b"
if convertOutFile == "" && isBinary {
return fmt.Errorf("output format %q is binary; use -o to write to a file", convertTo)
}
output, err := formatConvertOutput(formatConvertInput{

Compare with bundle.go in this PR, where isBinary is checked inside the if jsonOutput block and correctly base64-encodes the data.

Additionally, even if the guard were moved, line 164 uses out.Data = string(output) which would corrupt binary bytes (P12/JKS/DER/P7B) by treating them as UTF-8. The fix should:

  1. Move the binary guard inside an else branch after "if jsonOutput { ... }", or add "&& !jsonOutput" to the guard condition.
  2. Use base64.StdEncoding.EncodeToString(output) for binary formats in the JSON path (matching bundle.go).

@claude
Copy link
Copy Markdown

claude bot commented Feb 27, 2026

Bug: format field missing from JSON output for text-based formats

When --json is used without -o and the format is pem, chain, or fullchain, the JSON output omits the format field. The else branch sets out.ChainPEM but never sets out.Format.

Compare with convert.go where out.Format = convertTo is set in all branches — this was even noted in the CHANGELOG as "Fix convert --json without -o missing format field in JSON output" but the same fix was not applied to bundle.

With this missing assignment, bundle --json --format chain and bundle --json --format fullchain both emit {"chain_pem": "..."} with no format field, making it impossible to distinguish which format was used.

@claude
Copy link
Copy Markdown

claude bot commented Feb 27, 2026

Code review

Two issues found.

1. Bug: bundle --json omits format field for text-based formats

In cmd/certkit/bundle.go, when --json is used without -o and the format is pem, chain, or fullchain, the JSON output omits the format field. The else branch sets out.ChainPEM but never sets out.Format.

Compare with convert.go where out.Format = convertTo is set in all branches — this was even noted in the CHANGELOG as "Fix convert --json without -o missing format field in JSON output" but the same fix was not applied to bundle.

Affected code (cmd/certkit/bundle.go lines 139–141):

out.Data = base64.StdEncoding.EncodeToString(output)
out.Format = bundleFormat
} else {
out.ChainPEM = string(output)
}
data, err := json.MarshalIndent(out, "", " ")

Fix: Add out.Format = bundleFormat in the else branch.


2. CLI-4: Inconsistent JSON field names between bundle and convert

CLAUDE.md CLI-4 requires: "JSON field names must be consistent across commands. Same concept uses the same key everywhere."

bundleJSON uses chain_pem for inline PEM content:

// bundleJSON is the JSON output structure for the bundle command.
type bundleJSON struct {
ChainPEM string `json:"chain_pem,omitempty"`
Data string `json:"data,omitempty"`
File string `json:"file,omitempty"`
Format string `json:"format,omitempty"`
Size int `json:"size,omitempty"`
}

convertJSON uses data for the same concept (inline output content without -o):

type convertJSON struct {
Data string `json:"data,omitempty"`
File string `json:"file,omitempty"`
Format string `json:"format,omitempty"`
Size int `json:"size,omitempty"`
}

When both commands produce inline PEM output (bundle --format pem --json and convert --to pem --json), the field names differ. Additionally, within bundleJSON itself the same positional concept uses two names: chain_pem for text formats and data for binary — consumers must check both fields to find the content.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants