|
1 | 1 | import { loadConfig } from "../config/config.js"; |
2 | 2 | import { info } from "../globals.js"; |
3 | 3 | import type { RuntimeEnv } from "../runtime.js"; |
| 4 | +import { |
| 5 | + listTaskAuditFindings, |
| 6 | + summarizeTaskAuditFindings, |
| 7 | + type TaskAuditCode, |
| 8 | + type TaskAuditFinding, |
| 9 | + type TaskAuditSeverity, |
| 10 | +} from "../tasks/task-registry.audit.js"; |
4 | 11 | import { cancelTaskById, getTaskById, updateTaskNotifyPolicyById } from "../tasks/task-registry.js"; |
5 | 12 | import { |
6 | 13 | reconcileInspectableTasks, |
@@ -89,6 +96,60 @@ function formatTaskListSummary(tasks: TaskRecord[]) { |
89 | 96 | return `${summary.byStatus.queued} queued · ${summary.byStatus.running} running · ${summary.failures} issues`; |
90 | 97 | } |
91 | 98 |
|
| 99 | +function formatAgeMs(ageMs: number | undefined): string { |
| 100 | + if (typeof ageMs !== "number" || ageMs < 1000) { |
| 101 | + return "fresh"; |
| 102 | + } |
| 103 | + const totalSeconds = Math.floor(ageMs / 1000); |
| 104 | + const days = Math.floor(totalSeconds / 86_400); |
| 105 | + const hours = Math.floor((totalSeconds % 86_400) / 3600); |
| 106 | + const minutes = Math.floor((totalSeconds % 3600) / 60); |
| 107 | + if (days > 0) { |
| 108 | + return `${days}d${hours}h`; |
| 109 | + } |
| 110 | + if (hours > 0) { |
| 111 | + return `${hours}h${minutes}m`; |
| 112 | + } |
| 113 | + if (minutes > 0) { |
| 114 | + return `${minutes}m`; |
| 115 | + } |
| 116 | + return `${totalSeconds}s`; |
| 117 | +} |
| 118 | + |
| 119 | +function formatAuditRows(findings: TaskAuditFinding[], rich: boolean) { |
| 120 | + const header = [ |
| 121 | + "Severity".padEnd(8), |
| 122 | + "Code".padEnd(22), |
| 123 | + "Task".padEnd(ID_PAD), |
| 124 | + "Status".padEnd(STATUS_PAD), |
| 125 | + "Age".padEnd(8), |
| 126 | + "Detail", |
| 127 | + ].join(" "); |
| 128 | + const lines = [rich ? theme.heading(header) : header]; |
| 129 | + for (const finding of findings) { |
| 130 | + const severity = finding.severity.padEnd(8); |
| 131 | + const status = formatTaskStatusCell(finding.task.status, rich); |
| 132 | + const severityCell = !rich |
| 133 | + ? severity |
| 134 | + : finding.severity === "error" |
| 135 | + ? theme.error(severity) |
| 136 | + : theme.warn(severity); |
| 137 | + lines.push( |
| 138 | + [ |
| 139 | + severityCell, |
| 140 | + finding.code.padEnd(22), |
| 141 | + shortToken(finding.task.taskId).padEnd(ID_PAD), |
| 142 | + status, |
| 143 | + formatAgeMs(finding.ageMs).padEnd(8), |
| 144 | + truncate(finding.detail, 88), |
| 145 | + ] |
| 146 | + .join(" ") |
| 147 | + .trimEnd(), |
| 148 | + ); |
| 149 | + } |
| 150 | + return lines; |
| 151 | +} |
| 152 | + |
92 | 153 | export async function tasksListCommand( |
93 | 154 | opts: { json?: boolean; runtime?: string; status?: string }, |
94 | 155 | runtime: RuntimeEnv, |
@@ -241,3 +302,77 @@ export async function tasksCancelCommand(opts: { lookup: string }, runtime: Runt |
241 | 302 | `Cancelled ${updated?.taskId ?? task.taskId} (${updated?.runtime ?? task.runtime})${updated?.runId ? ` run ${updated.runId}` : ""}.`, |
242 | 303 | ); |
243 | 304 | } |
| 305 | + |
| 306 | +export async function tasksAuditCommand( |
| 307 | + opts: { |
| 308 | + json?: boolean; |
| 309 | + severity?: TaskAuditSeverity; |
| 310 | + code?: TaskAuditCode; |
| 311 | + limit?: number; |
| 312 | + }, |
| 313 | + runtime: RuntimeEnv, |
| 314 | +) { |
| 315 | + const severityFilter = opts.severity?.trim() as TaskAuditSeverity | undefined; |
| 316 | + const codeFilter = opts.code?.trim() as TaskAuditCode | undefined; |
| 317 | + const allFindings = listTaskAuditFindings(); |
| 318 | + const findings = allFindings.filter((finding) => { |
| 319 | + if (severityFilter && finding.severity !== severityFilter) { |
| 320 | + return false; |
| 321 | + } |
| 322 | + if (codeFilter && finding.code !== codeFilter) { |
| 323 | + return false; |
| 324 | + } |
| 325 | + return true; |
| 326 | + }); |
| 327 | + const limit = typeof opts.limit === "number" && opts.limit > 0 ? opts.limit : undefined; |
| 328 | + const displayed = limit ? findings.slice(0, limit) : findings; |
| 329 | + const summary = summarizeTaskAuditFindings(allFindings); |
| 330 | + |
| 331 | + if (opts.json) { |
| 332 | + runtime.log( |
| 333 | + JSON.stringify( |
| 334 | + { |
| 335 | + count: allFindings.length, |
| 336 | + filteredCount: findings.length, |
| 337 | + displayed: displayed.length, |
| 338 | + filters: { |
| 339 | + severity: severityFilter ?? null, |
| 340 | + code: codeFilter ?? null, |
| 341 | + limit: limit ?? null, |
| 342 | + }, |
| 343 | + summary, |
| 344 | + findings: displayed, |
| 345 | + }, |
| 346 | + null, |
| 347 | + 2, |
| 348 | + ), |
| 349 | + ); |
| 350 | + return; |
| 351 | + } |
| 352 | + |
| 353 | + runtime.log( |
| 354 | + info( |
| 355 | + `Task audit: ${summary.total} findings · ${summary.errors} errors · ${summary.warnings} warnings`, |
| 356 | + ), |
| 357 | + ); |
| 358 | + if (severityFilter || codeFilter) { |
| 359 | + runtime.log(info(`Showing ${findings.length} matching findings.`)); |
| 360 | + } |
| 361 | + if (severityFilter) { |
| 362 | + runtime.log(info(`Severity filter: ${severityFilter}`)); |
| 363 | + } |
| 364 | + if (codeFilter) { |
| 365 | + runtime.log(info(`Code filter: ${codeFilter}`)); |
| 366 | + } |
| 367 | + if (limit) { |
| 368 | + runtime.log(info(`Limit: ${limit}`)); |
| 369 | + } |
| 370 | + if (displayed.length === 0) { |
| 371 | + runtime.log("No task audit findings."); |
| 372 | + return; |
| 373 | + } |
| 374 | + const rich = isRich(); |
| 375 | + for (const line of formatAuditRows(displayed, rich)) { |
| 376 | + runtime.log(line); |
| 377 | + } |
| 378 | +} |
0 commit comments