cmd/cycloid/<feature>/verb.go
│ (cobra RunE)
▼
cyargs.Get*() ← parse ALL flags first (required — see CLAUDE.md Hard Rules)
│
common.NewAPI() ← build API config (URL, token) from flags/env/config file
│
middleware.NewMiddleware() ← construct middleware struct with HTTP client
│
m.GetX / m.ListX / ... ← middleware method in cmd/cycloid/middleware/<feature>.go
│
m.GenericRequest(Request{...}, &result)
│
▼
HTTP → Cycloid REST API
│
▼
JSON {"data": <payload>} ← GenericRequest unwraps envelope; &result receives payload
│
cyout.PrintWithOptions() ← stdout (success) or stderr (error); dispatches to printer
Defined in cmd/cycloid/middleware/http_client.go:
type Request struct {
Method string
Organization *string // used for auth token lookup; nil = no org context
NoAuth bool // set true to skip Authorization header
Route []string // joined onto base URL path: ["organizations", org, "projects"]
Query any // struct with `url` tags, or url.Values
Headers map[string]string // extra headers merged into request
Accept *string // overrides default Accept header
Body any // JSON-marshalled when non-nil
}Route segments are path-joined onto the base URL (e.g., CY_API_URL). The route should not start with /.
Defined in cmd/cycloid/middleware/generic_client.go.
- Builds the full URL from
m.api.Config.URL+req.Route - Encodes
req.Queryvia struct tags (url:"param_name") - JSON-marshals
req.Body - Sets
Content-Type: application/jsonand (unlessNoAuth)Authorization: Bearer <token> - Executes the HTTP call
- On non-2xx: returns
*APIResponseError - On 2xx: unwraps the
{"data": ...}JSON envelope intoresponse(the second argument) — callers pass&resultdirectly, not astruct{ Data *X }wrapper
// Correct pattern:
func (m *middleware) GetProject(org, project string) (*models.Project, *http.Response, error) {
var result *models.Project
resp, err := m.GenericRequest(Request{
Method: "GET",
Organization: &org,
Route: []string{"organizations", org, "projects", project},
}, &result)
if err != nil {
return nil, resp, err
}
return result, resp, nil
}The Cycloid API wraps all responses:
{ "data": { ... } }GenericRequest strips this envelope before deserializing into response. If the response body is not envelope-shaped, it falls back to direct unmarshal.
Pass nil as response to discard the body (e.g., DELETE calls).
m.api.GetToken(org) resolves the bearer token in this priority order:
--api-keyflagCY_API_KEYenv varCY_API_TOKENenv var (legacy)- Per-org token stored in the config file (
~/.cy/config.yml)
Pass Organization: &org in Request to allow token lookup. Set NoAuth: true for unauthenticated endpoints (e.g., login).
| Type | Cause | Go type |
|---|---|---|
| API error | Server returned non-2xx | *APIResponseError |
| Network error | Transport failure (DNS, TLS, timeout) | *url.Error or stdlib |
| Unexpected error | Anything else | error (do not wrap) |
type APIResponseError struct {
StatusCode int
Status string
Body []byte // raw response body
Payload *models.ErrorPayload // parsed if body was valid JSON error
Path string // request path (+ query) for fallback errors
}
// Error() format:
// - payload message available: "API error 422: <message from payload>"
// - fallback body/path: "API error 422 on "/path?query": <raw body>"Check with errors.As:
var apiErr *middleware.APIResponseError
if errors.As(err, &apiErr) {
if apiErr.StatusCode == 409 {
// conflict
}
}Common status codes: 400 bad request, 401 unauthorized, 403 forbidden, 404 not found, 409 conflict, 422 unprocessable entity.
Errors that implement printer.ErrHTTPResponse attach the HTTP status code and raw response body (*APIResponseError for non-2xx, plus the decode error type returned by GenericRequest when a 2xx body cannot be unmarshaled). When --output json is used, the JSON printer first tries to marshal the value normally. If marshaling fails but the value is an error satisfying ErrHTTPResponse, it prints a small JSON object instead: cli_marshal_error, http_status, api_response_preview (first 10 lines of the body), and optionally request_path when the error also implements printer.RequestPather (as *APIResponseError does).
The repository used to use the go-swagger generated operations package (client/client/). It was removed in the middleware refactor (see docs/middleware-refactor.md) in favour of GenericRequest:
- Generated operations had inconsistent error handling
- Every API change required re-running the full swagger codegen cycle
GenericRequestgives explicit control over routing, auth, headers, and response decoding
The client/models/ package (data types) is still auto-generated from swagger.yaml and must not be edited manually.
The --output flag (default: table) controls how results are rendered. It is parsed by printer/factory/factory.go into a printer.Printer implementation.
| Value | Result |
|---|---|
table |
Default curated table (columns defined per command) |
table=col1,col2 |
Table with explicit column selection |
table:noheader |
Table without the header row |
table=col1,col2:noheader |
Combined column selection + no header |
json |
Full JSON (pretty-printed) |
yaml |
Full YAML |
jq=<expr> |
Run jq expression over full JSON; strings printed raw, objects as pretty JSON |
<field> |
Extract named field, one value per line (case-insensitive, dot notation OK) |
--jq <expr> is a shorthand flag for -o jq=<expr> and resolves identically.
cy project list -o canonical # one canonical per line
cy project list -o owner.username # nested field
cy project list -o "jq=.[].canonical" # equivalent via jq
cy project delete $(cy project list -o canonical) # pipe patternCommands call cyout.RegisterModel(cmd, models.X{}) in their constructor to register their model's fields. The global --output completion function reads these and suggests field names alongside json, yaml, table, and jq=:
cy project list -o <tab> → json yaml table table= jq= canonical name id ...
cy project list -o table=<tab> → table=canonical table=name table=id ...
cy project list -o can<tab> → canonicalDefine a printer.Options var in the command package and pass it to cyout.PrintWithOptions:
var widgetTableOptions = printer.Options{
Columns: []string{"Canonical", "Name", "Description"},
Identifier: "Canonical", // never dropped when terminal is narrow
}
// in RunE:
return cyout.PrintWithOptions(cmd, result, err, "unable to get widget", widgetTableOptions)Column names support dot notation for nested fields ("Owner.Username"). The Identifier column is always shown even when the terminal is too narrow to show all columns.
internal/cyout/ provides two helpers that replace the 3-line GetOutput/GetPrinter/SmartPrint boilerplate:
// Simple (no column customisation):
cyout.Print(cmd, obj, err, "unable to do X")
// With column options:
cyout.PrintWithOptions(cmd, obj, err, "unable to do X", tableOptions)Errors are routed to cmd.ErrOrStderr(), results to cmd.OutOrStdout().
cy pipeline build trigger --watch streams build events while polling until the build finishes. Human vs raw NDJSON formatting lives in internal/buildwatch; the cobra command only passes options and calls buildwatch.Watch. If it causes problems, see pipeline-build-watch-output.md for how to disable or remove it without touching the HTTP client.