@gpc-cli/core 0.9.23 → 0.9.25

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;
@@ -755,6 +757,7 @@ interface AppStatus {
755
757
  packageName: string;
756
758
  fetchedAt: string;
757
759
  cached: boolean;
760
+ sections: string[];
758
761
  releases: StatusRelease[];
759
762
  vitals: {
760
763
  windowDays: number;
@@ -765,8 +768,34 @@ interface AppStatus {
765
768
  };
766
769
  reviews: StatusReviews;
767
770
  }
771
+ interface StatusDiff {
772
+ versionCode: {
773
+ from: string | null;
774
+ to: string | null;
775
+ };
776
+ crashRate: {
777
+ from: number | null;
778
+ to: number | null;
779
+ delta: number | null;
780
+ };
781
+ anrRate: {
782
+ from: number | null;
783
+ to: number | null;
784
+ delta: number | null;
785
+ };
786
+ reviewCount: {
787
+ from: number | null;
788
+ to: number | null;
789
+ };
790
+ averageRating: {
791
+ from: number | null;
792
+ to: number | null;
793
+ delta: number | null;
794
+ };
795
+ }
768
796
  interface GetAppStatusOptions {
769
797
  days?: number;
798
+ sections?: string[];
770
799
  vitalThresholds?: {
771
800
  crashRate?: number;
772
801
  anrRate?: number;
@@ -774,10 +803,23 @@ interface GetAppStatusOptions {
774
803
  slowRenderingRate?: number;
775
804
  };
776
805
  }
806
+ interface WatchOptions {
807
+ intervalSeconds: number;
808
+ render: (status: AppStatus) => string;
809
+ fetch: () => Promise<AppStatus>;
810
+ save: (status: AppStatus) => Promise<void>;
811
+ }
777
812
  declare function loadStatusCache(packageName: string, ttlSeconds?: number): Promise<AppStatus | null>;
778
813
  declare function saveStatusCache(packageName: string, data: AppStatus, ttlSeconds?: number): Promise<void>;
779
814
  declare function getAppStatus(client: PlayApiClient, reporting: ReportingApiClient, packageName: string, options?: GetAppStatusOptions): Promise<AppStatus>;
780
815
  declare function formatStatusTable(status: AppStatus): string;
816
+ declare function formatStatusSummary(status: AppStatus): string;
817
+ declare function computeStatusDiff(prev: AppStatus, curr: AppStatus): StatusDiff;
818
+ declare function formatStatusDiff(diff: StatusDiff, since: string): string;
819
+ declare function runWatchLoop(opts: WatchOptions): Promise<void>;
820
+ /** Returns true if breach state changed (breach started or cleared). */
821
+ declare function trackBreachState(packageName: string, isBreaching: boolean): Promise<boolean>;
822
+ declare function sendNotification(title: string, body: string): void;
781
823
  declare function statusHasBreach(status: AppStatus): boolean;
782
824
 
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 };
825
+ 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;
@@ -3987,7 +3988,12 @@ async function loadStatusCache(packageName, ttlSeconds = DEFAULT_TTL_SECONDS) {
3987
3988
  const entry = JSON.parse(raw);
3988
3989
  const age = (Date.now() - new Date(entry.fetchedAt).getTime()) / 1e3;
3989
3990
  if (age > (entry.ttl ?? ttlSeconds)) return null;
3990
- return { ...entry.data, cached: true };
3991
+ const data = entry.data;
3992
+ return {
3993
+ ...data,
3994
+ sections: data.sections ?? ["releases", "vitals", "reviews"],
3995
+ cached: true
3996
+ };
3991
3997
  } catch {
3992
3998
  return null;
3993
3999
  }
@@ -3996,12 +4002,11 @@ async function saveStatusCache(packageName, data, ttlSeconds = DEFAULT_TTL_SECON
3996
4002
  try {
3997
4003
  const dir = getCacheDir();
3998
4004
  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 });
4005
+ const entry = { fetchedAt: data.fetchedAt, ttl: ttlSeconds, data };
4006
+ await writeFile7(cacheFilePath(packageName), JSON.stringify(entry, null, 2), {
4007
+ encoding: "utf-8",
4008
+ mode: 384
4009
+ });
4005
4010
  } catch {
4006
4011
  }
4007
4012
  }
@@ -4021,9 +4026,9 @@ var WARN_MARGIN = 0.2;
4021
4026
  function toApiDate(d) {
4022
4027
  return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };
4023
4028
  }
4024
- async function queryVitalForStatus(reporting, packageName, metricSet, days) {
4029
+ async function queryVitalForStatus(reporting, packageName, metricSet, days, offsetDays = 0) {
4025
4030
  const DAY_MS = 24 * 60 * 60 * 1e3;
4026
- const baseMs = Date.now() - 2 * DAY_MS;
4031
+ const baseMs = Date.now() - 2 * DAY_MS - offsetDays * DAY_MS;
4027
4032
  const end = new Date(baseMs);
4028
4033
  const start = new Date(baseMs - days * DAY_MS);
4029
4034
  const metrics = METRIC_SET_METRICS2[metricSet] ?? ["distinctUsers"];
@@ -4044,13 +4049,26 @@ async function queryVitalForStatus(reporting, packageName, metricSet, days) {
4044
4049
  if (values.length === 0) return void 0;
4045
4050
  return values.reduce((a, b) => a + b, 0) / values.length;
4046
4051
  }
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" };
4052
+ async function queryVitalWithTrend(reporting, packageName, metricSet, days) {
4053
+ const [current, previous] = await Promise.all([
4054
+ queryVitalForStatus(reporting, packageName, metricSet, days, 0),
4055
+ queryVitalForStatus(reporting, packageName, metricSet, days, days)
4056
+ ]);
4057
+ let trend = null;
4058
+ if (current !== void 0 && previous !== void 0) {
4059
+ if (current > previous) trend = "up";
4060
+ else if (current < previous) trend = "down";
4061
+ else trend = "flat";
4062
+ }
4063
+ return { current, previous, trend };
4064
+ }
4065
+ var SKIPPED_VITAL = { current: void 0, previous: void 0, trend: null };
4066
+ function toVitalMetric(value, threshold, previousValue, trend) {
4067
+ const base = previousValue !== void 0 ? { value, threshold, status: "unknown", previousValue, trend: trend ?? null } : { value, threshold, status: "unknown" };
4068
+ if (value === void 0) return { ...base, status: "unknown" };
4069
+ if (value > threshold) return { ...base, status: "breach" };
4070
+ if (value > threshold * (1 - WARN_MARGIN)) return { ...base, status: "warn" };
4071
+ return { ...base, status: "ok" };
4054
4072
  }
4055
4073
  function computeReviewSentiment(reviews, windowDays) {
4056
4074
  const now = Date.now();
@@ -4089,6 +4107,7 @@ function computeReviewSentiment(reviews, windowDays) {
4089
4107
  }
4090
4108
  async function getAppStatus(client, reporting, packageName, options = {}) {
4091
4109
  const days = options.days ?? 7;
4110
+ const sections = new Set(options.sections ?? ["releases", "vitals", "reviews"]);
4092
4111
  const thresholds = {
4093
4112
  crashRate: options.vitalThresholds?.crashRate ?? DEFAULT_THRESHOLDS.crashRate,
4094
4113
  anrRate: options.vitalThresholds?.anrRate ?? DEFAULT_THRESHOLDS.anrRate,
@@ -4096,12 +4115,12 @@ async function getAppStatus(client, reporting, packageName, options = {}) {
4096
4115
  slowRenderingRate: options.vitalThresholds?.slowRenderingRate ?? DEFAULT_THRESHOLDS.slowRenderingRate
4097
4116
  };
4098
4117
  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 })
4118
+ sections.has("releases") ? getReleasesStatus(client, packageName) : Promise.resolve([]),
4119
+ sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "crashRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
4120
+ sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "anrRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
4121
+ sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "slowStartRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
4122
+ sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "slowRenderingRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
4123
+ sections.has("reviews") ? listReviews(client, packageName, { maxResults: 500 }) : Promise.resolve([])
4105
4124
  ]);
4106
4125
  const rawReleases = releasesResult.status === "fulfilled" ? releasesResult.value : [];
4107
4126
  const releases = rawReleases.map((r) => ({
@@ -4110,36 +4129,50 @@ async function getAppStatus(client, reporting, packageName, options = {}) {
4110
4129
  status: r.status,
4111
4130
  userFraction: r.userFraction ?? null
4112
4131
  }));
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;
4132
+ const crashes = crashesResult.status === "fulfilled" ? crashesResult.value : SKIPPED_VITAL;
4133
+ const anr = anrResult.status === "fulfilled" ? anrResult.value : SKIPPED_VITAL;
4134
+ const slowStart = slowStartResult.status === "fulfilled" ? slowStartResult.value : SKIPPED_VITAL;
4135
+ const slowRender = slowRenderResult.status === "fulfilled" ? slowRenderResult.value : SKIPPED_VITAL;
4117
4136
  const rawReviews = reviewsResult.status === "fulfilled" ? reviewsResult.value : [];
4118
4137
  const reviews = computeReviewSentiment(rawReviews, 30);
4119
- const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
4120
4138
  return {
4121
4139
  packageName,
4122
- fetchedAt,
4140
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
4123
4141
  cached: false,
4142
+ sections: [...sections],
4124
4143
  releases,
4125
4144
  vitals: {
4126
4145
  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)
4146
+ crashes: toVitalMetric(crashes.current, thresholds.crashRate, crashes.previous, crashes.trend),
4147
+ anr: toVitalMetric(anr.current, thresholds.anrRate, anr.previous, anr.trend),
4148
+ slowStarts: toVitalMetric(
4149
+ slowStart.current,
4150
+ thresholds.slowStartRate,
4151
+ slowStart.previous,
4152
+ slowStart.trend
4153
+ ),
4154
+ slowRender: toVitalMetric(
4155
+ slowRender.current,
4156
+ thresholds.slowRenderingRate,
4157
+ slowRender.previous,
4158
+ slowRender.trend
4159
+ )
4131
4160
  },
4132
4161
  reviews
4133
4162
  };
4134
4163
  }
4135
4164
  function vitalIndicator(metric) {
4136
- if (metric.status === "unknown") return "?";
4165
+ if (metric.status === "unknown") return "\u2014";
4137
4166
  if (metric.status === "breach") return "\u2717";
4138
4167
  if (metric.status === "warn") return "\u26A0";
4139
4168
  return "\u2713";
4140
4169
  }
4170
+ function vitalTrendArrow(metric) {
4171
+ if (!metric.trend || metric.trend === "flat") return "";
4172
+ return metric.trend === "up" ? " \u2191" : " \u2193";
4173
+ }
4141
4174
  function formatVitalValue(metric) {
4142
- if (metric.value === void 0) return "n/a";
4175
+ if (metric.value === void 0) return "\u2014";
4143
4176
  return `${(metric.value * 100).toFixed(2)}%`;
4144
4177
  }
4145
4178
  function formatFraction(fraction) {
@@ -4147,7 +4180,7 @@ function formatFraction(fraction) {
4147
4180
  return `${Math.round(fraction * 100)}%`;
4148
4181
  }
4149
4182
  function formatRating(rating) {
4150
- if (rating === void 0) return "n/a";
4183
+ if (rating === void 0) return "\u2014";
4151
4184
  return `\u2605 ${rating.toFixed(1)}`;
4152
4185
  }
4153
4186
  function formatTrend(current, previous) {
@@ -4156,43 +4189,218 @@ function formatTrend(current, previous) {
4156
4189
  if (current < previous) return ` \u2193 from ${previous.toFixed(1)}`;
4157
4190
  return "";
4158
4191
  }
4192
+ function allVitalsUnknown(vitals) {
4193
+ return vitals.crashes.status === "unknown" && vitals.anr.status === "unknown" && vitals.slowStarts.status === "unknown" && vitals.slowRender.status === "unknown";
4194
+ }
4159
4195
  function formatStatusTable(status) {
4160
4196
  const lines = [];
4197
+ const sectionSet = new Set(status.sections);
4161
4198
  const cachedLabel = status.cached ? ` (cached ${new Date(status.fetchedAt).toLocaleTimeString()})` : ` (fetched ${new Date(status.fetchedAt).toLocaleTimeString()})`;
4162
4199
  lines.push(`App: ${status.packageName}${cachedLabel}`);
4163
- lines.push("");
4164
- lines.push("RELEASES");
4165
- if (status.releases.length === 0) {
4166
- lines.push(" No releases found.");
4167
- } else {
4168
- const trackW = Math.max(10, ...status.releases.map((r) => r.track.length));
4169
- const versionW = Math.max(7, ...status.releases.map((r) => r.versionCode.length));
4170
- const statusW = Math.max(8, ...status.releases.map((r) => r.status.length));
4171
- for (const r of status.releases) {
4200
+ if (sectionSet.has("releases")) {
4201
+ lines.push("");
4202
+ lines.push("RELEASES");
4203
+ if (status.releases.length === 0) {
4204
+ lines.push(" No releases found.");
4205
+ } else {
4206
+ const trackW = Math.max(10, ...status.releases.map((r) => r.track.length));
4207
+ const versionW = Math.max(7, ...status.releases.map((r) => r.versionCode.length));
4208
+ const statusW = Math.max(8, ...status.releases.map((r) => r.status.length));
4209
+ for (const r of status.releases) {
4210
+ lines.push(
4211
+ ` ${r.track.padEnd(trackW)} ${r.versionCode.padEnd(versionW)} ${r.status.padEnd(statusW)} ${formatFraction(r.userFraction)}`
4212
+ );
4213
+ }
4214
+ }
4215
+ }
4216
+ if (sectionSet.has("vitals")) {
4217
+ lines.push("");
4218
+ lines.push(`VITALS (last ${status.vitals.windowDays} days)`);
4219
+ if (allVitalsUnknown(status.vitals)) {
4220
+ lines.push(" No vitals data available for this period.");
4221
+ } else {
4222
+ const { crashes, anr, slowStarts, slowRender } = status.vitals;
4223
+ const crashVal = `${formatVitalValue(crashes)}${vitalTrendArrow(crashes)}`;
4224
+ const anrVal = `${formatVitalValue(anr)}${vitalTrendArrow(anr)}`;
4225
+ const slowStartVal = `${formatVitalValue(slowStarts)}${vitalTrendArrow(slowStarts)}`;
4226
+ const slowRenderVal = `${formatVitalValue(slowRender)}${vitalTrendArrow(slowRender)}`;
4172
4227
  lines.push(
4173
- ` ${r.track.padEnd(trackW)} ${r.versionCode.padEnd(versionW)} ${r.status.padEnd(statusW)} ${formatFraction(r.userFraction)}`
4228
+ ` crashes ${crashVal.padEnd(10)} ${vitalIndicator(crashes)} anr ${anrVal.padEnd(10)} ${vitalIndicator(anr)}`
4229
+ );
4230
+ lines.push(
4231
+ ` slow starts ${slowStartVal.padEnd(10)} ${vitalIndicator(slowStarts)} slow render ${slowRenderVal.padEnd(10)} ${vitalIndicator(slowRender)}`
4174
4232
  );
4175
4233
  }
4176
4234
  }
4177
- lines.push("");
4178
- 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
- );
4235
+ if (sectionSet.has("reviews")) {
4236
+ lines.push("");
4237
+ lines.push(`REVIEWS (last ${status.reviews.windowDays} days)`);
4238
+ const { averageRating, previousAverageRating, totalNew, positivePercent } = status.reviews;
4239
+ if (totalNew === 0 && averageRating === void 0) {
4240
+ lines.push(" No reviews in this period.");
4241
+ } else {
4242
+ const trend = formatTrend(averageRating, previousAverageRating);
4243
+ const positiveStr = positivePercent !== void 0 ? ` ${positivePercent}% positive` : "";
4244
+ lines.push(` ${formatRating(averageRating)} ${totalNew} new${positiveStr}${trend}`);
4245
+ }
4246
+ }
4247
+ return lines.join("\n");
4248
+ }
4249
+ function formatStatusSummary(status) {
4250
+ const parts = [status.packageName];
4251
+ const latestRelease = status.releases.find((r) => r.status !== "draft") ?? status.releases[0];
4252
+ if (latestRelease) {
4253
+ parts.push(`v${latestRelease.versionCode} ${latestRelease.track}`);
4254
+ }
4255
+ const { crashes, anr } = status.vitals;
4256
+ if (crashes.status !== "unknown") {
4257
+ const arrow = crashes.trend === "up" ? " \u2191" : crashes.trend === "down" ? " \u2193" : "";
4258
+ parts.push(`crashes ${formatVitalValue(crashes)}${arrow} ${vitalIndicator(crashes)}`);
4259
+ }
4260
+ if (anr.status !== "unknown") {
4261
+ const arrow = anr.trend === "up" ? " \u2191" : anr.trend === "down" ? " \u2193" : "";
4262
+ parts.push(`ANR ${formatVitalValue(anr)}${arrow} ${vitalIndicator(anr)}`);
4263
+ }
4264
+ const { averageRating, totalNew } = status.reviews;
4265
+ if (averageRating !== void 0) {
4266
+ parts.push(`avg ${averageRating.toFixed(1)}\u2605`);
4267
+ }
4268
+ if (totalNew > 0) {
4269
+ parts.push(`${totalNew} reviews`);
4270
+ }
4271
+ return parts.join(" \xB7 ") + (statusHasBreach(status) ? " [ALERT]" : "");
4272
+ }
4273
+ function computeStatusDiff(prev, curr) {
4274
+ const prevVersion = prev.releases[0]?.versionCode ?? null;
4275
+ const currVersion = curr.releases[0]?.versionCode ?? null;
4276
+ const prevCrash = prev.vitals.crashes.value ?? null;
4277
+ const currCrash = curr.vitals.crashes.value ?? null;
4278
+ const prevAnr = prev.vitals.anr.value ?? null;
4279
+ const currAnr = curr.vitals.anr.value ?? null;
4280
+ const prevRating = prev.reviews.averageRating ?? null;
4281
+ const currRating = curr.reviews.averageRating ?? null;
4282
+ return {
4283
+ versionCode: { from: prevVersion, to: currVersion },
4284
+ crashRate: {
4285
+ from: prevCrash,
4286
+ to: currCrash,
4287
+ delta: currCrash !== null && prevCrash !== null ? currCrash - prevCrash : null
4288
+ },
4289
+ anrRate: {
4290
+ from: prevAnr,
4291
+ to: currAnr,
4292
+ delta: currAnr !== null && prevAnr !== null ? currAnr - prevAnr : null
4293
+ },
4294
+ reviewCount: { from: prev.reviews.totalNew, to: curr.reviews.totalNew },
4295
+ averageRating: {
4296
+ from: prevRating,
4297
+ to: currRating,
4298
+ delta: currRating !== null && prevRating !== null ? Math.round((currRating - prevRating) * 10) / 10 : null
4299
+ }
4300
+ };
4301
+ }
4302
+ function formatStatusDiff(diff, since) {
4303
+ const lines = [`Changes since ${since}:`];
4304
+ if (diff.versionCode.from !== diff.versionCode.to) {
4305
+ lines.push(` Version: ${diff.versionCode.from ?? "\u2014"} \u2192 ${diff.versionCode.to ?? "\u2014"}`);
4306
+ }
4307
+ const fmtRate = (v) => v !== null ? `${(v * 100).toFixed(2)}%` : "\u2014";
4308
+ const fmtDelta = (d, lowerIsBetter = true) => {
4309
+ if (d === null || Math.abs(d) < 1e-4) return "no change";
4310
+ const sign = d > 0 ? "+" : "";
4311
+ const good = lowerIsBetter ? d < 0 : d > 0;
4312
+ return `${sign}${(d * 100).toFixed(2)}% ${good ? "\u2713" : "\u2717"}`;
4313
+ };
4183
4314
  lines.push(
4184
- ` slow starts ${formatVitalValue(slowStarts).padEnd(8)} ${vitalIndicator(slowStarts)} slow render ${formatVitalValue(slowRender).padEnd(8)} ${vitalIndicator(slowRender)}`
4315
+ ` Crash rate: ${fmtRate(diff.crashRate.from)} \u2192 ${fmtRate(diff.crashRate.to)} (${fmtDelta(diff.crashRate.delta)})`
4185
4316
  );
4186
- lines.push("");
4187
- lines.push(`REVIEWS (last ${status.reviews.windowDays} days)`);
4188
- const { averageRating, previousAverageRating, totalNew, positivePercent } = status.reviews;
4189
- const trend = formatTrend(averageRating, previousAverageRating);
4190
- const positiveStr = positivePercent !== void 0 ? ` ${positivePercent}% positive` : "";
4191
4317
  lines.push(
4192
- ` ${formatRating(averageRating)} ${totalNew} new${positiveStr}${trend}`
4318
+ ` ANR rate: ${fmtRate(diff.anrRate.from)} \u2192 ${fmtRate(diff.anrRate.to)} (${fmtDelta(diff.anrRate.delta)})`
4193
4319
  );
4320
+ const ratingDelta = diff.averageRating.delta;
4321
+ const prevR = diff.averageRating.from !== null ? `${diff.averageRating.from.toFixed(1)}\u2605` : "\u2014";
4322
+ const currR = diff.averageRating.to !== null ? `${diff.averageRating.to.toFixed(1)}\u2605` : "\u2014";
4323
+ const ratingStr = ratingDelta === null || Math.abs(ratingDelta) < 0.05 ? "no change" : `${ratingDelta > 0 ? "+" : ""}${ratingDelta.toFixed(1)} ${ratingDelta > 0 ? "\u2713" : "\u2717"}`;
4324
+ lines.push(` Reviews: ${prevR} \u2192 ${currR} (${ratingStr})`);
4194
4325
  return lines.join("\n");
4195
4326
  }
4327
+ async function runWatchLoop(opts) {
4328
+ if (opts.intervalSeconds < 10) {
4329
+ console.error("Error: --watch interval must be at least 10 seconds");
4330
+ process.exit(2);
4331
+ }
4332
+ let running = true;
4333
+ const cleanup = () => {
4334
+ running = false;
4335
+ process.stdout.write("\n");
4336
+ process.exit(0);
4337
+ };
4338
+ process.on("SIGINT", cleanup);
4339
+ process.on("SIGTERM", cleanup);
4340
+ while (running) {
4341
+ process.stdout.write("\x1B[2J\x1B[H");
4342
+ try {
4343
+ const status = await opts.fetch();
4344
+ await opts.save(status);
4345
+ console.log(opts.render(status));
4346
+ } catch (err) {
4347
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
4348
+ }
4349
+ console.log(`
4350
+ [gpc status] Refreshing in ${opts.intervalSeconds}s\u2026 (Ctrl+C to stop)`);
4351
+ for (let i = 0; i < opts.intervalSeconds && running; i++) {
4352
+ await new Promise((r) => setTimeout(r, 1e3));
4353
+ }
4354
+ }
4355
+ }
4356
+ function breachStateFilePath(packageName) {
4357
+ return join7(getCacheDir(), `breach-state-${packageName}.json`);
4358
+ }
4359
+ async function trackBreachState(packageName, isBreaching) {
4360
+ const filePath = breachStateFilePath(packageName);
4361
+ let prevBreaching = false;
4362
+ try {
4363
+ const raw = await readFile10(filePath, "utf-8");
4364
+ prevBreaching = JSON.parse(raw).breaching;
4365
+ } catch {
4366
+ }
4367
+ if (prevBreaching !== isBreaching) {
4368
+ try {
4369
+ await mkdir5(getCacheDir(), { recursive: true });
4370
+ await writeFile7(
4371
+ filePath,
4372
+ JSON.stringify({ breaching: isBreaching, since: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
4373
+ { encoding: "utf-8", mode: 384 }
4374
+ );
4375
+ } catch {
4376
+ }
4377
+ return true;
4378
+ }
4379
+ return false;
4380
+ }
4381
+ function sendNotification(title, body) {
4382
+ if (process.env["CI"]) return;
4383
+ try {
4384
+ const p = process.platform;
4385
+ if (p === "darwin") {
4386
+ execSync(
4387
+ `osascript -e 'display notification ${JSON.stringify(body)} with title ${JSON.stringify(title)}'`,
4388
+ { stdio: "ignore" }
4389
+ );
4390
+ } else if (p === "linux") {
4391
+ execSync(`notify-send ${JSON.stringify(title)} ${JSON.stringify(body)}`, {
4392
+ stdio: "ignore"
4393
+ });
4394
+ } else if (p === "win32") {
4395
+ const escaped = (s) => s.replace(/'/g, "''");
4396
+ execSync(
4397
+ `powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${escaped(body)}', '${escaped(title)}')"`,
4398
+ { stdio: "ignore" }
4399
+ );
4400
+ }
4401
+ } catch {
4402
+ }
4403
+ }
4196
4404
  function statusHasBreach(status) {
4197
4405
  return status.vitals.crashes.status === "breach" || status.vitals.anr.status === "breach" || status.vitals.slowStarts.status === "breach" || status.vitals.slowRender.status === "breach";
4198
4406
  }
@@ -4220,6 +4428,7 @@ export {
4220
4428
  clearAuditLog,
4221
4429
  compareBundles,
4222
4430
  compareVitalsTrend,
4431
+ computeStatusDiff,
4223
4432
  consumeProductPurchase,
4224
4433
  convertRegionPrices,
4225
4434
  createAuditEntry,
@@ -4265,6 +4474,8 @@ export {
4265
4474
  formatJunit,
4266
4475
  formatOutput,
4267
4476
  formatSlackPayload,
4477
+ formatStatusDiff,
4478
+ formatStatusSummary,
4268
4479
  formatStatusTable,
4269
4480
  generateMigrationPlan,
4270
4481
  generateNotesFromGit,
@@ -4340,16 +4551,19 @@ export {
4340
4551
  removeUser,
4341
4552
  replyToReview,
4342
4553
  revokeSubscriptionPurchase,
4554
+ runWatchLoop,
4343
4555
  safePath,
4344
4556
  safePathWithin,
4345
4557
  saveStatusCache,
4346
4558
  scaffoldPlugin,
4347
4559
  searchAuditEvents,
4348
4560
  searchVitalsErrors,
4561
+ sendNotification,
4349
4562
  sendWebhook,
4350
4563
  sortResults,
4351
4564
  statusHasBreach,
4352
4565
  syncInAppProducts,
4566
+ trackBreachState,
4353
4567
  updateAppDetails,
4354
4568
  updateDataSafety,
4355
4569
  updateInAppProduct,