package cmd import ( "encoding/json" "fmt" "net/url" "os" "strconv" "strings" "github.com/spf13/cobra" ) var ( apiFields []string apiRawFields []string ) func init() { apiCmd := &cobra.Command{ Use: "api ", Short: "Make an authenticated request to an arbitrary API endpoint", Long: `Low-level escape hatch, like "gh api". METHOD is GET, POST, PATCH, PUT, DELETE. PATH is relative to /api/v1 (a leading /api/v1 or / is optional). Fields (--field/-f) are added as query parameters for GET/HEAD and as a JSON body otherwise. --field values are type-inferred (true/false/null/numbers); use --raw-field/-F to force a string. The JSON response is printed to stdout. Examples: rickub api GET /user rickub api GET search/repos -f q=api rickub api POST /repos -f name=demo -f visibility=public rickub api PATCH repos/me/demo -F description="hello world"`, Args: cobra.ExactArgs(2), RunE: runAPI, } apiCmd.Flags().StringArrayVarP(&apiFields, "field", "f", nil, "typed field key=value (repeatable)") apiCmd.Flags().StringArrayVarP(&apiRawFields, "raw-field", "F", nil, "string field key=value (repeatable)") rootCmd.AddCommand(apiCmd) } func runAPI(cmd *cobra.Command, args []string) error { client, err := newClient() if err != nil { return err } method := strings.ToUpper(args[0]) path := normalizeAPIPath(args[1]) fields := map[string]any{} for _, f := range apiRawFields { k, v, ok := splitKV(f) if !ok { return fmt.Errorf("invalid --raw-field %q (want key=value)", f) } fields[k] = v } for _, f := range apiFields { k, v, ok := splitKV(f) if !ok { return fmt.Errorf("invalid --field %q (want key=value)", f) } fields[k] = inferType(v) } var query url.Values var body any isRead := method == "GET" || method == "HEAD" if len(fields) > 0 { if isRead { query = url.Values{} for k, v := range fields { query.Set(k, fmt.Sprintf("%v", v)) } } else { body = fields } } data, _, err := client.RawJSON(cmd.Context(), method, path, query, body) if err != nil { return err } // Pretty-print JSON when possible, else emit raw bytes. var pretty any if len(strings.TrimSpace(string(data))) > 0 && json.Unmarshal(data, &pretty) == nil { return printJSON(cmd.OutOrStdout(), pretty) } _, err = os.Stdout.Write(data) return err } func normalizeAPIPath(p string) string { p = strings.TrimPrefix(p, "/api/v1") if !strings.HasPrefix(p, "/") { p = "/" + p } return p } func splitKV(s string) (string, string, bool) { i := strings.IndexByte(s, '=') if i < 0 { return "", "", false } return s[:i], s[i+1:], true } // inferType coerces common scalar spellings so JSON bodies get proper types. func inferType(v string) any { switch v { case "true": return true case "false": return false case "null": return nil } if n, err := strconv.Atoi(v); err == nil { return n } if f, err := strconv.ParseFloat(v, 64); err == nil { return f } return v }