Add first-class CLI support for the entire Cronitor REST API as top-level resource commands with consistent subcommands (list, get, create, update, delete, plus resource-specific actions).
When working on a task, prefix it with [WORKING] to indicate it is actively in progress. When the task is complete, remove the prefix and mark it as done ([x]). Only one task should be marked [WORKING] at a time.
Branch: claude/cronitor-api-support-PLatz
API Version: Configurable via --api-version flag, CRONITOR_API_VERSION env var, or config file (header omitted when unset)
Base URL: https://cronitor.io/api/
- Each API resource is a top-level cobra command (e.g.
cronitor monitor,cronitor group) - Subcommands follow CRUD conventions:
list,get,create,update,delete - Resources with special actions get additional subcommands (e.g.
monitor pause,group resume) - Shared flags across all resources:
--format(json/table/yaml),--output,--page - API client lives in
lib/cronitor.go/lib/api_client.gowith GET/POST/PUT/DELETE helpers - Table output uses lipgloss styling via shared helpers in
cmd/ui.go
- API client with HTTP Basic Auth (
lib/api_client.go,lib/cronitor.go) -
Cronitor-Version: 2025-11-28header sent on all requests - Shared output formatting: JSON, YAML, table (
--format,--outputflags) - Table rendering with lipgloss styling (
cmd/ui.go) - Color palette, status badges, and formatting helpers
All 9 resources implemented with Run functions wired to real API calls:
-
monitor — list, get, search, create, update, delete, clone, pause, unpause
- Filters:
--type,--group,--tag,--state,--search,--sort,--env - File:
cmd/monitor.go
- Filters:
-
group — list, get, create, update, delete, pause, resume
- Filters:
--env,--with-status,--page-size,--sort - File:
cmd/group.go
- Filters:
-
environment (alias:
env) — list, get, create, update, delete- File:
cmd/environment.go
- File:
-
notification (alias:
notifications) — list, get, create, update, delete- Supports all channels: email, slack, pagerduty, opsgenie, victorops, microsoft-teams, discord, telegram, gchat, larksuite, webhooks
- File:
cmd/notification.go
-
issue — list, get, create, update, resolve, delete
- Filters:
--state,--severity,--monitor,--group,--tag,--env,--search,--time,--order-by - File:
cmd/issue.go
- Filters:
-
maintenance (alias:
maint) — list, get, create, update, delete- Filters:
--past,--ongoing,--upcoming,--statuspage,--env,--with-monitors - File:
cmd/maintenance.go
- Filters:
-
statuspage — list, get, create, update, delete
- Nested:
component list,component create,component delete - Filters:
--with-status,--with-components - File:
cmd/statuspage.go
- Nested:
-
metric (alias:
metrics) — get, aggregate- Filters:
--monitor,--group,--tag,--type,--time,--start,--end,--env,--region,--with-nulls - Fields: duration_p10/p50/p90/p99, duration_mean, success_rate, run_count, complete_count, fail_count, tick_count, alert_count
- File:
cmd/metric.go
- Filters:
-
site — list, get, create, update, delete, query, error {list, get}
- Query kinds: aggregation, breakdown, timeseries, search_options, error_groups
- File:
cmd/site.go
-
ping — updated with richer flags:
--run,--complete,--fail,--ok,--tick,--msg,--series,--status-code,--duration,--metric- File:
cmd/ping.go
- File:
All resources have test files verifying:
- Command and subcommand hierarchy
- Flag presence and types
- Argument validation
- Aliases
- Help text and examples
Test files: cmd/*_test.go (monitor, environment, issue, notification, statuspage, group, maintenance, metric, site, discover)
-
Statuspage component update —
component updatesubcommand (PUT /statuspage_components/:key)- Updatable fields: name, description, autopublish
- File:
cmd/statuspage.go
-
Issue bulk actions —
issue bulksubcommand (POST /issues/bulk)- Actions: delete, change_state, assign_to
- Accepts:
--action,--issues(comma-separated keys),--state,--assign-to - File:
cmd/issue.go
-
Issue expansion flags —
--with-statuspage-details,--with-monitor-details,--with-alert-details,--with-component-detailsonissue listandissue get- These map to query params:
withStatusPageDetails,withMonitorDetails,withAlertDetails,withComponentDetails - File:
cmd/issue.go
- These map to query params:
Current state: All 10 resource test files (cmd/*_test.go) only verify command structure (subcommands, flags, aliases, argument counts). There is no HTTP mocking, no behavioral testing, and no output verification. This phase adds robust API-level testing.
-
Shared test helpers — Create
lib/api_test_helpers.go(orlib/testutil_test.go)NewMockAPIServer()— returns anhttptest.NewServerthat:- Records incoming requests (method, path, query params, headers, body) for assertion
- Returns configurable JSON responses per route (method + path pattern)
- Supports setting response status codes (200, 400, 403, 404, 429, 500)
- Validates
Authorizationheader (HTTP Basic with API key) - Validates
Cronitor-Versionheader presence/absence
AssertRequest(t, recorded, expected)— helper to compare method, path, query params, body fieldsLoadFixture(name string)— reads JSON fixture files fromtestdata/directoryCaptureOutput(fn func()) string— captures stdout for output format assertions
-
Test fixtures — Create
testdata/directory with representative API responsestestdata/monitors_list.json— paginated list response with 2-3 monitorstestdata/monitor_get.json— single monitor with all fields populatedtestdata/groups_list.json,testdata/group_get.jsontestdata/environments_list.json,testdata/environment_get.jsontestdata/notifications_list.json,testdata/notification_get.jsontestdata/issues_list.json,testdata/issue_get.jsontestdata/maintenance_list.json,testdata/maintenance_get.jsontestdata/statuspages_list.json,testdata/statuspage_get.jsontestdata/components_list.jsontestdata/metrics_get.json,testdata/aggregates_get.jsontestdata/sites_list.json,testdata/site_get.jsontestdata/site_query.json,testdata/site_errors_list.jsontestdata/error_responses/— 400, 403, 404, 429, 500 responses
- Authentication — Verify API key is sent as HTTP Basic Auth (username = API key, no password)
- Cronitor-Version header — Verify header is sent (currently hardcoded to
2025-11-28). Version-absent test will be added after Phase 6 makes it configurable. - HTTP methods — Each helper (GET, POST, PUT, DELETE) sends the correct method
- URL construction — Base URL + resource path + query params are built correctly
- Request body — POST/PUT send correct JSON body from
--dataflag - Error handling — Client returns meaningful errors for:
- 400 Bad Request (validation errors from API)
- 403 Forbidden (invalid API key)
- 404 Not Found (invalid resource key)
- 429 Rate Limited (with Retry-After header)
- 500 Server Error
- Network errors (connection refused, timeout)
- Malformed JSON response
All per-resource endpoint tests are in lib/api_client_test.go using table-driven tests against the mock server. Tests cover correct HTTP method, path, query params, and request body for every endpoint.
-
Monitor tests (
cmd/monitor_test.go— extend existing file)list— GET /monitors, with each filter flag mapped to correct query param (--type→type,--group→group,--tag→tag,--state→state,--search→search,--sort→sort,--env→env,--page→page)get KEY— GET /monitors/KEYcreate --data '{...}'— POST /monitors with JSON bodyupdate KEY --data '{...}'— PUT /monitors with JSON body containing keydelete KEY— DELETE /monitors/KEYdelete KEY1 KEY2— bulk delete via DELETE /monitors with bodyclone KEY --name NEW— POST /monitors/clone with correct bodypause KEY— GET /monitors/KEY/pause (no duration)pause KEY --hours 4— GET /monitors/KEY/pause/4unpause KEY— GET /monitors/KEY/pause/0search QUERY— GET /api/search?query=QUERY
-
Group tests (
cmd/group_test.go— extend)list— GET /groups, with filters (--env,--with-status,--page-size,--sort)get KEY— GET /groups/KEYcreate --data '{...}'— POST /groupsupdate KEY --data '{...}'— PUT /groups/KEYdelete KEY— DELETE /groups/KEYpause KEY 4— GET /groups/KEY/pause/4resume KEY— GET /groups/KEY/pause/0
-
Environment tests (
cmd/environment_test.go— extend)list— GET /environmentsget KEY— GET /environments/KEYcreate --data '{...}'— POST /environmentsupdate KEY --data '{...}'— PUT /environments/KEYdelete KEY— DELETE /environments/KEY
-
Notification tests (
cmd/notification_test.go— extend)list— GET /notificationsget KEY— GET /notifications/KEYcreate --data '{...}'— POST /notificationsupdate KEY --data '{...}'— PUT /notifications/KEYdelete KEY— DELETE /notifications/KEY
-
Issue tests (
cmd/issue_test.go— extend)list— GET /issues, with all filter flags (--state,--severity,--monitor,--group,--tag,--env,--search,--time,--order-by)get KEY— GET /issues/KEYcreate --data '{...}'— POST /issuesupdate KEY --data '{...}'— PUT /issues/KEYresolve KEY— PUT /issues/KEY with state=resolveddelete KEY— DELETE /issues/KEYbulk --action delete --issues KEY1,KEY2— POST /issues/bulk (after Phase 4)
-
Maintenance tests (
cmd/maintenance_test.go— extend)list— GET /maintenance_windows, with filters (--past,--ongoing,--upcoming,--statuspage,--env,--with-monitors)get KEY— GET /maintenance_windows/KEYcreate --data '{...}'— POST /maintenance_windowsupdate KEY --data '{...}'— PUT /maintenance_windows/KEYdelete KEY— DELETE /maintenance_windows/KEY
-
Statuspage tests (
cmd/statuspage_test.go— extend)list— GET /statuspages, with filters (--with-status,--with-components)get KEY— GET /statuspages/KEYcreate --data '{...}'— POST /statuspagesupdate KEY --data '{...}'— PUT /statuspages/KEYdelete KEY— DELETE /statuspages/KEYcomponent list— GET /statuspage_componentscomponent create --data '{...}'— POST /statuspage_componentscomponent update KEY --data '{...}'— PUT /statuspage_components/KEY (after Phase 4)component delete KEY— DELETE /statuspage_components/KEY
-
Metric tests (
cmd/metric_test.go— extend)get— GET /metrics, with filters (--monitor,--group,--tag,--type,--time,--start,--end,--env,--region,--with-nulls,--field)aggregate— GET /aggregates, with same filters
-
Site tests (
cmd/site_test.go— extend)list— GET /sitesget KEY— GET /sites/KEYcreate --data '{...}'— POST /sitesupdate KEY --data '{...}'— PUT /sites/KEYdelete KEY— DELETE /sites/KEYquery --site KEY --type aggregation— POST /sites/query with correct bodyerror list --site KEY— GET /site_errors?site=KEYerror get KEY— GET /site_errors/KEY
- JSON output —
FormatJSON()tested: pretty-prints valid JSON, returns raw on invalid
The remaining items require command-level integration tests that execute cobra commands against a mock server and verify stdout/file output. These test the glue between "API returns JSON" and "user sees formatted output."
Known limitation: Commands call os.Exit(1) on errors, which kills the test process. Error-path integration tests are deferred. A future improvement would be to refactor commands to return errors instead of calling os.Exit directly.
Scope note: These integration tests are intentionally representative, not exhaustive. The goal is to verify each output format works end-to-end for a couple of commands, not to re-test every endpoint (already covered by lib/api_client_test.go).
- Create
internal/testutil/mock_api.gowith exportedMockAPI,NewMockAPI(),RecordedRequest,On(),OnWithHeaders(),SetDefault(),LastRequest(),RequestCount(),Reset()— copied from the existing package-private implementation inlib/api_client_test.go
- Create
CaptureStdout(fn func()) stringhelper- Redirects
os.Stdoutto anos.Pipe(), runsfn, reads the pipe, restores stdout - Needed because commands use
fmt.Printlndirectly, not cobra'scmd.OutOrStdout()
- Redirects
- Create
ExecuteCommand(root *cobra.Command, args ...string) (string, error)helper- Calls
root.SetArgs(args), wrapsroot.Execute()insideCaptureStdout, returns captured output + error - Also handles setup boilerplate: sets
lib.BaseURLOverrideto the mock server URL andviper.Set("CRONITOR_API_KEY", "test-key")
- Calls
- Replace the local
MockAPI/RecordedRequest/NewMockAPIinlib/api_client_test.gowith imports frominternal/testutil- Verify all existing lib tests still pass after refactor
- Add test in
cmd/ui_test.go(or create it if it doesn't exist)- Given two page response bodies:
{"items":[{"id":1}]}and{"items":[{"id":2}]} - Assert
MergePagedJSON(bodies, "items")returns[{"id":1},{"id":2}] - Test edge cases: empty pages, single page, mismatched keys
- Given two page response bodies:
- Add test in
cmd/ui_test.gousing mock server frominternal/testutil- Mock returns items on page 1 and 2, empty array on page 3
- Assert
FetchAllPagesreturns 2 bodies (stops at empty page) - Assert it sends incrementing
pagequery param - Test safety limit behavior (mock always returns items, assert it stops at 200)
- Add
cmd/integration_test.go- Test
monitor list(default format = table):- Mock returns
testdata/monitors_list.jsonfixture onGET /monitors - Assert output contains column headers: "NAME", "KEY", "TYPE", "STATUS"
- Assert output contains monitor names/keys from the fixture
- Mock returns
- Test
issue list --format table:- Mock returns
testdata/issues_list.jsonfixture - Assert output contains "NAME", "KEY", "STATE", "SEVERITY"
- Mock returns
- Test
- Test
monitor list --format json:- Mock returns fixture on
GET /monitors - Assert output is valid JSON (
json.Valid()) - Assert output contains expected monitor keys from the fixture
- Mock returns fixture on
- Test
monitor get my-job --format json:- Mock returns fixture on
GET /monitors/my-job - Assert output is valid pretty-printed JSON
- Mock returns fixture on
- Test
monitor list --format yaml:- Mock returns YAML-formatted body when
format=yamlquery param is present - Assert output is non-empty and matches what the mock returned (passthrough test)
- Mock returns YAML-formatted body when
- Test
monitor list --format json --output <tmpfile>:- Execute command with
--outputpointing tot.TempDir()file - Assert file exists, contains valid JSON matching the fixture
- Assert captured stdout contains "Output written to" but NOT the JSON data
- Execute command with
- Test
monitor list(table format) with pagination:- Mock returns fixture with
page_info.totalMonitorCount> page size - Assert output contains pagination string (e.g., "Showing page 1")
- Mock returns fixture with
- Test
monitor list --all --format json:- Mock returns different items on
GET /monitors?page=1vspage=2, empty onpage=3 - Assert output is a merged JSON array containing items from both pages
- Mock returns different items on
All error handling tested in lib/api_client_test.go:
- Invalid API key — 403 response parses "Invalid API key" from error body
- Resource not found — 404
IsNotFound()correctly returns true - Validation errors — 400
ParseError()extracts messages fromerrors[]array - Rate limiting — 429 response captures
Retry-Afterheader - Server errors — 500
ParseError()returns "Internal server error" - Network errors — Connection refused returns
request failederror (not panic) - Malformed responses — Invalid JSON handled gracefully by
FormatJSON()andParseError() - Response helpers —
IsSuccess()tested for all status code ranges (2xx true, 3xx/4xx/5xx false)
All version header tests implemented in lib/api_client_test.go after Phase 6 made the header configurable via viper:
- No version configured —
TestVersionHeader_NotSentWhenUnsetandTestVersionHeader_NotSentAcrossAllMethodsverify no header whenCRONITOR_API_VERSIONis empty - Version in config file / env var —
TestVersionHeader_SentWhenConfiguredandTestVersionHeader_DifferentVersionValuesverify header sent with correct value viaviper.Set() - All HTTP methods —
TestVersionHeader_AppliesAcrossAllMethodsverifies header on GET, POST, PUT, DELETE, PATCH - Priority order —
TestVersionHeader_ViperPriority_EnvOverridesConfigverifies viper precedence (env var overrides config)
- Run all tests —
go test ./cmd/... ./lib/...passes (all existing structural tests + all new API client tests) - Fix any failures — No failures found; all tests pass
- Verify test coverage —
go test -cover ./cmd/... ./lib/...shows adequate coverage for new code
-
Configurable
Cronitor-Versionheader — Remove hardcoded version, make it configurable across the entire CLI- Removed hardcoded
2025-11-28fromlib/api_client.goandlib/cronitor.go(bothsend()andsendWithContentType()) - Added
varApiVersion = "CRONITOR_API_VERSION"tocmd/root.go - Added
--api-versionpersistent flag onRootCmd(available to all commands) - Added
ApiVersionfield toConfigFilestruct incmd/configure.go - Configure command reads and displays API version
- Header only sent when
CRONITOR_API_VERSIONis non-empty (via env var, config file, or--api-versionflag) - Extended
Monitor.UnmarshalJSON()to normalize singularschedule(string) intoschedules([]string) for cross-version compatibility
- Removed hardcoded
-
Consistent error messaging — Audited all commands for consistent error output
- All API errors use:
Error(fmt.Sprintf("API Error (%d): %s", resp.StatusCode, resp.ParseError())) - All network errors use:
Error(fmt.Sprintf("Failed to <action> <resource>: %s", err)) - Added missing
IsNotFound()checks to: group get/delete, issue update, notification update, maintenance delete, statuspage update/component update/component delete, site delete, monitor update
- All API errors use:
-
Pagination helpers — Added
--allflag to all list commandsFetchAllPages()andMergePagedJSON()helpers incmd/ui.go- For JSON: merges all pages into a single JSON array
- For table: accumulates rows from all pages, renders once
- Added to: monitor, group, environment, issue, notification, maintenance, statuspage, site
-
Output to file — Verified
--outputflag works correctly across all commands- Fixed group.go: added missing newline in file write, standardized success message to
Info() - Fixed bypass issues: routed "no results found" messages through output functions in group.go, maintenance.go, metric.go, site.go
- Fixed group.go: added missing newline in file write, standardized success message to
| CLI Command | API Endpoint | Methods |
|---|---|---|
monitor list |
GET /monitors |
GET |
monitor get KEY |
GET /monitors/:key |
GET |
monitor search QUERY |
GET /api/search |
GET |
monitor create |
POST /monitors (single), PUT /monitors (batch) |
POST, PUT |
monitor update KEY |
PUT /monitors |
PUT |
monitor delete KEY |
DELETE /monitors/:key or DELETE /monitors (bulk) |
DELETE |
monitor clone KEY |
POST /monitors/clone |
POST |
monitor pause KEY |
GET /monitors/:key/pause[/:hours] |
GET |
monitor unpause KEY |
GET /monitors/:key/pause/0 |
GET |
group list |
GET /groups |
GET |
group get KEY |
GET /groups/:key |
GET |
group create |
POST /groups |
POST |
group update KEY |
PUT /groups/:key |
PUT |
group delete KEY |
DELETE /groups/:key |
DELETE |
group pause KEY HOURS |
GET /groups/:key/pause/:hours |
GET |
group resume KEY |
GET /groups/:key/pause/0 |
GET |
environment list |
GET /environments |
GET |
environment get KEY |
GET /environments/:key |
GET |
environment create |
POST /environments |
POST |
environment update KEY |
PUT /environments/:key |
PUT |
environment delete KEY |
DELETE /environments/:key |
DELETE |
notification list |
GET /notifications |
GET |
notification get KEY |
GET /notifications/:key |
GET |
notification create |
POST /notifications |
POST |
notification update KEY |
PUT /notifications/:key |
PUT |
notification delete KEY |
DELETE /notifications/:key |
DELETE |
issue list |
GET /issues |
GET |
issue get KEY |
GET /issues/:key |
GET |
issue create |
POST /issues |
POST |
issue update KEY |
PUT /issues/:key |
PUT |
issue resolve KEY |
PUT /issues/:key (state=resolved) |
PUT |
issue delete KEY |
DELETE /issues/:key |
DELETE |
issue bulk |
POST /issues/bulk |
POST |
maintenance list |
GET /maintenance_windows |
GET |
maintenance get KEY |
GET /maintenance_windows/:key |
GET |
maintenance create |
POST /maintenance_windows |
POST |
maintenance update KEY |
PUT /maintenance_windows/:key |
PUT |
maintenance delete KEY |
DELETE /maintenance_windows/:key |
DELETE |
statuspage list |
GET /statuspages |
GET |
statuspage get KEY |
GET /statuspages/:key |
GET |
statuspage create |
POST /statuspages |
POST |
statuspage update KEY |
PUT /statuspages/:key |
PUT |
statuspage delete KEY |
DELETE /statuspages/:key |
DELETE |
statuspage component list |
GET /statuspage_components |
GET |
statuspage component create |
POST /statuspage_components |
POST |
statuspage component update KEY |
PUT /statuspage_components/:key |
PUT |
statuspage component delete KEY |
DELETE /statuspage_components/:key |
DELETE |
metric get |
GET /metrics |
GET |
metric aggregate |
GET /aggregates |
GET |
site list |
GET /sites |
GET |
site get KEY |
GET /sites/:key |
GET |
site create |
POST /sites |
POST |
site update KEY |
PUT /sites/:key |
PUT |
site delete KEY |
DELETE /sites/:key |
DELETE |
site query |
POST /sites/query |
POST |
site error list |
GET /site_errors |
GET |
site error get KEY |
GET /site_errors/:key |
GET |