infra-cost 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1662 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/demo/test-enhanced-ui.ts
4
+ import chalk2 from "chalk";
5
+
6
+ // src/visualization/terminal-ui.ts
7
+ import Table from "cli-table3";
8
+ import chalk from "chalk";
9
+ import cliProgress from "cli-progress";
10
+ import moment from "moment";
11
+ var TerminalUIEngine = class {
12
+ constructor() {
13
+ this.progressBar = null;
14
+ }
15
+ /**
16
+ * Creates a formatted table with enhanced styling
17
+ */
18
+ createTable(columns, rows) {
19
+ const table = new Table({
20
+ head: columns.map((col) => chalk.bold(col.header)),
21
+ colWidths: columns.map((col) => col.width || 20),
22
+ colAligns: columns.map((col) => col.align || "left"),
23
+ style: {
24
+ head: [],
25
+ border: [],
26
+ compact: false
27
+ },
28
+ chars: {
29
+ "top": "\u2500",
30
+ "top-mid": "\u252C",
31
+ "top-left": "\u250C",
32
+ "top-right": "\u2510",
33
+ "bottom": "\u2500",
34
+ "bottom-mid": "\u2534",
35
+ "bottom-left": "\u2514",
36
+ "bottom-right": "\u2518",
37
+ "left": "\u2502",
38
+ "left-mid": "\u251C",
39
+ "mid": "\u2500",
40
+ "mid-mid": "\u253C",
41
+ "right": "\u2502",
42
+ "right-mid": "\u2524",
43
+ "middle": "\u2502"
44
+ }
45
+ });
46
+ rows.forEach((row) => {
47
+ const formattedRow = columns.map((col, index) => {
48
+ const value = Object.values(row)[index];
49
+ const colorKey = col.color;
50
+ if (colorKey && typeof value === "string") {
51
+ return chalk[colorKey](value);
52
+ }
53
+ return String(value);
54
+ });
55
+ table.push(formattedRow);
56
+ });
57
+ return table.toString();
58
+ }
59
+ /**
60
+ * Creates a cost breakdown table with rich formatting
61
+ * Optimized for large datasets with pagination and filtering
62
+ */
63
+ createCostTable(costBreakdown, options = {
64
+ showPercentages: true,
65
+ highlightTop: 5,
66
+ currency: "USD",
67
+ compact: false
68
+ }) {
69
+ const { totals, totalsByService } = costBreakdown;
70
+ let output = "\n" + chalk.bold.cyan("\u{1F4B0} Cost Analysis Summary") + "\n";
71
+ output += "\u2550".repeat(50) + "\n\n";
72
+ const summaryColumns = [
73
+ { header: "Period", width: 15, align: "left", color: "cyan" },
74
+ { header: "Cost", width: 15, align: "right", color: "yellow" },
75
+ { header: "Change", width: 20, align: "right" }
76
+ ];
77
+ const summaryRows = [
78
+ {
79
+ period: "Yesterday",
80
+ cost: this.formatCurrency(totals.yesterday, options.currency),
81
+ change: ""
82
+ },
83
+ {
84
+ period: "Last 7 Days",
85
+ cost: this.formatCurrency(totals.last7Days, options.currency),
86
+ change: this.calculateChange(totals.last7Days, totals.yesterday * 7)
87
+ },
88
+ {
89
+ period: "This Month",
90
+ cost: this.formatCurrency(totals.thisMonth, options.currency),
91
+ change: this.calculateChange(totals.thisMonth, totals.lastMonth)
92
+ },
93
+ {
94
+ period: "Last Month",
95
+ cost: this.formatCurrency(totals.lastMonth, options.currency),
96
+ change: ""
97
+ }
98
+ ];
99
+ output += this.createTable(summaryColumns, summaryRows) + "\n\n";
100
+ output += chalk.bold.cyan("\u{1F4CA} Service Breakdown (This Month)") + "\n";
101
+ output += "\u2550".repeat(50) + "\n\n";
102
+ const allServiceEntries = Object.entries(totalsByService.thisMonth);
103
+ const significantServices = allServiceEntries.filter(([_, cost]) => cost > 0.01).sort(([, a], [, b]) => b - a);
104
+ const maxDisplay = options.highlightTop || 15;
105
+ const serviceEntries = significantServices.slice(0, maxDisplay);
106
+ if (allServiceEntries.length > maxDisplay) {
107
+ const hiddenServices = allServiceEntries.length - maxDisplay;
108
+ const hiddenCost = significantServices.slice(maxDisplay).reduce((sum, [_, cost]) => sum + cost, 0);
109
+ if (hiddenCost > 0) {
110
+ output += chalk.gray(`Showing top ${maxDisplay} of ${allServiceEntries.length} services `) + chalk.gray(`(${hiddenServices} services with $${hiddenCost.toFixed(2)} hidden)
111
+
112
+ `);
113
+ }
114
+ }
115
+ const serviceColumns = [
116
+ { header: "Service", width: 25, align: "left", color: "blue" },
117
+ { header: "Cost", width: 15, align: "right", color: "yellow" },
118
+ { header: "Share", width: 10, align: "right" },
119
+ { header: "Trend", width: 15, align: "center" }
120
+ ];
121
+ const serviceRows = serviceEntries.map(([service, cost]) => {
122
+ const share = (cost / totals.thisMonth * 100).toFixed(1);
123
+ const lastMonthCost = totalsByService.lastMonth[service] || 0;
124
+ const trend = this.getTrendIndicator(cost, lastMonthCost);
125
+ return {
126
+ service,
127
+ cost: this.formatCurrency(cost, options.currency),
128
+ share: `${share}%`,
129
+ trend
130
+ };
131
+ });
132
+ output += this.createTable(serviceColumns, serviceRows);
133
+ return output;
134
+ }
135
+ /**
136
+ * Creates ASCII trend chart for cost visualization
137
+ */
138
+ createTrendChart(trendData, options = {
139
+ width: 60,
140
+ showLabels: true,
141
+ currency: "USD"
142
+ }) {
143
+ if (!trendData || trendData.length === 0) {
144
+ return chalk.red("No trend data available");
145
+ }
146
+ let output = "\n" + chalk.bold.cyan("\u{1F4C8} Cost Trend Analysis") + "\n";
147
+ output += "\u2550".repeat(50) + "\n\n";
148
+ const maxCost = Math.max(...trendData.map((d) => d.actualCost));
149
+ const minCost = Math.min(...trendData.map((d) => d.actualCost));
150
+ const range = maxCost - minCost;
151
+ trendData.forEach((data, index) => {
152
+ const normalizedValue = range > 0 ? (data.actualCost - minCost) / range : 0.5;
153
+ const barLength = Math.round(normalizedValue * options.width);
154
+ const bar = "\u2588".repeat(barLength) + "\u2591".repeat(options.width - barLength);
155
+ const coloredBar = this.colorizeBar(bar, normalizedValue, options.colorThreshold);
156
+ const period = moment(data.period).format("MMM YYYY");
157
+ const cost = this.formatCurrency(data.actualCost, options.currency);
158
+ const change = data.changeFromPrevious ? this.formatChangeIndicator(data.changeFromPrevious.percentage) : "";
159
+ output += `${period.padEnd(10)} ${coloredBar} ${cost.padStart(10)} ${change}
160
+ `;
161
+ });
162
+ output += "\n" + "\u2500".repeat(options.width + 25) + "\n";
163
+ output += `Range: ${this.formatCurrency(minCost, options.currency)} - ${this.formatCurrency(maxCost, options.currency)}
164
+ `;
165
+ return output;
166
+ }
167
+ /**
168
+ * Creates a progress bar for long-running operations
169
+ */
170
+ startProgress(label, total = 100) {
171
+ if (this.progressBar) {
172
+ this.progressBar.stop();
173
+ }
174
+ this.progressBar = new cliProgress.SingleBar({
175
+ format: `${chalk.cyan(label)} ${chalk.cyan("[")}${chalk.yellow("{bar}")}${chalk.cyan("]")} {percentage}% | ETA: {eta}s | {value}/{total}`,
176
+ barCompleteChar: "\u2588",
177
+ barIncompleteChar: "\u2591",
178
+ hideCursor: true
179
+ });
180
+ this.progressBar.start(total, 0);
181
+ }
182
+ /**
183
+ * Updates progress bar
184
+ */
185
+ updateProgress(value, payload) {
186
+ if (this.progressBar) {
187
+ this.progressBar.update(value, payload);
188
+ }
189
+ }
190
+ /**
191
+ * Stops and clears progress bar
192
+ */
193
+ stopProgress() {
194
+ if (this.progressBar) {
195
+ this.progressBar.stop();
196
+ this.progressBar = null;
197
+ }
198
+ }
199
+ /**
200
+ * Creates a cost anomaly alert box
201
+ */
202
+ createAnomalyAlert(anomalies) {
203
+ if (anomalies.length === 0) {
204
+ return chalk.green("\u2705 No cost anomalies detected");
205
+ }
206
+ let output = "\n" + chalk.bold.red("\u{1F6A8} Cost Anomalies Detected") + "\n";
207
+ output += "\u2550".repeat(50) + "\n\n";
208
+ anomalies.forEach((anomaly) => {
209
+ const severityColor = this.getSeverityColor(anomaly.severity);
210
+ const icon = this.getSeverityIcon(anomaly.severity);
211
+ output += chalk[severityColor](`${icon} ${anomaly.date}
212
+ `);
213
+ output += ` Expected: ${this.formatCurrency(anomaly.expectedCost, "USD")}
214
+ `;
215
+ output += ` Actual: ${this.formatCurrency(anomaly.actualCost, "USD")}
216
+ `;
217
+ output += ` Deviation: ${anomaly.deviation > 0 ? "+" : ""}${anomaly.deviation.toFixed(1)}%
218
+ `;
219
+ if (anomaly.description) {
220
+ output += ` ${chalk.gray(anomaly.description)}
221
+ `;
222
+ }
223
+ output += "\n";
224
+ });
225
+ return output;
226
+ }
227
+ /**
228
+ * Creates a fancy header with branding
229
+ */
230
+ createHeader(title, subtitle) {
231
+ const width = 60;
232
+ let output = "\n";
233
+ output += chalk.cyan("\u250C" + "\u2500".repeat(width - 2) + "\u2510") + "\n";
234
+ const titlePadding = Math.floor((width - title.length - 4) / 2);
235
+ output += chalk.cyan("\u2502") + " ".repeat(titlePadding) + chalk.bold.white(title) + " ".repeat(width - title.length - titlePadding - 2) + chalk.cyan("\u2502") + "\n";
236
+ if (subtitle) {
237
+ const subtitlePadding = Math.floor((width - subtitle.length - 4) / 2);
238
+ output += chalk.cyan("\u2502") + " ".repeat(subtitlePadding) + chalk.gray(subtitle) + " ".repeat(width - subtitle.length - subtitlePadding - 2) + chalk.cyan("\u2502") + "\n";
239
+ }
240
+ output += chalk.cyan("\u2514" + "\u2500".repeat(width - 2) + "\u2518") + "\n\n";
241
+ return output;
242
+ }
243
+ // Helper methods
244
+ formatCurrency(amount, currency) {
245
+ return new Intl.NumberFormat("en-US", {
246
+ style: "currency",
247
+ currency,
248
+ minimumFractionDigits: 2,
249
+ maximumFractionDigits: 2
250
+ }).format(amount);
251
+ }
252
+ calculateChange(current, previous) {
253
+ if (previous === 0)
254
+ return "";
255
+ const change = (current - previous) / previous * 100;
256
+ const changeStr = `${change >= 0 ? "+" : ""}${change.toFixed(1)}%`;
257
+ if (change > 0) {
258
+ return chalk.red(`\u2197 ${changeStr}`);
259
+ } else if (change < 0) {
260
+ return chalk.green(`\u2198 ${changeStr}`);
261
+ } else {
262
+ return chalk.gray("\u2192 0.0%");
263
+ }
264
+ }
265
+ getTrendIndicator(current, previous) {
266
+ if (previous === 0)
267
+ return chalk.gray("\u2500");
268
+ const change = (current - previous) / previous * 100;
269
+ if (change > 10)
270
+ return chalk.red("\u2197\u2197");
271
+ if (change > 0)
272
+ return chalk.yellow("\u2197");
273
+ if (change < -10)
274
+ return chalk.green("\u2198\u2198");
275
+ if (change < 0)
276
+ return chalk.green("\u2198");
277
+ return chalk.gray("\u2192");
278
+ }
279
+ colorizeBar(bar, normalizedValue, threshold) {
280
+ const thresholdValue = threshold || 0.7;
281
+ if (normalizedValue > thresholdValue) {
282
+ return chalk.red(bar);
283
+ } else if (normalizedValue > 0.4) {
284
+ return chalk.yellow(bar);
285
+ } else {
286
+ return chalk.green(bar);
287
+ }
288
+ }
289
+ formatChangeIndicator(percentage) {
290
+ const indicator = percentage >= 0 ? "\u2197" : "\u2198";
291
+ const color = percentage >= 0 ? "red" : "green";
292
+ const sign = percentage >= 0 ? "+" : "";
293
+ return chalk[color](`${indicator} ${sign}${percentage.toFixed(1)}%`);
294
+ }
295
+ getSeverityColor(severity) {
296
+ switch (severity.toLowerCase()) {
297
+ case "critical":
298
+ return "red";
299
+ case "high":
300
+ return "red";
301
+ case "medium":
302
+ return "yellow";
303
+ case "low":
304
+ return "cyan";
305
+ default:
306
+ return "gray";
307
+ }
308
+ }
309
+ getSeverityIcon(severity) {
310
+ switch (severity.toLowerCase()) {
311
+ case "critical":
312
+ return "\u{1F534}";
313
+ case "high":
314
+ return "\u{1F7E0}";
315
+ case "medium":
316
+ return "\u{1F7E1}";
317
+ case "low":
318
+ return "\u{1F535}";
319
+ default:
320
+ return "\u26AA";
321
+ }
322
+ }
323
+ };
324
+
325
+ // src/demo/demo-data-generator.ts
326
+ var DemoDataGenerator = class {
327
+ /**
328
+ * Generate sample account information
329
+ */
330
+ static generateAccountInfo() {
331
+ return {
332
+ id: "123456789012",
333
+ name: "Demo Production Account",
334
+ provider: "aws" /* AWS */
335
+ };
336
+ }
337
+ /**
338
+ * Generate realistic cost breakdown data
339
+ */
340
+ static generateCostBreakdown() {
341
+ const services = [
342
+ "Amazon EC2-Instance",
343
+ "Amazon S3",
344
+ "Amazon RDS",
345
+ "AWS Lambda",
346
+ "Amazon CloudFront",
347
+ "Amazon EBS",
348
+ "Amazon VPC",
349
+ "Amazon Route 53",
350
+ "AWS Application Load Balancer",
351
+ "Amazon ElastiCache",
352
+ "Amazon SES",
353
+ "Amazon CloudWatch",
354
+ "AWS Data Transfer",
355
+ "Amazon DynamoDB"
356
+ ];
357
+ const generateServiceCosts = (baseMultiplier) => {
358
+ const costs = {};
359
+ services.forEach((service) => {
360
+ const baseCost = Math.random() * 1e3 * baseMultiplier;
361
+ const serviceMultiplier = this.getServiceMultiplier(service);
362
+ costs[service] = Math.round(baseCost * serviceMultiplier * 100) / 100;
363
+ });
364
+ return costs;
365
+ };
366
+ const lastMonth = generateServiceCosts(1);
367
+ const thisMonth = generateServiceCosts(0.85);
368
+ const last7Days = generateServiceCosts(0.2);
369
+ const yesterday = generateServiceCosts(0.03);
370
+ return {
371
+ totals: {
372
+ lastMonth: Object.values(lastMonth).reduce((sum, cost) => sum + cost, 0),
373
+ thisMonth: Object.values(thisMonth).reduce((sum, cost) => sum + cost, 0),
374
+ last7Days: Object.values(last7Days).reduce((sum, cost) => sum + cost, 0),
375
+ yesterday: Object.values(yesterday).reduce((sum, cost) => sum + cost, 0)
376
+ },
377
+ totalsByService: {
378
+ lastMonth,
379
+ thisMonth,
380
+ last7Days,
381
+ yesterday
382
+ }
383
+ };
384
+ }
385
+ /**
386
+ * Generate trend analysis data for the last 6 months
387
+ */
388
+ static generateTrendAnalysis() {
389
+ var _a;
390
+ const months = 6;
391
+ const trendData = [];
392
+ for (let i = months - 1; i >= 0; i--) {
393
+ const date = /* @__PURE__ */ new Date();
394
+ date.setMonth(date.getMonth() - i);
395
+ const baselineCost = 3500;
396
+ const seasonalFactor = 1 + 0.2 * Math.sin(date.getMonth() / 12 * 2 * Math.PI);
397
+ const trendFactor = 1 + (months - i - 1) * 0.05;
398
+ const randomVariation = 0.9 + Math.random() * 0.2;
399
+ const actualCost = baselineCost * seasonalFactor * trendFactor * randomVariation;
400
+ const previousCost = i === months - 1 ? actualCost * 0.95 : ((_a = trendData[i - 1]) == null ? void 0 : _a.actualCost) || actualCost;
401
+ trendData.push({
402
+ period: date.toISOString().substring(0, 7),
403
+ // YYYY-MM format
404
+ actualCost: Math.round(actualCost * 100) / 100,
405
+ forecastedCost: actualCost * (0.95 + Math.random() * 0.1),
406
+ budgetLimit: 4e3,
407
+ previousPeriodCost: previousCost,
408
+ changeFromPrevious: {
409
+ amount: actualCost - previousCost,
410
+ percentage: (actualCost - previousCost) / previousCost * 100
411
+ }
412
+ });
413
+ }
414
+ const topServices = [
415
+ { serviceName: "Amazon EC2-Instance", cost: 1250.8, percentage: 32.1, trend: "INCREASING" },
416
+ { serviceName: "Amazon RDS", cost: 890.45, percentage: 22.9, trend: "STABLE" },
417
+ { serviceName: "Amazon S3", cost: 420.3, percentage: 10.8, trend: "DECREASING" },
418
+ { serviceName: "AWS Lambda", cost: 380.7, percentage: 9.8, trend: "INCREASING" },
419
+ { serviceName: "Amazon CloudFront", cost: 220.15, percentage: 5.7, trend: "STABLE" }
420
+ ];
421
+ const costAnomalies = [
422
+ {
423
+ date: new Date(Date.now() - 5 * 24 * 60 * 60 * 1e3).toISOString().substring(0, 10),
424
+ actualCost: 4850.3,
425
+ expectedCost: 3200.5,
426
+ deviation: 51.6,
427
+ severity: "HIGH",
428
+ possibleCause: "EC2 instance scale-out event",
429
+ description: "Unusual spike in EC2 costs detected during weekend scaling event"
430
+ },
431
+ {
432
+ date: new Date(Date.now() - 12 * 24 * 60 * 60 * 1e3).toISOString().substring(0, 10),
433
+ actualCost: 2100.8,
434
+ expectedCost: 3100.2,
435
+ deviation: -32.2,
436
+ severity: "MEDIUM",
437
+ possibleCause: "Scheduled maintenance window",
438
+ description: "Lower than expected costs during planned maintenance"
439
+ }
440
+ ];
441
+ return {
442
+ provider: "aws" /* AWS */,
443
+ timeRange: {
444
+ start: trendData[0].period + "-01",
445
+ end: trendData[trendData.length - 1].period + "-28"
446
+ },
447
+ granularity: "MONTHLY",
448
+ trendData,
449
+ totalCost: trendData.reduce((sum, data) => sum + data.actualCost, 0),
450
+ averageDailyCost: trendData[trendData.length - 1].actualCost / 30,
451
+ projectedMonthlyCost: trendData[trendData.length - 1].actualCost * 1.05,
452
+ avgMonthOverMonthGrowth: 5.2,
453
+ topServices,
454
+ costAnomalies,
455
+ analytics: {
456
+ insights: [
457
+ "EC2 costs have increased by 15% over the last 3 months due to increased workload",
458
+ "S3 storage costs decreased by 8% after implementing lifecycle policies",
459
+ "Lambda usage shows steady growth pattern aligned with application scaling",
460
+ "RDS costs remain stable with good resource utilization",
461
+ "CloudFront costs show seasonal variation with higher usage in Q4"
462
+ ],
463
+ recommendations: [
464
+ "Consider Reserved Instances for EC2 to reduce costs by up to 30%",
465
+ "Implement automated S3 lifecycle management for additional savings",
466
+ "Review Lambda memory allocation for cost optimization",
467
+ "Consider RDS instance rightsizing based on utilization metrics"
468
+ ],
469
+ volatilityScore: 0.15,
470
+ trendStrength: 0.72
471
+ }
472
+ };
473
+ }
474
+ /**
475
+ * Generate realistic FinOps recommendations
476
+ */
477
+ static generateRecommendations() {
478
+ return [
479
+ {
480
+ id: "rec-001",
481
+ type: "COST_OPTIMIZATION",
482
+ title: "Purchase EC2 Reserved Instances",
483
+ description: "Based on your consistent EC2 usage patterns, purchasing Reserved Instances could save up to 30% on compute costs.",
484
+ potentialSavings: {
485
+ amount: 450.3,
486
+ percentage: 30,
487
+ timeframe: "MONTHLY"
488
+ },
489
+ effort: "LOW",
490
+ priority: "HIGH",
491
+ resources: ["i-1234567890abcdef0", "i-0987654321fedcba0"],
492
+ implementationSteps: [
493
+ "Analyze current EC2 usage patterns over the last 12 months",
494
+ "Purchase 1-year Reserved Instances for consistent workloads",
495
+ "Set up automated monitoring for RI utilization",
496
+ "Review and optimize instance sizing before purchasing RIs"
497
+ ],
498
+ tags: ["ec2", "reserved-instances", "cost-optimization"]
499
+ },
500
+ {
501
+ id: "rec-002",
502
+ type: "RESOURCE_RIGHTSIZING",
503
+ title: "Rightsize RDS Instances",
504
+ description: "Several RDS instances show low CPU utilization. Downsizing these instances could reduce costs without impacting performance.",
505
+ potentialSavings: {
506
+ amount: 280.75,
507
+ percentage: 25,
508
+ timeframe: "MONTHLY"
509
+ },
510
+ effort: "MEDIUM",
511
+ priority: "MEDIUM",
512
+ resources: ["mydb-instance-1", "mydb-instance-2"],
513
+ implementationSteps: [
514
+ "Review RDS CloudWatch metrics for CPU, memory, and I/O utilization",
515
+ "Create RDS snapshots before making changes",
516
+ "Modify instance class during maintenance window",
517
+ "Monitor performance after changes for 1 week"
518
+ ],
519
+ tags: ["rds", "rightsizing", "database-optimization"]
520
+ },
521
+ {
522
+ id: "rec-003",
523
+ type: "ARCHITECTURE",
524
+ title: "Implement S3 Intelligent Tiering",
525
+ description: "Enable S3 Intelligent Tiering to automatically optimize storage costs based on access patterns.",
526
+ potentialSavings: {
527
+ amount: 125.5,
528
+ percentage: 15,
529
+ timeframe: "MONTHLY"
530
+ },
531
+ effort: "LOW",
532
+ priority: "MEDIUM",
533
+ resources: ["s3-bucket-logs", "s3-bucket-backups"],
534
+ implementationSteps: [
535
+ "Enable S3 Intelligent Tiering on identified buckets",
536
+ "Set up lifecycle policies for automatic transitions",
537
+ "Monitor cost impact over 3 months",
538
+ "Review and adjust policies based on access patterns"
539
+ ],
540
+ tags: ["s3", "storage-optimization", "intelligent-tiering"]
541
+ },
542
+ {
543
+ id: "rec-004",
544
+ type: "COST_OPTIMIZATION",
545
+ title: "Optimize Lambda Memory Allocation",
546
+ description: "Lambda functions are over-provisioned with memory. Optimizing memory allocation can reduce costs significantly.",
547
+ potentialSavings: {
548
+ amount: 95.8,
549
+ percentage: 20,
550
+ timeframe: "MONTHLY"
551
+ },
552
+ effort: "HIGH",
553
+ priority: "LOW",
554
+ resources: ["process-images-lambda", "data-processor-lambda"],
555
+ implementationSteps: [
556
+ "Use AWS Lambda Power Tuning tool to find optimal memory settings",
557
+ "Test performance with different memory allocations",
558
+ "Update Lambda configurations incrementally",
559
+ "Set up monitoring for execution duration and cost metrics"
560
+ ],
561
+ tags: ["lambda", "serverless-optimization", "memory-tuning"]
562
+ }
563
+ ];
564
+ }
565
+ /**
566
+ * Generate sample resource inventory
567
+ */
568
+ static generateResourceInventory() {
569
+ return {
570
+ provider: "aws" /* AWS */,
571
+ region: "us-east-1",
572
+ totalResources: 145,
573
+ resourcesByType: {
574
+ ["compute" /* COMPUTE */]: 35,
575
+ ["storage" /* STORAGE */]: 28,
576
+ ["database" /* DATABASE */]: 12,
577
+ ["network" /* NETWORK */]: 25,
578
+ ["security" /* SECURITY */]: 18,
579
+ ["serverless" /* SERVERLESS */]: 15,
580
+ ["container" /* CONTAINER */]: 8,
581
+ ["analytics" /* ANALYTICS */]: 4
582
+ },
583
+ totalCost: 3890.5,
584
+ resources: {
585
+ compute: [],
586
+ storage: [],
587
+ database: [],
588
+ network: []
589
+ },
590
+ lastUpdated: /* @__PURE__ */ new Date()
591
+ };
592
+ }
593
+ /**
594
+ * Generate cost anomalies for testing
595
+ */
596
+ static generateCostAnomalies() {
597
+ return [
598
+ {
599
+ date: new Date(Date.now() - 2 * 24 * 60 * 60 * 1e3).toISOString().substring(0, 10),
600
+ actualCost: 5200.8,
601
+ expectedCost: 3500.2,
602
+ deviation: 48.6,
603
+ severity: "CRITICAL",
604
+ description: "Significant cost spike detected in EC2 Auto Scaling group due to unexpected traffic surge"
605
+ },
606
+ {
607
+ date: new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3).toISOString().substring(0, 10),
608
+ actualCost: 2800.3,
609
+ expectedCost: 3200.5,
610
+ deviation: -12.5,
611
+ severity: "LOW",
612
+ description: "Lower than expected S3 costs due to successful data archiving policies"
613
+ }
614
+ ];
615
+ }
616
+ /**
617
+ * Helper method to generate realistic service cost multipliers
618
+ */
619
+ static getServiceMultiplier(serviceName) {
620
+ const multipliers = {
621
+ "Amazon EC2-Instance": 2.5,
622
+ "Amazon RDS": 1.8,
623
+ "Amazon S3": 1,
624
+ "AWS Lambda": 0.8,
625
+ "Amazon CloudFront": 0.6,
626
+ "Amazon EBS": 0.9,
627
+ "Amazon VPC": 0.3,
628
+ "Amazon Route 53": 0.2,
629
+ "AWS Application Load Balancer": 0.4,
630
+ "Amazon ElastiCache": 0.7,
631
+ "Amazon SES": 0.1,
632
+ "Amazon CloudWatch": 0.3,
633
+ "AWS Data Transfer": 0.5,
634
+ "Amazon DynamoDB": 0.6
635
+ };
636
+ return multipliers[serviceName] || 0.5;
637
+ }
638
+ };
639
+
640
+ // src/exporters/pdf-exporter.ts
641
+ import puppeteer from "puppeteer";
642
+ import { existsSync, mkdirSync } from "fs";
643
+ import { join } from "path";
644
+ import moment2 from "moment";
645
+ var PDFExporter = class {
646
+ constructor(options = {}) {
647
+ this.browser = null;
648
+ this.options = {
649
+ outputPath: "./reports",
650
+ includeCharts: true,
651
+ includeSummary: true,
652
+ includeDetails: true,
653
+ includeRecommendations: true,
654
+ format: "A4",
655
+ orientation: "portrait",
656
+ theme: "light",
657
+ ...options
658
+ };
659
+ }
660
+ /**
661
+ * Generate comprehensive audit report PDF
662
+ */
663
+ async generateAuditReport(data) {
664
+ try {
665
+ await this.initializeBrowser();
666
+ const html = this.generateAuditHTML(data);
667
+ const filename = this.options.filename || `infra-cost-audit-${moment2(data.generatedAt).format("YYYY-MM-DD-HHmm")}.pdf`;
668
+ const outputPath = this.ensureOutputDirectory();
669
+ const fullPath = join(outputPath, filename);
670
+ const page = await this.browser.newPage();
671
+ await page.setContent(html, { waitUntil: "networkidle0" });
672
+ await page.pdf({
673
+ path: fullPath,
674
+ format: this.options.format,
675
+ orientation: this.options.orientation,
676
+ printBackground: true,
677
+ margin: {
678
+ top: "1in",
679
+ bottom: "1in",
680
+ left: "0.8in",
681
+ right: "0.8in"
682
+ },
683
+ displayHeaderFooter: true,
684
+ headerTemplate: this.generateHeaderTemplate(data.accountInfo),
685
+ footerTemplate: this.generateFooterTemplate()
686
+ });
687
+ await page.close();
688
+ await this.closeBrowser();
689
+ return fullPath;
690
+ } catch (error) {
691
+ await this.closeBrowser();
692
+ throw new Error(`PDF generation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
693
+ }
694
+ }
695
+ /**
696
+ * Generate cost trend report PDF
697
+ */
698
+ async generateTrendReport(accountInfo, trendAnalysis) {
699
+ var _a, _b;
700
+ const data = {
701
+ accountInfo,
702
+ costBreakdown: {
703
+ totals: {
704
+ lastMonth: 0,
705
+ thisMonth: 0,
706
+ last7Days: 0,
707
+ yesterday: 0
708
+ },
709
+ totalsByService: {
710
+ lastMonth: {},
711
+ thisMonth: {},
712
+ last7Days: {},
713
+ yesterday: {}
714
+ }
715
+ },
716
+ trendAnalysis,
717
+ generatedAt: /* @__PURE__ */ new Date(),
718
+ reportPeriod: {
719
+ start: new Date(((_a = trendAnalysis.timeRange) == null ? void 0 : _a.start) || Date.now() - 6 * 30 * 24 * 60 * 60 * 1e3),
720
+ end: new Date(((_b = trendAnalysis.timeRange) == null ? void 0 : _b.end) || Date.now())
721
+ }
722
+ };
723
+ return this.generateAuditReport(data);
724
+ }
725
+ /**
726
+ * Generate executive summary PDF
727
+ */
728
+ async generateExecutiveSummary(data) {
729
+ this.options.includeDetails = false;
730
+ this.options.includeCharts = true;
731
+ this.options.includeSummary = true;
732
+ this.options.includeRecommendations = true;
733
+ const filename = `infra-cost-executive-summary-${moment2(data.generatedAt).format("YYYY-MM-DD")}.pdf`;
734
+ this.options.filename = filename;
735
+ return this.generateAuditReport(data);
736
+ }
737
+ generateAuditHTML(data) {
738
+ const theme = this.getThemeStyles();
739
+ return `
740
+ <!DOCTYPE html>
741
+ <html>
742
+ <head>
743
+ <meta charset="UTF-8">
744
+ <title>Infrastructure Cost Audit Report</title>
745
+ <style>
746
+ ${theme}
747
+ ${this.getBaseStyles()}
748
+ </style>
749
+ </head>
750
+ <body>
751
+ <div class="report-container">
752
+ ${this.generateCoverPage(data)}
753
+ ${this.options.includeSummary ? this.generateExecutiveSummary(data) : ""}
754
+ ${this.generateCostBreakdownSection(data.costBreakdown)}
755
+ ${data.trendAnalysis && this.options.includeCharts ? this.generateTrendAnalysisSection(data.trendAnalysis) : ""}
756
+ ${data.resourceInventory && this.options.includeDetails ? this.generateResourceInventorySection(data.resourceInventory) : ""}
757
+ ${data.anomalies && data.anomalies.length > 0 ? this.generateAnomaliesSection(data.anomalies) : ""}
758
+ ${data.recommendations && this.options.includeRecommendations ? this.generateRecommendationsSection(data.recommendations) : ""}
759
+ ${this.generateAppendixSection(data)}
760
+ </div>
761
+ </body>
762
+ </html>`;
763
+ }
764
+ generateCoverPage(data) {
765
+ return `
766
+ <div class="cover-page">
767
+ <div class="header">
768
+ <h1>Infrastructure Cost Analysis Report</h1>
769
+ <div class="subtitle">Comprehensive Cloud Cost Audit & Optimization</div>
770
+ </div>
771
+
772
+ <div class="account-info">
773
+ <h2>Account Information</h2>
774
+ <div class="info-grid">
775
+ <div class="info-item">
776
+ <strong>Account ID:</strong> ${data.accountInfo.id}
777
+ </div>
778
+ <div class="info-item">
779
+ <strong>Account Name:</strong> ${data.accountInfo.name || "N/A"}
780
+ </div>
781
+ <div class="info-item">
782
+ <strong>Cloud Provider:</strong> ${this.getProviderDisplayName(data.accountInfo.provider)}
783
+ </div>
784
+ <div class="info-item">
785
+ <strong>Report Generated:</strong> ${moment2(data.generatedAt).format("MMMM Do YYYY, h:mm:ss a")}
786
+ </div>
787
+ <div class="info-item">
788
+ <strong>Report Period:</strong>
789
+ ${moment2(data.reportPeriod.start).format("MMM DD, YYYY")} -
790
+ ${moment2(data.reportPeriod.end).format("MMM DD, YYYY")}
791
+ </div>
792
+ </div>
793
+ </div>
794
+
795
+ <div class="cost-summary-card">
796
+ <h2>Cost Summary</h2>
797
+ <div class="summary-grid">
798
+ <div class="summary-item">
799
+ <div class="amount">${this.formatCurrency(data.costBreakdown.totals.thisMonth)}</div>
800
+ <div class="label">This Month</div>
801
+ </div>
802
+ <div class="summary-item">
803
+ <div class="amount">${this.formatCurrency(data.costBreakdown.totals.lastMonth)}</div>
804
+ <div class="label">Last Month</div>
805
+ </div>
806
+ <div class="summary-item">
807
+ <div class="amount">${this.formatCurrency(data.costBreakdown.totals.last7Days)}</div>
808
+ <div class="label">Last 7 Days</div>
809
+ </div>
810
+ <div class="summary-item">
811
+ <div class="amount">${this.formatCurrency(data.costBreakdown.totals.yesterday)}</div>
812
+ <div class="label">Yesterday</div>
813
+ </div>
814
+ </div>
815
+ </div>
816
+
817
+ <div class="report-scope">
818
+ <h2>Report Scope</h2>
819
+ <ul>
820
+ ${this.options.includeSummary ? "<li>Executive Summary</li>" : ""}
821
+ <li>Cost Breakdown Analysis</li>
822
+ ${this.options.includeCharts ? "<li>Trend Analysis & Visualizations</li>" : ""}
823
+ ${this.options.includeDetails ? "<li>Resource Inventory</li>" : ""}
824
+ ${this.options.includeRecommendations ? "<li>Cost Optimization Recommendations</li>" : ""}
825
+ </ul>
826
+ </div>
827
+ </div>
828
+ <div class="page-break"></div>`;
829
+ }
830
+ generateExecutiveSummary(data) {
831
+ var _a;
832
+ const monthlyChange = this.calculatePercentageChange(
833
+ data.costBreakdown.totals.thisMonth,
834
+ data.costBreakdown.totals.lastMonth
835
+ );
836
+ const topServices = Object.entries(data.costBreakdown.totalsByService.thisMonth).sort(([, a], [, b]) => b - a).slice(0, 5);
837
+ return `
838
+ <div class="section">
839
+ <h1>Executive Summary</h1>
840
+
841
+ <div class="executive-insights">
842
+ <div class="insight-card ${monthlyChange >= 0 ? "warning" : "success"}">
843
+ <h3>Monthly Cost Trend</h3>
844
+ <p>Your cloud spending has ${monthlyChange >= 0 ? "increased" : "decreased"} by
845
+ <strong>${Math.abs(monthlyChange).toFixed(1)}%</strong> compared to last month.</p>
846
+ </div>
847
+
848
+ <div class="insight-card info">
849
+ <h3>Top Cost Drivers</h3>
850
+ <ul>
851
+ ${topServices.map(([service, cost]) => `
852
+ <li>${service}: ${this.formatCurrency(cost)}
853
+ (${(cost / data.costBreakdown.totals.thisMonth * 100).toFixed(1)}% of total)
854
+ </li>
855
+ `).join("")}
856
+ </ul>
857
+ </div>
858
+
859
+ ${data.anomalies && data.anomalies.length > 0 ? `
860
+ <div class="insight-card warning">
861
+ <h3>Cost Anomalies</h3>
862
+ <p><strong>${data.anomalies.length}</strong> cost anomalies detected requiring attention.</p>
863
+ </div>
864
+ ` : ""}
865
+ </div>
866
+
867
+ <div class="key-metrics">
868
+ <h3>Key Performance Indicators</h3>
869
+ <div class="metrics-grid">
870
+ <div class="metric">
871
+ <div class="metric-value">${this.formatCurrency(data.costBreakdown.totals.thisMonth / moment2().date())}</div>
872
+ <div class="metric-label">Avg Daily Spend</div>
873
+ </div>
874
+ <div class="metric">
875
+ <div class="metric-value">${topServices.length}</div>
876
+ <div class="metric-label">Active Services</div>
877
+ </div>
878
+ <div class="metric">
879
+ <div class="metric-value">${((_a = data.trendAnalysis) == null ? void 0 : _a.projectedMonthlyCost) ? this.formatCurrency(data.trendAnalysis.projectedMonthlyCost) : "N/A"}</div>
880
+ <div class="metric-label">Projected Monthly</div>
881
+ </div>
882
+ </div>
883
+ </div>
884
+ </div>
885
+ <div class="page-break"></div>`;
886
+ }
887
+ generateCostBreakdownSection(costBreakdown) {
888
+ const serviceEntries = Object.entries(costBreakdown.totalsByService.thisMonth).filter(([, cost]) => cost > 0).sort(([, a], [, b]) => b - a);
889
+ return `
890
+ <div class="section">
891
+ <h1>Cost Breakdown Analysis</h1>
892
+
893
+ <div class="cost-comparison">
894
+ <h3>Period Comparison</h3>
895
+ <table class="data-table">
896
+ <thead>
897
+ <tr>
898
+ <th>Period</th>
899
+ <th>Total Cost</th>
900
+ <th>Change</th>
901
+ <th>Trend</th>
902
+ </tr>
903
+ </thead>
904
+ <tbody>
905
+ <tr>
906
+ <td>This Month</td>
907
+ <td>${this.formatCurrency(costBreakdown.totals.thisMonth)}</td>
908
+ <td>${this.formatChangePercentage(costBreakdown.totals.thisMonth, costBreakdown.totals.lastMonth)}</td>
909
+ <td>${this.getTrendIcon(costBreakdown.totals.thisMonth, costBreakdown.totals.lastMonth)}</td>
910
+ </tr>
911
+ <tr>
912
+ <td>Last Month</td>
913
+ <td>${this.formatCurrency(costBreakdown.totals.lastMonth)}</td>
914
+ <td>-</td>
915
+ <td>-</td>
916
+ </tr>
917
+ <tr>
918
+ <td>Last 7 Days</td>
919
+ <td>${this.formatCurrency(costBreakdown.totals.last7Days)}</td>
920
+ <td>${this.formatChangePercentage(costBreakdown.totals.last7Days, costBreakdown.totals.yesterday * 7)}</td>
921
+ <td>${this.getTrendIcon(costBreakdown.totals.last7Days, costBreakdown.totals.yesterday * 7)}</td>
922
+ </tr>
923
+ <tr>
924
+ <td>Yesterday</td>
925
+ <td>${this.formatCurrency(costBreakdown.totals.yesterday)}</td>
926
+ <td>-</td>
927
+ <td>-</td>
928
+ </tr>
929
+ </tbody>
930
+ </table>
931
+ </div>
932
+
933
+ <div class="service-breakdown">
934
+ <h3>Service-Level Breakdown (This Month)</h3>
935
+ <table class="data-table">
936
+ <thead>
937
+ <tr>
938
+ <th>Service</th>
939
+ <th>Cost</th>
940
+ <th>Share</th>
941
+ <th>vs Last Month</th>
942
+ </tr>
943
+ </thead>
944
+ <tbody>
945
+ ${serviceEntries.map(([service, cost]) => {
946
+ const share = (cost / costBreakdown.totals.thisMonth * 100).toFixed(1);
947
+ const lastMonthCost = costBreakdown.totalsByService.lastMonth[service] || 0;
948
+ const change = this.formatChangePercentage(cost, lastMonthCost);
949
+ return `
950
+ <tr>
951
+ <td>${service}</td>
952
+ <td>${this.formatCurrency(cost)}</td>
953
+ <td>${share}%</td>
954
+ <td>${change}</td>
955
+ </tr>
956
+ `;
957
+ }).join("")}
958
+ </tbody>
959
+ </table>
960
+ </div>
961
+ </div>
962
+ <div class="page-break"></div>`;
963
+ }
964
+ generateTrendAnalysisSection(trendAnalysis) {
965
+ var _a;
966
+ return `
967
+ <div class="section">
968
+ <h1>Trend Analysis</h1>
969
+
970
+ <div class="trend-insights">
971
+ <div class="insight-grid">
972
+ <div class="insight-item">
973
+ <div class="insight-value">${this.formatCurrency(trendAnalysis.totalCost)}</div>
974
+ <div class="insight-label">Total Period Cost</div>
975
+ </div>
976
+ <div class="insight-item">
977
+ <div class="insight-value">${this.formatCurrency(trendAnalysis.averageDailyCost)}</div>
978
+ <div class="insight-label">Average Daily Cost</div>
979
+ </div>
980
+ <div class="insight-item">
981
+ <div class="insight-value">${this.formatCurrency(trendAnalysis.projectedMonthlyCost)}</div>
982
+ <div class="insight-label">Projected Monthly</div>
983
+ </div>
984
+ <div class="insight-item">
985
+ <div class="insight-value">${((_a = trendAnalysis.avgMonthOverMonthGrowth) == null ? void 0 : _a.toFixed(1)) || "N/A"}%</div>
986
+ <div class="insight-label">Avg MoM Growth</div>
987
+ </div>
988
+ </div>
989
+ </div>
990
+
991
+ ${trendAnalysis.topServices ? `
992
+ <div class="top-services">
993
+ <h3>Top Services by Cost</h3>
994
+ <table class="data-table">
995
+ <thead>
996
+ <tr>
997
+ <th>Service</th>
998
+ <th>Cost</th>
999
+ <th>Share</th>
1000
+ <th>Trend</th>
1001
+ </tr>
1002
+ </thead>
1003
+ <tbody>
1004
+ ${trendAnalysis.topServices.map((service) => `
1005
+ <tr>
1006
+ <td>${service.serviceName}</td>
1007
+ <td>${this.formatCurrency(service.cost)}</td>
1008
+ <td>${service.percentage.toFixed(1)}%</td>
1009
+ <td>${service.trend}</td>
1010
+ </tr>
1011
+ `).join("")}
1012
+ </tbody>
1013
+ </table>
1014
+ </div>
1015
+ ` : ""}
1016
+
1017
+ ${trendAnalysis.analytics ? `
1018
+ <div class="analytics-insights">
1019
+ <h3>Analytics Insights</h3>
1020
+ <ul>
1021
+ ${trendAnalysis.analytics.insights.map((insight) => `<li>${insight}</li>`).join("")}
1022
+ </ul>
1023
+
1024
+ <div class="analytics-metrics">
1025
+ <div class="metric-row">
1026
+ <span>Volatility Score:</span>
1027
+ <span class="metric-value">${trendAnalysis.analytics.volatilityScore.toFixed(2)}</span>
1028
+ </div>
1029
+ <div class="metric-row">
1030
+ <span>Trend Strength:</span>
1031
+ <span class="metric-value">${trendAnalysis.analytics.trendStrength.toFixed(2)}</span>
1032
+ </div>
1033
+ </div>
1034
+ </div>
1035
+ ` : ""}
1036
+ </div>
1037
+ <div class="page-break"></div>`;
1038
+ }
1039
+ generateResourceInventorySection(inventory) {
1040
+ return `
1041
+ <div class="section">
1042
+ <h1>Resource Inventory</h1>
1043
+
1044
+ <div class="inventory-summary">
1045
+ <div class="summary-stats">
1046
+ <div class="stat">
1047
+ <div class="stat-value">${inventory.totalResources}</div>
1048
+ <div class="stat-label">Total Resources</div>
1049
+ </div>
1050
+ <div class="stat">
1051
+ <div class="stat-value">${this.formatCurrency(inventory.totalCost)}</div>
1052
+ <div class="stat-label">Total Cost</div>
1053
+ </div>
1054
+ <div class="stat">
1055
+ <div class="stat-value">${inventory.region}</div>
1056
+ <div class="stat-label">Primary Region</div>
1057
+ </div>
1058
+ </div>
1059
+ </div>
1060
+
1061
+ <div class="resource-breakdown">
1062
+ <h3>Resources by Type</h3>
1063
+ <table class="data-table">
1064
+ <thead>
1065
+ <tr>
1066
+ <th>Resource Type</th>
1067
+ <th>Count</th>
1068
+ <th>Percentage</th>
1069
+ </tr>
1070
+ </thead>
1071
+ <tbody>
1072
+ ${Object.entries(inventory.resourcesByType).map(([type, count]) => `
1073
+ <tr>
1074
+ <td>${type.charAt(0).toUpperCase() + type.slice(1)}</td>
1075
+ <td>${count}</td>
1076
+ <td>${(count / inventory.totalResources * 100).toFixed(1)}%</td>
1077
+ </tr>
1078
+ `).join("")}
1079
+ </tbody>
1080
+ </table>
1081
+ </div>
1082
+ </div>
1083
+ <div class="page-break"></div>`;
1084
+ }
1085
+ generateAnomaliesSection(anomalies) {
1086
+ return `
1087
+ <div class="section">
1088
+ <h1>Cost Anomalies</h1>
1089
+
1090
+ <div class="anomalies-overview">
1091
+ <p class="overview-text">
1092
+ <strong>${anomalies.length}</strong> cost anomalies have been detected during the report period.
1093
+ These represent significant deviations from expected spending patterns and require investigation.
1094
+ </p>
1095
+ </div>
1096
+
1097
+ <div class="anomalies-list">
1098
+ ${anomalies.map((anomaly) => `
1099
+ <div class="anomaly-card severity-${anomaly.severity.toLowerCase()}">
1100
+ <div class="anomaly-header">
1101
+ <h4>${moment2(anomaly.date).format("MMMM DD, YYYY")}</h4>
1102
+ <span class="severity-badge ${anomaly.severity.toLowerCase()}">${anomaly.severity}</span>
1103
+ </div>
1104
+ <div class="anomaly-details">
1105
+ <div class="detail-row">
1106
+ <span>Expected Cost:</span>
1107
+ <span>${this.formatCurrency(anomaly.expectedCost)}</span>
1108
+ </div>
1109
+ <div class="detail-row">
1110
+ <span>Actual Cost:</span>
1111
+ <span>${this.formatCurrency(anomaly.actualCost)}</span>
1112
+ </div>
1113
+ <div class="detail-row">
1114
+ <span>Deviation:</span>
1115
+ <span class="${anomaly.deviation > 0 ? "increase" : "decrease"}">
1116
+ ${anomaly.deviation > 0 ? "+" : ""}${anomaly.deviation.toFixed(1)}%
1117
+ </span>
1118
+ </div>
1119
+ ${anomaly.description ? `
1120
+ <div class="anomaly-description">
1121
+ <p>${anomaly.description}</p>
1122
+ </div>
1123
+ ` : ""}
1124
+ </div>
1125
+ </div>
1126
+ `).join("")}
1127
+ </div>
1128
+ </div>
1129
+ <div class="page-break"></div>`;
1130
+ }
1131
+ generateRecommendationsSection(recommendations) {
1132
+ const totalPotentialSavings = recommendations.reduce((sum, rec) => sum + rec.potentialSavings.amount, 0);
1133
+ return `
1134
+ <div class="section">
1135
+ <h1>Cost Optimization Recommendations</h1>
1136
+
1137
+ <div class="recommendations-overview">
1138
+ <div class="savings-potential">
1139
+ <h3>Potential Savings: ${this.formatCurrency(totalPotentialSavings)}</h3>
1140
+ <p>${recommendations.length} optimization opportunities identified</p>
1141
+ </div>
1142
+ </div>
1143
+
1144
+ <div class="recommendations-list">
1145
+ ${recommendations.map((rec) => `
1146
+ <div class="recommendation-card priority-${rec.priority.toLowerCase()}">
1147
+ <div class="recommendation-header">
1148
+ <h4>${rec.title}</h4>
1149
+ <div class="recommendation-meta">
1150
+ <span class="priority-badge ${rec.priority.toLowerCase()}">${rec.priority}</span>
1151
+ <span class="effort-badge effort-${rec.effort.toLowerCase()}">${rec.effort} Effort</span>
1152
+ </div>
1153
+ </div>
1154
+
1155
+ <div class="recommendation-body">
1156
+ <p>${rec.description}</p>
1157
+
1158
+ <div class="savings-info">
1159
+ <div class="savings-amount">
1160
+ Potential Savings: <strong>${this.formatCurrency(rec.potentialSavings.amount)}</strong>
1161
+ <span class="timeframe">(${rec.potentialSavings.timeframe.toLowerCase()})</span>
1162
+ </div>
1163
+ <div class="savings-percentage">
1164
+ ${rec.potentialSavings.percentage.toFixed(1)}% reduction
1165
+ </div>
1166
+ </div>
1167
+
1168
+ ${rec.implementationSteps.length > 0 ? `
1169
+ <div class="implementation-steps">
1170
+ <h5>Implementation Steps:</h5>
1171
+ <ol>
1172
+ ${rec.implementationSteps.map((step) => `<li>${step}</li>`).join("")}
1173
+ </ol>
1174
+ </div>
1175
+ ` : ""}
1176
+
1177
+ ${rec.resources && rec.resources.length > 0 ? `
1178
+ <div class="affected-resources">
1179
+ <h5>Affected Resources:</h5>
1180
+ <ul>
1181
+ ${rec.resources.map((resource) => `<li>${resource}</li>`).join("")}
1182
+ </ul>
1183
+ </div>
1184
+ ` : ""}
1185
+
1186
+ ${rec.tags.length > 0 ? `
1187
+ <div class="recommendation-tags">
1188
+ ${rec.tags.map((tag) => `<span class="tag">${tag}</span>`).join("")}
1189
+ </div>
1190
+ ` : ""}
1191
+ </div>
1192
+ </div>
1193
+ `).join("")}
1194
+ </div>
1195
+ </div>
1196
+ <div class="page-break"></div>`;
1197
+ }
1198
+ generateAppendixSection(data) {
1199
+ return `
1200
+ <div class="section">
1201
+ <h1>Appendix</h1>
1202
+
1203
+ <div class="report-metadata">
1204
+ <h3>Report Generation Details</h3>
1205
+ <table class="metadata-table">
1206
+ <tr>
1207
+ <td>Report Generated:</td>
1208
+ <td>${moment2(data.generatedAt).format("MMMM Do YYYY, h:mm:ss a")}</td>
1209
+ </tr>
1210
+ <tr>
1211
+ <td>Tool Version:</td>
1212
+ <td>infra-cost v0.1.0</td>
1213
+ </tr>
1214
+ <tr>
1215
+ <td>Report Type:</td>
1216
+ <td>Comprehensive Audit Report</td>
1217
+ </tr>
1218
+ <tr>
1219
+ <td>Data Source:</td>
1220
+ <td>${this.getProviderDisplayName(data.accountInfo.provider)} Cost Explorer API</td>
1221
+ </tr>
1222
+ <tr>
1223
+ <td>Analysis Period:</td>
1224
+ <td>${moment2(data.reportPeriod.start).format("MMM DD, YYYY")} - ${moment2(data.reportPeriod.end).format("MMM DD, YYYY")}</td>
1225
+ </tr>
1226
+ </table>
1227
+ </div>
1228
+
1229
+ <div class="methodology">
1230
+ <h3>Methodology</h3>
1231
+ <ul>
1232
+ <li>Cost data retrieved from cloud provider's native billing APIs</li>
1233
+ <li>Trend analysis based on historical spending patterns</li>
1234
+ <li>Anomaly detection using statistical analysis and machine learning algorithms</li>
1235
+ <li>Recommendations generated based on FinOps best practices and usage patterns</li>
1236
+ <li>All costs displayed in USD unless otherwise specified</li>
1237
+ </ul>
1238
+ </div>
1239
+
1240
+ <div class="disclaimers">
1241
+ <h3>Important Notes</h3>
1242
+ <ul>
1243
+ <li>Cost data is subject to cloud provider billing cycles and may include estimates</li>
1244
+ <li>Savings projections are estimates based on current usage patterns</li>
1245
+ <li>Implementation of recommendations should be carefully planned and tested</li>
1246
+ <li>This report is generated automatically and should be reviewed by qualified personnel</li>
1247
+ </ul>
1248
+ </div>
1249
+ </div>`;
1250
+ }
1251
+ // Helper methods and styles...
1252
+ getThemeStyles() {
1253
+ if (this.options.theme === "dark") {
1254
+ return `
1255
+ :root {
1256
+ --primary-color: #2563eb;
1257
+ --secondary-color: #64748b;
1258
+ --background-color: #1e293b;
1259
+ --text-color: #f8fafc;
1260
+ --border-color: #374151;
1261
+ --success-color: #10b981;
1262
+ --warning-color: #f59e0b;
1263
+ --danger-color: #ef4444;
1264
+ --card-background: #334155;
1265
+ }
1266
+ `;
1267
+ }
1268
+ return `
1269
+ :root {
1270
+ --primary-color: #2563eb;
1271
+ --secondary-color: #64748b;
1272
+ --background-color: #ffffff;
1273
+ --text-color: #1f2937;
1274
+ --border-color: #e5e7eb;
1275
+ --success-color: #10b981;
1276
+ --warning-color: #f59e0b;
1277
+ --danger-color: #ef4444;
1278
+ --card-background: #f9fafb;
1279
+ }
1280
+ `;
1281
+ }
1282
+ getBaseStyles() {
1283
+ return `
1284
+ body {
1285
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1286
+ line-height: 1.6;
1287
+ color: var(--text-color);
1288
+ background-color: var(--background-color);
1289
+ margin: 0;
1290
+ padding: 0;
1291
+ }
1292
+
1293
+ .report-container {
1294
+ max-width: 100%;
1295
+ margin: 0 auto;
1296
+ }
1297
+
1298
+ .page-break {
1299
+ page-break-before: always;
1300
+ }
1301
+
1302
+ .cover-page {
1303
+ text-align: center;
1304
+ padding: 2rem;
1305
+ min-height: 80vh;
1306
+ display: flex;
1307
+ flex-direction: column;
1308
+ justify-content: center;
1309
+ }
1310
+
1311
+ .header h1 {
1312
+ font-size: 2.5rem;
1313
+ color: var(--primary-color);
1314
+ margin-bottom: 0.5rem;
1315
+ }
1316
+
1317
+ .subtitle {
1318
+ font-size: 1.2rem;
1319
+ color: var(--secondary-color);
1320
+ margin-bottom: 3rem;
1321
+ }
1322
+
1323
+ .section {
1324
+ padding: 2rem;
1325
+ margin-bottom: 2rem;
1326
+ }
1327
+
1328
+ .section h1 {
1329
+ color: var(--primary-color);
1330
+ border-bottom: 2px solid var(--primary-color);
1331
+ padding-bottom: 0.5rem;
1332
+ margin-bottom: 2rem;
1333
+ }
1334
+
1335
+ .data-table {
1336
+ width: 100%;
1337
+ border-collapse: collapse;
1338
+ margin: 1rem 0;
1339
+ }
1340
+
1341
+ .data-table th,
1342
+ .data-table td {
1343
+ padding: 0.75rem;
1344
+ text-align: left;
1345
+ border-bottom: 1px solid var(--border-color);
1346
+ }
1347
+
1348
+ .data-table th {
1349
+ background-color: var(--card-background);
1350
+ font-weight: 600;
1351
+ }
1352
+
1353
+ .cost-summary-card {
1354
+ background: var(--card-background);
1355
+ border-radius: 8px;
1356
+ padding: 2rem;
1357
+ margin: 2rem 0;
1358
+ border: 1px solid var(--border-color);
1359
+ }
1360
+
1361
+ .summary-grid {
1362
+ display: grid;
1363
+ grid-template-columns: repeat(4, 1fr);
1364
+ gap: 1rem;
1365
+ text-align: center;
1366
+ }
1367
+
1368
+ .summary-item .amount {
1369
+ font-size: 1.5rem;
1370
+ font-weight: bold;
1371
+ color: var(--primary-color);
1372
+ }
1373
+
1374
+ .summary-item .label {
1375
+ color: var(--secondary-color);
1376
+ font-size: 0.9rem;
1377
+ }
1378
+
1379
+ .anomaly-card {
1380
+ border-left: 4px solid;
1381
+ padding: 1rem;
1382
+ margin: 1rem 0;
1383
+ background: var(--card-background);
1384
+ border-radius: 0 8px 8px 0;
1385
+ }
1386
+
1387
+ .anomaly-card.severity-critical {
1388
+ border-left-color: var(--danger-color);
1389
+ }
1390
+
1391
+ .anomaly-card.severity-high {
1392
+ border-left-color: #ff6b35;
1393
+ }
1394
+
1395
+ .anomaly-card.severity-medium {
1396
+ border-left-color: var(--warning-color);
1397
+ }
1398
+
1399
+ .anomaly-card.severity-low {
1400
+ border-left-color: #3b82f6;
1401
+ }
1402
+
1403
+ .recommendation-card {
1404
+ border: 1px solid var(--border-color);
1405
+ border-radius: 8px;
1406
+ padding: 1.5rem;
1407
+ margin: 1rem 0;
1408
+ background: var(--card-background);
1409
+ }
1410
+
1411
+ .priority-badge {
1412
+ padding: 0.25rem 0.5rem;
1413
+ border-radius: 4px;
1414
+ font-size: 0.75rem;
1415
+ font-weight: 600;
1416
+ text-transform: uppercase;
1417
+ }
1418
+
1419
+ .priority-badge.critical {
1420
+ background-color: var(--danger-color);
1421
+ color: white;
1422
+ }
1423
+
1424
+ .priority-badge.high {
1425
+ background-color: #ff6b35;
1426
+ color: white;
1427
+ }
1428
+
1429
+ .priority-badge.medium {
1430
+ background-color: var(--warning-color);
1431
+ color: white;
1432
+ }
1433
+
1434
+ .priority-badge.low {
1435
+ background-color: #3b82f6;
1436
+ color: white;
1437
+ }
1438
+
1439
+ .trend-chart {
1440
+ font-family: monospace;
1441
+ background: var(--card-background);
1442
+ padding: 1rem;
1443
+ border-radius: 4px;
1444
+ margin: 1rem 0;
1445
+ }
1446
+
1447
+ @media print {
1448
+ body { print-color-adjust: exact; }
1449
+ .page-break { page-break-before: always; }
1450
+ }
1451
+ `;
1452
+ }
1453
+ generateHeaderTemplate(accountInfo) {
1454
+ return `
1455
+ <div style="font-size: 10px; color: #666; width: 100%; text-align: center; margin-top: 10px;">
1456
+ Infrastructure Cost Report - ${accountInfo.name || accountInfo.id} - ${this.getProviderDisplayName(accountInfo.provider)}
1457
+ </div>`;
1458
+ }
1459
+ generateFooterTemplate() {
1460
+ return `
1461
+ <div style="font-size: 10px; color: #666; width: 100%; text-align: center; margin-bottom: 10px;">
1462
+ <div style="display: flex; justify-content: space-between; width: 100%; align-items: center;">
1463
+ <span>Generated by infra-cost v0.1.0</span>
1464
+ <span>Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
1465
+ <span>${moment2().format("YYYY-MM-DD HH:mm")}</span>
1466
+ </div>
1467
+ </div>`;
1468
+ }
1469
+ async initializeBrowser() {
1470
+ if (!this.browser) {
1471
+ this.browser = await puppeteer.launch({
1472
+ headless: true,
1473
+ args: ["--no-sandbox", "--disable-setuid-sandbox"]
1474
+ });
1475
+ }
1476
+ }
1477
+ async closeBrowser() {
1478
+ if (this.browser) {
1479
+ await this.browser.close();
1480
+ this.browser = null;
1481
+ }
1482
+ }
1483
+ ensureOutputDirectory() {
1484
+ const outputPath = this.options.outputPath;
1485
+ if (!existsSync(outputPath)) {
1486
+ mkdirSync(outputPath, { recursive: true });
1487
+ }
1488
+ return outputPath;
1489
+ }
1490
+ formatCurrency(amount) {
1491
+ return new Intl.NumberFormat("en-US", {
1492
+ style: "currency",
1493
+ currency: "USD",
1494
+ minimumFractionDigits: 2,
1495
+ maximumFractionDigits: 2
1496
+ }).format(amount);
1497
+ }
1498
+ calculatePercentageChange(current, previous) {
1499
+ if (previous === 0)
1500
+ return 0;
1501
+ return (current - previous) / previous * 100;
1502
+ }
1503
+ formatChangePercentage(current, previous) {
1504
+ const change = this.calculatePercentageChange(current, previous);
1505
+ const sign = change >= 0 ? "+" : "";
1506
+ return `${sign}${change.toFixed(1)}%`;
1507
+ }
1508
+ getTrendIcon(current, previous) {
1509
+ const change = this.calculatePercentageChange(current, previous);
1510
+ if (change > 5)
1511
+ return "\u2197\uFE0F";
1512
+ if (change < -5)
1513
+ return "\u2198\uFE0F";
1514
+ return "\u27A1\uFE0F";
1515
+ }
1516
+ getProviderDisplayName(provider) {
1517
+ const displayNames = {
1518
+ ["aws" /* AWS */]: "Amazon Web Services",
1519
+ ["gcp" /* GOOGLE_CLOUD */]: "Google Cloud Platform",
1520
+ ["azure" /* AZURE */]: "Microsoft Azure",
1521
+ ["alicloud" /* ALIBABA_CLOUD */]: "Alibaba Cloud",
1522
+ ["oracle" /* ORACLE_CLOUD */]: "Oracle Cloud Infrastructure"
1523
+ };
1524
+ return displayNames[provider] || provider;
1525
+ }
1526
+ };
1527
+
1528
+ // src/demo/test-enhanced-ui.ts
1529
+ async function testEnhancedUI() {
1530
+ console.log(chalk2.bold.cyan("\u{1F9EA} Testing Enhanced Terminal UI Features\n"));
1531
+ const ui = new TerminalUIEngine();
1532
+ console.log(chalk2.bold.yellow("Test 1: Enhanced Header"));
1533
+ console.log("\u2500".repeat(50));
1534
+ const header = ui.createHeader("\u{1F680} Infrastructure Cost Analysis", "Demo Production Account");
1535
+ console.log(header);
1536
+ console.log(chalk2.bold.yellow("Test 2: Rich Cost Breakdown Table"));
1537
+ console.log("\u2500".repeat(50));
1538
+ const costBreakdown = DemoDataGenerator.generateCostBreakdown();
1539
+ const costTable = ui.createCostTable(costBreakdown, {
1540
+ showPercentages: true,
1541
+ highlightTop: 8,
1542
+ currency: "USD",
1543
+ compact: false
1544
+ });
1545
+ console.log(costTable);
1546
+ console.log(chalk2.bold.yellow("Test 3: ASCII Trend Chart"));
1547
+ console.log("\u2500".repeat(50));
1548
+ const trendAnalysis = DemoDataGenerator.generateTrendAnalysis();
1549
+ if (trendAnalysis.trendData) {
1550
+ const trendChart = ui.createTrendChart(trendAnalysis.trendData, {
1551
+ width: 50,
1552
+ showLabels: true,
1553
+ currency: "USD",
1554
+ colorThreshold: 0.7
1555
+ });
1556
+ console.log(trendChart);
1557
+ }
1558
+ console.log(chalk2.bold.yellow("Test 4: Cost Anomaly Alerts"));
1559
+ console.log("\u2500".repeat(50));
1560
+ const anomalies = DemoDataGenerator.generateCostAnomalies();
1561
+ const anomalyAlert = ui.createAnomalyAlert(anomalies);
1562
+ console.log(anomalyAlert);
1563
+ console.log(chalk2.bold.yellow("Test 5: Progress Indicators"));
1564
+ console.log("\u2500".repeat(50));
1565
+ ui.startProgress("Processing cost analysis", 100);
1566
+ for (let i = 0; i <= 100; i += 20) {
1567
+ await new Promise((resolve) => setTimeout(resolve, 200));
1568
+ ui.updateProgress(i, { step: `Processing step ${i / 20 + 1}/6` });
1569
+ }
1570
+ ui.stopProgress();
1571
+ console.log(chalk2.green("\u2705 Processing completed!\n"));
1572
+ console.log(chalk2.bold.yellow("Test 6: Executive Summary Format"));
1573
+ console.log("\u2500".repeat(50));
1574
+ const recommendations = DemoDataGenerator.generateRecommendations();
1575
+ const totalSavings = recommendations.reduce((sum, rec) => sum + rec.potentialSavings.amount, 0);
1576
+ console.log("\n" + chalk2.bold.cyan("\u{1F3AF} Key Performance Indicators"));
1577
+ console.log("\u2550".repeat(50));
1578
+ console.log(`Monthly Spend: ${chalk2.yellow("$" + costBreakdown.totals.thisMonth.toFixed(2))}`);
1579
+ console.log(`Cost Change: ${costBreakdown.totals.thisMonth > costBreakdown.totals.lastMonth ? chalk2.red("\u2197 +" + ((costBreakdown.totals.thisMonth - costBreakdown.totals.lastMonth) / costBreakdown.totals.lastMonth * 100).toFixed(1) + "%") : chalk2.green("\u2198 " + ((costBreakdown.totals.thisMonth - costBreakdown.totals.lastMonth) / costBreakdown.totals.lastMonth * 100).toFixed(1) + "%")}`);
1580
+ console.log(`Optimization Potential: ${chalk2.green("$" + totalSavings.toFixed(2))}`);
1581
+ console.log(`Active Recommendations: ${chalk2.cyan(recommendations.length.toString())}`);
1582
+ console.log("\n" + chalk2.bold.cyan("\u{1F4A1} Top Cost Optimization Recommendations"));
1583
+ console.log("\u2550".repeat(60));
1584
+ recommendations.slice(0, 3).forEach((rec, index) => {
1585
+ console.log(`
1586
+ ${index + 1}. ${chalk2.bold(rec.title)}`);
1587
+ console.log(` ${rec.description}`);
1588
+ console.log(` \u{1F4B0} Potential savings: ${chalk2.green("$" + rec.potentialSavings.amount.toFixed(2))} ${rec.potentialSavings.timeframe.toLowerCase()}`);
1589
+ console.log(` \u{1F3AF} Priority: ${chalk2[rec.priority === "HIGH" ? "red" : rec.priority === "MEDIUM" ? "yellow" : "cyan"](rec.priority)}`);
1590
+ console.log(` \u{1F527} Effort: ${chalk2.gray(rec.effort)}`);
1591
+ });
1592
+ console.log("\n" + chalk2.gray("\u2501".repeat(70)));
1593
+ console.log(chalk2.gray("\u{1F4A1} Demo completed successfully! All enhanced UI features are working."));
1594
+ console.log(chalk2.gray("\u{1F4CA} Use --trend, --audit, or --executive-summary with real data"));
1595
+ return true;
1596
+ }
1597
+ async function testPDFGeneration() {
1598
+ console.log("\n" + chalk2.bold.cyan("\u{1F4C4} Testing PDF Generation"));
1599
+ console.log("\u2500".repeat(50));
1600
+ try {
1601
+ const pdfExporter = new PDFExporter({
1602
+ outputPath: "./demo-reports",
1603
+ includeCharts: true,
1604
+ includeSummary: true,
1605
+ includeDetails: true,
1606
+ includeRecommendations: true
1607
+ });
1608
+ const accountInfo = DemoDataGenerator.generateAccountInfo();
1609
+ const costBreakdown = DemoDataGenerator.generateCostBreakdown();
1610
+ const recommendations = DemoDataGenerator.generateRecommendations();
1611
+ const anomalies = DemoDataGenerator.generateCostAnomalies();
1612
+ const inventory = DemoDataGenerator.generateResourceInventory();
1613
+ console.log("\u{1F504} Generating comprehensive demo audit report...");
1614
+ const auditData = {
1615
+ accountInfo,
1616
+ costBreakdown,
1617
+ resourceInventory: inventory,
1618
+ recommendations,
1619
+ anomalies,
1620
+ generatedAt: /* @__PURE__ */ new Date(),
1621
+ reportPeriod: {
1622
+ start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3),
1623
+ end: /* @__PURE__ */ new Date()
1624
+ }
1625
+ };
1626
+ const pdfPath = await pdfExporter.generateAuditReport(auditData);
1627
+ console.log(chalk2.green(`\u2705 Demo PDF report generated: ${pdfPath}`));
1628
+ return true;
1629
+ } catch (error) {
1630
+ console.log(chalk2.yellow(`\u26A0\uFE0F PDF generation test skipped: ${error instanceof Error ? error.message : "Unknown error"}`));
1631
+ console.log(chalk2.gray(" (This is expected in environments without browser support)"));
1632
+ return false;
1633
+ }
1634
+ }
1635
+ async function main() {
1636
+ try {
1637
+ console.log(chalk2.bold.blue("\u{1F680} Enhanced infra-cost UI Testing Suite"));
1638
+ console.log(chalk2.gray("Testing all new features with realistic demo data\n"));
1639
+ const uiTestResult = await testEnhancedUI();
1640
+ const pdfTestResult = await testPDFGeneration();
1641
+ console.log("\n" + chalk2.bold.green("\u{1F389} Testing Complete!"));
1642
+ console.log(`Terminal UI Features: ${uiTestResult ? chalk2.green("\u2705 PASS") : chalk2.red("\u274C FAIL")}`);
1643
+ console.log(`PDF Generation: ${pdfTestResult ? chalk2.green("\u2705 PASS") : chalk2.yellow("\u26A0\uFE0F SKIP")}`);
1644
+ if (uiTestResult) {
1645
+ console.log("\n" + chalk2.bold.cyan("Next Steps:"));
1646
+ console.log("\u2022 Run with real AWS credentials: infra-cost --trend");
1647
+ console.log("\u2022 Generate audit reports: infra-cost --audit --pdf-report");
1648
+ console.log("\u2022 Create executive summaries: infra-cost --executive-summary");
1649
+ }
1650
+ } catch (error) {
1651
+ console.error(chalk2.red("\u274C Test failed:"), error instanceof Error ? error.message : error);
1652
+ process.exit(1);
1653
+ }
1654
+ }
1655
+ if (import.meta.url === `file://${process.argv[1]}`) {
1656
+ main();
1657
+ }
1658
+ export {
1659
+ testEnhancedUI,
1660
+ testPDFGeneration
1661
+ };
1662
+ //# sourceMappingURL=test-enhanced-ui.js.map