From 27424dbd200f9551eb5959b4a7758e56b12fbef2 Mon Sep 17 00:00:00 2001 From: Herve Labas Date: Tue, 10 Feb 2026 18:33:15 +0000 Subject: [PATCH] feat: add `checks list` command using versioned API Adds a new `checks list` CLI command that uses the /api/checks endpoint with cursor pagination, DTO types, and the X-Checkly-API-Version header. Co-Authored-By: Claude Opus 4.6 --- packages/cli/package.json | 3 + packages/cli/src/commands/checks/list.ts | 113 +++++++++++++++++++++++ packages/cli/src/rest/api.ts | 13 +++ packages/cli/src/rest/checks.ts | 80 ++++++++++++++++ 4 files changed, 209 insertions(+) create mode 100644 packages/cli/src/commands/checks/list.ts create mode 100644 packages/cli/src/rest/checks.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index e743a32c0..d0f539275 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -64,6 +64,9 @@ "message": "<%= config.name %> update available from <%= chalk.greenBright(config.version) %> to <%= chalk.greenBright(latest) %>. To update, run `npm install -D checkly@latest`" }, "topics": { + "checks": { + "description": "Manage and inspect Checkly checks." + }, "env": { "description": "Manage Checkly environment variables." }, diff --git a/packages/cli/src/commands/checks/list.ts b/packages/cli/src/commands/checks/list.ts new file mode 100644 index 000000000..e427dc752 --- /dev/null +++ b/packages/cli/src/commands/checks/list.ts @@ -0,0 +1,113 @@ +import { Flags } from '@oclif/core' +import * as api from '../../rest/api' +import { AuthCommand } from '../authCommand' +import type { CheckDTO } from '../../rest/checks' + +export default class ChecksList extends AuthCommand { + static hidden = false + static description = 'List all checks in your Checkly account.' + + static examples = [ + '$ npx checkly checks list', + '$ npx checkly checks list --check-type BROWSER', + '$ npx checkly checks list --tag production --activated', + '$ npx checkly checks list --json', + ] + + static flags = { + 'check-type': Flags.string({ + description: 'Filter by check type (API, BROWSER, HEARTBEAT, MULTI_STEP, TCP, PLAYWRIGHT, URL, DNS, ICMP)', + options: ['API', 'BROWSER', 'HEARTBEAT', 'ICMP', 'MULTI_STEP', 'TCP', 'PLAYWRIGHT', 'URL', 'DNS'], + }), + 'tag': Flags.string({ + description: 'Filter by tag name', + }), + 'activated': Flags.boolean({ + description: 'Show only activated checks', + allowNo: true, + }), + 'muted': Flags.boolean({ + description: 'Show only muted checks', + allowNo: true, + }), + 'json': Flags.boolean({ + description: 'Output as JSON', + default: false, + }), + } + + async run (): Promise { + const { flags } = await this.parse(ChecksList) + + const checks = await api.checks.listAll({ + checkType: flags['check-type'], + tag: flags.tag, + activated: flags.activated, + muted: flags.muted, + }) + + if (checks.length === 0) { + this.log('No checks found.') + return + } + + if (flags.json) { + this.log(JSON.stringify(checks, null, 2)) + return + } + + this.log(`Found ${checks.length} check${checks.length === 1 ? '' : 's'}:\n`) + + // Column widths + const nameWidth = Math.min(40, Math.max(4, ...checks.map(c => c.name.length))) + + // Header + const header = [ + pad('NAME', nameWidth), + pad('TYPE', 12), + pad('STATUS', 10), + pad('FREQ', 6), + pad('LOCATIONS', 20), + 'TAGS', + ].join(' ') + this.log(header) + this.log('─'.repeat(header.length)) + + for (const check of checks) { + const status = formatStatus(check) + const locations = formatLocations(check) + const tags = check.tags.length > 0 ? check.tags.join(', ') : '-' + + this.log([ + pad(truncate(check.name, nameWidth), nameWidth), + pad(check.checkType, 12), + pad(status, 10), + pad(`${check.frequency}m`, 6), + pad(locations, 20), + tags, + ].join(' ')) + } + } +} + +function formatStatus (check: CheckDTO): string { + if (!check.activated) return 'disabled' + if (check.muted) return 'muted' + return 'active' +} + +function formatLocations (check: CheckDTO): string { + const count = check.locations.length + check.privateLocations.length + if (count === 0) return '-' + if (count <= 2) return [...check.locations, ...check.privateLocations].join(', ') + return `${count} locations` +} + +function truncate (str: string, len: number): string { + if (str.length <= len) return str + return str.slice(0, len - 1) + '…' +} + +function pad (str: string, len: number): string { + return str.padEnd(len) +} diff --git a/packages/cli/src/rest/api.ts b/packages/cli/src/rest/api.ts index 0ed58f103..8b97d9aa2 100644 --- a/packages/cli/src/rest/api.ts +++ b/packages/cli/src/rest/api.ts @@ -12,9 +12,16 @@ import Locations from './locations' import TestSessions from './test-sessions' import EnvironmentVariables from './environment-variables' import HeartbeatChecks from './heartbeat-checks' +import Checks from './checks' import ChecklyStorage from './checkly-storage' import { handleErrorResponse, UnauthorizedError } from './errors' +/** + * The API version date for the new versioned API surface (/api/). + * This is pinned per CLI release — clients opt in to new versions explicitly. + */ +export const CHECKLY_API_VERSION = '2026-02-10' + export function getDefaults () { const apiKey = config.getApiKey() const accountId = config.getAccountId() @@ -66,6 +73,11 @@ export function requestInterceptor (config: InternalAxiosRequestConfig) { config.headers['x-checkly-ci-name'] = CIname + // Pin API version header for all /api/ requests + if (config.url?.startsWith('/api/')) { + config.headers['x-checkly-api-version'] = CHECKLY_API_VERSION + } + return config } @@ -101,4 +113,5 @@ export const privateLocations = new PrivateLocations(api) export const testSessions = new TestSessions(api) export const environmentVariables = new EnvironmentVariables(api) export const heartbeatCheck = new HeartbeatChecks(api) +export const checks = new Checks(api) export const checklyStorage = new ChecklyStorage(api) diff --git a/packages/cli/src/rest/checks.ts b/packages/cli/src/rest/checks.ts new file mode 100644 index 000000000..f1ec136f3 --- /dev/null +++ b/packages/cli/src/rest/checks.ts @@ -0,0 +1,80 @@ +import type { AxiosInstance } from 'axios' + +export interface CheckDTO { + id: string + name: string + checkType: string + activated: boolean + muted: boolean + frequency: number + locations: string[] + privateLocations: string[] + tags: string[] + groupId: string | null + runtimeId: string | null + doubleCheck: boolean + degradedResponseTime: number | null + maxResponseTime: number | null + createdAt: string + updatedAt: string +} + +export interface Pagination { + nextCursor: string | null + hasMore: boolean +} + +export interface ResponseMeta { + apiVersion: string + stability: string + requestId: string +} + +export interface ChecksListResponse { + data: CheckDTO[] + pagination: Pagination + meta: ResponseMeta +} + +export interface ChecksListParams { + limit?: number + cursor?: string + checkType?: string + tag?: string + muted?: boolean + activated?: boolean +} + +class Checks { + api: AxiosInstance + constructor (api: AxiosInstance) { + this.api = api + } + + /** + * List all checks via the versioned API. + * Automatically paginates through all results when no cursor is provided. + */ + async list (params: ChecksListParams = {}) { + const { data } = await this.api.get('/api/checks', { params }) + return data + } + + /** + * List all checks, automatically paginating through all pages. + */ + async listAll (params: Omit = {}): Promise { + const allChecks: CheckDTO[] = [] + let cursor: string | undefined + + do { + const response = await this.list({ ...params, limit: 100, cursor }) + allChecks.push(...response.data) + cursor = response.pagination.nextCursor ?? undefined + } while (cursor) + + return allChecks + } +} + +export default Checks