rc-pulse 1.1.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,416 @@
1
+ import { calcHealthScore } from "./report.js";
2
+ export function generateDashboardHtml(metrics, projectName) {
3
+ const health = calcHealthScore(metrics);
4
+ const date = new Date().toISOString().slice(0, 10);
5
+ const { overview, mrr, churn, revenue, trialConversion } = metrics;
6
+ // Format MRR history for chart
7
+ const mrrHistory = mrr.history.slice(-12).map((p) => ({
8
+ label: p.date.toISOString().slice(0, 7),
9
+ value: Math.round(p.value),
10
+ }));
11
+ const gradeColor = {
12
+ A: "#3fb950",
13
+ B: "#d29922",
14
+ C: "#d29922",
15
+ D: "#f85149",
16
+ F: "#f85149",
17
+ }[health.grade];
18
+ const signalColor = { good: "#3fb950", warning: "#d29922", bad: "#f85149" };
19
+ const signalsHtml = health.signals
20
+ .map((s) => `
21
+ <div class="signal ${s.status}">
22
+ <span class="signal-icon">${s.status === "good" ? "✓" : s.status === "warning" ? "~" : "✗"}</span>
23
+ <div class="signal-body">
24
+ <span class="signal-label">${s.label}</span>
25
+ <span class="signal-detail">${s.detail}</span>
26
+ </div>
27
+ </div>`)
28
+ .join("");
29
+ const trendBadge = (t) => {
30
+ const cls = t > 1 ? "up" : t < -1 ? "down" : "flat";
31
+ const arrow = t > 1 ? "↑" : t < -1 ? "↓" : "→";
32
+ return `<span class="trend ${cls}">${arrow} ${t > 0 ? "+" : ""}${t.toFixed(1)}%</span>`;
33
+ };
34
+ const fmt = (n, unit = "#") => {
35
+ if (unit === "$")
36
+ return n >= 1000 ? `$${(n / 1000).toFixed(1)}k` : `$${n.toFixed(0)}`;
37
+ if (unit === "%")
38
+ return `${n.toFixed(1)}%`;
39
+ return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${Math.round(n)}`;
40
+ };
41
+ const mrrLabels = JSON.stringify(mrrHistory.map((p) => p.label));
42
+ const mrrValues = JSON.stringify(mrrHistory.map((p) => p.value));
43
+ // Gauge arc calculation (0-100 → 0-180°, semicircle)
44
+ const gaugeAngle = (health.score / 100) * 180;
45
+ return `<!DOCTYPE html>
46
+ <html lang="en">
47
+ <head>
48
+ <meta charset="UTF-8" />
49
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
50
+ <title>rc-pulse${projectName ? ` · ${projectName}` : ""} · ${date}</title>
51
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
52
+ <style>
53
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
54
+
55
+ :root {
56
+ --bg: #0d1117;
57
+ --bg-card: #161b22;
58
+ --border: #30363d;
59
+ --accent: #58a6ff;
60
+ --green: #3fb950;
61
+ --orange: #d29922;
62
+ --red: #f85149;
63
+ --text: #f0f6fc;
64
+ --text-secondary: #8b949e;
65
+ --text-muted: #484f58;
66
+ --radius: 12px;
67
+ --grade-color: ${gradeColor};
68
+ }
69
+
70
+ body {
71
+ background: var(--bg);
72
+ color: var(--text);
73
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
74
+ min-height: 100vh;
75
+ padding: 32px 24px;
76
+ }
77
+
78
+ .container { max-width: 1100px; margin: 0 auto; }
79
+
80
+ /* Header */
81
+ .header {
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: space-between;
85
+ margin-bottom: 32px;
86
+ padding-bottom: 24px;
87
+ border-bottom: 1px solid var(--border);
88
+ }
89
+ .header-left h1 {
90
+ font-family: 'Courier New', monospace;
91
+ font-size: 28px;
92
+ font-weight: 700;
93
+ letter-spacing: -1px;
94
+ }
95
+ .header-left h1 span { color: var(--accent); }
96
+ .header-left .project { color: var(--text-secondary); font-size: 16px; margin-top: 4px; }
97
+ .header-right { color: var(--text-muted); font-size: 14px; text-align: right; }
98
+
99
+ /* Health score */
100
+ .health-section {
101
+ display: grid;
102
+ grid-template-columns: 280px 1fr;
103
+ gap: 24px;
104
+ margin-bottom: 24px;
105
+ }
106
+
107
+ .gauge-card {
108
+ background: var(--bg-card);
109
+ border: 1px solid var(--border);
110
+ border-radius: var(--radius);
111
+ padding: 32px 24px;
112
+ display: flex;
113
+ flex-direction: column;
114
+ align-items: center;
115
+ gap: 12px;
116
+ }
117
+ .gauge-title { font-size: 12px; text-transform: uppercase; letter-spacing: 2px; color: var(--text-secondary); }
118
+ .gauge-wrap { position: relative; width: 180px; height: 100px; }
119
+ .gauge-wrap svg { overflow: visible; }
120
+ .gauge-score {
121
+ position: absolute;
122
+ bottom: 0; left: 0; right: 0;
123
+ text-align: center;
124
+ }
125
+ .gauge-score .number {
126
+ font-family: 'Courier New', monospace;
127
+ font-size: 48px;
128
+ font-weight: 700;
129
+ color: var(--grade-color);
130
+ line-height: 1;
131
+ }
132
+ .gauge-score .denom { font-size: 16px; color: var(--text-secondary); margin-left: 2px; }
133
+ .grade-badge {
134
+ background: color-mix(in srgb, var(--grade-color) 15%, transparent);
135
+ border: 1px solid color-mix(in srgb, var(--grade-color) 50%, transparent);
136
+ border-radius: 20px;
137
+ padding: 6px 20px;
138
+ font-size: 16px;
139
+ font-weight: 600;
140
+ color: var(--grade-color);
141
+ }
142
+
143
+ /* Signals */
144
+ .signals-card {
145
+ background: var(--bg-card);
146
+ border: 1px solid var(--border);
147
+ border-radius: var(--radius);
148
+ padding: 24px;
149
+ }
150
+ .signals-card h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 2px; color: var(--text-secondary); margin-bottom: 16px; }
151
+ .signal {
152
+ display: flex;
153
+ align-items: center;
154
+ gap: 12px;
155
+ padding: 12px 0;
156
+ border-bottom: 1px solid var(--border);
157
+ }
158
+ .signal:last-child { border-bottom: none; }
159
+ .signal-icon {
160
+ font-family: monospace;
161
+ font-size: 18px;
162
+ font-weight: 700;
163
+ width: 24px;
164
+ text-align: center;
165
+ flex-shrink: 0;
166
+ }
167
+ .signal.good .signal-icon { color: var(--green); }
168
+ .signal.warning .signal-icon { color: var(--orange); }
169
+ .signal.bad .signal-icon { color: var(--red); }
170
+ .signal-body { display: flex; justify-content: space-between; width: 100%; align-items: center; }
171
+ .signal-label { font-weight: 600; font-size: 15px; }
172
+ .signal-detail { color: var(--text-secondary); font-size: 14px; font-family: monospace; }
173
+
174
+ /* Overview grid */
175
+ .overview-grid {
176
+ display: grid;
177
+ grid-template-columns: repeat(4, 1fr);
178
+ gap: 16px;
179
+ margin-bottom: 24px;
180
+ }
181
+ .metric-card {
182
+ background: var(--bg-card);
183
+ border: 1px solid var(--border);
184
+ border-radius: var(--radius);
185
+ padding: 20px;
186
+ }
187
+ .metric-card .label { font-size: 12px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-secondary); margin-bottom: 8px; }
188
+ .metric-card .value { font-family: 'Courier New', monospace; font-size: 28px; font-weight: 700; }
189
+ .metric-card .sub { font-size: 13px; color: var(--text-secondary); margin-top: 4px; display: flex; align-items: center; gap: 6px; }
190
+
191
+ /* Trend badges */
192
+ .trend { font-size: 13px; font-weight: 600; padding: 2px 8px; border-radius: 4px; font-family: monospace; }
193
+ .trend.up { background: rgba(63,185,80,0.15); color: var(--green); }
194
+ .trend.down { background: rgba(248,81,73,0.15); color: var(--red); }
195
+ .trend.flat { background: rgba(139,148,158,0.15); color: var(--text-secondary); }
196
+
197
+ /* Chart */
198
+ .chart-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 24px; }
199
+ .chart-card {
200
+ background: var(--bg-card);
201
+ border: 1px solid var(--border);
202
+ border-radius: var(--radius);
203
+ padding: 20px;
204
+ }
205
+ .chart-card h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-secondary); margin-bottom: 16px; }
206
+ .chart-card canvas { max-height: 180px; }
207
+
208
+ /* Footer */
209
+ footer {
210
+ margin-top: 40px;
211
+ padding-top: 20px;
212
+ border-top: 1px solid var(--border);
213
+ text-align: center;
214
+ color: var(--text-muted);
215
+ font-size: 13px;
216
+ }
217
+ footer a { color: var(--accent); text-decoration: none; }
218
+ </style>
219
+ </head>
220
+ <body>
221
+ <div class="container">
222
+
223
+ <header class="header">
224
+ <div class="header-left">
225
+ <h1>rc-<span>pulse</span></h1>
226
+ <div class="project">${projectName ?? "RevenueCat Project"} · Health Dashboard</div>
227
+ </div>
228
+ <div class="header-right">
229
+ Generated ${date}<br>
230
+ <a href="https://github.com/dimitriharding/rc-pulse" style="color:var(--accent)">github.com/dimitriharding/rc-pulse</a>
231
+ </div>
232
+ </header>
233
+
234
+ <!-- Health Score + Signals -->
235
+ <div class="health-section">
236
+ <div class="gauge-card">
237
+ <span class="gauge-title">Health Score</span>
238
+ <div class="gauge-wrap">
239
+ <svg viewBox="0 0 180 100" width="180" height="100">
240
+ <!-- Background arc -->
241
+ <path d="M 10 90 A 80 80 0 0 1 170 90"
242
+ fill="none" stroke="#30363d" stroke-width="14" stroke-linecap="round"/>
243
+ <!-- Score arc -->
244
+ <path id="gaugeArc" d="M 10 90 A 80 80 0 0 1 170 90"
245
+ fill="none" stroke="${gradeColor}" stroke-width="14" stroke-linecap="round"
246
+ stroke-dasharray="0 251"
247
+ style="transition: stroke-dasharray 1.2s ease"/>
248
+ </svg>
249
+ <div class="gauge-score">
250
+ <span class="number">${health.score}</span><span class="denom">/100</span>
251
+ </div>
252
+ </div>
253
+ <div class="grade-badge">${health.grade} · ${health.label}</div>
254
+ </div>
255
+
256
+ <div class="signals-card">
257
+ <h3>Signals</h3>
258
+ ${signalsHtml}
259
+ </div>
260
+ </div>
261
+
262
+ <!-- Overview metrics -->
263
+ <div class="overview-grid">
264
+ <div class="metric-card">
265
+ <div class="label">Active Subscribers</div>
266
+ <div class="value">${fmt(overview.activeSubscriptions)}</div>
267
+ <div class="sub">paying subscribers</div>
268
+ </div>
269
+ <div class="metric-card">
270
+ <div class="label">MRR</div>
271
+ <div class="value">${fmt(mrr.current, "$")}</div>
272
+ <div class="sub">${trendBadge(mrr.trend)} vs last month</div>
273
+ </div>
274
+ <div class="metric-card">
275
+ <div class="label">Churn Rate</div>
276
+ <div class="value">${fmt(churn.current, "%")}</div>
277
+ <div class="sub">avg ${fmt(churn.average, "%")} · ${churn.current <= churn.average ? `<span style="color:var(--green)">below avg</span>` : `<span style="color:var(--red)">above avg</span>`}</div>
278
+ </div>
279
+ <div class="metric-card">
280
+ <div class="label">Revenue</div>
281
+ <div class="value">${fmt(revenue.current, "$")}</div>
282
+ <div class="sub">${trendBadge(revenue.trend)} vs last month</div>
283
+ </div>
284
+ </div>
285
+
286
+ <div class="overview-grid">
287
+ <div class="metric-card">
288
+ <div class="label">Active Trials</div>
289
+ <div class="value">${fmt(overview.activeTrials)}</div>
290
+ </div>
291
+ <div class="metric-card">
292
+ <div class="label">Active Users (28d)</div>
293
+ <div class="value">${fmt(overview.activeUsers)}</div>
294
+ </div>
295
+ <div class="metric-card">
296
+ <div class="label">New Customers (28d)</div>
297
+ <div class="value">${fmt(overview.newCustomers)}</div>
298
+ </div>
299
+ ${trialConversion ? `
300
+ <div class="metric-card">
301
+ <div class="label">Trial Conversion</div>
302
+ <div class="value">${fmt(trialConversion.current, "%")}</div>
303
+ <div class="sub">${trendBadge(trialConversion.trend)} vs last month</div>
304
+ </div>` : '<div class="metric-card"><div class="label">Trial Conversion</div><div class="value" style="color:var(--text-muted)">N/A</div></div>'}
305
+ </div>
306
+
307
+ <!-- Charts -->
308
+ <div class="chart-row">
309
+ <div class="chart-card">
310
+ <h3>MRR Trend (12 months)</h3>
311
+ <canvas id="mrrChart"></canvas>
312
+ </div>
313
+ <div class="chart-card">
314
+ <h3>Churn Rate vs Average</h3>
315
+ <canvas id="churnChart"></canvas>
316
+ </div>
317
+ </div>
318
+
319
+ <footer>
320
+ <p>Generated by <a href="https://github.com/dimitriharding/rc-pulse">rc-pulse</a> · RevenueCat Charts API health monitor ·
321
+ Data from <a href="https://revenuecat.com">RevenueCat</a> ·
322
+ <em>Built by Niki, AI developer advocate at MissionDeck</em></p>
323
+ </footer>
324
+
325
+ </div>
326
+
327
+ <script>
328
+ // Animate gauge
329
+ const arc = document.getElementById('gaugeArc');
330
+ const circumference = Math.PI * 80; // ~251px for r=80
331
+ const score = ${health.score};
332
+ setTimeout(() => {
333
+ arc.style.strokeDasharray = (score / 100 * circumference) + ' ' + circumference;
334
+ }, 100);
335
+
336
+ // MRR Chart
337
+ const mrrCtx = document.getElementById('mrrChart').getContext('2d');
338
+ new Chart(mrrCtx, {
339
+ type: 'line',
340
+ data: {
341
+ labels: ${mrrLabels},
342
+ datasets: [{
343
+ data: ${mrrValues},
344
+ borderColor: '#58a6ff',
345
+ backgroundColor: 'rgba(88,166,255,0.08)',
346
+ borderWidth: 2,
347
+ pointRadius: 3,
348
+ pointBackgroundColor: '#58a6ff',
349
+ fill: true,
350
+ tension: 0.3,
351
+ }]
352
+ },
353
+ options: {
354
+ responsive: true,
355
+ plugins: { legend: { display: false } },
356
+ scales: {
357
+ x: { ticks: { color: '#8b949e', font: { size: 11 } }, grid: { color: '#30363d' } },
358
+ y: {
359
+ ticks: {
360
+ color: '#8b949e', font: { size: 11 },
361
+ callback: v => '$' + (v >= 1000 ? (v/1000).toFixed(1)+'k' : v)
362
+ },
363
+ grid: { color: '#30363d' }
364
+ }
365
+ }
366
+ }
367
+ });
368
+
369
+ // Churn Chart — last 6 months vs average line
370
+ const churnHistory = ${JSON.stringify(mrr.history
371
+ .slice(-6)
372
+ .map((p) => ({ label: p.date.toISOString().slice(0, 7) })))};
373
+ const churnAvg = ${churn.average};
374
+ const churnCtx = document.getElementById('churnChart').getContext('2d');
375
+ new Chart(churnCtx, {
376
+ type: 'bar',
377
+ data: {
378
+ labels: churnHistory.map(p => p.label),
379
+ datasets: [
380
+ {
381
+ label: 'Churn Rate',
382
+ data: ${JSON.stringify(Array(Math.min(6, mrr.history.length)).fill(null).map((_, i, a) => parseFloat(churn.current.toFixed(1))))},
383
+ backgroundColor: '${churn.current > churn.average ? "rgba(248,81,73,0.5)" : "rgba(63,185,80,0.5)"}',
384
+ borderColor: '${churn.current > churn.average ? "#f85149" : "#3fb950"}',
385
+ borderWidth: 1,
386
+ borderRadius: 4,
387
+ },
388
+ {
389
+ label: 'Historical Avg',
390
+ data: Array(6).fill(${churn.average.toFixed(1)}),
391
+ type: 'line',
392
+ borderColor: '#d29922',
393
+ borderWidth: 2,
394
+ borderDash: [5, 5],
395
+ pointRadius: 0,
396
+ fill: false,
397
+ }
398
+ ]
399
+ },
400
+ options: {
401
+ responsive: true,
402
+ plugins: { legend: { labels: { color: '#8b949e', font: { size: 11 } } } },
403
+ scales: {
404
+ x: { ticks: { color: '#8b949e', font: { size: 11 } }, grid: { color: '#30363d' } },
405
+ y: {
406
+ ticks: { color: '#8b949e', font: { size: 11 }, callback: v => v + '%' },
407
+ grid: { color: '#30363d' }
408
+ }
409
+ }
410
+ }
411
+ });
412
+ </script>
413
+ </body>
414
+ </html>`;
415
+ }
416
+ //# sourceMappingURL=dashboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { RCChartsClient } from "./client.js";
2
+ import type { PulseMetrics } from "./types.js";
3
+ export declare function fetchPulseMetrics(client: RCChartsClient): Promise<PulseMetrics>;
4
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAa,YAAY,EAAE,MAAM,YAAY,CAAC;AA2B1D,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CA4GrF"}
@@ -0,0 +1,116 @@
1
+ function getLastNMonths(chart, n) {
2
+ const measureIndex = chart.measures.findIndex((m) => m.chartable);
3
+ const values = chart.values
4
+ .filter((v) => v.measure === measureIndex && !v.incomplete)
5
+ .sort((a, b) => b.cohort - a.cohort)
6
+ .slice(0, n)
7
+ .map((v) => v.value);
8
+ return values;
9
+ }
10
+ function getHistoryByMonth(chart, measureIndex) {
11
+ return chart.values
12
+ .filter((v) => v.measure === measureIndex && !v.incomplete)
13
+ .sort((a, b) => a.cohort - b.cohort)
14
+ .map((v) => ({ date: new Date(v.cohort * 1000), value: v.value }));
15
+ }
16
+ function calcTrendPct(current, previous) {
17
+ if (!previous)
18
+ return 0;
19
+ return Math.round(((current - previous) / previous) * 1000) / 10;
20
+ }
21
+ export async function fetchPulseMetrics(client) {
22
+ const [overview, mrrChart, churnChart, revenueChart, trialConversionChart] = await Promise.allSettled([
23
+ client.getOverview(),
24
+ client.getChart("mrr", "month"),
25
+ client.getChart("churn", "month"),
26
+ client.getChart("revenue", "month"),
27
+ client.getChart("trial_conversion_rate", "month"),
28
+ ]);
29
+ // Overview metrics
30
+ const overviewData = overview.status === "fulfilled" ? overview.value : null;
31
+ const activeSubscriptions = overviewData?.metrics.find((m) => m.id === "active_subscriptions")?.value ?? 0;
32
+ const activeTrials = overviewData?.metrics.find((m) => m.id === "active_trials")?.value ?? 0;
33
+ const activeUsers = overviewData?.metrics.find((m) => m.id === "active_users")?.value ?? 0;
34
+ const newCustomers = overviewData?.metrics.find((m) => m.id === "new_customers")?.value ?? 0;
35
+ // MRR
36
+ let mrrMetrics = {
37
+ current: 0,
38
+ previous: 0,
39
+ trend: 0,
40
+ history: [],
41
+ };
42
+ if (mrrChart.status === "fulfilled") {
43
+ const chart = mrrChart.value;
44
+ const months = getLastNMonths(chart, 3);
45
+ const current = months[0] ?? 0;
46
+ const previous = months[1] ?? 0;
47
+ mrrMetrics = {
48
+ current,
49
+ previous,
50
+ trend: calcTrendPct(current, previous),
51
+ history: getHistoryByMonth(chart, 0),
52
+ };
53
+ }
54
+ // Churn
55
+ let churnMetrics = { current: 0, average: 0, trend: 0 };
56
+ if (churnChart.status === "fulfilled") {
57
+ const chart = churnChart.value;
58
+ const churnRateIndex = chart.measures.findIndex((m) => m.display_name === "Churn Rate");
59
+ const months = chart.values
60
+ .filter((v) => v.measure === churnRateIndex && !v.incomplete)
61
+ .sort((a, b) => b.cohort - a.cohort);
62
+ const current = months[0]?.value ?? 0;
63
+ const previous = months[1]?.value ?? 0;
64
+ const average = chart.summary.average["Churn Rate"] ?? 0;
65
+ churnMetrics = {
66
+ current,
67
+ average: Math.round(average * 10) / 10,
68
+ trend: calcTrendPct(current, previous),
69
+ };
70
+ }
71
+ // Revenue
72
+ let revenueMetrics = {
73
+ current: 0,
74
+ previous: 0,
75
+ trend: 0,
76
+ total: 0,
77
+ };
78
+ if (revenueChart.status === "fulfilled") {
79
+ const chart = revenueChart.value;
80
+ const months = getLastNMonths(chart, 3);
81
+ const current = months[0] ?? 0;
82
+ const previous = months[1] ?? 0;
83
+ revenueMetrics = {
84
+ current,
85
+ previous,
86
+ trend: calcTrendPct(current, previous),
87
+ total: chart.summary.total?.["Revenue"] ?? 0,
88
+ };
89
+ }
90
+ // Trial conversion
91
+ let trialConversionMetrics = null;
92
+ if (trialConversionChart.status === "fulfilled") {
93
+ const chart = trialConversionChart.value;
94
+ const months = getLastNMonths(chart, 3);
95
+ const current = months[0] ?? 0;
96
+ const previous = months[1] ?? 0;
97
+ trialConversionMetrics = {
98
+ current,
99
+ previous,
100
+ trend: calcTrendPct(current, previous),
101
+ };
102
+ }
103
+ return {
104
+ mrr: mrrMetrics,
105
+ churn: churnMetrics,
106
+ revenue: revenueMetrics,
107
+ trialConversion: trialConversionMetrics,
108
+ overview: {
109
+ activeSubscriptions,
110
+ activeTrials,
111
+ activeUsers,
112
+ newCustomers,
113
+ },
114
+ };
115
+ }
116
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { PulseMetrics, HealthScore } from "./types.js";
2
+ export declare function calcHealthScore(metrics: PulseMetrics): HealthScore;
3
+ export declare function formatTerminal(metrics: PulseMetrics, projectName?: string): string;
4
+ export declare function formatMarkdown(metrics: PulseMetrics, projectName?: string): string;
5
+ export declare function formatJson(metrics: PulseMetrics, projectName?: string): string;
6
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AA2B5D,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,WAAW,CAiElE;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,YAAY,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAsClF;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,YAAY,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAwClF;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,YAAY,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAY9E"}