This file mirrors CLAUDE.md for LLM agents that use the AGENTS.md convention. Keep in sync when updating either file.
This file is auto-loaded by Claude Code on every invocation. See also AGENTS.md (kept identical, for non-Claude LLM agents).
# Build
make build # build all platform binaries
go build -o cy . # quick local build
# Test (requires local backend)
make be-reset # start backend via docker compose
go test ./... # run all tests
make test # same
make be-stop # stop backend
# Run a specific test
go test ./e2e/... -run TestProjects
go test ./cmd/cycloid/middleware/... -run TestGetProject
# Lint & format
make lint # golangci-lint + shellcheck
make format # gci + goimports + shfmt
# Client regeneration (when swagger.yaml changes)
make client-generate # regenerates ./client/ from swagger.yaml
# Changelog
make new-changelog-entry # add unreleased changelog entry (uses changie via docker)cmd/cycloid/<feature>/*.go → cmd/cycloid/middleware/ → GenericRequest()
(cobra commands) (Middleware interface) (generic_client.go)
client/models/— Auto-generated go-swagger models fromswagger.yaml. Never edit manually. Regenerate withmake client-generate.client/client/— Intentionally NOT used. The generated operations package was removed in the middleware refactor. UseGenericRequestinstead.cmd/cycloid/middleware/— TheMiddlewareinterface (middleware.go) makes HTTP calls viaGenericRequest(generic_client.go). Each feature gets its own file (e.g.,organization_projects.go).cmd/cycloid/<feature>/— Cobra command definitions. Each feature directory has:cmd.go(registers subcommands), plus one file per verb (list.go,get.go,create.go,update.go,delete.go), andcommon.gofor shared logic.internal/cyargs/— All shared flag definitions and completion functions. Every flag used by multiple commands must be declared here.printer/— Output formatting. Useprinter.SmartPrint(p, obj, err, errStr, opts, writer)— success tocmd.OutOrStdout(), errors tocmd.OutOrStderr().e2e/— End-to-end tests that run real CLI commands against a live backend.
These are invariants that LLM agents and new contributors must not violate:
- NEVER edit
client/models/— auto-generated. Runmake client-generateto update. - NEVER import or use
client/client/— deprecated. UseGenericRequestin middleware methods. - NEVER call
NewMiddlewareoutside a cobraRunEfunction — not at package init, not in tests directly. - ALWAYS parse ALL flags via
cyargs.Get*BEFORE callingcommon.NewAPI()andNewMiddleware()—GenericRequestreadsverbosityfrom Viper at call time; unparsed flags produce stale values. - ALWAYS add shared flags to
internal/cyargs/— never inline a flag used by more than one command. - ALWAYS use
printer.SmartPrint— errors go toOutOrStderr(), results go toOutOrStdout(). - E2E tests require a running backend (
make be-start). Never run e2e in parallel. - Run
make format && make lintafter every code change. - Ship tests with every feature — in the same change, add or extend coverage for what you introduce: new or changed middleware in
cmd/cycloid/middleware/*_test.go(or focused unit tests where appropriate), and user-facing CLI behavior ine2e/*_test.gowhen that resource already has e2e tests. Do not land behavior-only changes without tests.
GenericRequest handles auth, JSON marshaling, and {"data":...} envelope unwrapping. Pass &result directly — do not wrap in a struct{ Data *X }.
// Standard middleware method 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
}Request fields: Method, Organization (*string, for auth), NoAuth (bool), Route ([]string), Query (struct with url tags), LHSFilters ([]LHSFilter, see below), Headers (map), Accept (*string), Body (any, JSON-marshalled).
The Cycloid API supports LHS bracket filters on List routes: attribute[condition]=value. The condition is typically eq, rlike, gt, lt, etc.
Rule: all new List middleware methods must accept filters ...LHSFilter as their last parameter.
LHSFilter is defined in cmd/cycloid/middleware/lhs_filter.go:
type LHSFilter struct {
Attribute string
Condition string
Value string
}Pass filters via the LHSFilters field of Request. Brackets are kept literal (not percent-encoded) so the API receives name[eq]=my-project, not name%5Beq%5D=my-project. Regex metacharacters in values (?, *, +, etc.) are also preserved.
// Example: list projects filtered by name prefix
func (m *middleware) ListProjects(org string, filters ...LHSFilter) ([]*models.Project, *http.Response, error) {
var result []*models.Project
resp, err := m.GenericRequest(Request{
Method: "GET",
Organization: &org,
Route: []string{"organizations", org, "projects"},
LHSFilters: filters,
}, &result)
...
}
// Caller usage:
projects, _, err := m.ListProjects(org, middleware.LHSFilter{
Attribute: "name",
Condition: "rlike",
Value: "proj.*",
})Offline (no-backend) unit tests for LHS filter encoding live in cmd/cycloid/middleware/offline/lhs_filter_test.go.
| Verb | Return |
|---|---|
| Get / Create | (*models.X, *http.Response, error) |
| List | ([]*models.X, *http.Response, error) |
| Delete / void | (*http.Response, error) |
Always return the *http.Response so callers can inspect status codes. Assign _ if unused.
func getProject(cmd *cobra.Command, args []string) error {
// Step 1: ALL flags first
org, err := cyargs.GetOrg(cmd)
if err != nil { return err }
project, err := cyargs.GetProject(cmd)
if err != nil { return err }
output, err := cmd.Flags().GetString("output")
if err != nil { return errors.Wrap(err, "unable to get output flag") }
// Step 2: printer
p, err := factory.GetPrinter(output)
if err != nil { return errors.Wrap(err, "unable to get printer") }
// Step 3: API + middleware
api := common.NewAPI()
m := middleware.NewMiddleware(api)
// Step 4: call + print
result, _, err := m.GetProject(org, project)
if err != nil {
return printer.SmartPrint(p, nil, err, "unable to get project", printer.Options{}, cmd.OutOrStderr())
}
return printer.SmartPrint(p, result, nil, "", printer.Options{}, cmd.OutOrStdout())
}All shared flag definitions live in internal/cyargs/. Pattern:
func AddWidgetFlag(cmd *cobra.Command) {
cmd.Flags().String("widget", "", "Widget canonical")
_ = cmd.RegisterFlagCompletionFunc("widget", widgetCompletion)
}
func GetWidget(cmd *cobra.Command) (string, error) {
return cmd.Flags().GetString("widget")
}Register in the command constructor (NewGetX()), never inside RunE.
- Tests ship with the feature (see Hard Rule 9): middleware and command changes should include tests in the same PR; extend existing
e2e/*_test.gofiles when the resource is already covered there. - Middleware tests (
cmd/cycloid/middleware/*_test.go):TestMaincallstestcfg.NewConfig("middleware"). Fixtures:config.Project,config.Environment,config.Component,config.ConfigRepo,config.CatalogRepo. - E2E tests (
e2e/*_test.go):executeCommand(args)runs the real CLI. Sametestcfgsetup. - E2E tests are not parallel (backend git writes are not concurrent-safe).
CY_TEST_VERBOSITY=debug→ full HTTP request/response logs. Auth header redacted toBearer ***XXXXX(last 5 chars).
common.NewAPI() resolves config from flags → env vars → config file. Token priority: --api-key → CY_API_KEY → CY_API_TOKEN → per-org token in config file.
@docs/architecture.md— HTTP layer, Request struct, auth flow, error taxonomy@docs/pipeline-build-watch-output.md—pipeline build trigger --watchhuman SSE output; how to disable or remove@docs/adding-a-command.md— full walkthrough with working example@docs/testing.md— middleware + e2e test patterns, testcfg deep-dive@docs/middleware-refactor.md— what changed and why, migration reference
This project is indexed by GitNexus as cycloid-cli (11392 symbols, 69497 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
If any GitNexus tool warns the index is stale, run
npx gitnexus analyzein terminal first.
- Before modifying a symbol, prefer running
gitnexus_impact({target: "symbolName", direction: "upstream"})to understand callers and blast radius. - Before committing, consider running
gitnexus_detect_changes()to verify the affected scope matches expectations. - If impact analysis returns HIGH or CRITICAL risk, surface it to the user before proceeding.
- When exploring unfamiliar code, prefer
gitnexus_query({query: "concept"})over grepping — it returns process-grouped results ranked by relevance. - For full context on a specific symbol — callers, callees, execution flows — use
gitnexus_context({name: "symbolName"}). - Prefer
gitnexus_renameover find-and-replace for symbol renames — it understands the call graph.
| Resource | Use for |
|---|---|
gitnexus://repo/cycloid-cli/context |
Codebase overview, check index freshness |
gitnexus://repo/cycloid-cli/clusters |
All functional areas |
gitnexus://repo/cycloid-cli/processes |
All execution flows |
gitnexus://repo/cycloid-cli/process/{name} |
Step-by-step execution trace |
| Task | Read this skill file |
|---|---|
| Understand architecture / "How does X work?" | .claude/skills/gitnexus/gitnexus-exploring/SKILL.md |
| Blast radius / "What breaks if I change X?" | .claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md |
| Trace bugs / "Why is X failing?" | .claude/skills/gitnexus/gitnexus-debugging/SKILL.md |
| Rename / extract / split / refactor | .claude/skills/gitnexus/gitnexus-refactoring/SKILL.md |
| Tools, resources, schema reference | .claude/skills/gitnexus/gitnexus-guide/SKILL.md |
| Index, status, clean, wiki CLI commands | .claude/skills/gitnexus/gitnexus-cli/SKILL.md |