@gpc-cli/core 0.9.22 → 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;
@@ -242,11 +242,29 @@ function buildTestCase(item, commandName, index = 0) {
242
242
  };
243
243
  }
244
244
  const record = item;
245
- const name = escapeXml(
246
- String(
247
- record["name"] ?? record["title"] ?? record["sku"] ?? record["id"] ?? record["reviewId"] ?? record["productId"] ?? record["packageName"] ?? record["track"] ?? record["trackId"] ?? record["versionCode"] ?? record["region"] ?? record["languageCode"] ?? `item-${index + 1}`
248
- )
249
- );
245
+ const CANDIDATE_KEYS = [
246
+ "name",
247
+ "title",
248
+ "sku",
249
+ "id",
250
+ "reviewId",
251
+ "productId",
252
+ "packageName",
253
+ "track",
254
+ "trackId",
255
+ "versionCode",
256
+ "region",
257
+ "languageCode"
258
+ ];
259
+ let resolvedName = `item-${index + 1}`;
260
+ for (const key of CANDIDATE_KEYS) {
261
+ const val = record[key];
262
+ if (val != null && val !== "" && val !== "-") {
263
+ resolvedName = String(val);
264
+ break;
265
+ }
266
+ }
267
+ const name = escapeXml(resolvedName);
250
268
  const classname = `gpc.${escapeXml(commandName)}`;
251
269
  const breached = record["breached"];
252
270
  if (breached === true) {
@@ -3165,23 +3183,23 @@ async function deleteOneTimeOffer(client, packageName, productId, offerId) {
3165
3183
  }
3166
3184
 
3167
3185
  // src/utils/spinner.ts
3168
- import process2 from "process";
3186
+ import process3 from "process";
3169
3187
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3170
3188
  var INTERVAL_MS = 80;
3171
3189
  function createSpinner(message) {
3172
- const isTTY = process2.stderr.isTTY === true;
3190
+ const isTTY = process3.stderr.isTTY === true;
3173
3191
  let frameIndex = 0;
3174
3192
  let timer;
3175
3193
  let currentMessage = message;
3176
3194
  let started = false;
3177
3195
  function clearLine() {
3178
3196
  if (isTTY) {
3179
- process2.stderr.write("\r\x1B[K");
3197
+ process3.stderr.write("\r\x1B[K");
3180
3198
  }
3181
3199
  }
3182
3200
  function renderFrame() {
3183
3201
  const frame = FRAMES[frameIndex % FRAMES.length];
3184
- process2.stderr.write(`\r\x1B[K${frame} ${currentMessage}`);
3202
+ process3.stderr.write(`\r\x1B[K${frame} ${currentMessage}`);
3185
3203
  frameIndex++;
3186
3204
  }
3187
3205
  return {
@@ -3189,7 +3207,7 @@ function createSpinner(message) {
3189
3207
  if (started) return;
3190
3208
  started = true;
3191
3209
  if (!isTTY) {
3192
- process2.stderr.write(`${currentMessage}
3210
+ process3.stderr.write(`${currentMessage}
3193
3211
  `);
3194
3212
  return;
3195
3213
  }
@@ -3204,10 +3222,10 @@ function createSpinner(message) {
3204
3222
  const text = msg ?? currentMessage;
3205
3223
  if (isTTY) {
3206
3224
  clearLine();
3207
- process2.stderr.write(`\u2714 ${text}
3225
+ process3.stderr.write(`\u2714 ${text}
3208
3226
  `);
3209
3227
  } else if (!started) {
3210
- process2.stderr.write(`${text}
3228
+ process3.stderr.write(`${text}
3211
3229
  `);
3212
3230
  }
3213
3231
  started = false;
@@ -3220,10 +3238,10 @@ function createSpinner(message) {
3220
3238
  const text = msg ?? currentMessage;
3221
3239
  if (isTTY) {
3222
3240
  clearLine();
3223
- process2.stderr.write(`\u2718 ${text}
3241
+ process3.stderr.write(`\u2718 ${text}
3224
3242
  `);
3225
3243
  } else if (!started) {
3226
- process2.stderr.write(`${text}
3244
+ process3.stderr.write(`${text}
3227
3245
  `);
3228
3246
  }
3229
3247
  started = false;
@@ -3957,6 +3975,7 @@ function compareBundles(before, after) {
3957
3975
 
3958
3976
  // src/commands/status.ts
3959
3977
  import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
3978
+ import { execSync } from "child_process";
3960
3979
  import { join as join7 } from "path";
3961
3980
  import { getCacheDir } from "@gpc-cli/config";
3962
3981
  var DEFAULT_TTL_SECONDS = 3600;
@@ -3978,12 +3997,11 @@ async function saveStatusCache(packageName, data, ttlSeconds = DEFAULT_TTL_SECON
3978
3997
  try {
3979
3998
  const dir = getCacheDir();
3980
3999
  await mkdir5(dir, { recursive: true });
3981
- const entry = {
3982
- fetchedAt: data.fetchedAt,
3983
- ttl: ttlSeconds,
3984
- data
3985
- };
3986
- 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
+ });
3987
4005
  } catch {
3988
4006
  }
3989
4007
  }
@@ -4003,9 +4021,9 @@ var WARN_MARGIN = 0.2;
4003
4021
  function toApiDate(d) {
4004
4022
  return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };
4005
4023
  }
4006
- async function queryVitalForStatus(reporting, packageName, metricSet, days) {
4024
+ async function queryVitalForStatus(reporting, packageName, metricSet, days, offsetDays = 0) {
4007
4025
  const DAY_MS = 24 * 60 * 60 * 1e3;
4008
- const baseMs = Date.now() - 2 * DAY_MS;
4026
+ const baseMs = Date.now() - 2 * DAY_MS - offsetDays * DAY_MS;
4009
4027
  const end = new Date(baseMs);
4010
4028
  const start = new Date(baseMs - days * DAY_MS);
4011
4029
  const metrics = METRIC_SET_METRICS2[metricSet] ?? ["distinctUsers"];
@@ -4026,13 +4044,26 @@ async function queryVitalForStatus(reporting, packageName, metricSet, days) {
4026
4044
  if (values.length === 0) return void 0;
4027
4045
  return values.reduce((a, b) => a + b, 0) / values.length;
4028
4046
  }
4029
- function toVitalMetric(value, threshold) {
4030
- if (value === void 0) {
4031
- return { value: void 0, threshold, status: "unknown" };
4032
- }
4033
- if (value > threshold) return { value, threshold, status: "breach" };
4034
- if (value > threshold * (1 - WARN_MARGIN)) return { value, threshold, status: "warn" };
4035
- 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" };
4036
4067
  }
4037
4068
  function computeReviewSentiment(reviews, windowDays) {
4038
4069
  const now = Date.now();
@@ -4071,6 +4102,7 @@ function computeReviewSentiment(reviews, windowDays) {
4071
4102
  }
4072
4103
  async function getAppStatus(client, reporting, packageName, options = {}) {
4073
4104
  const days = options.days ?? 7;
4105
+ const sections = new Set(options.sections ?? ["releases", "vitals", "reviews"]);
4074
4106
  const thresholds = {
4075
4107
  crashRate: options.vitalThresholds?.crashRate ?? DEFAULT_THRESHOLDS.crashRate,
4076
4108
  anrRate: options.vitalThresholds?.anrRate ?? DEFAULT_THRESHOLDS.anrRate,
@@ -4078,12 +4110,12 @@ async function getAppStatus(client, reporting, packageName, options = {}) {
4078
4110
  slowRenderingRate: options.vitalThresholds?.slowRenderingRate ?? DEFAULT_THRESHOLDS.slowRenderingRate
4079
4111
  };
4080
4112
  const [releasesResult, crashesResult, anrResult, slowStartResult, slowRenderResult, reviewsResult] = await Promise.allSettled([
4081
- getReleasesStatus(client, packageName),
4082
- queryVitalForStatus(reporting, packageName, "crashRateMetricSet", days),
4083
- queryVitalForStatus(reporting, packageName, "anrRateMetricSet", days),
4084
- queryVitalForStatus(reporting, packageName, "slowStartRateMetricSet", days),
4085
- queryVitalForStatus(reporting, packageName, "slowRenderingRateMetricSet", days),
4086
- 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([])
4087
4119
  ]);
4088
4120
  const rawReleases = releasesResult.status === "fulfilled" ? releasesResult.value : [];
4089
4121
  const releases = rawReleases.map((r) => ({
@@ -4092,36 +4124,49 @@ async function getAppStatus(client, reporting, packageName, options = {}) {
4092
4124
  status: r.status,
4093
4125
  userFraction: r.userFraction ?? null
4094
4126
  }));
4095
- const crashValue = crashesResult.status === "fulfilled" ? crashesResult.value : void 0;
4096
- const anrValue = anrResult.status === "fulfilled" ? anrResult.value : void 0;
4097
- const slowStartValue = slowStartResult.status === "fulfilled" ? slowStartResult.value : void 0;
4098
- 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;
4099
4131
  const rawReviews = reviewsResult.status === "fulfilled" ? reviewsResult.value : [];
4100
4132
  const reviews = computeReviewSentiment(rawReviews, 30);
4101
- const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
4102
4133
  return {
4103
4134
  packageName,
4104
- fetchedAt,
4135
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
4105
4136
  cached: false,
4106
4137
  releases,
4107
4138
  vitals: {
4108
4139
  windowDays: days,
4109
- crashes: toVitalMetric(crashValue, thresholds.crashRate),
4110
- anr: toVitalMetric(anrValue, thresholds.anrRate),
4111
- slowStarts: toVitalMetric(slowStartValue, thresholds.slowStartRate),
4112
- 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
+ )
4113
4154
  },
4114
4155
  reviews
4115
4156
  };
4116
4157
  }
4117
4158
  function vitalIndicator(metric) {
4118
- if (metric.status === "unknown") return "?";
4159
+ if (metric.status === "unknown") return "\u2014";
4119
4160
  if (metric.status === "breach") return "\u2717";
4120
4161
  if (metric.status === "warn") return "\u26A0";
4121
4162
  return "\u2713";
4122
4163
  }
4164
+ function vitalTrendArrow(metric) {
4165
+ if (!metric.trend || metric.trend === "flat") return "";
4166
+ return metric.trend === "up" ? " \u2191" : " \u2193";
4167
+ }
4123
4168
  function formatVitalValue(metric) {
4124
- if (metric.value === void 0) return "n/a";
4169
+ if (metric.value === void 0) return "\u2014";
4125
4170
  return `${(metric.value * 100).toFixed(2)}%`;
4126
4171
  }
4127
4172
  function formatFraction(fraction) {
@@ -4129,7 +4174,7 @@ function formatFraction(fraction) {
4129
4174
  return `${Math.round(fraction * 100)}%`;
4130
4175
  }
4131
4176
  function formatRating(rating) {
4132
- if (rating === void 0) return "n/a";
4177
+ if (rating === void 0) return "\u2014";
4133
4178
  return `\u2605 ${rating.toFixed(1)}`;
4134
4179
  }
4135
4180
  function formatTrend(current, previous) {
@@ -4138,6 +4183,9 @@ function formatTrend(current, previous) {
4138
4183
  if (current < previous) return ` \u2193 from ${previous.toFixed(1)}`;
4139
4184
  return "";
4140
4185
  }
4186
+ function allVitalsUnknown(vitals) {
4187
+ return vitals.crashes.status === "unknown" && vitals.anr.status === "unknown" && vitals.slowStarts.status === "unknown" && vitals.slowRender.status === "unknown";
4188
+ }
4141
4189
  function formatStatusTable(status) {
4142
4190
  const lines = [];
4143
4191
  const cachedLabel = status.cached ? ` (cached ${new Date(status.fetchedAt).toLocaleTimeString()})` : ` (fetched ${new Date(status.fetchedAt).toLocaleTimeString()})`;
@@ -4158,23 +4206,188 @@ function formatStatusTable(status) {
4158
4206
  }
4159
4207
  lines.push("");
4160
4208
  lines.push(`VITALS (last ${status.vitals.windowDays} days)`);
4161
- const { crashes, anr, slowStarts, slowRender } = status.vitals;
4162
- lines.push(
4163
- ` crashes ${formatVitalValue(crashes).padEnd(8)} ${vitalIndicator(crashes)} anr ${formatVitalValue(anr).padEnd(8)} ${vitalIndicator(anr)}`
4164
- );
4165
- lines.push(
4166
- ` slow starts ${formatVitalValue(slowStarts).padEnd(8)} ${vitalIndicator(slowStarts)} slow render ${formatVitalValue(slowRender).padEnd(8)} ${vitalIndicator(slowRender)}`
4167
- );
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
+ }
4168
4224
  lines.push("");
4169
4225
  lines.push(`REVIEWS (last ${status.reviews.windowDays} days)`);
4170
4226
  const { averageRating, previousAverageRating, totalNew, positivePercent } = status.reviews;
4171
- const trend = formatTrend(averageRating, previousAverageRating);
4172
- 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
+ };
4301
+ lines.push(
4302
+ ` Crash rate: ${fmtRate(diff.crashRate.from)} \u2192 ${fmtRate(diff.crashRate.to)} (${fmtDelta(diff.crashRate.delta)})`
4303
+ );
4173
4304
  lines.push(
4174
- ` ${formatRating(averageRating)} ${totalNew} new${positiveStr}${trend}`
4305
+ ` ANR rate: ${fmtRate(diff.anrRate.from)} \u2192 ${fmtRate(diff.anrRate.to)} (${fmtDelta(diff.anrRate.delta)})`
4175
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})`);
4176
4312
  return lines.join("\n");
4177
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
+ }
4178
4391
  function statusHasBreach(status) {
4179
4392
  return status.vitals.crashes.status === "breach" || status.vitals.anr.status === "breach" || status.vitals.slowStarts.status === "breach" || status.vitals.slowRender.status === "breach";
4180
4393
  }
@@ -4202,6 +4415,7 @@ export {
4202
4415
  clearAuditLog,
4203
4416
  compareBundles,
4204
4417
  compareVitalsTrend,
4418
+ computeStatusDiff,
4205
4419
  consumeProductPurchase,
4206
4420
  convertRegionPrices,
4207
4421
  createAuditEntry,
@@ -4247,6 +4461,8 @@ export {
4247
4461
  formatJunit,
4248
4462
  formatOutput,
4249
4463
  formatSlackPayload,
4464
+ formatStatusDiff,
4465
+ formatStatusSummary,
4250
4466
  formatStatusTable,
4251
4467
  generateMigrationPlan,
4252
4468
  generateNotesFromGit,
@@ -4322,16 +4538,19 @@ export {
4322
4538
  removeUser,
4323
4539
  replyToReview,
4324
4540
  revokeSubscriptionPurchase,
4541
+ runWatchLoop,
4325
4542
  safePath,
4326
4543
  safePathWithin,
4327
4544
  saveStatusCache,
4328
4545
  scaffoldPlugin,
4329
4546
  searchAuditEvents,
4330
4547
  searchVitalsErrors,
4548
+ sendNotification,
4331
4549
  sendWebhook,
4332
4550
  sortResults,
4333
4551
  statusHasBreach,
4334
4552
  syncInAppProducts,
4553
+ trackBreachState,
4335
4554
  updateAppDetails,
4336
4555
  updateDataSafety,
4337
4556
  updateInAppProduct,