| 39 | import { applyImagePolicy } from "./image-policy"; |
| 40 | |
| 41 | class ProgressReporter { |
| 42 | private readonly quiet: boolean; |
| 43 | private readonly verbose: boolean; |
| 44 | private readonly stageStart = new Map<string, number>(); |
| 45 | private readonly totalStart: number; |
| 46 | constructor(opts: { quiet?: boolean; verbose?: boolean }) { |
| 47 | this.quiet = opts.quiet === true; |
| 48 | this.verbose = opts.verbose === true; |
| 49 | this.totalStart = Date.now(); |
| 50 | } |
| 51 | begin(stage: string): void { |
| 52 | this.stageStart.set(stage, Date.now()); |
| 53 | if (this.quiet) return; |
| 54 | process.stderr.write(`\r\x1b[K${stage}...`); |
| 55 | } |
| 56 | end(stage: string, extra?: string): void { |
| 57 | const start = this.stageStart.get(stage) ?? Date.now(); |
| 58 | const ms = Date.now() - start; |
| 59 | if (this.quiet) return; |
| 60 | if (this.verbose) { |
| 61 | process.stderr.write(`\r\x1b[K${stage} (${ms}ms)${extra ? ` — ${extra}` : ""}\n`); |
| 62 | } |
| 63 | } |
| 64 | done(extra: string): void { |
| 65 | if (this.quiet) return; |
| 66 | const total = ((Date.now() - this.totalStart) / 1000).toFixed(1); |
| 67 | process.stderr.write(`\r\x1b[KDone in ${total}s. ${extra}\n`); |
| 68 | } |
| 69 | fail(stage: string, err: Error): void { |
| 70 | if (!this.quiet) process.stderr.write("\r\x1b[K"); |
| 71 | // Always emit failure info, even in quiet mode — this is an error path. |
| 72 | process.stderr.write(`${stage} failed: ${err.message}\n`); |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | /** |
| 77 | * generate — full pipeline. Returns the output PDF path on success. |
nothing calls this directly
no outgoing calls
no test coverage detected