Skip to content

Comments

feat(cli): add checks list/get commands with typed formatters#1233

Open
MichaelHogers wants to merge 10 commits intomainfrom
michael/tim-9-10-11-checks-cli
Open

feat(cli): add checks list/get commands with typed formatters#1233
MichaelHogers wants to merge 10 commits intomainfrom
michael/tim-9-10-11-checks-cli

Conversation

@MichaelHogers
Copy link

@MichaelHogers MichaelHogers commented Feb 23, 2026

Summary

Add checkly checks list and checkly checks get CLI commands for viewing checks, results, and error groups from the terminal.

  • REST clients for checks, check-results, check-statuses, check-groups, error-groups
  • Formatters with typed field definitions (DetailField<T>, ColumnDef<T>) — fields defined once, rendered for both terminal and markdown
  • Unit tests for formatters and render primitives, e2e tests for both commands

Also adds a new e2e testing account with no checks, representing a new user, to cover any potential edge cases we may miss in our own e2e prod account.

config.emptyApiKey = process.env.CHECKLY_EMPTY_API_KEY
config.emptyAccountId = process.env.CHECKLY_EMPTY_ACCOUNT_ID

Test plan

  • vitest --run src/formatters/__tests__/ src/commands/checks/__tests__/ — 115 tests pass
  • E2E: checks-list.spec.ts, checks-get.spec.ts (require prod env)

Add `checkly checks list` and `checkly checks get` commands with
REST clients, formatters, and e2e tests.

Formatters use typed field definitions (DetailField, ColumnDef) with
generic renderDetailFields/renderTable primitives, defining fields
once and rendering them for both terminal and markdown output formats.
- Extract appendErrors/appendAssets helpers in check-result-detail
- Collapse duplicated formatBody try/catch branches
- De-duplicate Tags column definition in checks table
- Simplify formatCheckType to pass-through
- Use stripAnsi/formatDate in get.ts instead of inline regex/formatting
@MichaelHogers MichaelHogers force-pushed the michael/tim-9-10-11-checks-cli branch from 78aeeb3 to 482d32e Compare February 23, 2026 11:02
…tive checks

- Replace this.exit(1) with process.exitCode = 1 to avoid oclif's global
  handler printing the error a second time
- Exclude inactive checks from --status filter results
- Extract filterByStatus() and add unit tests
Add .catch() fallbacks for check-statuses and check-groups API calls
so accounts with no checks don't crash with a 404.
Test checks list and get commands against an account with no checks
to verify graceful handling of empty responses and 404 fallbacks.
…nfig

- Add markdown output branch to showErrorGroupDetail (was rendering
  terminal/chalk output regardless of --output flag)
- Switch empty account e2e tests to use node-config pattern with
  describe.skipIf for graceful skipping when creds aren't set
- Add emptyApiKey/emptyAccountId to e2e config default.js
- Document optional empty account test setup in e2e README
@MichaelHogers
Copy link
Author

checking something with totals not adding up as expected, monitors causing some funniness in the counting

Show per-type counts (e.g. BROWSER: 140  API: 77  ...) on a dim line
below the status summary. Filters by activeCheckIds so the subcounts
sum to the displayed total.
@MichaelHogers
Copy link
Author

@sorccu could you give this an initial review please? 🙇

@MichaelHogers
Copy link
Author

@sorccu what do you typically do with CLI releases? do we best first dogfood these type of changes?

Copy link
Contributor

@thebiglabasky thebiglabasky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine, though also believe there could be a case for already centralizing some elements that'll be common across most commands like pagination, command output parameter, or formatting.
The second point is regarding the contract, though I think we could go with this first and refactor later to move the definitions you have there into the contract package.
This made me wonder if by "contract" we wouldn't actually mean a TS SDK that we'd import both from the CLI and webapp and would act as the actual contract against the openapi spec from the BE API.

})
expect(result.status).toBe(0)
expect(result.stdout).toContain('#')
expect(result.stdout).toContain('| Field |')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's fine but is that the most relevant keyword to be looking for? That felt surprising at first glance

})
expect(result.status).toBe(0)
expect(result.stdout).toContain('| Name |')
expect(result.stdout).toContain('| --- |')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That line feels superfluous

import type { CheckGroup } from '../../../rest/check-groups'
import type { CheckWithStatus } from '../../../formatters/checks'

function makeCheck (overrides: Partial<Check> = {}): Check {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like fixtures we could reuse and worth having centralized for future use on other commands? Or do we want to limit the scope of these to make the tests more self-contained/less dependent?

expect(result.has('c2')).toBe(false)
})

it('includes heartbeat checks', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's kinda strange to test 'includes activated non-heartbeat checks' and another 'includes hearbeat checks': I don't really see the expected difference between both tests so might as well drop one?
Note: I haven't checked what buildActiveCheckIds is currently doing so there might be a good reason for that, though it's unclear at surface level.

'results-cursor': Flags.string({
description: 'Cursor for results pagination (from previous output).',
}),
'output': Flags.string({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be good to generalize to all commands so we don't need to repeat this and risk drifts going forward. I'd take it that every command will need to be able to customize the output, so worth centralizing imo

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That file mixes a lot of concerns in one, I'd likely split between helpers, and per-check-type formatting

return [
{
header: 'Error',
width: 60,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels a bit arbitrary but I guess why not? 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, skimmed through since secondary


// --- API check result ---

export interface ApiCheckAssertion {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So these should ideally come from the contract package, correct?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exactly, soon-ish

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤞

return this.api.get<Check[]>('/v1/checks', { params })
}

async getAllPaginated (params: ListChecksParams = {}): Promise<PaginatedChecks> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like something we could centralize for all paginated endpoints

@MichaelHogers
Copy link
Author

thanks @thebiglabasky going through the comments now (on the formatting side i didn't feel super strongly about the code, because it is bound to change and nothing depends directly on that for a while)

@MichaelHogers
Copy link
Author

MichaelHogers commented Feb 24, 2026

I think it's fine, though also believe there could be a case for already centralizing some elements that'll be common across most commands like pagination, command output parameter, or formatting. The second point is regarding the contract, though I think we could go with this first and refactor later to move the definitions you have there into the contract package. This made me wonder if by "contract" we wouldn't actually mean a TS SDK that we'd import both from the CLI and webapp and would act as the actual contract against the openapi spec from the BE API.

@thebiglabasky the way i've done this before is:

  • backend exports an openapi.json (to me this can be seen as "the contract") - with the versioned header approach these should be permanently stored and named by date* (could sit in the repo)
  • any client/consumer can turn that openapi.json into a sdk (CLI and webapp likely use different libraries to turn it into their own SDK, e.g. webapp wants a vue friendly one) + i used to have a CI check to ensure webapp uses latest backend openapi.json

*: we need to also build some tooling to check that older contracts still work against latest backend, this could be by validating the contracts directly or simply e2e suits between the range of versions of clients and latest backend, probably former is easiest

@thebiglabasky
Copy link
Contributor

  • any client/consumer can turn that openapi.json into a sdk (CLI and webapp likely use different libraries to turn it into their own SDK, e.g. webapp wants a vue friendly one) + i used to have a CI check to ensure webapp uses latest backend openapi.json

*: we need to also build some tooling to check that older contracts still work against latest backend, this could be by validating the contracts directly or simply e2e suits between the range of versions of clients and latest backend, probably former is easiest

I was thinking this but then wondered why we'd actually bother to separate all these things since it's all TypeScript in the end, so having all the openapi, types and even handy wrapping call methods in a single package suddenly looked appealing to me: so you don't even bother with the API endpoint and structure, it's all typed through the SDK. The openapi part becomes the elements used by the backend and for docs.
I might be naive though, but I'm not sure about the benefits of splitting the Vue consumption from the CLI. Maybe skeletons / streaming or stuff I'm overlooking?

@thebiglabasky
Copy link
Contributor

Note for self: came back from the SDK idea, and now sold on sticking to only zod so we can generate the openapi spec, and get runtime validation all in a single place

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants