Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ exports[`formatErrorGroups > renders markdown error groups > error-groups-md 1`]

exports[`formatErrorGroups > renders terminal error groups > error-groups-terminal 1`] = `
"ERROR GROUPS
ERROR FIRST SEEN LAST SEEN RCA ERROR GROUP ID
TimeoutError: page.click: Timeout 30000ms exceeded 14d ago 5m ago - eg-1
ERROR FIRST SEEN LAST SEEN RCA ERROR GROUP ID
TimeoutError: page.click: Timeout 30000ms exceeded 14d ago 5m ago - eg-1

Run root cause analysis: checkly rca run -e eg-1 -w"
`;
Expand All @@ -99,7 +99,7 @@ exports[`formatResults > renders markdown table > results-table-md 1`] = `
`;

exports[`formatResults > renders terminal table > results-table-terminal 1`] = `
"TIME LOCATION STATUS RESPONSE TIME RESULT ID
5m ago eu-west-1 passing 245ms result-1
5m ago us-east-1 failing 5.20s result-2"
"TIME LOCATION STATUS RESPONSE TIME RESULT ID
5m ago eu-west-1 passing 245ms result-1
5m ago us-east-1 failing 5.20s result-2"
`;
26 changes: 25 additions & 1 deletion packages/cli/src/formatters/__tests__/alert-channels.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import type { AlertChannel } from '../../rest/alert-channels.js'
import type { AlertNotification } from '../../rest/alert-notifications.js'
import { stripAnsi } from '../render.js'
import { stripAnsi, visWidth } from '../render.js'
import {
formatAlertChannelDetail,
formatAlertChannels,
Expand Down Expand Up @@ -43,6 +43,30 @@ describe('formatAlertChannels', () => {
expect(result).toContain('2026-03-01 10:00:00 UTC')
expect(result).toContain('123')
})

it('keeps terminal list rows within narrow terminal width', () => {
const originalColumns = process.stdout.columns
Object.defineProperty(process.stdout, 'columns', { configurable: true, value: 80 })
try {
const channels = [
{
...baseChannel,
id: '0056ce7a-52f6-4315-a2d6-0392369abac5',
name: 'Primary production incident response webhook',
type: 'WEBHOOK',
config: { name: 'Webhook with a verbose target name' },
},
] as AlertChannel[]

const result = stripAnsi(formatAlertChannels(channels, 'terminal'))

for (const line of result.split('\n')) {
expect(visWidth(line)).toBeLessThanOrEqual(80)
}
} finally {
Object.defineProperty(process.stdout, 'columns', { configurable: true, value: originalColumns })
}
})
})

describe('formatAlertChannelDetail', () => {
Expand Down
24 changes: 10 additions & 14 deletions packages/cli/src/formatters/account-members.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import {
type ColumnDef,
type OutputFormat,
formatDate,
renderTable,
truncateToWidth,
renderAdaptiveTable,
} from './render.js'

export interface AccountMembersTableOptions {
Expand Down Expand Up @@ -71,13 +70,7 @@ function buildAccountMemberColumns (
}

const showId = options.showId !== false
const termWidth = process.stdout.columns || 120
const idReserve = showId ? 38 : 0
const fixedWidth = 8 + 13 + 10 + 5 + 5 + 9 + 25 + idReserve
const flexibleWidth = Math.max(24, termWidth - fixedWidth)
const hasNames = members.some(member => member.type === 'member' && member.name)
const emailWidth = Math.max(20, Math.min(34, Math.floor(flexibleWidth * (hasNames ? 0.6 : 1))))
const nameWidth = hasNames ? Math.max(14, Math.min(24, flexibleWidth - emailWidth)) : 0

const columns: ColumnDef<AccountMember>[] = [
{
Expand All @@ -87,16 +80,18 @@ function buildAccountMemberColumns (
},
{
header: 'Email',
width: emailWidth,
value: m => truncateToWidth(m.email, emailWidth - 2),
minWidth: 21,
maxWidth: 34,
value: m => m.email,
},
]

if (hasNames) {
columns.push({
header: 'Name',
width: nameWidth,
value: (m, fmt) => truncateToWidth(memberName(m, fmt), nameWidth - 2),
minWidth: 14,
maxWidth: 24,
value: (m, fmt) => memberName(m, fmt),
})
}

Expand Down Expand Up @@ -129,13 +124,14 @@ function buildAccountMemberColumns (
{
header: 'Expires',
width: 25,
value: (m, fmt) => truncateToWidth(expiresAt(m, fmt), 23),
value: (m, fmt) => expiresAt(m, fmt),
},
)

if (showId) {
columns.push({
header: 'ID',
width: 38,
value: m => chalk.dim(memberId(m)),
})
}
Expand All @@ -148,5 +144,5 @@ export function formatAccountMembers (
format: OutputFormat,
options: AccountMembersTableOptions = {},
): string {
return renderTable(buildAccountMemberColumns(members, format, options), members, format)
return renderAdaptiveTable(buildAccountMemberColumns(members, format, options), members, format)
}
30 changes: 19 additions & 11 deletions packages/cli/src/formatters/account-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
type OutputFormat,
type ColumnDef,
type DetailField,
renderTable,
renderAdaptiveTable,
renderDetailFields,
} from './render.js'

Expand Down Expand Up @@ -70,8 +70,6 @@ export function formatLocations (locations: AccountLocations, format: OutputForm

// --- Column definitions ---

const NAME_WIDTH = 50
const UPGRADE_WIDTH = 45
export const CONTACT_SALES_URL = 'https://www.checklyhq.com/contact-sales/'
const CONTACT_SALES_LABEL = 'Contact sales'

Expand All @@ -94,14 +92,16 @@ function upgradeLabel (e: Entitlement): string {

const upgradeColumn: ColumnDef<Entitlement> = {
header: 'Required Upgrade',
width: UPGRADE_WIDTH,
minWidth: 18,
maxWidth: 45,
value: e => upgradeLabel(e),
}

const meteredColumns: ColumnDef<Entitlement>[] = [
{
header: 'Name',
width: NAME_WIDTH,
minWidth: 20,
maxWidth: 50,
value: e => e.name,
},
{
Expand All @@ -113,14 +113,17 @@ const meteredColumns: ColumnDef<Entitlement>[] = [
upgradeColumn,
{
header: 'Key',
minWidth: 12,
maxWidth: 36,
value: e => chalk.dim(e.key),
},
]

const flagColumns: ColumnDef<Entitlement>[] = [
{
header: 'Name',
width: NAME_WIDTH,
minWidth: 20,
maxWidth: 50,
value: e => e.name,
},
{
Expand All @@ -131,14 +134,17 @@ const flagColumns: ColumnDef<Entitlement>[] = [
upgradeColumn,
{
header: 'Key',
minWidth: 12,
maxWidth: 36,
value: e => chalk.dim(e.key),
},
]

const mixedColumns: ColumnDef<Entitlement>[] = [
{
header: 'Name',
width: NAME_WIDTH,
minWidth: 20,
maxWidth: 50,
value: e => e.name,
},
{
Expand All @@ -160,6 +166,8 @@ const mixedColumns: ColumnDef<Entitlement>[] = [
upgradeColumn,
{
header: 'Key',
minWidth: 12,
maxWidth: 36,
value: e => chalk.dim(e.key),
},
]
Expand Down Expand Up @@ -287,7 +295,7 @@ export function formatPlanSummary (plan: AccountPlan, format: OutputFormat, upgr
lines.push(chalk.cyan.bold(`Metered entitlements (${enabledMeteredCount} of ${metered.length} enabled):`))
}
lines.push('')
const tableStr = renderTable(meteredColumns, metered, format)
const tableStr = renderAdaptiveTable(meteredColumns, metered, format)
lines.push(format === 'terminal' ? highlightDisabledRows(tableStr, metered) : tableStr)

// Flag summary line
Expand Down Expand Up @@ -343,11 +351,11 @@ export function formatFilteredEntitlements (

let tableStr: string
if (hasMetered && !hasFlags) {
tableStr = renderTable(meteredColumns, filtered, format)
tableStr = renderAdaptiveTable(meteredColumns, filtered, format)
} else if (hasFlags && !hasMetered) {
tableStr = renderTable(flagColumns, filtered, format)
tableStr = renderAdaptiveTable(flagColumns, filtered, format)
} else {
tableStr = renderTable(mixedColumns, filtered, format)
tableStr = renderAdaptiveTable(mixedColumns, filtered, format)
}
// Only highlight disabled rows when there's a mix — if everything is disabled
// (e.g. --disabled filter), the pink becomes noise rather than signal.
Expand Down
32 changes: 14 additions & 18 deletions packages/cli/src/formatters/alert-channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import {
type DetailField,
type ColumnDef,
renderDetailFields,
renderTable,
renderAdaptiveTable,
formatDate,
truncateError,
truncateToWidth,
} from './render.js'

export interface AlertChannelPaginationInfo {
Expand Down Expand Up @@ -103,22 +102,18 @@ function buildAlertChannelColumns (format: OutputFormat): ColumnDef<AlertChannel
]
}

const termWidth = process.stdout.columns || 120
const nameWidth = Math.min(34, Math.floor(termWidth * 0.28))
const targetWidth = Math.min(28, Math.floor(termWidth * 0.22))

return [
{ header: 'Type', width: 14, value: c => normalizeType(c.type) },
{ header: 'Name', width: nameWidth, value: c => truncateToWidth(titleFromConfig(c), nameWidth - 2) },
{ header: 'Target', width: targetWidth, value: (c, fmt) => truncateToWidth(targetFromConfig(c, fmt), targetWidth - 2) },
{ header: 'Name', minWidth: 12, maxWidth: 34, value: c => titleFromConfig(c) },
{ header: 'Target', minWidth: 12, maxWidth: 28, value: (c, fmt) => targetFromConfig(c, fmt) },
{ header: 'Subs', width: 8, align: 'right', value: subscriptionCount },
{ header: 'Created', width: 24, value: (c, fmt) => formatDate(c.created_at ?? c.createdAt, fmt) },
{ header: 'ID', value: c => chalk.dim(String(c.id)) },
{ header: 'ID', minWidth: 8, maxWidth: 38, value: c => chalk.dim(String(c.id)) },
]
}

export function formatAlertChannels (channels: AlertChannel[], format: OutputFormat): string {
return renderTable(buildAlertChannelColumns(format), channels, format)
return renderAdaptiveTable(buildAlertChannelColumns(format), channels, format)
}

export function formatAlertChannelPaginationInfo (pagination: AlertChannelPaginationInfo): string {
Expand Down Expand Up @@ -209,13 +204,14 @@ function buildSubscriptionColumns (

const columns: ColumnDef<AlertChannelSubscription>[] = [
{ header: 'Type', width: 14, value: subscriptionType },
{ header: 'ID', value: s => chalk.dim(subscriptionId(s)) },
{ header: 'ID', minWidth: 8, maxWidth: 38, value: s => chalk.dim(subscriptionId(s)) },
]
if (includeName) {
columns.splice(1, 0, {
header: 'Name',
width: 32,
value: (s, fmt) => truncateToWidth(subscriptionName(s, fmt), 30),
minWidth: 12,
maxWidth: 32,
value: (s, fmt) => subscriptionName(s, fmt),
})
}
return columns
Expand All @@ -224,7 +220,7 @@ function buildSubscriptionColumns (
export function formatAlertChannelSubscriptions (channel: AlertChannel, format: OutputFormat): string {
const subscriptions = channel.subscriptions ?? []
if (subscriptions.length === 0) return 'No subscriptions configured.'
return renderTable(buildSubscriptionColumns(format, subscriptions), subscriptions, format)
return renderAdaptiveTable(buildSubscriptionColumns(format, subscriptions), subscriptions, format)
}

export function formatAlertChannelDetail (channel: AlertChannel, format: OutputFormat): string {
Expand Down Expand Up @@ -293,13 +289,13 @@ function buildAlertNotificationColumns (format: OutputFormat): ColumnDef<AlertNo
{ header: 'Time', width: 22, value: l => formatDate(l.timestamp, format) },
{ header: 'Status', width: 14, value: l => formatNotificationStatus(l.status, format) },
{ header: 'Type', width: 12, value: l => normalizeType(l.type) },
{ header: 'Check', width: 28, value: l => truncateToWidth(notificationCheckLabel(l), 26) },
{ header: 'Result', width: 22, value: l => chalk.dim(truncateToWidth(notificationResultLabel(l), 20)) },
{ header: 'Message', value: l => truncateError(notificationMessage(l), 80) },
{ header: 'Check', minWidth: 12, maxWidth: 28, value: notificationCheckLabel },
{ header: 'Result', minWidth: 8, maxWidth: 22, value: l => chalk.dim(notificationResultLabel(l)) },
{ header: 'Message', minWidth: 16, maxWidth: 80, value: l => truncateError(notificationMessage(l), 160) },
]
}

export function formatAlertNotificationLogs (logs: AlertNotification[], format: OutputFormat): string {
if (logs.length === 0) return 'No alert channel logs found.'
return renderTable(buildAlertNotificationColumns(format), logs, format)
return renderAdaptiveTable(buildAlertNotificationColumns(format), logs, format)
}
20 changes: 6 additions & 14 deletions packages/cli/src/formatters/batch-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import chalk from 'chalk'
import type { BatchAnalyticsResult } from '../rest/batch-analytics.js'
import { type CheckWithStatus, type PaginationInfo, resolveStatus } from './checks.js'
import type { OutputFormat, ColumnDef } from './render.js'
import { renderTable, truncateToWidth, visWidth } from './render.js'
import { renderAdaptiveTable } from './render.js'
import { findUnit, rangeLabels } from './analytics.js'
import type { QuickRange } from '../rest/analytics.js'

Expand Down Expand Up @@ -102,25 +102,17 @@ function buildColumns (rows: StatsRow[], format: OutputFormat): ColumnDef<StatsR
return cols
}

// Terminal
const termWidth = process.stdout.columns || 120
const typeWidth = 12
const statusWidth = 10
const availWidth = 10
const metricWidth = 12

const metricCols = (hasTiming ? 2 : 0) + (hasIcmp ? 2 : 0)
const fixedWidth = typeWidth + statusWidth + availWidth + metricCols * metricWidth
const nameWidth = Math.min(
Math.max(4, ...rows.map(r => visWidth(r.name))) + 2,
Math.max(20, termWidth - fixedWidth - 2),
)

const cols: ColumnDef<StatsRow>[] = [
{
header: 'Name',
width: nameWidth,
value: r => truncateToWidth(r.name, nameWidth - 2),
minWidth: 20,
maxWidth: 42,
value: r => r.name,
},
{
header: 'Type',
Expand Down Expand Up @@ -179,11 +171,11 @@ export function formatBatchStats (rows: StatsRow[], range: string, format: Outpu

if (format === 'md') {
const heading = `## Stats (${rangeDisplay})\n\n`
return heading + renderTable(columns, rows, format)
return heading + renderAdaptiveTable(columns, rows, format)
}

const heading = chalk.bold('STATS') + ' ' + chalk.dim(`(${rangeDisplay})`)
return heading + '\n\n' + renderTable(columns, rows, format)
return heading + '\n\n' + renderAdaptiveTable(columns, rows, format)
}

export function formatBatchStatsNavigationHints (
Expand Down
Loading
Loading