feat(api): add --json, --json-fields, --jq to fj api
Some checks are pending
CI / lint (push) Waiting to run
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / functional (push) Blocked by required conditions

`fj api` was the only command that returned raw API JSON without exposing
the same projection/filtering knobs that `fj repo list`, `fj pr list`,
etc. already provide. Callers had to pipe to `python -m json.tool` or
`jq` to extract fields, which is inconsistent and discoverable only
after hitting the gap.

Wire the existing addJSONFlags / wantJSON / outputJSON helpers from
cmd/json.go so the API command participates in the same JSON output
pipeline. No behavioral change when none of the new flags are set —
default still pretty-prints JSON and writes raw bytes for non-JSON
responses.

Verified against live forgejo:

  $ fj api repos/public/claude-code-proxy --jq .full_name
  public/claude-code-proxy

  $ fj api repos/public/claude-code-proxy --json=full_name,description
  { "description": "...", "full_name": "public/claude-code-proxy" }

  $ fj api 'repos/public/claude-code-proxy/commits?limit=3' \
        --jq '.[] | "\(.sha[0:8]) \(.commit.message | split("\n")[0])"'
  8e550b97 Local fork: hardening + ops improvements ...
  b9da198e Harden proxy auth, storage, and conversation access
  6cda3631 Harden streaming, pagination, and config loading

Note: `--json=fields` requires the equals sign because the flag has
NoOptDefVal=" " (so `--json` alone is valid for "everything as JSON").
The Example block in --help documents both the `--json=` form and the
`--json-fields` alias which doesn't have that quirk.
This commit is contained in:
sid 2026-05-02 15:22:44 -06:00
parent 0fda0b8679
commit f75b831a53

View file

@ -35,7 +35,17 @@ If --field is used and no --method is specified, the method defaults to POST.`,
fj api /users/johndoe
# Use raw body from stdin
echo '{"title":"test"}' | fj api /repos/{owner}/{repo}/issues --input -`,
echo '{"title":"test"}' | fj api /repos/{owner}/{repo}/issues --input -
# Filter the response with a jq expression
fj api /repos/{owner}/{repo}/issues --jq '.[].title'
# Project the response down to specific fields (requires "=" because --json
# also accepts an empty value to mean "all fields as JSON")
fj api /repos/{owner}/{repo} --json=full_name,description,private
# Same projection without the "=" quirk
fj api /repos/{owner}/{repo} --json-fields full_name,description,private`,
Args: cobra.ExactArgs(1),
RunE: runAPI,
}
@ -50,6 +60,7 @@ func init() {
apiCmd.Flags().StringArrayP("header", "H", nil, "Add an HTTP request header (key:value)")
apiCmd.Flags().Bool("silent", false, "Do not print the response body")
apiCmd.Flags().BoolP("include", "i", false, "Include HTTP response headers in the output")
addJSONFlags(apiCmd, "Output the response as JSON; pass a comma-separated field list to project specific keys")
}
func runAPI(cmd *cobra.Command, args []string) error {
@ -212,18 +223,31 @@ func runAPI(cmd *cobra.Command, args []string) error {
return nil
}
// Pretty-print JSON, or output raw if not JSON
contentType := resp.Header.Get("Content-Type")
if strings.Contains(contentType, "json") || json.Valid(respBody) {
isJSON := strings.Contains(contentType, "json") || json.Valid(respBody)
// If the user asked for JSON projection or jq filtering, route through
// the shared JSON output helpers so the API command is consistent with
// `fj repo list`, `fj pr list`, etc.
if wantJSON(cmd) {
if !isJSON {
return fmt.Errorf("--json/--json-fields/--jq requires a JSON response, but the server returned %s", contentType)
}
var parsed any
if err := json.Unmarshal(respBody, &parsed); err != nil {
return fmt.Errorf("response is not valid JSON: %w", err)
}
return outputJSON(cmd, parsed)
}
// Pretty-print JSON by default, otherwise emit raw bytes.
if isJSON {
var parsed any
if err := json.Unmarshal(respBody, &parsed); err == nil {
enc := json.NewEncoder(ios.Out)
enc.SetIndent("", " ")
return enc.Encode(parsed)
return writeJSON(parsed)
}
}
// Raw output for non-JSON responses
_, err = ios.Out.Write(respBody)
return err
}