eas-cli 18.7.0 → 18.8.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.
Files changed (41) hide show
  1. package/README.md +147 -90
  2. package/build/channel/insights/formatInsights.d.ts +47 -0
  3. package/build/channel/insights/formatInsights.js +108 -0
  4. package/build/commands/channel/insights.d.ts +18 -0
  5. package/build/commands/channel/insights.js +71 -0
  6. package/build/commands/observe/events.d.ts +1 -0
  7. package/build/commands/observe/events.js +17 -4
  8. package/build/commands/observe/metrics.d.ts +1 -0
  9. package/build/commands/observe/metrics.js +18 -6
  10. package/build/commands/observe/versions.d.ts +1 -0
  11. package/build/commands/observe/versions.js +17 -4
  12. package/build/commands/update/insights.d.ts +19 -0
  13. package/build/commands/update/insights.js +75 -0
  14. package/build/commands/update/view.d.ts +4 -0
  15. package/build/commands/update/view.js +47 -2
  16. package/build/credentials/ios/appstore/capabilityIdentifiers.js +28 -3
  17. package/build/graphql/client.d.ts +13 -0
  18. package/build/graphql/client.js +36 -1
  19. package/build/graphql/generated.d.ts +193 -0
  20. package/build/graphql/generated.js +8 -2
  21. package/build/graphql/queries/ChannelInsightsQuery.d.ts +12 -0
  22. package/build/graphql/queries/ChannelInsightsQuery.js +81 -0
  23. package/build/graphql/queries/UpdateInsightsQuery.d.ts +10 -0
  24. package/build/graphql/queries/UpdateInsightsQuery.js +53 -0
  25. package/build/graphql/types/Observe.js +1 -0
  26. package/build/insights/formatTimespan.d.ts +7 -0
  27. package/build/insights/formatTimespan.js +15 -0
  28. package/build/insights/timeRange.d.ts +10 -0
  29. package/build/insights/timeRange.js +10 -0
  30. package/build/metadata/apple/tasks/previews.js +41 -15
  31. package/build/observe/formatEvents.d.ts +3 -0
  32. package/build/observe/formatEvents.js +4 -0
  33. package/build/observe/formatMetrics.d.ts +3 -2
  34. package/build/observe/formatMetrics.js +16 -27
  35. package/build/observe/formatVersions.js +2 -8
  36. package/build/update/insights/formatInsights.d.ts +34 -0
  37. package/build/update/insights/formatInsights.js +128 -0
  38. package/build/utils/renderTextTable.d.ts +6 -0
  39. package/build/utils/renderTextTable.js +23 -0
  40. package/oclif.manifest.json +585 -287
  41. package/package.json +5 -5
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.INSIGHTS_DEFAULT_DAYS_BACK = void 0;
4
+ exports.resolveInsightsTimeRange = resolveInsightsTimeRange;
5
+ const startAndEndTime_1 = require("../observe/startAndEndTime");
6
+ exports.INSIGHTS_DEFAULT_DAYS_BACK = 7;
7
+ function resolveInsightsTimeRange(flags) {
8
+ const days = flags.days ?? (flags.start ? undefined : exports.INSIGHTS_DEFAULT_DAYS_BACK);
9
+ return (0, startAndEndTime_1.resolveTimeRange)({ ...flags, days });
10
+ }
@@ -95,19 +95,45 @@ class PreviewsTask extends task_1.AppleTask {
95
95
  }
96
96
  for (const localeCode of locales) {
97
97
  const previews = config.getPreviews(localeCode);
98
- if (!previews || Object.keys(previews).length === 0) {
99
- continue;
100
- }
98
+ const existingSets = context.previewSets.get(localeCode);
101
99
  const localization = context.versionLocales.find(l => l.attributes.locale === localeCode);
102
100
  if (!localization) {
103
101
  log_1.default.warn((0, chalk_1.default) `{yellow Skipping video previews for ${localeCode} - locale not found}`);
104
102
  continue;
105
103
  }
106
- for (const [previewType, previewConfig] of Object.entries(previews)) {
107
- if (!previewConfig) {
108
- continue;
104
+ // Upload/sync configured previews
105
+ if (previews) {
106
+ for (const [previewType, previewConfig] of Object.entries(previews)) {
107
+ if (!previewConfig) {
108
+ continue;
109
+ }
110
+ if (!apple_utils_1.ALL_PREVIEW_TYPES.includes(previewType)) {
111
+ const strippedType = previewType.replace(/^APP_/, '');
112
+ const suggestion = apple_utils_1.ALL_PREVIEW_TYPES.includes(strippedType)
113
+ ? (0, chalk_1.default) ` Did you mean {bold ${strippedType}}? Preview types don't use the "APP_" prefix (that's only for screenshots).`
114
+ : '';
115
+ log_1.default.warn((0, chalk_1.default) `{yellow Unknown preview type {bold ${previewType}} for ${localeCode}, skipping.${suggestion}}`);
116
+ log_1.default.warn((0, chalk_1.default) `{yellow Valid preview types: ${apple_utils_1.ALL_PREVIEW_TYPES.join(', ')}}`);
117
+ continue;
118
+ }
119
+ await syncPreviewSetAsync(context.projectDir, localization, previewType, normalizePreviewConfig(previewConfig), existingSets);
120
+ }
121
+ }
122
+ // Delete remote previews for types no longer in config
123
+ if (existingSets) {
124
+ for (const [previewType, previewSet] of existingSets) {
125
+ if (previews?.[previewType]) {
126
+ continue;
127
+ }
128
+ const existingPreviews = previewSet.attributes.appPreviews || [];
129
+ for (const preview of existingPreviews) {
130
+ await (0, log_2.logAsync)(() => preview.deleteAsync(), {
131
+ pending: `Deleting video preview ${chalk_1.default.bold(preview.attributes.fileName)} (${localeCode})...`,
132
+ success: `Deleted video preview ${chalk_1.default.bold(preview.attributes.fileName)} (${localeCode})`,
133
+ failure: `Failed deleting video preview ${chalk_1.default.bold(preview.attributes.fileName)} (${localeCode})`,
134
+ });
135
+ }
109
136
  }
110
- await syncPreviewSetAsync(context.projectDir, localization, previewType, normalizePreviewConfig(previewConfig), context.previewSets.get(localeCode));
111
137
  }
112
138
  }
113
139
  }
@@ -156,15 +182,15 @@ async function syncPreviewSetAsync(projectDir, localization, previewType, previe
156
182
  log_1.default.log((0, chalk_1.default) `{dim Preview ${fileName} already exists, skipping upload}`);
157
183
  return;
158
184
  }
159
- // Delete existing previews that don't match
185
+ // Delete all existing previews before uploading the new one.
186
+ // Apple limits each set to 3 previews, and we manage one preview per set,
187
+ // so we need to clean up stale entries to avoid "Too many app previews" errors.
160
188
  for (const preview of existingPreviews) {
161
- if (preview.attributes.fileName !== fileName) {
162
- await (0, log_2.logAsync)(() => preview.deleteAsync(), {
163
- pending: `Deleting old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})...`,
164
- success: `Deleted old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})`,
165
- failure: `Failed deleting old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})`,
166
- });
167
- }
189
+ await (0, log_2.logAsync)(() => preview.deleteAsync(), {
190
+ pending: `Deleting old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})...`,
191
+ success: `Deleted old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})`,
192
+ failure: `Failed deleting old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})`,
193
+ });
168
194
  }
169
195
  // Upload new preview
170
196
  await (0, log_2.logAsync)(() => apple_utils_1.AppPreview.uploadAsync(localization.context, {
@@ -13,6 +13,9 @@ export interface ObserveEventJson {
13
13
  sessionId: string | null;
14
14
  easClientId: string;
15
15
  timestamp: string;
16
+ customParams: {
17
+ [key: string]: any;
18
+ } | null;
16
19
  }
17
20
  export interface BuildEventsTableOptions {
18
21
  metricName: string;
@@ -23,6 +23,9 @@ function formatDate(isoString) {
23
23
  day: 'numeric',
24
24
  });
25
25
  }
26
+ function resolveCustomParams(event) {
27
+ return event.customParams ?? null;
28
+ }
26
29
  function buildObserveEventsTable(events, pageInfo, options) {
27
30
  if (events.length === 0) {
28
31
  return chalk_1.default.yellow('No events found.');
@@ -90,6 +93,7 @@ function buildObserveEventsJson(events, pageInfo) {
90
93
  sessionId: event.sessionId ?? null,
91
94
  easClientId: event.easClientId,
92
95
  timestamp: event.timestamp,
96
+ customParams: resolveCustomParams(event),
93
97
  })),
94
98
  pageInfo: {
95
99
  hasNextPage: pageInfo.hasNextPage,
@@ -22,17 +22,18 @@ export type MetricValuesJson = Partial<Record<StatisticKey, number | null>>;
22
22
  export interface ObserveMetricsVersionResult {
23
23
  appVersion: string;
24
24
  platform: AppPlatform;
25
+ buildNumbers: string[];
26
+ updateIds: string[];
25
27
  metrics: Record<string, MetricValuesJson>;
26
28
  }
27
29
  export interface ObserveMetricsJsonOutput {
28
30
  versions: ObserveMetricsVersionResult[];
29
31
  totalEventCounts: Record<string, Record<string, number>>;
30
32
  }
31
- export declare function buildObserveMetricsJson(metricsMap: ObserveMetricsMap, metricNames: string[], stats: StatisticKey[], totalEventCounts?: Map<string, number>): ObserveMetricsJsonOutput;
33
+ export declare function buildObserveMetricsJson(metricsMap: ObserveMetricsMap, metricNames: string[], stats: StatisticKey[], totalEventCounts?: Map<string, number>, buildNumbersMap?: BuildNumbersMap, updateIdsMap?: UpdateIdsMap): ObserveMetricsJsonOutput;
32
34
  export declare function buildObserveMetricsTable(metricsMap: ObserveMetricsMap, metricNames: string[], stats: StatisticKey[], options?: {
33
35
  daysBack?: number;
34
36
  buildNumbersMap?: BuildNumbersMap;
35
- updateIdsMap?: UpdateIdsMap;
36
37
  totalEventCounts?: Map<string, number>;
37
38
  }): string;
38
39
  export {};
@@ -9,6 +9,7 @@ const tslib_1 = require("tslib");
9
9
  const chalk_1 = tslib_1.__importDefault(require("chalk"));
10
10
  const errors_1 = require("../commandUtils/errors");
11
11
  const platform_1 = require("../platform");
12
+ const renderTextTable_1 = tslib_1.__importDefault(require("../utils/renderTextTable"));
12
13
  const metricNames_1 = require("./metricNames");
13
14
  exports.STAT_ALIASES = {
14
15
  min: 'min',
@@ -65,7 +66,7 @@ function parseMetricsKey(key) {
65
66
  platform: key.slice(lastColon + 1),
66
67
  };
67
68
  }
68
- function buildObserveMetricsJson(metricsMap, metricNames, stats, totalEventCounts) {
69
+ function buildObserveMetricsJson(metricsMap, metricNames, stats, totalEventCounts, buildNumbersMap, updateIdsMap) {
69
70
  const versions = [];
70
71
  for (const [key, versionMetrics] of metricsMap) {
71
72
  const { appVersion, platform } = parseMetricsKey(key);
@@ -78,7 +79,13 @@ function buildObserveMetricsJson(metricsMap, metricNames, stats, totalEventCount
78
79
  }
79
80
  metrics[metricName] = statValues;
80
81
  }
81
- versions.push({ appVersion, platform, metrics });
82
+ versions.push({
83
+ appVersion,
84
+ platform,
85
+ buildNumbers: buildNumbersMap?.get(key) ?? [],
86
+ updateIds: updateIdsMap?.get(key) ?? [],
87
+ metrics,
88
+ });
82
89
  }
83
90
  // Group total event counts by metric → platform
84
91
  const counts = {};
@@ -104,19 +111,6 @@ function buildTimeRangeDescription(daysBack) {
104
111
  }
105
112
  return '';
106
113
  }
107
- function renderTable(headers, rows, footerRow) {
108
- const allRows = footerRow ? [...rows, footerRow] : rows;
109
- const colWidths = headers.map((h, i) => Math.max(h.length, ...allRows.map(r => (r[i] ?? '').length)));
110
- const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' ');
111
- const separatorLine = colWidths.map(w => '-'.repeat(w)).join(' ');
112
- const dataLines = rows.map(row => row.map((cell, i) => cell.padEnd(colWidths[i])).join(' '));
113
- const lines = [chalk_1.default.bold(headerLine), separatorLine, ...dataLines];
114
- if (footerRow) {
115
- lines.push(separatorLine);
116
- lines.push(footerRow.map((cell, i) => cell.padEnd(colWidths[i])).join(' '));
117
- }
118
- return lines.join('\n');
119
- }
120
114
  function buildObserveMetricsTable(metricsMap, metricNames, stats, options) {
121
115
  const { versions: results } = buildObserveMetricsJson(metricsMap, metricNames, stats);
122
116
  if (results.length === 0) {
@@ -154,23 +148,18 @@ function buildObserveMetricsTable(metricsMap, metricNames, stats, options) {
154
148
  }
155
149
  }
156
150
  }
157
- // Check if any version has updates
158
- const hasUpdates = options?.updateIdsMap
159
- ? Array.from(options.updateIdsMap.values()).some(ids => ids.length > 0)
160
- : false;
161
- const headers = ['App Version', ...(hasUpdates ? ['Updates'] : []), ...metricHeaders];
151
+ const headers = ['App Version', ...metricHeaders];
162
152
  const sections = [chalk_1.default.bold(summaryLine)];
163
153
  for (const [platform, platformResults] of byPlatform) {
164
154
  sections.push('');
165
155
  sections.push(chalk_1.default.bold(platform_1.appPlatformDisplayNames[platform]));
166
- const rows = platformResults.map(result => {
156
+ const rows = [];
157
+ for (const result of platformResults) {
167
158
  const key = makeMetricsKey(result.appVersion, result.platform);
168
159
  const buildNumbers = options?.buildNumbersMap?.get(key);
169
160
  const versionLabel = buildNumbers?.length
170
161
  ? `${result.appVersion} (${buildNumbers.join(', ')})`
171
162
  : result.appVersion;
172
- const updateIds = options?.updateIdsMap?.get(key);
173
- const updatesLabel = updateIds?.length ? updateIds.join(', ') : '';
174
163
  const metricCells = [];
175
164
  for (const m of metricNames) {
176
165
  const values = result.metrics[m];
@@ -187,8 +176,8 @@ function buildObserveMetricsTable(metricsMap, metricNames, stats, options) {
187
176
  }
188
177
  }
189
178
  }
190
- return [versionLabel, ...(hasUpdates ? [updatesLabel] : []), ...metricCells];
191
- });
179
+ rows.push([versionLabel, ...metricCells]);
180
+ }
192
181
  let footerRow;
193
182
  if (options?.totalEventCounts) {
194
183
  const countCells = [];
@@ -197,10 +186,10 @@ function buildObserveMetricsTable(metricsMap, metricNames, stats, options) {
197
186
  countCells.push(count != null ? count.toLocaleString() : '-');
198
187
  }
199
188
  if (countCells.some(c => c !== '-')) {
200
- footerRow = ['Total events', ...(hasUpdates ? [''] : []), ...countCells];
189
+ footerRow = ['Total events', ...countCells];
201
190
  }
202
191
  }
203
- sections.push(renderTable(headers, rows, footerRow));
192
+ sections.push((0, renderTextTable_1.default)(headers, rows, footerRow));
204
193
  }
205
194
  return sections.join('\n');
206
195
  }
@@ -5,6 +5,7 @@ exports.buildObserveVersionsTable = buildObserveVersionsTable;
5
5
  const tslib_1 = require("tslib");
6
6
  const chalk_1 = tslib_1.__importDefault(require("chalk"));
7
7
  const platform_1 = require("../platform");
8
+ const renderTextTable_1 = tslib_1.__importDefault(require("../utils/renderTextTable"));
8
9
  function formatDate(isoString) {
9
10
  const date = new Date(isoString);
10
11
  return date.toLocaleDateString('en-US', {
@@ -56,13 +57,6 @@ function buildObserveVersionsJson(results) {
56
57
  }
57
58
  return output;
58
59
  }
59
- function renderTable(headers, rows) {
60
- const colWidths = headers.map((h, i) => Math.max(h.length, ...rows.map(r => r[i].length)));
61
- const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' ');
62
- const separatorLine = colWidths.map(w => '-'.repeat(w)).join(' ');
63
- const dataLines = rows.map(row => row.map((cell, i) => cell.padEnd(colWidths[i])).join(' '));
64
- return [chalk_1.default.bold(headerLine), separatorLine, ...dataLines].join('\n');
65
- }
66
60
  function buildObserveVersionsTable(results) {
67
61
  const hasAnyVersions = results.some(r => r.appVersions.length > 0);
68
62
  if (!hasAnyVersions) {
@@ -86,7 +80,7 @@ function buildObserveVersionsTable(results) {
86
80
  String(version.buildNumbers.length),
87
81
  String(version.updates.length),
88
82
  ]);
89
- sections.push(renderTable(headers, rows));
83
+ sections.push((0, renderTextTable_1.default)(headers, rows));
90
84
  }
91
85
  return sections.join('\n');
92
86
  }
@@ -0,0 +1,34 @@
1
+ import { UpdateWithInsightsObject } from '../../graphql/queries/UpdateInsightsQuery';
2
+ export interface UpdateInsightsTimespan {
3
+ startTime: string;
4
+ endTime: string;
5
+ daysBack?: number;
6
+ }
7
+ export interface UpdateInsightsDailyEntry {
8
+ date: string;
9
+ installs: number;
10
+ failedInstalls: number;
11
+ }
12
+ export interface UpdateInsightsPlatformSummary {
13
+ platform: string;
14
+ updateId: string;
15
+ totalUniqueUsers: number;
16
+ totalInstalls: number;
17
+ totalFailedInstalls: number;
18
+ crashRatePercent: number;
19
+ launchAssetCount: number;
20
+ averageUpdatePayloadBytes: number;
21
+ daily: UpdateInsightsDailyEntry[];
22
+ }
23
+ export interface UpdateInsightsSummary {
24
+ groupId: string;
25
+ startTime: string;
26
+ endTime: string;
27
+ daysBack?: number;
28
+ platforms: UpdateInsightsPlatformSummary[];
29
+ }
30
+ export declare function toUpdateInsightsSummary(groupId: string, updates: UpdateWithInsightsObject[], timespan: UpdateInsightsTimespan): UpdateInsightsSummary;
31
+ export declare function buildUpdateInsightsJson(summary: UpdateInsightsSummary): object;
32
+ export declare function buildUpdateInsightsTable(summary: UpdateInsightsSummary): string;
33
+ export declare function formatPercent(value: number): string;
34
+ export declare function formatBytes(bytes: number): string;
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toUpdateInsightsSummary = toUpdateInsightsSummary;
4
+ exports.buildUpdateInsightsJson = buildUpdateInsightsJson;
5
+ exports.buildUpdateInsightsTable = buildUpdateInsightsTable;
6
+ exports.formatPercent = formatPercent;
7
+ exports.formatBytes = formatBytes;
8
+ const tslib_1 = require("tslib");
9
+ const chalk_1 = tslib_1.__importDefault(require("chalk"));
10
+ const formatTimespan_1 = require("../../insights/formatTimespan");
11
+ const formatFields_1 = tslib_1.__importDefault(require("../../utils/formatFields"));
12
+ const renderTextTable_1 = tslib_1.__importDefault(require("../../utils/renderTextTable"));
13
+ function toUpdateInsightsSummary(groupId, updates, timespan) {
14
+ const platforms = updates
15
+ .map(toPlatformSummary)
16
+ .sort((a, b) => a.platform.localeCompare(b.platform));
17
+ return {
18
+ groupId,
19
+ startTime: timespan.startTime,
20
+ endTime: timespan.endTime,
21
+ daysBack: timespan.daysBack,
22
+ platforms,
23
+ };
24
+ }
25
+ function toPlatformSummary(update) {
26
+ const { insights } = update;
27
+ const { totalInstalls, totalFailedInstalls } = insights.cumulativeMetrics.metricsAtLastTimestamp;
28
+ const denom = totalInstalls + totalFailedInstalls;
29
+ const crashRatePercent = denom === 0 ? 0 : (totalFailedInstalls / denom) * 100;
30
+ const { labels, installsDataset, failedInstallsDataset } = insights.cumulativeMetrics.data;
31
+ const daily = labels.map((date, i) => ({
32
+ date,
33
+ installs: installsDataset.difference[i] ?? 0,
34
+ failedInstalls: failedInstallsDataset.difference[i] ?? 0,
35
+ }));
36
+ return {
37
+ platform: update.platform,
38
+ updateId: update.id,
39
+ totalUniqueUsers: insights.totalUniqueUsers,
40
+ totalInstalls,
41
+ totalFailedInstalls,
42
+ crashRatePercent,
43
+ launchAssetCount: insights.cumulativeAverageMetrics.launchAssetCount,
44
+ averageUpdatePayloadBytes: insights.cumulativeAverageMetrics.averageUpdatePayloadBytes,
45
+ daily,
46
+ };
47
+ }
48
+ function buildUpdateInsightsJson(summary) {
49
+ return {
50
+ groupId: summary.groupId,
51
+ timespan: {
52
+ start: summary.startTime,
53
+ end: summary.endTime,
54
+ ...(summary.daysBack !== undefined ? { daysBack: summary.daysBack } : {}),
55
+ },
56
+ platforms: summary.platforms.map(p => ({
57
+ platform: p.platform,
58
+ updateId: p.updateId,
59
+ totals: {
60
+ uniqueUsers: p.totalUniqueUsers,
61
+ installs: p.totalInstalls,
62
+ failedInstalls: p.totalFailedInstalls,
63
+ crashRatePercent: p.crashRatePercent,
64
+ },
65
+ payload: {
66
+ launchAssetCount: p.launchAssetCount,
67
+ averageUpdatePayloadBytes: p.averageUpdatePayloadBytes,
68
+ },
69
+ daily: p.daily,
70
+ })),
71
+ };
72
+ }
73
+ function buildUpdateInsightsTable(summary) {
74
+ const sections = [];
75
+ sections.push(chalk_1.default.bold('Update group insights:'));
76
+ sections.push((0, formatFields_1.default)([
77
+ { label: 'Group ID', value: summary.groupId },
78
+ { label: 'Time range', value: (0, formatTimespan_1.formatTimespan)(summary) },
79
+ { label: 'Platforms', value: summary.platforms.map(p => p.platform).join(', ') || 'N/A' },
80
+ ]));
81
+ const dailyHeader = summary.daysBack ? ` (last ${summary.daysBack} days)` : '';
82
+ for (const platform of summary.platforms) {
83
+ sections.push('');
84
+ sections.push(chalk_1.default.bold(`${chalk_1.default.cyan(platform.platform)}:`));
85
+ sections.push((0, formatFields_1.default)([
86
+ { label: 'Update ID', value: platform.updateId },
87
+ { label: 'Launches', value: platform.totalInstalls.toLocaleString() },
88
+ { label: 'Failed launches', value: platform.totalFailedInstalls.toLocaleString() },
89
+ { label: 'Crash rate', value: formatPercent(platform.crashRatePercent) },
90
+ { label: 'Unique users', value: platform.totalUniqueUsers.toLocaleString() },
91
+ { label: 'Launch assets', value: platform.launchAssetCount.toLocaleString() },
92
+ { label: 'Avg payload size', value: formatBytes(platform.averageUpdatePayloadBytes) },
93
+ ]));
94
+ if (platform.daily.length > 0) {
95
+ sections.push('');
96
+ sections.push(chalk_1.default.bold(` Daily breakdown${dailyHeader}:`));
97
+ sections.push('');
98
+ sections.push(indent(renderDailyTable(platform.daily), 2));
99
+ }
100
+ }
101
+ return sections.join('\n');
102
+ }
103
+ function formatPercent(value) {
104
+ return `${value.toFixed(2)}%`;
105
+ }
106
+ function formatBytes(bytes) {
107
+ if (bytes < 1024) {
108
+ return `${bytes} B`;
109
+ }
110
+ if (bytes < 1024 * 1024) {
111
+ return `${(bytes / 1024).toFixed(1)} KB`;
112
+ }
113
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
114
+ }
115
+ function renderDailyTable(rows) {
116
+ return (0, renderTextTable_1.default)(['Date', 'Launches', 'Crashes'], rows.map(r => [
117
+ (0, formatTimespan_1.toDateOnly)(r.date),
118
+ r.installs.toLocaleString(),
119
+ r.failedInstalls.toLocaleString(),
120
+ ]));
121
+ }
122
+ function indent(text, spaces) {
123
+ const pad = ' '.repeat(spaces);
124
+ return text
125
+ .split('\n')
126
+ .map(line => (line.length > 0 ? pad + line : line))
127
+ .join('\n');
128
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Render a simple column-aligned text table with a bold header row, a dashed
3
+ * separator, the data rows, and an optional footer row (preceded by its own
4
+ * separator). Columns are padded to the widest cell in that column.
5
+ */
6
+ export default function renderTextTable(headers: string[], rows: string[][], footerRow?: string[]): string;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = renderTextTable;
4
+ const tslib_1 = require("tslib");
5
+ const chalk_1 = tslib_1.__importDefault(require("chalk"));
6
+ /**
7
+ * Render a simple column-aligned text table with a bold header row, a dashed
8
+ * separator, the data rows, and an optional footer row (preceded by its own
9
+ * separator). Columns are padded to the widest cell in that column.
10
+ */
11
+ function renderTextTable(headers, rows, footerRow) {
12
+ const allRows = footerRow ? [...rows, footerRow] : rows;
13
+ const colWidths = headers.map((h, i) => Math.max(h.length, ...allRows.map(r => (r[i] ?? '').length)));
14
+ const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' ');
15
+ const separatorLine = colWidths.map(w => '-'.repeat(w)).join(' ');
16
+ const dataLines = rows.map(row => row.map((cell, i) => (cell ?? '').padEnd(colWidths[i])).join(' '));
17
+ const lines = [chalk_1.default.bold(headerLine), separatorLine, ...dataLines];
18
+ if (footerRow) {
19
+ lines.push(separatorLine);
20
+ lines.push(footerRow.map((cell, i) => (cell ?? '').padEnd(colWidths[i])).join(' '));
21
+ }
22
+ return lines.join('\n');
23
+ }