From fb8f25fb970ae53fcd3a5da4f6937645f4fd2b64 Mon Sep 17 00:00:00 2001 From: Resham Joshi <65915470+iamtoruk@users.noreply.github.com> Date: Wed, 6 May 2026 23:03:41 -0700 Subject: [PATCH] Reject invalid --format and --period values instead of silently falling back (#258) getDateRange() silently fell back to week on unknown periods, and no command validated --format. A typo like --period mounth or --format yaml produced wrong output with exit 0. Now all 6 format-accepting commands and all period-accepting paths reject unknown values with a clear message and exit 1. Also fixes the status description (said today+week+month, only shows today+month). --- src/cli-date.ts | 6 ++++-- src/cli.ts | 17 ++++++++++++++++- tests/cli-date.test.ts | 6 ++---- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/cli-date.ts b/src/cli-date.ts index a7b3202..250884b 100644 --- a/src/cli-date.ts +++ b/src/cli-date.ts @@ -137,8 +137,10 @@ export function getDateRange(period: string): { range: DateRange; label: string return { range: { start, end }, label: 'Last 6 months' } } default: { - const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7) - return { range: { start, end }, label: 'Last 7 Days' } + process.stderr.write( + `codeburn: unknown period "${period}". Valid values: today, week, 30days, month, all.\n` + ) + process.exit(1) } } } diff --git a/src/cli.ts b/src/cli.ts index df0f2bf..080bdbd 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -75,6 +75,15 @@ function toJsonPlanSummary(planUsage: PlanUsage): JsonPlanSummary { } } +function assertFormat(value: string, allowed: readonly string[], command: string): void { + if (!allowed.includes(value)) { + process.stderr.write( + `codeburn ${command}: unknown format "${value}". Valid values: ${allowed.join(', ')}.\n` + ) + process.exit(1) + } +} + async function runJsonReport(period: Period, provider: string, project: string[], exclude: string[]): Promise { await loadPricing() const { range, label } = getDateRange(period) @@ -273,6 +282,7 @@ program .option('--exclude ', 'Exclude projects matching name (repeatable)', collect, []) .option('--refresh ', 'Auto-refresh interval in seconds (0 to disable)', parseInteger, 30) .action(async (opts) => { + assertFormat(opts.format, ['tui', 'json'], 'report') let customRange: DateRange | null = null try { customRange = parseDateRangeFlags(opts.from, opts.to) @@ -346,7 +356,7 @@ function buildPeriodData(label: string, projects: ProjectSummary[]): PeriodData program .command('status') - .description('Compact status output (today + week + month)') + .description('Compact status output (today + month)') .option('--format ', 'Output format: terminal, menubar-json, json', 'terminal') .option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all') .option('--project ', 'Show only projects matching name (repeatable)', collect, []) @@ -354,6 +364,7 @@ program .option('--period ', 'Primary period for menubar-json: today, week, 30days, month, all', 'today') .option('--no-optimize', 'Skip optimize findings (menubar-json only, faster)') .action(async (opts) => { + assertFormat(opts.format, ['terminal', 'menubar-json', 'json'], 'status') await loadPricing() const pf = opts.provider const fp = (p: ProjectSummary[]) => filterProjectsByName(p, opts.project, opts.exclude) @@ -518,6 +529,7 @@ program .option('--exclude ', 'Exclude projects matching name (repeatable)', collect, []) .option('--refresh ', 'Auto-refresh interval in seconds (0 to disable)', parseInteger, 30) .action(async (opts) => { + assertFormat(opts.format, ['tui', 'json'], 'today') if (opts.format === 'json') { await runJsonReport('today', opts.provider, opts.project, opts.exclude) return @@ -535,6 +547,7 @@ program .option('--exclude ', 'Exclude projects matching name (repeatable)', collect, []) .option('--refresh ', 'Auto-refresh interval in seconds (0 to disable)', parseInteger, 30) .action(async (opts) => { + assertFormat(opts.format, ['tui', 'json'], 'month') if (opts.format === 'json') { await runJsonReport('month', opts.provider, opts.project, opts.exclude) return @@ -554,6 +567,7 @@ program .option('--project ', 'Show only projects matching name (repeatable)', collect, []) .option('--exclude ', 'Exclude projects matching name (repeatable)', collect, []) .action(async (opts) => { + assertFormat(opts.format, ['csv', 'json'], 'export') await loadPricing() await hydrateCache() const pf = opts.provider @@ -727,6 +741,7 @@ program .option('--provider ', 'Provider scope: all, claude, codex, cursor', 'all') .option('--reset-day ', 'Day of month plan resets (1-28)', parseInteger, 1) .action(async (action?: string, id?: string, opts?: { format?: string; monthlyUsd?: number; provider?: string; resetDay?: number }) => { + assertFormat(opts?.format ?? 'text', ['text', 'json'], 'plan') const mode = action ?? 'show' if (mode === 'show') { diff --git a/tests/cli-date.test.ts b/tests/cli-date.test.ts index 45578b7..296d292 100644 --- a/tests/cli-date.test.ts +++ b/tests/cli-date.test.ts @@ -81,10 +81,8 @@ describe('getDateRange', () => { expect(range.end.getHours()).toBe(23) }) - it('unknown period falls back to "week"', () => { - const fallback = getDateRange('not-a-period') - const week = getDateRange('week') - expect(fallback.label).toBe(week.label) + it('unknown period exits with an error instead of silently falling back', () => { + expect(() => getDateRange('not-a-period')).toThrow() }) })