File size: 5,214 Bytes
1da8cb0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import fs from "node:fs";
import path from "node:path";

interface RunLog {
  topic: string;
  startedAt: string;
  stages: { name: string; duration: number; status: string; error?: string }[];
  totalCost?: { estimated: number; actual?: number };
}

interface RunEntry {
  dir: string;
  topic: string;
  date: string;
  stages: { done: number; total: number };
  totalDuration: number;
  estimated: number | null;
  actual: number | null;
}

/**
 * Scan the output directory for log.json files and display aggregated cost usage.
 */
export function showUsageReport(outputDir: string): void {
  const resolved = path.resolve(outputDir);

  if (!fs.existsSync(resolved)) {
    console.log(`\nNo output directory found at: ${resolved}`);
    console.log("Run the pipeline first to generate output.\n");
    return;
  }

  const entries: RunEntry[] = [];
  let dirs: string[];
  try {
    dirs = fs.readdirSync(resolved).sort();
  } catch {
    console.log(`\nCannot read output directory: ${resolved}\n`);
    return;
  }

  for (const dir of dirs) {
    const logPath = path.join(resolved, dir, "log.json");
    if (!fs.existsSync(logPath)) continue;

    try {
      const raw = fs.readFileSync(logPath, "utf-8");
      const log: RunLog = JSON.parse(raw);

      const doneStages = log.stages.filter((s) => s.status === "done").length;
      const totalDuration = log.stages.reduce((sum, s) => sum + (s.duration ?? 0), 0);

      entries.push({
        dir,
        topic: log.topic,
        date: log.startedAt,
        stages: { done: doneStages, total: log.stages.length },
        totalDuration,
        estimated: log.totalCost?.estimated ?? null,
        actual: log.totalCost?.actual ?? null,
      });
    } catch {
      // Skip malformed log files
    }
  }

  if (entries.length === 0) {
    console.log(`\nNo run logs found in: ${resolved}`);
    console.log("Runs without log.json files (e.g. deleted or incomplete) are not included.\n");
    return;
  }

  // Aggregate totals
  const totalEstimated = entries.reduce((sum, e) => sum + (e.estimated ?? 0), 0);
  const totalActual = entries.reduce((sum, e) => sum + (e.actual ?? 0), 0);
  const runsWithActual = entries.filter((e) => e.actual !== null).length;
  const runsWithEstimate = entries.filter((e) => e.estimated !== null).length;

  const BOLD = "\x1b[1m";
  const DIM = "\x1b[2m";
  const RESET = "\x1b[0m";
  const CYAN = "\x1b[36m";
  const GREEN = "\x1b[32m";
  const YELLOW = "\x1b[33m";

  // Header
  console.log();
  console.log(`${BOLD}OpenReels Usage Report${RESET}`);
  console.log(`${DIM}${"─".repeat(60)}${RESET}`);
  console.log(`${DIM}Source: ${resolved}${RESET}`);
  console.log(`${DIM}Runs found: ${entries.length} (from log.json files in output directories)${RESET}`);
  console.log();

  // Summary box
  console.log(`${BOLD}  Total Cost Summary${RESET}`);
  console.log(`${DIM}  ${"─".repeat(40)}${RESET}`);
  if (runsWithActual > 0) {
    console.log(`  ${GREEN}Actual cost:${RESET}    ${BOLD}$${totalActual.toFixed(4)}${RESET}  ${DIM}(${runsWithActual} runs)${RESET}`);
  }
  if (runsWithEstimate > 0) {
    console.log(`  ${CYAN}Estimated cost:${RESET} ${BOLD}$${totalEstimated.toFixed(4)}${RESET}  ${DIM}(${runsWithEstimate} runs)${RESET}`);
  }
  if (runsWithActual === 0 && runsWithEstimate === 0) {
    console.log(`  ${DIM}No cost data recorded in any run.${RESET}`);
  }
  console.log();

  // Per-run table
  console.log(`${BOLD}  Run Details${RESET}`);
  console.log(`${DIM}  ${"─".repeat(40)}${RESET}`);

  for (const entry of entries) {
    const dateStr = formatDate(entry.date);
    const topicDisplay = truncate(entry.topic, 50);
    const stageStr = `${entry.stages.done}/${entry.stages.total} stages`;
    const durationStr = formatDuration(entry.totalDuration);

    console.log();
    console.log(`  ${BOLD}${topicDisplay}${RESET}`);
    console.log(`  ${DIM}${dateStr}  |  ${stageStr}  |  ${durationStr}${RESET}`);

    if (entry.actual !== null) {
      console.log(`  ${GREEN}Actual: $${entry.actual.toFixed(4)}${RESET}`);
    } else if (entry.estimated !== null) {
      console.log(`  ${CYAN}Estimated: $${entry.estimated.toFixed(4)}${RESET}  ${DIM}(pipeline did not complete)${RESET}`);
    } else {
      console.log(`  ${DIM}No cost data${RESET}`);
    }
  }

  // Footer
  console.log();
  console.log(`${DIM}${"─".repeat(60)}${RESET}`);
  console.log(`${YELLOW}Note:${RESET} This report is based on log.json files in the output directory.`);
  console.log(`If you delete a run folder, its cost data will no longer appear here.`);
  console.log();
}

function formatDate(iso: string): string {
  try {
    const d = new Date(iso);
    return d.toLocaleDateString("en-US", {
      year: "numeric",
      month: "short",
      day: "numeric",
      hour: "2-digit",
      minute: "2-digit",
    });
  } catch {
    return iso;
  }
}

function formatDuration(seconds: number): string {
  if (seconds < 60) return `${seconds.toFixed(1)}s`;
  const mins = Math.floor(seconds / 60);
  const secs = seconds % 60;
  return `${mins}m ${secs.toFixed(0)}s`;
}

function truncate(str: string, max: number): string {
  if (str.length <= max) return str;
  return str.slice(0, max - 1) + "…";
}