@gpc-cli/core 0.9.23 → 0.9.24

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.
package/dist/index.d.ts CHANGED
@@ -737,6 +737,8 @@ interface StatusVitalMetric {
737
737
  value: number | undefined;
738
738
  threshold: number;
739
739
  status: "ok" | "warn" | "breach" | "unknown";
740
+ previousValue?: number | undefined;
741
+ trend?: "up" | "down" | "flat" | null;
740
742
  }
741
743
  interface StatusRelease {
742
744
  track: string;
@@ -765,8 +767,34 @@ interface AppStatus {
765
767
  };
766
768
  reviews: StatusReviews;
767
769
  }
770
+ interface StatusDiff {
771
+ versionCode: {
772
+ from: string | null;
773
+ to: string | null;
774
+ };
775
+ crashRate: {
776
+ from: number | null;
777
+ to: number | null;
778
+ delta: number | null;
779
+ };
780
+ anrRate: {
781
+ from: number | null;
782
+ to: number | null;
783
+ delta: number | null;
784
+ };
785
+ reviewCount: {
786
+ from: number | null;
787
+ to: number | null;
788
+ };
789
+ averageRating: {
790
+ from: number | null;
791
+ to: number | null;
792
+ delta: number | null;
793
+ };
794
+ }
768
795
  interface GetAppStatusOptions {
769
796
  days?: number;
797
+ sections?: string[];
770
798
  vitalThresholds?: {
771
799
  crashRate?: number;
772
800
  anrRate?: number;
@@ -774,10 +802,23 @@ interface GetAppStatusOptions {
774
802
  slowRenderingRate?: number;
775
803
  };
776
804
  }
805
+ interface WatchOptions {
806
+ intervalSeconds: number;
807
+ render: (status: AppStatus) => string;
808
+ fetch: () => Promise<AppStatus>;
809
+ save: (status: AppStatus) => Promise<void>;
810
+ }
777
811
  declare function loadStatusCache(packageName: string, ttlSeconds?: number): Promise<AppStatus | null>;
778
812
  declare function saveStatusCache(packageName: string, data: AppStatus, ttlSeconds?: number): Promise<void>;
779
813
  declare function getAppStatus(client: PlayApiClient, reporting: ReportingApiClient, packageName: string, options?: GetAppStatusOptions): Promise<AppStatus>;
780
814
  declare function formatStatusTable(status: AppStatus): string;
815
+ declare function formatStatusSummary(status: AppStatus): string;
816
+ declare function computeStatusDiff(prev: AppStatus, curr: AppStatus): StatusDiff;
817
+ declare function formatStatusDiff(diff: StatusDiff, since: string): string;
818
+ declare function runWatchLoop(opts: WatchOptions): Promise<void>;
819
+ /** Returns true if breach state changed (breach started or cleared). */
820
+ declare function trackBreachState(packageName: string, isBreaching: boolean): Promise<boolean>;
821
+ declare function sendNotification(title: string, body: string): void;
781
822
  declare function statusHasBreach(status: AppStatus): boolean;
782
823
 
783
- export { ApiError, type AppInfo, type AppStatus, type AuditEntry, type BatchSyncResult, type BundleAnalysis, type BundleComparison, type BundleEntry, type CommandContext, ConfigError, type DiscoverPluginsOptions, type DryRunPublishResult, type DryRunResult, type DryRunUploadResult, type ExportImagesOptions, type ExportImagesSummary, type FastlaneDetection, type FastlaneLane, type FileValidationResult, GOOGLE_PLAY_LANGUAGES, type GetAppStatusOptions, type GitNotesOptions, type GitReleaseNotes, GpcError, type ImageValidationResult, type InternalSharingUploadResult, type ListIapOptions, type ListSubscriptionsOptions, type ListUsersOptions, type ListVoidedOptions, type ListingDiff, type ListingsResult, type LoadedPlugin, type MigrationResult, NetworkError, type OneTimeProductDiff, PERMISSION_PROPAGATION_WARNING, type ParsedMonth, PluginManager, type PublishOptions, type PublishResult, type PushResult, type ReleaseDiff, type ReleaseNotesValidation, type ReleaseStatusResult, type ReviewExportOptions, type ReviewsFilterOptions, SENSITIVE_ARG_KEYS, SENSITIVE_KEYS, type ScaffoldOptions, type ScaffoldResult, type Spinner, type StatusRelease, type StatusReviews, type StatusVitalMetric, type SubscriptionDiff, type SyncResult, type ThresholdResult, type UploadResult, type ValidateCheck, type ValidateOptions, type ValidateResult, type VitalsOverview, type VitalsQueryOptions, type VitalsTrendComparison, type WebhookPayload, acknowledgeProductPurchase, activateBasePlan, activateOffer, activatePurchaseOption, addRecoveryTargeting, addTesters, analyzeBundle, batchSyncInAppProducts, cancelRecoveryAction, cancelSubscriptionPurchase, checkThreshold, clearAuditLog, compareBundles, compareVitalsTrend, consumeProductPurchase, convertRegionPrices, createAuditEntry, createDeviceTier, createExternalTransaction, createInAppProduct, createOffer, createOneTimeOffer, createOneTimeProduct, createPurchaseOption, createRecoveryAction, createSpinner, createSubscription, createTrack, deactivateBasePlan, deactivateOffer, deactivatePurchaseOption, deferSubscriptionPurchase, deleteBasePlan, deleteImage, deleteInAppProduct, deleteListing, deleteOffer, deleteOneTimeOffer, deleteOneTimeProduct, deleteSubscription, deployRecoveryAction, detectFastlane, detectOutputFormat, diffListings, diffListingsCommand, diffOneTimeProduct, diffReleases, diffSubscription, discoverPlugins, downloadGeneratedApk, downloadReport, exportDataSafety, exportImages, exportReviews, formatCustomPayload, formatDiscordPayload, formatJunit, formatOutput, formatSlackPayload, formatStatusTable, generateMigrationPlan, generateNotesFromGit, getAppInfo, getAppStatus, getCountryAvailability, getDataSafety, getDeviceTier, getExternalTransaction, getInAppProduct, getListings, getOffer, getOneTimeOffer, getOneTimeProduct, getProductPurchase, getPurchaseOption, getReleasesStatus, getReview, getSubscription, getSubscriptionPurchase, getUser, getVitalsAnomalies, getVitalsAnr, getVitalsBattery, getVitalsCrashes, getVitalsMemory, getVitalsOverview, getVitalsRendering, getVitalsStartup, importDataSafety, importTestersFromCsv, initAudit, inviteUser, isFinancialReportType, isStatsReportType, isValidBcp47, isValidReportType, isValidStatsDimension, listAuditEvents, listDeviceTiers, listGeneratedApks, listImages, listInAppProducts, listOffers, listOneTimeOffers, listOneTimeProducts, listPurchaseOptions, listRecoveryActions, listReports, listReviews, listSubscriptions, listTesters, listTracks, listUsers, listVoidedPurchases, loadStatusCache, migratePrices, parseAppfile, parseFastfile, parseGrantArg, parseMonth, promoteRelease, publish, pullListings, pushListings, readListingsFromDir, readReleaseNotesFromDir, redactAuditArgs, redactSensitive, refundExternalTransaction, refundOrder, removeTesters, removeUser, replyToReview, revokeSubscriptionPurchase, safePath, safePathWithin, saveStatusCache, scaffoldPlugin, searchAuditEvents, searchVitalsErrors, sendWebhook, sortResults, statusHasBreach, syncInAppProducts, updateAppDetails, updateDataSafety, updateInAppProduct, updateListing, updateOffer, updateOneTimeOffer, updateOneTimeProduct, updateRollout, updateSubscription, updateTrackConfig, updateUser, uploadExternallyHosted, uploadImage, uploadInternalSharing, uploadRelease, validateImage, validateLanguageCode, validatePackageName, validatePreSubmission, validateReleaseNotes, validateSku, validateTrackName, validateUploadFile, validateVersionCode, writeAuditLog, writeListingsToDir, writeMigrationOutput };
824
+ export { ApiError, type AppInfo, type AppStatus, type AuditEntry, type BatchSyncResult, type BundleAnalysis, type BundleComparison, type BundleEntry, type CommandContext, ConfigError, type DiscoverPluginsOptions, type DryRunPublishResult, type DryRunResult, type DryRunUploadResult, type ExportImagesOptions, type ExportImagesSummary, type FastlaneDetection, type FastlaneLane, type FileValidationResult, GOOGLE_PLAY_LANGUAGES, type GetAppStatusOptions, type GitNotesOptions, type GitReleaseNotes, GpcError, type ImageValidationResult, type InternalSharingUploadResult, type ListIapOptions, type ListSubscriptionsOptions, type ListUsersOptions, type ListVoidedOptions, type ListingDiff, type ListingsResult, type LoadedPlugin, type MigrationResult, NetworkError, type OneTimeProductDiff, PERMISSION_PROPAGATION_WARNING, type ParsedMonth, PluginManager, type PublishOptions, type PublishResult, type PushResult, type ReleaseDiff, type ReleaseNotesValidation, type ReleaseStatusResult, type ReviewExportOptions, type ReviewsFilterOptions, SENSITIVE_ARG_KEYS, SENSITIVE_KEYS, type ScaffoldOptions, type ScaffoldResult, type Spinner, type StatusDiff, type StatusRelease, type StatusReviews, type StatusVitalMetric, type SubscriptionDiff, type SyncResult, type ThresholdResult, type UploadResult, type ValidateCheck, type ValidateOptions, type ValidateResult, type VitalsOverview, type VitalsQueryOptions, type VitalsTrendComparison, type WatchOptions, type WebhookPayload, acknowledgeProductPurchase, activateBasePlan, activateOffer, activatePurchaseOption, addRecoveryTargeting, addTesters, analyzeBundle, batchSyncInAppProducts, cancelRecoveryAction, cancelSubscriptionPurchase, checkThreshold, clearAuditLog, compareBundles, compareVitalsTrend, computeStatusDiff, consumeProductPurchase, convertRegionPrices, createAuditEntry, createDeviceTier, createExternalTransaction, createInAppProduct, createOffer, createOneTimeOffer, createOneTimeProduct, createPurchaseOption, createRecoveryAction, createSpinner, createSubscription, createTrack, deactivateBasePlan, deactivateOffer, deactivatePurchaseOption, deferSubscriptionPurchase, deleteBasePlan, deleteImage, deleteInAppProduct, deleteListing, deleteOffer, deleteOneTimeOffer, deleteOneTimeProduct, deleteSubscription, deployRecoveryAction, detectFastlane, detectOutputFormat, diffListings, diffListingsCommand, diffOneTimeProduct, diffReleases, diffSubscription, discoverPlugins, downloadGeneratedApk, downloadReport, exportDataSafety, exportImages, exportReviews, formatCustomPayload, formatDiscordPayload, formatJunit, formatOutput, formatSlackPayload, formatStatusDiff, formatStatusSummary, formatStatusTable, generateMigrationPlan, generateNotesFromGit, getAppInfo, getAppStatus, getCountryAvailability, getDataSafety, getDeviceTier, getExternalTransaction, getInAppProduct, getListings, getOffer, getOneTimeOffer, getOneTimeProduct, getProductPurchase, getPurchaseOption, getReleasesStatus, getReview, getSubscription, getSubscriptionPurchase, getUser, getVitalsAnomalies, getVitalsAnr, getVitalsBattery, getVitalsCrashes, getVitalsMemory, getVitalsOverview, getVitalsRendering, getVitalsStartup, importDataSafety, importTestersFromCsv, initAudit, inviteUser, isFinancialReportType, isStatsReportType, isValidBcp47, isValidReportType, isValidStatsDimension, listAuditEvents, listDeviceTiers, listGeneratedApks, listImages, listInAppProducts, listOffers, listOneTimeOffers, listOneTimeProducts, listPurchaseOptions, listRecoveryActions, listReports, listReviews, listSubscriptions, listTesters, listTracks, listUsers, listVoidedPurchases, loadStatusCache, migratePrices, parseAppfile, parseFastfile, parseGrantArg, parseMonth, promoteRelease, publish, pullListings, pushListings, readListingsFromDir, readReleaseNotesFromDir, redactAuditArgs, redactSensitive, refundExternalTransaction, refundOrder, removeTesters, removeUser, replyToReview, revokeSubscriptionPurchase, runWatchLoop, safePath, safePathWithin, saveStatusCache, scaffoldPlugin, searchAuditEvents, searchVitalsErrors, sendNotification, sendWebhook, sortResults, statusHasBreach, syncInAppProducts, trackBreachState, updateAppDetails, updateDataSafety, updateInAppProduct, updateListing, updateOffer, updateOneTimeOffer, updateOneTimeProduct, updateRollout, updateSubscription, updateTrackConfig, updateUser, uploadExternallyHosted, uploadImage, uploadInternalSharing, uploadRelease, validateImage, validateLanguageCode, validatePackageName, validatePreSubmission, validateReleaseNotes, validateSku, validateTrackName, validateUploadFile, validateVersionCode, writeAuditLog, writeListingsToDir, writeMigrationOutput };
package/dist/index.js CHANGED
@@ -39,9 +39,9 @@ var NetworkError = class extends GpcError {
39
39
  };
40
40
 
41
41
  // src/output.ts
42
- import process from "process";
42
+ import process2 from "process";
43
43
  function detectOutputFormat() {
44
- return process.stdout.isTTY ? "table" : "json";
44
+ return process2.stdout.isTTY ? "table" : "json";
45
45
  }
46
46
  function formatOutput(data, format, redact = true) {
47
47
  const safe = redact ? redactSensitive(data) : data;
@@ -3183,23 +3183,23 @@ async function deleteOneTimeOffer(client, packageName, productId, offerId) {
3183
3183
  }
3184
3184
 
3185
3185
  // src/utils/spinner.ts
3186
- import process2 from "process";
3186
+ import process3 from "process";
3187
3187
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3188
3188
  var INTERVAL_MS = 80;
3189
3189
  function createSpinner(message) {
3190
- const isTTY = process2.stderr.isTTY === true;
3190
+ const isTTY = process3.stderr.isTTY === true;
3191
3191
  let frameIndex = 0;
3192
3192
  let timer;
3193
3193
  let currentMessage = message;
3194
3194
  let started = false;
3195
3195
  function clearLine() {
3196
3196
  if (isTTY) {
3197
- process2.stderr.write("\r\x1B[K");
3197
+ process3.stderr.write("\r\x1B[K");
3198
3198
  }
3199
3199
  }
3200
3200
  function renderFrame() {
3201
3201
  const frame = FRAMES[frameIndex % FRAMES.length];
3202
- process2.stderr.write(`\r\x1B[K${frame} ${currentMessage}`);
3202
+ process3.stderr.write(`\r\x1B[K${frame} ${currentMessage}`);
3203
3203
  frameIndex++;
3204
3204
  }
3205
3205
  return {
@@ -3207,7 +3207,7 @@ function createSpinner(message) {
3207
3207
  if (started) return;
3208
3208
  started = true;
3209
3209
  if (!isTTY) {
3210
- process2.stderr.write(`${currentMessage}
3210
+ process3.stderr.write(`${currentMessage}
3211
3211
  `);
3212
3212
  return;
3213
3213
  }
@@ -3222,10 +3222,10 @@ function createSpinner(message) {
3222
3222
  const text = msg ?? currentMessage;
3223
3223
  if (isTTY) {
3224
3224
  clearLine();
3225
- process2.stderr.write(`\u2714 ${text}
3225
+ process3.stderr.write(`\u2714 ${text}
3226
3226
  `);
3227
3227
  } else if (!started) {
3228
- process2.stderr.write(`${text}
3228
+ process3.stderr.write(`${text}
3229
3229
  `);
3230
3230
  }
3231
3231
  started = false;
@@ -3238,10 +3238,10 @@ function createSpinner(message) {
3238
3238
  const text = msg ?? currentMessage;
3239
3239
  if (isTTY) {
3240
3240
  clearLine();
3241
- process2.stderr.write(`\u2718 ${text}
3241
+ process3.stderr.write(`\u2718 ${text}
3242
3242
  `);
3243
3243
  } else if (!started) {
3244
- process2.stderr.write(`${text}
3244
+ process3.stderr.write(`${text}
3245
3245
  `);
3246
3246
  }
3247
3247
  started = false;
@@ -3975,6 +3975,7 @@ function compareBundles(before, after) {
3975
3975
 
3976
3976
  // src/commands/status.ts
3977
3977
  import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
3978
+ import { execSync } from "child_process";
3978
3979
  import { join as join7 } from "path";
3979
3980
  import { getCacheDir } from "@gpc-cli/config";
3980
3981
  var DEFAULT_TTL_SECONDS = 3600;
@@ -3996,12 +3997,11 @@ async function saveStatusCache(packageName, data, ttlSeconds = DEFAULT_TTL_SECON
3996
3997
  try {
3997
3998
  const dir = getCacheDir();
3998
3999
  await mkdir5(dir, { recursive: true });
3999
- const entry = {
4000
- fetchedAt: data.fetchedAt,
4001
- ttl: ttlSeconds,
4002
- data
4003
- };
4004
- await writeFile7(cacheFilePath(packageName), JSON.stringify(entry, null, 2), { encoding: "utf-8", mode: 384 });
4000
+ const entry = { fetchedAt: data.fetchedAt, ttl: ttlSeconds, data };
4001
+ await writeFile7(cacheFilePath(packageName), JSON.stringify(entry, null, 2), {
4002
+ encoding: "utf-8",
4003
+ mode: 384
4004
+ });
4005
4005
  } catch {
4006
4006
  }
4007
4007
  }
@@ -4021,9 +4021,9 @@ var WARN_MARGIN = 0.2;
4021
4021
  function toApiDate(d) {
4022
4022
  return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };
4023
4023
  }
4024
- async function queryVitalForStatus(reporting, packageName, metricSet, days) {
4024
+ async function queryVitalForStatus(reporting, packageName, metricSet, days, offsetDays = 0) {
4025
4025
  const DAY_MS = 24 * 60 * 60 * 1e3;
4026
- const baseMs = Date.now() - 2 * DAY_MS;
4026
+ const baseMs = Date.now() - 2 * DAY_MS - offsetDays * DAY_MS;
4027
4027
  const end = new Date(baseMs);
4028
4028
  const start = new Date(baseMs - days * DAY_MS);
4029
4029
  const metrics = METRIC_SET_METRICS2[metricSet] ?? ["distinctUsers"];
@@ -4044,13 +4044,26 @@ async function queryVitalForStatus(reporting, packageName, metricSet, days) {
4044
4044
  if (values.length === 0) return void 0;
4045
4045
  return values.reduce((a, b) => a + b, 0) / values.length;
4046
4046
  }
4047
- function toVitalMetric(value, threshold) {
4048
- if (value === void 0) {
4049
- return { value: void 0, threshold, status: "unknown" };
4050
- }
4051
- if (value > threshold) return { value, threshold, status: "breach" };
4052
- if (value > threshold * (1 - WARN_MARGIN)) return { value, threshold, status: "warn" };
4053
- return { value, threshold, status: "ok" };
4047
+ async function queryVitalWithTrend(reporting, packageName, metricSet, days) {
4048
+ const [current, previous] = await Promise.all([
4049
+ queryVitalForStatus(reporting, packageName, metricSet, days, 0),
4050
+ queryVitalForStatus(reporting, packageName, metricSet, days, days)
4051
+ ]);
4052
+ let trend = null;
4053
+ if (current !== void 0 && previous !== void 0) {
4054
+ if (current > previous) trend = "up";
4055
+ else if (current < previous) trend = "down";
4056
+ else trend = "flat";
4057
+ }
4058
+ return { current, previous, trend };
4059
+ }
4060
+ var SKIPPED_VITAL = { current: void 0, previous: void 0, trend: null };
4061
+ function toVitalMetric(value, threshold, previousValue, trend) {
4062
+ const base = previousValue !== void 0 ? { value, threshold, status: "unknown", previousValue, trend: trend ?? null } : { value, threshold, status: "unknown" };
4063
+ if (value === void 0) return { ...base, status: "unknown" };
4064
+ if (value > threshold) return { ...base, status: "breach" };
4065
+ if (value > threshold * (1 - WARN_MARGIN)) return { ...base, status: "warn" };
4066
+ return { ...base, status: "ok" };
4054
4067
  }
4055
4068
  function computeReviewSentiment(reviews, windowDays) {
4056
4069
  const now = Date.now();
@@ -4089,6 +4102,7 @@ function computeReviewSentiment(reviews, windowDays) {
4089
4102
  }
4090
4103
  async function getAppStatus(client, reporting, packageName, options = {}) {
4091
4104
  const days = options.days ?? 7;
4105
+ const sections = new Set(options.sections ?? ["releases", "vitals", "reviews"]);
4092
4106
  const thresholds = {
4093
4107
  crashRate: options.vitalThresholds?.crashRate ?? DEFAULT_THRESHOLDS.crashRate,
4094
4108
  anrRate: options.vitalThresholds?.anrRate ?? DEFAULT_THRESHOLDS.anrRate,
@@ -4096,12 +4110,12 @@ async function getAppStatus(client, reporting, packageName, options = {}) {
4096
4110
  slowRenderingRate: options.vitalThresholds?.slowRenderingRate ?? DEFAULT_THRESHOLDS.slowRenderingRate
4097
4111
  };
4098
4112
  const [releasesResult, crashesResult, anrResult, slowStartResult, slowRenderResult, reviewsResult] = await Promise.allSettled([
4099
- getReleasesStatus(client, packageName),
4100
- queryVitalForStatus(reporting, packageName, "crashRateMetricSet", days),
4101
- queryVitalForStatus(reporting, packageName, "anrRateMetricSet", days),
4102
- queryVitalForStatus(reporting, packageName, "slowStartRateMetricSet", days),
4103
- queryVitalForStatus(reporting, packageName, "slowRenderingRateMetricSet", days),
4104
- listReviews(client, packageName, { maxResults: 500 })
4113
+ sections.has("releases") ? getReleasesStatus(client, packageName) : Promise.resolve([]),
4114
+ sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "crashRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
4115
+ sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "anrRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
4116
+ sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "slowStartRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
4117
+ sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "slowRenderingRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
4118
+ sections.has("reviews") ? listReviews(client, packageName, { maxResults: 500 }) : Promise.resolve([])
4105
4119
  ]);
4106
4120
  const rawReleases = releasesResult.status === "fulfilled" ? releasesResult.value : [];
4107
4121
  const releases = rawReleases.map((r) => ({
@@ -4110,36 +4124,49 @@ async function getAppStatus(client, reporting, packageName, options = {}) {
4110
4124
  status: r.status,
4111
4125
  userFraction: r.userFraction ?? null
4112
4126
  }));
4113
- const crashValue = crashesResult.status === "fulfilled" ? crashesResult.value : void 0;
4114
- const anrValue = anrResult.status === "fulfilled" ? anrResult.value : void 0;
4115
- const slowStartValue = slowStartResult.status === "fulfilled" ? slowStartResult.value : void 0;
4116
- const slowRenderValue = slowRenderResult.status === "fulfilled" ? slowRenderResult.value : void 0;
4127
+ const crashes = crashesResult.status === "fulfilled" ? crashesResult.value : SKIPPED_VITAL;
4128
+ const anr = anrResult.status === "fulfilled" ? anrResult.value : SKIPPED_VITAL;
4129
+ const slowStart = slowStartResult.status === "fulfilled" ? slowStartResult.value : SKIPPED_VITAL;
4130
+ const slowRender = slowRenderResult.status === "fulfilled" ? slowRenderResult.value : SKIPPED_VITAL;
4117
4131
  const rawReviews = reviewsResult.status === "fulfilled" ? reviewsResult.value : [];
4118
4132
  const reviews = computeReviewSentiment(rawReviews, 30);
4119
- const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
4120
4133
  return {
4121
4134
  packageName,
4122
- fetchedAt,
4135
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
4123
4136
  cached: false,
4124
4137
  releases,
4125
4138
  vitals: {
4126
4139
  windowDays: days,
4127
- crashes: toVitalMetric(crashValue, thresholds.crashRate),
4128
- anr: toVitalMetric(anrValue, thresholds.anrRate),
4129
- slowStarts: toVitalMetric(slowStartValue, thresholds.slowStartRate),
4130
- slowRender: toVitalMetric(slowRenderValue, thresholds.slowRenderingRate)
4140
+ crashes: toVitalMetric(crashes.current, thresholds.crashRate, crashes.previous, crashes.trend),
4141
+ anr: toVitalMetric(anr.current, thresholds.anrRate, anr.previous, anr.trend),
4142
+ slowStarts: toVitalMetric(
4143
+ slowStart.current,
4144
+ thresholds.slowStartRate,
4145
+ slowStart.previous,
4146
+ slowStart.trend
4147
+ ),
4148
+ slowRender: toVitalMetric(
4149
+ slowRender.current,
4150
+ thresholds.slowRenderingRate,
4151
+ slowRender.previous,
4152
+ slowRender.trend
4153
+ )
4131
4154
  },
4132
4155
  reviews
4133
4156
  };
4134
4157
  }
4135
4158
  function vitalIndicator(metric) {
4136
- if (metric.status === "unknown") return "?";
4159
+ if (metric.status === "unknown") return "\u2014";
4137
4160
  if (metric.status === "breach") return "\u2717";
4138
4161
  if (metric.status === "warn") return "\u26A0";
4139
4162
  return "\u2713";
4140
4163
  }
4164
+ function vitalTrendArrow(metric) {
4165
+ if (!metric.trend || metric.trend === "flat") return "";
4166
+ return metric.trend === "up" ? " \u2191" : " \u2193";
4167
+ }
4141
4168
  function formatVitalValue(metric) {
4142
- if (metric.value === void 0) return "n/a";
4169
+ if (metric.value === void 0) return "\u2014";
4143
4170
  return `${(metric.value * 100).toFixed(2)}%`;
4144
4171
  }
4145
4172
  function formatFraction(fraction) {
@@ -4147,7 +4174,7 @@ function formatFraction(fraction) {
4147
4174
  return `${Math.round(fraction * 100)}%`;
4148
4175
  }
4149
4176
  function formatRating(rating) {
4150
- if (rating === void 0) return "n/a";
4177
+ if (rating === void 0) return "\u2014";
4151
4178
  return `\u2605 ${rating.toFixed(1)}`;
4152
4179
  }
4153
4180
  function formatTrend(current, previous) {
@@ -4156,6 +4183,9 @@ function formatTrend(current, previous) {
4156
4183
  if (current < previous) return ` \u2193 from ${previous.toFixed(1)}`;
4157
4184
  return "";
4158
4185
  }
4186
+ function allVitalsUnknown(vitals) {
4187
+ return vitals.crashes.status === "unknown" && vitals.anr.status === "unknown" && vitals.slowStarts.status === "unknown" && vitals.slowRender.status === "unknown";
4188
+ }
4159
4189
  function formatStatusTable(status) {
4160
4190
  const lines = [];
4161
4191
  const cachedLabel = status.cached ? ` (cached ${new Date(status.fetchedAt).toLocaleTimeString()})` : ` (fetched ${new Date(status.fetchedAt).toLocaleTimeString()})`;
@@ -4176,23 +4206,188 @@ function formatStatusTable(status) {
4176
4206
  }
4177
4207
  lines.push("");
4178
4208
  lines.push(`VITALS (last ${status.vitals.windowDays} days)`);
4179
- const { crashes, anr, slowStarts, slowRender } = status.vitals;
4180
- lines.push(
4181
- ` crashes ${formatVitalValue(crashes).padEnd(8)} ${vitalIndicator(crashes)} anr ${formatVitalValue(anr).padEnd(8)} ${vitalIndicator(anr)}`
4182
- );
4183
- lines.push(
4184
- ` slow starts ${formatVitalValue(slowStarts).padEnd(8)} ${vitalIndicator(slowStarts)} slow render ${formatVitalValue(slowRender).padEnd(8)} ${vitalIndicator(slowRender)}`
4185
- );
4209
+ if (allVitalsUnknown(status.vitals)) {
4210
+ lines.push(" No vitals data available for this period.");
4211
+ } else {
4212
+ const { crashes, anr, slowStarts, slowRender } = status.vitals;
4213
+ const crashVal = `${formatVitalValue(crashes)}${vitalTrendArrow(crashes)}`;
4214
+ const anrVal = `${formatVitalValue(anr)}${vitalTrendArrow(anr)}`;
4215
+ const slowStartVal = `${formatVitalValue(slowStarts)}${vitalTrendArrow(slowStarts)}`;
4216
+ const slowRenderVal = `${formatVitalValue(slowRender)}${vitalTrendArrow(slowRender)}`;
4217
+ lines.push(
4218
+ ` crashes ${crashVal.padEnd(10)} ${vitalIndicator(crashes)} anr ${anrVal.padEnd(10)} ${vitalIndicator(anr)}`
4219
+ );
4220
+ lines.push(
4221
+ ` slow starts ${slowStartVal.padEnd(10)} ${vitalIndicator(slowStarts)} slow render ${slowRenderVal.padEnd(10)} ${vitalIndicator(slowRender)}`
4222
+ );
4223
+ }
4186
4224
  lines.push("");
4187
4225
  lines.push(`REVIEWS (last ${status.reviews.windowDays} days)`);
4188
4226
  const { averageRating, previousAverageRating, totalNew, positivePercent } = status.reviews;
4189
- const trend = formatTrend(averageRating, previousAverageRating);
4190
- const positiveStr = positivePercent !== void 0 ? ` ${positivePercent}% positive` : "";
4227
+ if (totalNew === 0 && averageRating === void 0) {
4228
+ lines.push(" No reviews in this period.");
4229
+ } else {
4230
+ const trend = formatTrend(averageRating, previousAverageRating);
4231
+ const positiveStr = positivePercent !== void 0 ? ` ${positivePercent}% positive` : "";
4232
+ lines.push(` ${formatRating(averageRating)} ${totalNew} new${positiveStr}${trend}`);
4233
+ }
4234
+ return lines.join("\n");
4235
+ }
4236
+ function formatStatusSummary(status) {
4237
+ const parts = [status.packageName];
4238
+ const latestRelease = status.releases.find((r) => r.status !== "draft") ?? status.releases[0];
4239
+ if (latestRelease) {
4240
+ parts.push(`v${latestRelease.versionCode} ${latestRelease.track}`);
4241
+ }
4242
+ const { crashes, anr } = status.vitals;
4243
+ if (crashes.status !== "unknown") {
4244
+ const arrow = crashes.trend === "up" ? " \u2191" : crashes.trend === "down" ? " \u2193" : "";
4245
+ parts.push(`crashes ${formatVitalValue(crashes)}${arrow} ${vitalIndicator(crashes)}`);
4246
+ }
4247
+ if (anr.status !== "unknown") {
4248
+ const arrow = anr.trend === "up" ? " \u2191" : anr.trend === "down" ? " \u2193" : "";
4249
+ parts.push(`ANR ${formatVitalValue(anr)}${arrow} ${vitalIndicator(anr)}`);
4250
+ }
4251
+ const { averageRating, totalNew } = status.reviews;
4252
+ if (averageRating !== void 0) {
4253
+ parts.push(`avg ${averageRating.toFixed(1)}\u2605`);
4254
+ }
4255
+ if (totalNew > 0) {
4256
+ parts.push(`${totalNew} reviews`);
4257
+ }
4258
+ return parts.join(" \xB7 ") + (statusHasBreach(status) ? " [ALERT]" : "");
4259
+ }
4260
+ function computeStatusDiff(prev, curr) {
4261
+ const prevVersion = prev.releases[0]?.versionCode ?? null;
4262
+ const currVersion = curr.releases[0]?.versionCode ?? null;
4263
+ const prevCrash = prev.vitals.crashes.value ?? null;
4264
+ const currCrash = curr.vitals.crashes.value ?? null;
4265
+ const prevAnr = prev.vitals.anr.value ?? null;
4266
+ const currAnr = curr.vitals.anr.value ?? null;
4267
+ const prevRating = prev.reviews.averageRating ?? null;
4268
+ const currRating = curr.reviews.averageRating ?? null;
4269
+ return {
4270
+ versionCode: { from: prevVersion, to: currVersion },
4271
+ crashRate: {
4272
+ from: prevCrash,
4273
+ to: currCrash,
4274
+ delta: currCrash !== null && prevCrash !== null ? currCrash - prevCrash : null
4275
+ },
4276
+ anrRate: {
4277
+ from: prevAnr,
4278
+ to: currAnr,
4279
+ delta: currAnr !== null && prevAnr !== null ? currAnr - prevAnr : null
4280
+ },
4281
+ reviewCount: { from: prev.reviews.totalNew, to: curr.reviews.totalNew },
4282
+ averageRating: {
4283
+ from: prevRating,
4284
+ to: currRating,
4285
+ delta: currRating !== null && prevRating !== null ? Math.round((currRating - prevRating) * 10) / 10 : null
4286
+ }
4287
+ };
4288
+ }
4289
+ function formatStatusDiff(diff, since) {
4290
+ const lines = [`Changes since ${since}:`];
4291
+ if (diff.versionCode.from !== diff.versionCode.to) {
4292
+ lines.push(` Version: ${diff.versionCode.from ?? "\u2014"} \u2192 ${diff.versionCode.to ?? "\u2014"}`);
4293
+ }
4294
+ const fmtRate = (v) => v !== null ? `${(v * 100).toFixed(2)}%` : "\u2014";
4295
+ const fmtDelta = (d, lowerIsBetter = true) => {
4296
+ if (d === null || Math.abs(d) < 1e-4) return "no change";
4297
+ const sign = d > 0 ? "+" : "";
4298
+ const good = lowerIsBetter ? d < 0 : d > 0;
4299
+ return `${sign}${(d * 100).toFixed(2)}% ${good ? "\u2713" : "\u2717"}`;
4300
+ };
4191
4301
  lines.push(
4192
- ` ${formatRating(averageRating)} ${totalNew} new${positiveStr}${trend}`
4302
+ ` Crash rate: ${fmtRate(diff.crashRate.from)} \u2192 ${fmtRate(diff.crashRate.to)} (${fmtDelta(diff.crashRate.delta)})`
4193
4303
  );
4304
+ lines.push(
4305
+ ` ANR rate: ${fmtRate(diff.anrRate.from)} \u2192 ${fmtRate(diff.anrRate.to)} (${fmtDelta(diff.anrRate.delta)})`
4306
+ );
4307
+ const ratingDelta = diff.averageRating.delta;
4308
+ const prevR = diff.averageRating.from !== null ? `${diff.averageRating.from.toFixed(1)}\u2605` : "\u2014";
4309
+ const currR = diff.averageRating.to !== null ? `${diff.averageRating.to.toFixed(1)}\u2605` : "\u2014";
4310
+ const ratingStr = ratingDelta === null || Math.abs(ratingDelta) < 0.05 ? "no change" : `${ratingDelta > 0 ? "+" : ""}${ratingDelta.toFixed(1)} ${ratingDelta > 0 ? "\u2713" : "\u2717"}`;
4311
+ lines.push(` Reviews: ${prevR} \u2192 ${currR} (${ratingStr})`);
4194
4312
  return lines.join("\n");
4195
4313
  }
4314
+ async function runWatchLoop(opts) {
4315
+ if (opts.intervalSeconds < 10) {
4316
+ console.error("Error: --watch interval must be at least 10 seconds");
4317
+ process.exit(2);
4318
+ }
4319
+ let running = true;
4320
+ const cleanup = () => {
4321
+ running = false;
4322
+ process.stdout.write("\n");
4323
+ process.exit(0);
4324
+ };
4325
+ process.on("SIGINT", cleanup);
4326
+ process.on("SIGTERM", cleanup);
4327
+ while (running) {
4328
+ process.stdout.write("\x1B[2J\x1B[H");
4329
+ try {
4330
+ const status = await opts.fetch();
4331
+ await opts.save(status);
4332
+ console.log(opts.render(status));
4333
+ } catch (err) {
4334
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
4335
+ }
4336
+ console.log(`
4337
+ [gpc status] Refreshing in ${opts.intervalSeconds}s\u2026 (Ctrl+C to stop)`);
4338
+ for (let i = 0; i < opts.intervalSeconds && running; i++) {
4339
+ await new Promise((r) => setTimeout(r, 1e3));
4340
+ }
4341
+ }
4342
+ }
4343
+ function breachStateFilePath(packageName) {
4344
+ return join7(getCacheDir(), `breach-state-${packageName}.json`);
4345
+ }
4346
+ async function trackBreachState(packageName, isBreaching) {
4347
+ const filePath = breachStateFilePath(packageName);
4348
+ let prevBreaching = false;
4349
+ try {
4350
+ const raw = await readFile10(filePath, "utf-8");
4351
+ prevBreaching = JSON.parse(raw).breaching;
4352
+ } catch {
4353
+ }
4354
+ if (prevBreaching !== isBreaching) {
4355
+ try {
4356
+ await mkdir5(getCacheDir(), { recursive: true });
4357
+ await writeFile7(
4358
+ filePath,
4359
+ JSON.stringify({ breaching: isBreaching, since: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
4360
+ { encoding: "utf-8", mode: 384 }
4361
+ );
4362
+ } catch {
4363
+ }
4364
+ return true;
4365
+ }
4366
+ return false;
4367
+ }
4368
+ function sendNotification(title, body) {
4369
+ if (process.env["CI"]) return;
4370
+ try {
4371
+ const p = process.platform;
4372
+ if (p === "darwin") {
4373
+ execSync(
4374
+ `osascript -e 'display notification ${JSON.stringify(body)} with title ${JSON.stringify(title)}'`,
4375
+ { stdio: "ignore" }
4376
+ );
4377
+ } else if (p === "linux") {
4378
+ execSync(`notify-send ${JSON.stringify(title)} ${JSON.stringify(body)}`, {
4379
+ stdio: "ignore"
4380
+ });
4381
+ } else if (p === "win32") {
4382
+ const escaped = (s) => s.replace(/'/g, "''");
4383
+ execSync(
4384
+ `powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${escaped(body)}', '${escaped(title)}')"`,
4385
+ { stdio: "ignore" }
4386
+ );
4387
+ }
4388
+ } catch {
4389
+ }
4390
+ }
4196
4391
  function statusHasBreach(status) {
4197
4392
  return status.vitals.crashes.status === "breach" || status.vitals.anr.status === "breach" || status.vitals.slowStarts.status === "breach" || status.vitals.slowRender.status === "breach";
4198
4393
  }
@@ -4220,6 +4415,7 @@ export {
4220
4415
  clearAuditLog,
4221
4416
  compareBundles,
4222
4417
  compareVitalsTrend,
4418
+ computeStatusDiff,
4223
4419
  consumeProductPurchase,
4224
4420
  convertRegionPrices,
4225
4421
  createAuditEntry,
@@ -4265,6 +4461,8 @@ export {
4265
4461
  formatJunit,
4266
4462
  formatOutput,
4267
4463
  formatSlackPayload,
4464
+ formatStatusDiff,
4465
+ formatStatusSummary,
4268
4466
  formatStatusTable,
4269
4467
  generateMigrationPlan,
4270
4468
  generateNotesFromGit,
@@ -4340,16 +4538,19 @@ export {
4340
4538
  removeUser,
4341
4539
  replyToReview,
4342
4540
  revokeSubscriptionPurchase,
4541
+ runWatchLoop,
4343
4542
  safePath,
4344
4543
  safePathWithin,
4345
4544
  saveStatusCache,
4346
4545
  scaffoldPlugin,
4347
4546
  searchAuditEvents,
4348
4547
  searchVitalsErrors,
4548
+ sendNotification,
4349
4549
  sendWebhook,
4350
4550
  sortResults,
4351
4551
  statusHasBreach,
4352
4552
  syncInAppProducts,
4553
+ trackBreachState,
4353
4554
  updateAppDetails,
4354
4555
  updateDataSafety,
4355
4556
  updateInAppProduct,