mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-14 16:12:13 +00:00
feat: add cache rebuild flag and progress
This commit is contained in:
parent
1b8e0f8289
commit
2a9daec0ea
5 changed files with 160 additions and 36 deletions
73
src/cli.ts
73
src/cli.ts
|
|
@ -12,6 +12,7 @@ import { aggregateProjectsIntoDays, buildPeriodDataFromDays, dateKey } from './d
|
|||
import { CATEGORY_LABELS, type DateRange, type ProjectSummary, type TaskCategory } from './types.js'
|
||||
import { renderDashboard } from './dashboard.js'
|
||||
import { parseDateRangeFlags } from './cli-date.js'
|
||||
import { createTerminalProgressReporter } from './parse-progress.js'
|
||||
import { runOptimize, scanAndDetect } from './optimize.js'
|
||||
import { renderCompare } from './compare.js'
|
||||
import { getAllProviders } from './providers/index.js'
|
||||
|
|
@ -120,10 +121,14 @@ function toJsonPlanSummary(planUsage: PlanUsage): JsonPlanSummary {
|
|||
}
|
||||
}
|
||||
|
||||
async function runJsonReport(period: Period, provider: string, project: string[], exclude: string[]): Promise<void> {
|
||||
async function runJsonReport(period: Period, provider: string, project: string[], exclude: string[], noCache = false): Promise<void> {
|
||||
await loadPricing()
|
||||
const { range, label } = getDateRange(period)
|
||||
const projects = filterProjectsByName(await parseAllSessions(range, provider), project, exclude)
|
||||
const projects = filterProjectsByName(
|
||||
await parseAllSessions(range, provider, { noCache, progress: null }),
|
||||
project,
|
||||
exclude,
|
||||
)
|
||||
const report: ReturnType<typeof buildJsonReport> & { plan?: JsonPlanSummary } = buildJsonReport(projects, label, period)
|
||||
const planUsage = await getPlanUsageOrNull()
|
||||
if (planUsage) {
|
||||
|
|
@ -132,6 +137,17 @@ async function runJsonReport(period: Period, provider: string, project: string[]
|
|||
console.log(JSON.stringify(report, null, 2))
|
||||
}
|
||||
|
||||
function noCacheRequested(opts: { cache?: boolean }): boolean {
|
||||
return opts.cache === false
|
||||
}
|
||||
|
||||
function buildParseOptions(noCache: boolean, enableProgress: boolean) {
|
||||
return {
|
||||
noCache,
|
||||
progress: createTerminalProgressReporter(enableProgress),
|
||||
}
|
||||
}
|
||||
|
||||
const program = new Command()
|
||||
.name('codeburn')
|
||||
.description('See where your AI coding tokens go - by task, tool, model, and project')
|
||||
|
|
@ -288,8 +304,10 @@ program
|
|||
.option('--format <format>', 'Output format: tui, json', 'tui')
|
||||
.option('--project <name>', 'Show only projects matching name (repeatable)', collect, [])
|
||||
.option('--exclude <name>', 'Exclude projects matching name (repeatable)', collect, [])
|
||||
.option('--no-cache', 'Rebuild the parsed source cache for this run')
|
||||
.option('--refresh <seconds>', 'Auto-refresh interval in seconds (0 to disable)', parseInt, 30)
|
||||
.action(async (opts) => {
|
||||
const noCache = noCacheRequested(opts)
|
||||
let customRange: DateRange | null = null
|
||||
try {
|
||||
customRange = parseDateRangeFlags(opts.from, opts.to)
|
||||
|
|
@ -305,17 +323,17 @@ program
|
|||
if (customRange) {
|
||||
const label = `${opts.from ?? 'all'} to ${opts.to ?? 'today'}`
|
||||
const projects = filterProjectsByName(
|
||||
await parseAllSessions(customRange, opts.provider),
|
||||
await parseAllSessions(customRange, opts.provider, { noCache, progress: null }),
|
||||
opts.project,
|
||||
opts.exclude,
|
||||
)
|
||||
console.log(JSON.stringify(buildJsonReport(projects, label, 'custom'), null, 2))
|
||||
} else {
|
||||
await runJsonReport(period, opts.provider, opts.project, opts.exclude)
|
||||
await runJsonReport(period, opts.provider, opts.project, opts.exclude, noCache)
|
||||
}
|
||||
return
|
||||
}
|
||||
await renderDashboard(period, opts.provider, opts.refresh, opts.project, opts.exclude, customRange)
|
||||
await renderDashboard(period, opts.provider, opts.refresh, opts.project, opts.exclude, customRange, noCache)
|
||||
})
|
||||
|
||||
function buildPeriodData(label: string, projects: ProjectSummary[]): PeriodData {
|
||||
|
|
@ -367,8 +385,11 @@ program
|
|||
.option('--exclude <name>', 'Exclude projects matching name (repeatable)', collect, [])
|
||||
.option('--period <period>', 'Primary period for menubar-json: today, week, 30days, month, all', 'today')
|
||||
.option('--no-optimize', 'Skip optimize findings (menubar-json only, faster)')
|
||||
.option('--no-cache', 'Rebuild the parsed source cache for this run')
|
||||
.action(async (opts) => {
|
||||
await loadPricing()
|
||||
const noCache = noCacheRequested(opts)
|
||||
const parseOptions = buildParseOptions(noCache, opts.format === 'terminal')
|
||||
const pf = opts.provider
|
||||
const fp = (p: ProjectSummary[]) => filterProjectsByName(p, opts.project, opts.exclude)
|
||||
if (opts.format === 'menubar-json') {
|
||||
|
|
@ -403,7 +424,7 @@ program
|
|||
|
||||
if (gapStart.getTime() <= yesterdayEnd.getTime()) {
|
||||
const gapRange: DateRange = { start: gapStart, end: yesterdayEnd }
|
||||
const gapProjects = filterProjectsByName(await parseAllSessions(gapRange, 'all'), opts.project, opts.exclude)
|
||||
const gapProjects = filterProjectsByName(await parseAllSessions(gapRange, 'all', { noCache, progress: null }), opts.project, opts.exclude)
|
||||
const gapDays = aggregateProjectsIntoDays(gapProjects)
|
||||
c = addNewDays(c, gapDays, yesterdayStr)
|
||||
await saveDailyCache(c)
|
||||
|
|
@ -420,7 +441,7 @@ program
|
|||
|
||||
if (isAllProviders) {
|
||||
const todayRange: DateRange = { start: todayStart, end: now }
|
||||
const todayProjects = fp(await parseAllSessions(todayRange, 'all'))
|
||||
const todayProjects = fp(await parseAllSessions(todayRange, 'all', { noCache, progress: null }))
|
||||
const todayDays = aggregateProjectsIntoDays(todayProjects)
|
||||
const rangeStartStr = toDateString(periodInfo.range.start)
|
||||
const rangeEndStr = toDateString(periodInfo.range.end)
|
||||
|
|
@ -431,7 +452,7 @@ program
|
|||
scanProjects = todayProjects
|
||||
scanRange = todayRange
|
||||
} else {
|
||||
const projects = fp(await parseAllSessions(periodInfo.range, pf))
|
||||
const projects = fp(await parseAllSessions(periodInfo.range, pf, { noCache, progress: null }))
|
||||
currentData = buildPeriodData(periodInfo.label, projects)
|
||||
scanProjects = projects
|
||||
scanRange = periodInfo.range
|
||||
|
|
@ -445,7 +466,7 @@ program
|
|||
const providers: ProviderCost[] = []
|
||||
if (isAllProviders) {
|
||||
const todayRangeForProviders: DateRange = { start: todayStart, end: now }
|
||||
const todayDaysForProviders = aggregateProjectsIntoDays(fp(await parseAllSessions(todayRangeForProviders, 'all')))
|
||||
const todayDaysForProviders = aggregateProjectsIntoDays(fp(await parseAllSessions(todayRangeForProviders, 'all', { noCache, progress: null })))
|
||||
const rangeStartStr = toDateString(periodInfo.range.start)
|
||||
const allDaysForProviders = [
|
||||
...getDaysInRange(cache, rangeStartStr, yesterdayStr),
|
||||
|
|
@ -476,7 +497,7 @@ program
|
|||
// in the cache, so the filtered view shows zero tokens (heatmap/trend still works on cost).
|
||||
const historyStartStr = toDateString(new Date(todayStart.getTime() - BACKFILL_DAYS * MS_PER_DAY))
|
||||
const allCacheDays = getDaysInRange(cache, historyStartStr, yesterdayStr)
|
||||
const allTodayDaysForHistory = aggregateProjectsIntoDays(fp(await parseAllSessions({ start: todayStart, end: now }, 'all')))
|
||||
const allTodayDaysForHistory = aggregateProjectsIntoDays(fp(await parseAllSessions({ start: todayStart, end: now }, 'all', { noCache, progress: null })))
|
||||
const fullHistory = [...allCacheDays, ...allTodayDaysForHistory]
|
||||
const dailyHistory = fullHistory.map(d => {
|
||||
if (isAllProviders) {
|
||||
|
|
@ -521,8 +542,8 @@ program
|
|||
}
|
||||
|
||||
if (opts.format === 'json') {
|
||||
const todayData = buildPeriodData('today', fp(await parseAllSessions(getDateRange('today').range, pf)))
|
||||
const monthData = buildPeriodData('month', fp(await parseAllSessions(getDateRange('month').range, pf)))
|
||||
const todayData = buildPeriodData('today', fp(await parseAllSessions(getDateRange('today').range, pf, { noCache, progress: null })))
|
||||
const monthData = buildPeriodData('month', fp(await parseAllSessions(getDateRange('month').range, pf, { noCache, progress: null })))
|
||||
const { code, rate } = getCurrency()
|
||||
const payload: {
|
||||
currency: string
|
||||
|
|
@ -542,7 +563,7 @@ program
|
|||
return
|
||||
}
|
||||
|
||||
const monthProjects = fp(await parseAllSessions(getDateRange('month').range, pf))
|
||||
const monthProjects = fp(await parseAllSessions(getDateRange('month').range, pf, parseOptions))
|
||||
console.log(renderStatusBar(monthProjects))
|
||||
})
|
||||
|
||||
|
|
@ -553,13 +574,15 @@ program
|
|||
.option('--format <format>', 'Output format: tui, json', 'tui')
|
||||
.option('--project <name>', 'Show only projects matching name (repeatable)', collect, [])
|
||||
.option('--exclude <name>', 'Exclude projects matching name (repeatable)', collect, [])
|
||||
.option('--no-cache', 'Rebuild the parsed source cache for this run')
|
||||
.option('--refresh <seconds>', 'Auto-refresh interval in seconds (0 to disable)', parseInt, 30)
|
||||
.action(async (opts) => {
|
||||
const noCache = noCacheRequested(opts)
|
||||
if (opts.format === 'json') {
|
||||
await runJsonReport('today', opts.provider, opts.project, opts.exclude)
|
||||
await runJsonReport('today', opts.provider, opts.project, opts.exclude, noCache)
|
||||
return
|
||||
}
|
||||
await renderDashboard('today', opts.provider, opts.refresh, opts.project, opts.exclude)
|
||||
await renderDashboard('today', opts.provider, opts.refresh, opts.project, opts.exclude, null, noCache)
|
||||
})
|
||||
|
||||
program
|
||||
|
|
@ -569,13 +592,15 @@ program
|
|||
.option('--format <format>', 'Output format: tui, json', 'tui')
|
||||
.option('--project <name>', 'Show only projects matching name (repeatable)', collect, [])
|
||||
.option('--exclude <name>', 'Exclude projects matching name (repeatable)', collect, [])
|
||||
.option('--no-cache', 'Rebuild the parsed source cache for this run')
|
||||
.option('--refresh <seconds>', 'Auto-refresh interval in seconds (0 to disable)', parseInt, 30)
|
||||
.action(async (opts) => {
|
||||
const noCache = noCacheRequested(opts)
|
||||
if (opts.format === 'json') {
|
||||
await runJsonReport('month', opts.provider, opts.project, opts.exclude)
|
||||
await runJsonReport('month', opts.provider, opts.project, opts.exclude, noCache)
|
||||
return
|
||||
}
|
||||
await renderDashboard('month', opts.provider, opts.refresh, opts.project, opts.exclude)
|
||||
await renderDashboard('month', opts.provider, opts.refresh, opts.project, opts.exclude, null, noCache)
|
||||
})
|
||||
|
||||
program
|
||||
|
|
@ -586,14 +611,16 @@ program
|
|||
.option('--provider <provider>', 'Filter by provider: all, claude, codex, cursor', 'all')
|
||||
.option('--project <name>', 'Show only projects matching name (repeatable)', collect, [])
|
||||
.option('--exclude <name>', 'Exclude projects matching name (repeatable)', collect, [])
|
||||
.option('--no-cache', 'Rebuild the parsed source cache for this run')
|
||||
.action(async (opts) => {
|
||||
await loadPricing()
|
||||
const parseOptions = buildParseOptions(noCacheRequested(opts), true)
|
||||
const pf = opts.provider
|
||||
const fp = (p: ProjectSummary[]) => filterProjectsByName(p, opts.project, opts.exclude)
|
||||
const periods: PeriodExport[] = [
|
||||
{ label: 'Today', projects: fp(await parseAllSessions(getDateRange('today').range, pf)) },
|
||||
{ label: '7 Days', projects: fp(await parseAllSessions(getDateRange('week').range, pf)) },
|
||||
{ label: '30 Days', projects: fp(await parseAllSessions(getDateRange('30days').range, pf)) },
|
||||
{ label: 'Today', projects: fp(await parseAllSessions(getDateRange('today').range, pf, parseOptions)) },
|
||||
{ label: '7 Days', projects: fp(await parseAllSessions(getDateRange('week').range, pf, parseOptions)) },
|
||||
{ label: '30 Days', projects: fp(await parseAllSessions(getDateRange('30days').range, pf, parseOptions)) },
|
||||
]
|
||||
|
||||
if (periods.every(p => p.projects.length === 0)) {
|
||||
|
|
@ -813,10 +840,11 @@ program
|
|||
.description('Find token waste and get exact fixes')
|
||||
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all', '30days')
|
||||
.option('--provider <provider>', 'Filter by provider: all, claude, codex, cursor', 'all')
|
||||
.option('--no-cache', 'Rebuild the parsed source cache for this run')
|
||||
.action(async (opts) => {
|
||||
await loadPricing()
|
||||
const { range, label } = getDateRange(opts.period)
|
||||
const projects = await parseAllSessions(range, opts.provider)
|
||||
const projects = await parseAllSessions(range, opts.provider, buildParseOptions(noCacheRequested(opts), true))
|
||||
await runOptimize(projects, label, range)
|
||||
})
|
||||
|
||||
|
|
@ -825,10 +853,11 @@ program
|
|||
.description('Compare two AI models side-by-side')
|
||||
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all', 'all')
|
||||
.option('--provider <provider>', 'Filter by provider: all, claude, codex, cursor', 'all')
|
||||
.option('--no-cache', 'Rebuild the parsed source cache for this run')
|
||||
.action(async (opts) => {
|
||||
await loadPricing()
|
||||
const { range } = getDateRange(opts.period)
|
||||
await renderCompare(range, opts.provider)
|
||||
await renderCompare(range, opts.provider, noCacheRequested(opts))
|
||||
})
|
||||
|
||||
program.parse()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { ModelStats, ComparisonRow, CategoryComparison, WorkingStyleRow } f
|
|||
import { aggregateModelStats, computeComparison, computeCategoryComparison, computeWorkingStyle, scanSelfCorrections } from './compare-stats.js'
|
||||
import { formatCost } from './format.js'
|
||||
import { parseAllSessions } from './parser.js'
|
||||
import { createTerminalProgressReporter } from './parse-progress.js'
|
||||
import { getAllProviders } from './providers/index.js'
|
||||
import type { ProjectSummary, DateRange } from './types.js'
|
||||
|
||||
|
|
@ -441,14 +442,17 @@ export function CompareView({ projects, onBack }: CompareViewProps) {
|
|||
)
|
||||
}
|
||||
|
||||
export async function renderCompare(range: DateRange, provider: string): Promise<void> {
|
||||
export async function renderCompare(range: DateRange, provider: string, noCache = false): Promise<void> {
|
||||
const isTTY = process.stdin.isTTY && process.stdout.isTTY
|
||||
if (!isTTY) {
|
||||
process.stdout.write('Model comparison requires an interactive terminal.\n')
|
||||
return
|
||||
}
|
||||
|
||||
const projects = await parseAllSessions(range, provider)
|
||||
const projects = await parseAllSessions(range, provider, {
|
||||
noCache,
|
||||
progress: createTerminalProgressReporter(true),
|
||||
})
|
||||
const { waitUntilExit } = render(
|
||||
<CompareView projects={projects} onBack={() => process.exit(0)} />
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { getAllProviders } from './providers/index.js'
|
|||
import { scanAndDetect, type WasteFinding, type WasteAction, type OptimizeResult } from './optimize.js'
|
||||
import { estimateContextBudget, discoverProjectCwd, type ContextBudget } from './context-budget.js'
|
||||
import { dateKey } from './day-aggregator.js'
|
||||
import { createTerminalProgressReporter } from './parse-progress.js'
|
||||
import { CompareView } from './compare.js'
|
||||
import { getPlanUsageOrNull, type PlanUsage } from './plan-usage.js'
|
||||
import { planDisplayName } from './plans.js'
|
||||
|
|
@ -620,7 +621,7 @@ function DashboardContent({ projects, period, columns, activeProvider, budgets,
|
|||
)
|
||||
}
|
||||
|
||||
function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, initialPlanUsage, refreshSeconds, projectFilter, excludeFilter }: {
|
||||
function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, initialPlanUsage, refreshSeconds, projectFilter, excludeFilter, noCache }: {
|
||||
initialProjects: ProjectSummary[]
|
||||
initialPeriod: Period
|
||||
initialProvider: string
|
||||
|
|
@ -628,6 +629,7 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
|
|||
refreshSeconds?: number
|
||||
projectFilter?: string[]
|
||||
excludeFilter?: string[]
|
||||
noCache?: boolean
|
||||
}) {
|
||||
const { exit } = useApp()
|
||||
const [period, setPeriod] = useState<Period>(initialPeriod)
|
||||
|
|
@ -697,13 +699,14 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
|
|||
setOptimizeResult(null)
|
||||
try {
|
||||
const range = getDateRange(p)
|
||||
const data = await parseAllSessions(range, prov)
|
||||
const data = filterProjectsByName(
|
||||
await parseAllSessions(range, prov, { noCache: noCache ?? false, progress: null }),
|
||||
projectFilter,
|
||||
excludeFilter,
|
||||
)
|
||||
if (reloadGenerationRef.current !== generation) return
|
||||
|
||||
const filteredProjects = filterProjectsByName(data, projectFilter, excludeFilter)
|
||||
if (reloadGenerationRef.current !== generation) return
|
||||
|
||||
setProjects(filteredProjects)
|
||||
setProjects(data)
|
||||
const usage = await getPlanUsageOrNull()
|
||||
if (reloadGenerationRef.current !== generation) return
|
||||
setPlanUsage(usage ?? undefined)
|
||||
|
|
@ -714,7 +717,7 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
|
|||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}, [projectFilter, excludeFilter])
|
||||
}, [excludeFilter, noCache, projectFilter])
|
||||
|
||||
useEffect(() => {
|
||||
if (!refreshSeconds || refreshSeconds <= 0) return
|
||||
|
|
@ -799,15 +802,36 @@ function StaticDashboard({ projects, period, activeProvider, planUsage }: { proj
|
|||
)
|
||||
}
|
||||
|
||||
export async function renderDashboard(period: Period = 'week', provider: string = 'all', refreshSeconds?: number, projectFilter?: string[], excludeFilter?: string[], customRange?: DateRange | null): Promise<void> {
|
||||
export async function renderDashboard(
|
||||
period: Period = 'week',
|
||||
provider: string = 'all',
|
||||
refreshSeconds?: number,
|
||||
projectFilter?: string[],
|
||||
excludeFilter?: string[],
|
||||
customRange?: DateRange | null,
|
||||
noCache = false,
|
||||
): Promise<void> {
|
||||
await loadPricing()
|
||||
const range = customRange ?? getDateRange(period)
|
||||
const filteredProjects = filterProjectsByName(await parseAllSessions(range, provider), projectFilter, excludeFilter)
|
||||
const planUsage = await getPlanUsageOrNull()
|
||||
const isTTY = process.stdin.isTTY && process.stdout.isTTY
|
||||
const range = customRange ?? getDateRange(period)
|
||||
const filteredProjects = filterProjectsByName(
|
||||
await parseAllSessions(range, provider, { noCache, progress: createTerminalProgressReporter(isTTY) }),
|
||||
projectFilter,
|
||||
excludeFilter,
|
||||
)
|
||||
const planUsage = await getPlanUsageOrNull()
|
||||
if (isTTY) {
|
||||
const { waitUntilExit } = render(
|
||||
<InteractiveDashboard initialProjects={filteredProjects} initialPeriod={period} initialProvider={provider} initialPlanUsage={planUsage ?? undefined} refreshSeconds={refreshSeconds} projectFilter={projectFilter} excludeFilter={excludeFilter} />
|
||||
<InteractiveDashboard
|
||||
initialProjects={filteredProjects}
|
||||
initialPeriod={period}
|
||||
initialProvider={provider}
|
||||
initialPlanUsage={planUsage ?? undefined}
|
||||
refreshSeconds={refreshSeconds}
|
||||
projectFilter={projectFilter}
|
||||
excludeFilter={excludeFilter}
|
||||
noCache={noCache}
|
||||
/>
|
||||
)
|
||||
await waitUntilExit()
|
||||
} else {
|
||||
|
|
|
|||
42
src/parse-progress.ts
Normal file
42
src/parse-progress.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import type { SourceProgressReporter } from './parser.js'
|
||||
|
||||
export function createTerminalProgressReporter(
|
||||
enabled: boolean,
|
||||
stream: NodeJS.WriteStream = process.stderr,
|
||||
): SourceProgressReporter | null {
|
||||
if (!enabled || !stream.isTTY) return null
|
||||
|
||||
let total = 0
|
||||
let current = 0
|
||||
let lastLineLength = 0
|
||||
let active = false
|
||||
|
||||
function writeLine(line: string, done = false) {
|
||||
const pad = lastLineLength > line.length ? ' '.repeat(lastLineLength - line.length) : ''
|
||||
lastLineLength = Math.max(lastLineLength, line.length)
|
||||
stream.write(`${line}${pad}${done ? '\n' : '\r'}`)
|
||||
}
|
||||
|
||||
return {
|
||||
start(label: string, nextTotal: number) {
|
||||
total = nextTotal
|
||||
current = 0
|
||||
lastLineLength = 0
|
||||
active = nextTotal > 0
|
||||
if (active) writeLine(`${label} 0/${total}`)
|
||||
},
|
||||
advance(itemLabel: string) {
|
||||
if (!active) return
|
||||
current += 1
|
||||
writeLine(`Updating cache ${current}/${total}${itemLabel ? ` ${itemLabel}` : ''}`)
|
||||
},
|
||||
finish() {
|
||||
if (!active) return
|
||||
writeLine(`Updating cache ${current}/${total}`, true)
|
||||
active = false
|
||||
total = 0
|
||||
current = 0
|
||||
lastLineLength = 0
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue