From 59cef1fa0611fc1cba8baa8506fb5119a8e98774 Mon Sep 17 00:00:00 2001 From: Herve Labas Date: Fri, 29 May 2026 15:45:50 +0200 Subject: [PATCH 1/3] refactor(cli): use adaptive table formatter broadly --- .../__snapshots__/checks.spec.ts.snap | 10 +-- .../cli/src/formatters/account-members.ts | 24 +++--- packages/cli/src/formatters/account-plan.ts | 30 ++++--- packages/cli/src/formatters/batch-stats.ts | 20 ++--- packages/cli/src/formatters/checks.ts | 21 ++--- packages/cli/src/formatters/incidents.ts | 20 ++--- packages/cli/src/formatters/status-pages.ts | 59 ++++++-------- packages/cli/src/formatters/test-sessions.ts | 81 +++++++------------ 8 files changed, 112 insertions(+), 153 deletions(-) diff --git a/packages/cli/src/formatters/__tests__/__snapshots__/checks.spec.ts.snap b/packages/cli/src/formatters/__tests__/__snapshots__/checks.spec.ts.snap index 4d07ced9e..4ddb8c6d4 100644 --- a/packages/cli/src/formatters/__tests__/__snapshots__/checks.spec.ts.snap +++ b/packages/cli/src/formatters/__tests__/__snapshots__/checks.spec.ts.snap @@ -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" `; @@ -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" `; diff --git a/packages/cli/src/formatters/account-members.ts b/packages/cli/src/formatters/account-members.ts index ecba2030c..09a56540f 100644 --- a/packages/cli/src/formatters/account-members.ts +++ b/packages/cli/src/formatters/account-members.ts @@ -4,8 +4,7 @@ import { type ColumnDef, type OutputFormat, formatDate, - renderTable, - truncateToWidth, + renderAdaptiveTable, } from './render.js' export interface AccountMembersTableOptions { @@ -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[] = [ { @@ -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), }) } @@ -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)), }) } @@ -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) } diff --git a/packages/cli/src/formatters/account-plan.ts b/packages/cli/src/formatters/account-plan.ts index 02d117707..41dc8c554 100644 --- a/packages/cli/src/formatters/account-plan.ts +++ b/packages/cli/src/formatters/account-plan.ts @@ -4,7 +4,7 @@ import { type OutputFormat, type ColumnDef, type DetailField, - renderTable, + renderAdaptiveTable, renderDetailFields, } from './render.js' @@ -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' @@ -94,14 +92,16 @@ function upgradeLabel (e: Entitlement): string { const upgradeColumn: ColumnDef = { header: 'Required Upgrade', - width: UPGRADE_WIDTH, + minWidth: 18, + maxWidth: 45, value: e => upgradeLabel(e), } const meteredColumns: ColumnDef[] = [ { header: 'Name', - width: NAME_WIDTH, + minWidth: 20, + maxWidth: 50, value: e => e.name, }, { @@ -113,6 +113,8 @@ const meteredColumns: ColumnDef[] = [ upgradeColumn, { header: 'Key', + minWidth: 12, + maxWidth: 36, value: e => chalk.dim(e.key), }, ] @@ -120,7 +122,8 @@ const meteredColumns: ColumnDef[] = [ const flagColumns: ColumnDef[] = [ { header: 'Name', - width: NAME_WIDTH, + minWidth: 20, + maxWidth: 50, value: e => e.name, }, { @@ -131,6 +134,8 @@ const flagColumns: ColumnDef[] = [ upgradeColumn, { header: 'Key', + minWidth: 12, + maxWidth: 36, value: e => chalk.dim(e.key), }, ] @@ -138,7 +143,8 @@ const flagColumns: ColumnDef[] = [ const mixedColumns: ColumnDef[] = [ { header: 'Name', - width: NAME_WIDTH, + minWidth: 20, + maxWidth: 50, value: e => e.name, }, { @@ -160,6 +166,8 @@ const mixedColumns: ColumnDef[] = [ upgradeColumn, { header: 'Key', + minWidth: 12, + maxWidth: 36, value: e => chalk.dim(e.key), }, ] @@ -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 @@ -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. diff --git a/packages/cli/src/formatters/batch-stats.ts b/packages/cli/src/formatters/batch-stats.ts index 9175e6a14..83109b9e4 100644 --- a/packages/cli/src/formatters/batch-stats.ts +++ b/packages/cli/src/formatters/batch-stats.ts @@ -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' @@ -102,25 +102,17 @@ function buildColumns (rows: StatsRow[], format: OutputFormat): ColumnDef visWidth(r.name))) + 2, - Math.max(20, termWidth - fixedWidth - 2), - ) - const cols: ColumnDef[] = [ { header: 'Name', - width: nameWidth, - value: r => truncateToWidth(r.name, nameWidth - 2), + minWidth: 20, + maxWidth: 42, + value: r => r.name, }, { header: 'Type', @@ -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 ( diff --git a/packages/cli/src/formatters/checks.ts b/packages/cli/src/formatters/checks.ts index ff933d20a..3ab07ec49 100644 --- a/packages/cli/src/formatters/checks.ts +++ b/packages/cli/src/formatters/checks.ts @@ -13,11 +13,9 @@ import { formatMs, timeAgo, stripAnsi, - truncateError, resolveResultStatus, renderDetailFields, renderAdaptiveTable, - renderTable, } from './render.js' export { formatFrequency, formatCheckType } from './render.js' @@ -312,19 +310,23 @@ function buildResultColumns (format: OutputFormat): ColumnDef[] { return [ { header: 'Time', width: 14, value: r => timeAgo(r.startedAt) }, - { header: 'Location', width: 16, value: r => r.runLocation }, + { header: 'Location', minWidth: 8, maxWidth: 16, value: r => r.runLocation }, { header: 'Status', width: 10, value: (r, fmt) => resolveResultStatus(r, fmt) }, { header: 'Response Time', width: 16, value: r => formatMs(r.responseTime) }, - { header: 'Result ID', value: r => chalk.dim(r.id) }, + { header: 'Result ID', minWidth: 12, maxWidth: 38, value: r => chalk.dim(r.id) }, ] } export function formatResults (results: CheckResult[], format: OutputFormat): string { - return renderTable(buildResultColumns(format), results, format) + return renderAdaptiveTable(buildResultColumns(format), results, format) } // --- Error groups --- +function cleanErrorMessage (msg: string): string { + return stripAnsi(msg).replace(/\n/g, ' ').replace(/\s+/g, ' ').trim() +} + function buildErrorGroupColumns (format: OutputFormat): ColumnDef[] { if (format === 'md') { return [ @@ -345,8 +347,9 @@ function buildErrorGroupColumns (format: OutputFormat): ColumnDef[] return [ { header: 'Error', - width: 60, - value: eg => chalk.red(truncateError(eg.cleanedErrorMessage, 58)), + minWidth: 20, + maxWidth: 60, + value: eg => chalk.red(cleanErrorMessage(eg.cleanedErrorMessage)), }, { header: 'First Seen', @@ -363,7 +366,7 @@ function buildErrorGroupColumns (format: OutputFormat): ColumnDef[] width: 6, value: eg => (eg.rootCauseAnalyses?.length ?? 0) > 0 ? chalk.cyan('Yes') : chalk.dim('-'), }, - { header: 'Error Group ID', value: eg => chalk.dim(eg.id) }, + { header: 'Error Group ID', minWidth: 12, maxWidth: 38, value: eg => chalk.dim(eg.id) }, ] } @@ -375,7 +378,7 @@ export function formatErrorGroups (errorGroups: ErrorGroup[], format: OutputForm const title = format === 'md' ? '## Error Groups\n\n' : chalk.bold('ERROR GROUPS') + '\n' - const table = title + renderTable(columns, active, format) + const table = title + renderAdaptiveTable(columns, active, format) if (format !== 'terminal') return table diff --git a/packages/cli/src/formatters/incidents.ts b/packages/cli/src/formatters/incidents.ts index 4831a55d4..23189d63b 100644 --- a/packages/cli/src/formatters/incidents.ts +++ b/packages/cli/src/formatters/incidents.ts @@ -4,8 +4,7 @@ import { type OutputFormat, type ColumnDef, type DetailField, - truncateToWidth, - renderTable, + renderAdaptiveTable, renderDetailFields, formatDate, timeAgo, @@ -70,14 +69,12 @@ function buildIncidentListColumns (format: OutputFormat): ColumnDef truncateToWidth(i.name, nameWidth - 2), + minWidth: 12, + maxWidth: 30, + value: i => i.name, }, { header: 'Severity', @@ -107,7 +104,7 @@ function buildIncidentListColumns (format: OutputFormat): ColumnDef[] = [ @@ -127,11 +124,8 @@ function buildIncidentServiceColumns (format: OutputFormat): ColumnDef<{ name: s ] } - const termWidth = process.stdout.columns || 120 - const nameWidth = Math.min(28, Math.floor(termWidth * 0.30)) - return [ - { header: 'Service', width: nameWidth, value: s => truncateToWidth(s.name, nameWidth - 2) }, + { header: 'Service', minWidth: 10, maxWidth: 28, value: s => s.name }, { header: 'ID', value: s => chalk.dim(s.id) }, ] } @@ -148,7 +142,7 @@ export function formatIncidentDetail (incident: StatusPageIncident, format: Outp } else { lines.push(chalk.bold('SERVICES')) } - lines.push(renderTable(buildIncidentServiceColumns(format), incident.services, format)) + lines.push(renderAdaptiveTable(buildIncidentServiceColumns(format), incident.services, format)) } const latest = incident.incidentUpdates[0] diff --git a/packages/cli/src/formatters/status-pages.ts b/packages/cli/src/formatters/status-pages.ts index 92c63bbc5..36920124e 100644 --- a/packages/cli/src/formatters/status-pages.ts +++ b/packages/cli/src/formatters/status-pages.ts @@ -4,8 +4,7 @@ import { type OutputFormat, type ColumnDef, type DetailField, - truncateToWidth, - renderTable, + renderAdaptiveTable, renderDetailFields, } from './render.js' @@ -78,35 +77,32 @@ function buildExpandedColumns (format: OutputFormat): ColumnDef[] { ] } - const termWidth = process.stdout.columns || 120 - const nameWidth = Math.min(24, Math.floor(termWidth * 0.18)) - const urlWidth = Math.min(26, Math.floor(termWidth * 0.20)) - const cardWidth = Math.min(20, Math.floor(termWidth * 0.16)) - const serviceWidth = Math.min(24, Math.floor(termWidth * 0.20)) - return [ { header: 'Name', - width: nameWidth, - value: r => truncateToWidth(r.name, nameWidth - 2), + minWidth: 10, + maxWidth: 24, + value: r => r.name, }, { header: 'URL', - width: urlWidth, + minWidth: 12, + maxWidth: 32, value: r => { - const display = r.customDomain ?? r.url - return truncateToWidth(display, urlWidth - 2) + return r.customDomain ?? r.url }, }, { header: 'Card', - width: cardWidth, - value: r => truncateToWidth(r.card, cardWidth - 2), + minWidth: 8, + maxWidth: 22, + value: r => r.card, }, { header: 'Service', - width: serviceWidth, - value: r => truncateToWidth(r.service, serviceWidth - 2), + minWidth: 8, + maxWidth: 24, + value: r => r.service, }, { header: 'ID', @@ -118,7 +114,7 @@ function buildExpandedColumns (format: OutputFormat): ColumnDef[] { export function formatStatusPagesExpanded (statusPages: StatusPage[], format: OutputFormat): string { const rows = expandStatusPages(statusPages) const columns = buildExpandedColumns(format) - return renderTable(columns, rows, format) + return renderAdaptiveTable(columns, rows, format) } // --- Compact table columns (one row per status page) --- @@ -134,22 +130,19 @@ function buildCompactColumns (format: OutputFormat): ColumnDef[] { ] } - const termWidth = process.stdout.columns || 120 - const nameWidth = Math.min(30, Math.floor(termWidth * 0.25)) - const urlWidth = Math.min(32, Math.floor(termWidth * 0.28)) - return [ { header: 'Name', - width: nameWidth, - value: sp => truncateToWidth(sp.name, nameWidth - 2), + minWidth: 12, + maxWidth: 30, + value: sp => sp.name, }, { header: 'URL', - width: urlWidth, + minWidth: 14, + maxWidth: 36, value: sp => { - const display = sp.customDomain ?? sp.url - return truncateToWidth(display, urlWidth - 2) + return sp.customDomain ?? sp.url }, }, { @@ -171,7 +164,7 @@ function buildCompactColumns (format: OutputFormat): ColumnDef[] { export function formatStatusPagesCompact (statusPages: StatusPage[], format: OutputFormat): string { const columns = buildCompactColumns(format) - return renderTable(columns, statusPages, format) + return renderAdaptiveTable(columns, statusPages, format) } // --- Cursor pagination helpers --- @@ -245,13 +238,9 @@ function buildServiceColumns (format: OutputFormat): ColumnDef[] { ] } - const termWidth = process.stdout.columns || 120 - const cardWidth = Math.min(24, Math.floor(termWidth * 0.22)) - const serviceWidth = Math.min(28, Math.floor(termWidth * 0.26)) - return [ - { header: 'Card', width: cardWidth, value: r => truncateToWidth(r.card, cardWidth - 2) }, - { header: 'Service', width: serviceWidth, value: r => truncateToWidth(r.service, serviceWidth - 2) }, + { header: 'Card', minWidth: 8, maxWidth: 24, value: r => r.card }, + { header: 'Service', minWidth: 8, maxWidth: 28, value: r => r.service }, { header: 'Service ID', value: r => chalk.dim(r.serviceId) }, ] } @@ -283,7 +272,7 @@ export function formatStatusPageDetail (sp: StatusPage, format: OutputFormat): s lines.push('') lines.push(chalk.bold('SERVICES')) } - lines.push(renderTable(columns, serviceRows, format)) + lines.push(renderAdaptiveTable(columns, serviceRows, format)) } return lines.join('\n') diff --git a/packages/cli/src/formatters/test-sessions.ts b/packages/cli/src/formatters/test-sessions.ts index ad510db67..cbd346964 100644 --- a/packages/cli/src/formatters/test-sessions.ts +++ b/packages/cli/src/formatters/test-sessions.ts @@ -14,9 +14,7 @@ import { formatCheckType, formatDate, renderDetailFields, - renderTable, - truncateToWidth, - visWidth, + renderAdaptiveTable, } from './render.js' const DEFAULT_ERROR_GROUPS_LIMIT = 5 @@ -107,10 +105,7 @@ function formatInvoker (session: TestSessionListEntry, format: OutputFormat): st return formatNullableString(session.commitOwner ?? session.invoker?.name, format) } -function buildTestSessionListColumns ( - sessions: TestSessionListEntry[], - format: OutputFormat, -): ColumnDef[] { +function buildTestSessionListColumns (format: OutputFormat): ColumnDef[] { if (format === 'md') { return [ { header: 'Status', value: (session, fmt) => formatOptionalStatus(session.status, fmt) }, @@ -127,43 +122,36 @@ function buildTestSessionListColumns ( ] } - const termWidth = process.stdout.columns || 120 const statusWidth = 11 - const startedWidth = 25 - const providerWidth = 13 - const branchWidth = 18 - const userWidth = 18 const resultBucketWidth = 10 - const idWidth = 38 - const fixedWidth = statusWidth + startedWidth + providerWidth + branchWidth + userWidth - + (resultBucketWidth * 4) + idWidth - const longestName = Math.max(4, ...sessions.map(session => visWidth(session.name))) - const nameWidth = Math.max(18, Math.min(longestName + 2, termWidth - fixedWidth, 42)) return [ { header: 'Status', width: statusWidth, value: (session, fmt) => formatOptionalStatus(session.status, fmt) }, { header: 'Started', - width: startedWidth, - value: (session, fmt) => truncateToWidth(formatDate(session.startedAt, fmt), startedWidth - 2), + minWidth: 12, + maxWidth: 25, + value: (session, fmt) => formatDate(session.startedAt, fmt), }, - { header: 'Name', width: nameWidth, value: session => truncateToWidth(session.name, nameWidth - 2) }, - { header: 'Provider', width: providerWidth, value: session => truncateToWidth(session.provider, providerWidth - 2) }, + { header: 'Name', minWidth: 12, maxWidth: 42, value: session => session.name }, + { header: 'Provider', minWidth: 8, maxWidth: 13, value: session => session.provider }, { header: 'Branch', - width: branchWidth, - value: (session, fmt) => truncateToWidth(formatNullableString(session.branchName, fmt), branchWidth - 2), + minWidth: 8, + maxWidth: 18, + value: (session, fmt) => formatNullableString(session.branchName, fmt), }, { header: 'User', - width: userWidth, - value: (session, fmt) => truncateToWidth(formatInvoker(session, fmt), userWidth - 2), + minWidth: 8, + maxWidth: 18, + value: (session, fmt) => formatInvoker(session, fmt), }, { header: 'Running', width: resultBucketWidth, value: (session, fmt) => formatResultBucketCount(session.running, fmt) }, { header: 'Passed', width: resultBucketWidth, value: (session, fmt) => formatResultBucketCount(session.passed, fmt) }, { header: 'Failed', width: resultBucketWidth, value: (session, fmt) => formatResultBucketCount(session.failed, fmt) }, { header: 'Cancelled', width: resultBucketWidth, value: (session, fmt) => formatResultBucketCount(session.cancelled, fmt) }, - { header: 'ID', value: session => chalk.dim(session.id) }, + { header: 'ID', minWidth: 12, maxWidth: 38, value: session => chalk.dim(session.id) }, ] } @@ -171,7 +159,7 @@ export function formatTestSessionsList ( sessions: TestSessionListEntry[], format: OutputFormat, ): string { - return renderTable(buildTestSessionListColumns(sessions, format), sessions, format) + return renderAdaptiveTable(buildTestSessionListColumns(format), sessions, format) } export function formatTestSessionsListPaginationInfo (count: number, nextId: string | null | undefined): string { @@ -241,36 +229,28 @@ function buildResultColumns (results: TestSessionResult[], format: OutputFormat) ] } - const termWidth = process.stdout.columns || 120 - const showCheckId = termWidth >= 160 && results.some(result => result.checkId) + const showCheckId = results.some(result => result.checkId) const typeWidth = 14 const statusWidth = 10 const resultTypeWidth = 10 - const locationWidth = 14 const errorGroupWidth = 14 - const resultIdWidth = showCheckId ? 38 : 0 - const checkIdWidth = showCheckId ? 38 : 0 - const fixedWidth = typeWidth + statusWidth + resultTypeWidth + locationWidth - + errorGroupWidth + resultIdWidth + checkIdWidth - const longestName = Math.max(4, ...results.map(result => visWidth(result.name || '-'))) - const nameWidth = Math.max(16, Math.min(longestName + 2, termWidth - fixedWidth, 42)) const columns: ColumnDef[] = [ - { header: 'Name', width: nameWidth, value: result => truncateToWidth(result.name || '-', nameWidth - 2) }, - { header: 'Type', width: typeWidth, value: result => truncateToWidth(formatCheckType(result.checkType), typeWidth - 2) }, + { header: 'Name', minWidth: 12, maxWidth: 42, value: result => result.name || '-' }, + { header: 'Type', width: typeWidth, value: result => formatCheckType(result.checkType) }, { header: 'Status', width: statusWidth, value: (result, fmt) => formatStatus(result.status, fmt) }, - { header: 'Result', width: resultTypeWidth, value: result => result.resultType ? truncateToWidth(result.resultType, resultTypeWidth - 2) : chalk.dim('-') }, - { header: 'Location', width: locationWidth, value: result => result.runLocation ? truncateToWidth(result.runLocation, locationWidth - 2) : chalk.dim('-') }, + { header: 'Result', width: resultTypeWidth, value: result => result.resultType ?? chalk.dim('-') }, + { header: 'Location', minWidth: 11, maxWidth: 14, value: result => result.runLocation ?? chalk.dim('-') }, { header: 'Error Groups', width: errorGroupWidth, value: (result, fmt) => formatErrorGroupCount(result.errorGroupIds, fmt) }, ] if (showCheckId) { columns.push( - { header: 'Result ID', width: resultIdWidth, value: result => chalk.dim(result.testSessionResultId) }, - { header: 'Check ID', value: result => result.checkId ? chalk.dim(result.checkId) : chalk.dim('-') }, + { header: 'Result ID', minWidth: 38, maxWidth: 38, value: result => chalk.dim(result.testSessionResultId) }, + { header: 'Check ID', minWidth: 8, maxWidth: 38, value: result => result.checkId ? chalk.dim(result.checkId) : chalk.dim('-') }, ) } else { - columns.push({ header: 'Result ID', value: result => chalk.dim(result.testSessionResultId) }) + columns.push({ header: 'Result ID', minWidth: 38, maxWidth: 38, value: result => chalk.dim(result.testSessionResultId) }) } return columns @@ -313,17 +293,14 @@ function buildErrorGroupReferenceColumns (format: OutputFormat): ColumnDef truncateToWidth(ref.sources.join(', '), sourceWidth - 2), + minWidth: 16, + maxWidth: 50, + value: ref => ref.sources.join(', '), }, - { header: 'Error Group ID', value: ref => chalk.dim(ref.id) }, + { header: 'Error Group ID', minWidth: 12, maxWidth: 38, value: ref => chalk.dim(ref.id) }, ] } @@ -343,7 +320,7 @@ export function formatTestSessionErrorGroupIds ( const lines: string[] = [ format === 'md' ? '## Error Group IDs' : chalk.bold('ERROR GROUP IDS'), '', - renderTable(buildErrorGroupReferenceColumns(format), visible, format), + renderAdaptiveTable(buildErrorGroupReferenceColumns(format), visible, format), ] if (hiddenCount > 0) { @@ -435,7 +412,7 @@ export function formatTestSessionDetail ( lines.push('') lines.push(format === 'md' ? '## Results' : chalk.bold('RESULTS')) lines.push('') - lines.push(renderTable(buildResultColumns(results, format), results, format)) + lines.push(renderAdaptiveTable(buildResultColumns(results, format), results, format)) } const errorGroupIds = formatTestSessionErrorGroupIds(session, format, options) From 618517ece2c698f1a0f86ff1d703b84efe6d1ee8 Mon Sep 17 00:00:00 2001 From: Herve Labas Date: Fri, 29 May 2026 15:54:00 +0200 Subject: [PATCH 2/3] fix(cli): preserve test session name width --- packages/cli/src/formatters/test-sessions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/formatters/test-sessions.ts b/packages/cli/src/formatters/test-sessions.ts index cbd346964..8f8f0d71c 100644 --- a/packages/cli/src/formatters/test-sessions.ts +++ b/packages/cli/src/formatters/test-sessions.ts @@ -133,7 +133,7 @@ function buildTestSessionListColumns (format: OutputFormat): ColumnDef formatDate(session.startedAt, fmt), }, - { header: 'Name', minWidth: 12, maxWidth: 42, value: session => session.name }, + { header: 'Name', minWidth: 18, maxWidth: 42, value: session => session.name }, { header: 'Provider', minWidth: 8, maxWidth: 13, value: session => session.provider }, { header: 'Branch', From cc4bcf8a036cf62c872aed4e7d5c79cecc5200ea Mon Sep 17 00:00:00 2001 From: Herve Labas Date: Fri, 29 May 2026 16:15:32 +0200 Subject: [PATCH 3/3] fix(cli): use adaptive tables for alert channels --- .../__tests__/alert-channels.spec.ts | 26 ++++++++++++++- packages/cli/src/formatters/alert-channels.ts | 32 ++++++++----------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/packages/cli/src/formatters/__tests__/alert-channels.spec.ts b/packages/cli/src/formatters/__tests__/alert-channels.spec.ts index 994ead2d5..60f3ac8f4 100644 --- a/packages/cli/src/formatters/__tests__/alert-channels.spec.ts +++ b/packages/cli/src/formatters/__tests__/alert-channels.spec.ts @@ -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, @@ -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', () => { diff --git a/packages/cli/src/formatters/alert-channels.ts b/packages/cli/src/formatters/alert-channels.ts index 869ae3a5d..a23e75567 100644 --- a/packages/cli/src/formatters/alert-channels.ts +++ b/packages/cli/src/formatters/alert-channels.ts @@ -6,10 +6,9 @@ import { type DetailField, type ColumnDef, renderDetailFields, - renderTable, + renderAdaptiveTable, formatDate, truncateError, - truncateToWidth, } from './render.js' export interface AlertChannelPaginationInfo { @@ -103,22 +102,18 @@ function buildAlertChannelColumns (format: OutputFormat): ColumnDef 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 { @@ -209,13 +204,14 @@ function buildSubscriptionColumns ( const columns: ColumnDef[] = [ { 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 @@ -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 { @@ -293,13 +289,13 @@ function buildAlertNotificationColumns (format: OutputFormat): ColumnDef 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) }