From f75b831a53cdde171bc44a612aa5f41cc16dc13a Mon Sep 17 00:00:00 2001 From: sid Date: Sat, 2 May 2026 15:22:44 -0600 Subject: [PATCH] feat(api): add --json, --json-fields, --jq to `fj api` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- cmd/api.go | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/cmd/api.go b/cmd/api.go index fe0fd85..e336c8f 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -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 }