@better-update/cli 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
+ import { spawn } from "node:child_process";
3
4
  import { defineCommand, runMain } from "citty";
4
5
  import { Console, Context, Data, Deferred, Duration, Effect, Fiber, Layer, Match, Option, ParseResult, Schema, Stream } from "effect";
5
6
  import { Command, FetchHttpClient, FileSystem, HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, HttpApiSecurity, HttpClient, HttpClientRequest, OpenApi } from "@effect/platform";
@@ -16,90 +17,14 @@ import { getFormattedSerialNumber, getX509Certificate, parsePKCS12 } from "@expo
16
17
  import { once } from "node:events";
17
18
  import { createServer } from "node:http";
18
19
  import { cancel, isCancel, password } from "@clack/prompts";
20
+ import { fileURLToPath } from "node:url";
19
21
 
20
22
  //#region \0rolldown/runtime.js
21
23
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
22
24
 
23
25
  //#endregion
24
26
  //#region package.json
25
- var version = "0.8.1";
26
-
27
- //#endregion
28
- //#region ../../packages/type-guards/src/index.ts
29
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
30
- const asRecord = (value) => isRecord(value) ? value : void 0;
31
-
32
- //#endregion
33
- //#region src/lib/exit-codes.ts
34
- var AuthRequiredError = class extends Data.TaggedError("AuthRequiredError") {};
35
- var ProjectNotLinkedError = class extends Data.TaggedError("ProjectNotLinkedError") {};
36
- var UploadFailedError = class extends Data.TaggedError("UploadFailedError") {};
37
- var BuildProfileError = class extends Data.TaggedError("BuildProfileError") {};
38
- var RuntimeVersionError = class extends Data.TaggedError("RuntimeVersionError") {};
39
- var MissingCredentialsError = class extends Data.TaggedError("MissingCredentialsError") {};
40
- var BuildFailedError = class extends Data.TaggedError("BuildFailedError") {};
41
- var ReserveError = class extends Data.TaggedError("ReserveError") {};
42
- var CompleteError = class extends Data.TaggedError("CompleteError") {};
43
- var PresignedUrlExpiredError = class extends Data.TaggedError("PresignedUrlExpiredError") {};
44
- var ArtifactNotFoundError = class extends Data.TaggedError("ArtifactNotFoundError") {};
45
- var KeychainError = class extends Data.TaggedError("KeychainError") {};
46
- var ProvisioningError = class extends Data.TaggedError("ProvisioningError") {};
47
- var EnvExportError = class extends Data.TaggedError("EnvExportError") {};
48
- var UpdatePublishError = class extends Data.TaggedError("UpdatePublishError") {};
49
- var UpdateRollbackError = class extends Data.TaggedError("UpdateRollbackError") {};
50
- var UpdatePromoteError = class extends Data.TaggedError("UpdatePromoteError") {};
51
- var CredentialValidationError = class extends Data.TaggedError("CredentialValidationError") {};
52
- var AppleAuthError = class extends Data.TaggedError("AppleAuthError") {};
53
- var InvalidArgumentError = class extends Data.TaggedError("InvalidArgumentError") {};
54
-
55
- //#endregion
56
- //#region src/lib/format-error.ts
57
- const formatCause = (cause) => {
58
- if (cause instanceof Error) return cause.message;
59
- if (typeof cause === "object" && cause !== null) {
60
- const tagged = cause;
61
- const tag = typeof tagged._tag === "string" ? tagged._tag : void 0;
62
- const message = typeof tagged.message === "string" ? tagged.message : void 0;
63
- if (tag && message) return `${tag}: ${message}`;
64
- if (message) return message;
65
- if (tag) return tag;
66
- }
67
- return String(cause);
68
- };
69
-
70
- //#endregion
71
- //#region src/lib/app-json.ts
72
- const readAppJson = Effect.gen(function* () {
73
- const content = yield* (yield* FileSystem.FileSystem).readFileString("./app.json").pipe(Effect.mapError(() => new ProjectNotLinkedError({ message: "app.json not found in current directory" })));
74
- const parsed = yield* Effect.try({
75
- try: () => JSON.parse(content),
76
- catch: () => new ProjectNotLinkedError({ message: "app.json contains malformed JSON" })
77
- });
78
- if (!isRecord(parsed)) return yield* new ProjectNotLinkedError({ message: "app.json must be a JSON object" });
79
- return parsed;
80
- });
81
- const readProjectId = Effect.gen(function* () {
82
- const projectId = asRecord(asRecord(asRecord((yield* readAppJson)["expo"])?.["extra"])?.["betterUpdate"])?.["projectId"];
83
- if (typeof projectId !== "string") return yield* new ProjectNotLinkedError({ message: "Project not linked. Run `better-update link` to connect this project, or set expo.extra.betterUpdate.projectId in app.json." });
84
- return projectId;
85
- });
86
- const readSlug = Effect.gen(function* () {
87
- const slug = asRecord((yield* readAppJson)["expo"])?.["slug"];
88
- if (typeof slug !== "string") return yield* new ProjectNotLinkedError({ message: "Missing expo.slug in app.json. Required to identify the project." });
89
- return slug;
90
- });
91
- const writeProjectId = (id) => Effect.gen(function* () {
92
- const fs = yield* FileSystem.FileSystem;
93
- const appJson = yield* readAppJson;
94
- const expo = asRecord(appJson["expo"]) ?? {};
95
- const extra = asRecord(expo["extra"]) ?? {};
96
- const betterUpdate = asRecord(extra["betterUpdate"]) ?? {};
97
- betterUpdate["projectId"] = id;
98
- extra["betterUpdate"] = betterUpdate;
99
- expo["extra"] = extra;
100
- appJson["expo"] = expo;
101
- yield* fs.writeFileString("./app.json", `${JSON.stringify(appJson, null, 2)}\n`);
102
- }).pipe(Effect.mapError((cause) => cause instanceof ProjectNotLinkedError ? cause : new ProjectNotLinkedError({ message: `Failed to write project ID to app.json: ${formatCause(cause)}` })));
27
+ var version = "0.9.0";
103
28
 
104
29
  //#endregion
105
30
  //#region ../../packages/api/src/auth/context.ts
@@ -1504,6 +1429,49 @@ var ProtocolApi = class extends HttpApi.make("protocol-api").add(ManifestGroup).
1504
1429
  description: "Expo Updates protocol endpoints (unauthenticated)"
1505
1430
  })) {};
1506
1431
 
1432
+ //#endregion
1433
+ //#region ../../packages/type-guards/src/index.ts
1434
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1435
+ const asRecord = (value) => isRecord(value) ? value : void 0;
1436
+
1437
+ //#endregion
1438
+ //#region src/lib/exit-codes.ts
1439
+ var AuthRequiredError = class extends Data.TaggedError("AuthRequiredError") {};
1440
+ var ProjectNotLinkedError = class extends Data.TaggedError("ProjectNotLinkedError") {};
1441
+ var UploadFailedError = class extends Data.TaggedError("UploadFailedError") {};
1442
+ var BuildProfileError = class extends Data.TaggedError("BuildProfileError") {};
1443
+ var RuntimeVersionError = class extends Data.TaggedError("RuntimeVersionError") {};
1444
+ var MissingCredentialsError = class extends Data.TaggedError("MissingCredentialsError") {};
1445
+ var BuildFailedError = class extends Data.TaggedError("BuildFailedError") {};
1446
+ var ReserveError = class extends Data.TaggedError("ReserveError") {};
1447
+ var CompleteError = class extends Data.TaggedError("CompleteError") {};
1448
+ var PresignedUrlExpiredError = class extends Data.TaggedError("PresignedUrlExpiredError") {};
1449
+ var ArtifactNotFoundError = class extends Data.TaggedError("ArtifactNotFoundError") {};
1450
+ var KeychainError = class extends Data.TaggedError("KeychainError") {};
1451
+ var ProvisioningError = class extends Data.TaggedError("ProvisioningError") {};
1452
+ var EnvExportError = class extends Data.TaggedError("EnvExportError") {};
1453
+ var UpdatePublishError = class extends Data.TaggedError("UpdatePublishError") {};
1454
+ var UpdateRollbackError = class extends Data.TaggedError("UpdateRollbackError") {};
1455
+ var UpdatePromoteError = class extends Data.TaggedError("UpdatePromoteError") {};
1456
+ var CredentialValidationError = class extends Data.TaggedError("CredentialValidationError") {};
1457
+ var AppleAuthError = class extends Data.TaggedError("AppleAuthError") {};
1458
+ var InvalidArgumentError = class extends Data.TaggedError("InvalidArgumentError") {};
1459
+
1460
+ //#endregion
1461
+ //#region src/lib/format-error.ts
1462
+ const formatCause = (cause) => {
1463
+ if (cause instanceof Error) return cause.message;
1464
+ if (typeof cause === "object" && cause !== null) {
1465
+ const tagged = cause;
1466
+ const tag = typeof tagged._tag === "string" ? tagged._tag : void 0;
1467
+ const message = typeof tagged.message === "string" ? tagged.message : void 0;
1468
+ if (tag && message) return `${tag}: ${message}`;
1469
+ if (message) return message;
1470
+ if (tag) return tag;
1471
+ }
1472
+ return String(cause);
1473
+ };
1474
+
1507
1475
  //#endregion
1508
1476
  //#region src/services/cli-runtime.ts
1509
1477
  const definedEnvironment = () => Object.fromEntries(Object.entries(process$1.env).flatMap(([key, value]) => typeof value === "string" ? [[key, value]] : []));
@@ -1559,7 +1527,7 @@ const AuthStoreLive = Layer.effect(AuthStore, Effect.gen(function* () {
1559
1527
 
1560
1528
  //#endregion
1561
1529
  //#region src/services/config-store.ts
1562
- const DEFAULT_BASE_URL = "https://graph.better-update.dev";
1530
+ const DEFAULT_BASE_URL = "https://better-update.dev";
1563
1531
  const DEFAULT_WEB_URL = "https://better-update.dev";
1564
1532
  var ConfigStoreParseError = class extends Data.TaggedError("ConfigStoreParseError") {};
1565
1533
  const normalizeUrl = (value) => value.replace(/\/$/u, "");
@@ -1706,6 +1674,53 @@ const UpdateAssetUploaderLive = Layer.effect(UpdateAssetUploader, Effect.gen(fun
1706
1674
  }) };
1707
1675
  }));
1708
1676
 
1677
+ //#endregion
1678
+ //#region src/services/version-check.ts
1679
+ const NPM_REGISTRY_URL = "https://registry.npmjs.org/@better-update/cli/latest";
1680
+ const CACHE_TTL_MS = 1440 * 60 * 1e3;
1681
+ const REFRESH_TIMEOUT_MS = 3e3;
1682
+ var VersionCheck = class extends Context.Tag("cli/VersionCheck")() {};
1683
+ const VersionCheckLive = Layer.effect(VersionCheck, Effect.gen(function* () {
1684
+ const fs = yield* FileSystem.FileSystem;
1685
+ const httpClient = yield* HttpClient.HttpClient;
1686
+ const homeDirectory = yield* (yield* CliRuntime).homeDirectory;
1687
+ const cacheDir = path.join(homeDirectory, ".better-update");
1688
+ const cacheFile = path.join(cacheDir, "version-check.json");
1689
+ const readCache = Effect.gen(function* () {
1690
+ const content = yield* fs.readFileString(cacheFile).pipe(Effect.catchAll(() => Effect.succeed("")));
1691
+ if (content.length === 0) return;
1692
+ const parsed = yield* Effect.try({
1693
+ try: () => JSON.parse(content),
1694
+ catch: () => "parse-error"
1695
+ }).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
1696
+ if (isRecord(parsed) && typeof parsed["latest"] === "string" && typeof parsed["checkedAt"] === "number") return {
1697
+ latest: parsed["latest"],
1698
+ checkedAt: parsed["checkedAt"]
1699
+ };
1700
+ });
1701
+ return {
1702
+ cachedLatest: readCache.pipe(Effect.map((entry) => entry?.latest)),
1703
+ cacheStale: readCache.pipe(Effect.map((entry) => {
1704
+ if (!entry) return true;
1705
+ const elapsed = Date.now() - entry.checkedAt;
1706
+ return elapsed < 0 || elapsed > CACHE_TTL_MS;
1707
+ })),
1708
+ refreshCache: Effect.gen(function* () {
1709
+ const request = HttpClientRequest.get(NPM_REGISTRY_URL).pipe(HttpClientRequest.setHeader("accept", "application/json"));
1710
+ const response = yield* httpClient.execute(request);
1711
+ if (response.status < 200 || response.status >= 300) return;
1712
+ const body = yield* response.json;
1713
+ if (!isRecord(body) || typeof body["version"] !== "string") return;
1714
+ const latest = body["version"];
1715
+ yield* fs.makeDirectory(cacheDir, { recursive: true });
1716
+ yield* fs.writeFileString(cacheFile, `${JSON.stringify({
1717
+ latest,
1718
+ checkedAt: Date.now()
1719
+ }, null, 2)}\n`);
1720
+ }).pipe(Effect.timeout(REFRESH_TIMEOUT_MS), Effect.catchAll(() => Effect.void))
1721
+ };
1722
+ }));
1723
+
1709
1724
  //#endregion
1710
1725
  //#region src/app-layer.ts
1711
1726
  const CliPlatformLayer = Layer.mergeAll(CliRuntimeLive, NodeContext.layer, FetchHttpClient.layer);
@@ -1714,7 +1729,8 @@ const CliAdapterDependencies = Layer.mergeAll(CliPlatformLayer, CliStoreLayer);
1714
1729
  const ApiClientLayer = ApiClientLive.pipe(Layer.provide(CliAdapterDependencies));
1715
1730
  const PresignedUploadLayer = PresignedUploadClientLive.pipe(Layer.provide(CliPlatformLayer));
1716
1731
  const UpdateAssetUploaderLayer = UpdateAssetUploaderLive.pipe(Layer.provide(Layer.mergeAll(ApiClientLayer, PresignedUploadLayer)));
1717
- const CliLive = Layer.mergeAll(CliAdapterDependencies, ApiClientLayer, PresignedUploadLayer, UpdateAssetUploaderLayer);
1732
+ const VersionCheckLayer = VersionCheckLive.pipe(Layer.provide(CliPlatformLayer));
1733
+ const CliLive = Layer.mergeAll(CliAdapterDependencies, ApiClientLayer, PresignedUploadLayer, UpdateAssetUploaderLayer, VersionCheckLayer);
1718
1734
 
1719
1735
  //#endregion
1720
1736
  //#region src/application/command-exit.ts
@@ -1764,6 +1780,105 @@ const runEffect = async (effect, extras = {}) => {
1764
1780
  return Effect.runPromise(provided.pipe(Effect.asVoid));
1765
1781
  };
1766
1782
 
1783
+ //#endregion
1784
+ //#region src/lib/expo-config.ts
1785
+ const loadExpoConfigModule = () => __require("@expo/config");
1786
+ const clearDynamicConfigCache = (projectRoot) => {
1787
+ const { dynamicConfigPath } = loadExpoConfigModule().getConfigFilePaths(projectRoot);
1788
+ if (dynamicConfigPath) delete __require.cache[dynamicConfigPath];
1789
+ };
1790
+ const applyEnvOverlay = (envVars) => {
1791
+ const previous = {};
1792
+ for (const [key, value] of Object.entries(envVars)) {
1793
+ previous[key] = process$1.env[key];
1794
+ process$1.env[key] = value;
1795
+ }
1796
+ return previous;
1797
+ };
1798
+ const restoreEnv = (previous) => {
1799
+ for (const [key, value] of Object.entries(previous)) if (value === void 0) delete process$1.env[key];
1800
+ else process$1.env[key] = value;
1801
+ };
1802
+ /**
1803
+ * Resolve the Expo config via `@expo/config`, supporting `app.json`,
1804
+ * `app.config.json`, `app.config.js`, and `app.config.ts`.
1805
+ *
1806
+ * `envVars` are applied as a scoped overlay on `process.env` for the duration
1807
+ * of the call so dynamic configs (`app.config.js`/`.ts`) can read them without
1808
+ * leaking server-side secrets to child processes.
1809
+ */
1810
+ const readExpoConfig = (projectRoot, envVars = {}) => Effect.acquireUseRelease(Effect.sync(() => {
1811
+ clearDynamicConfigCache(projectRoot);
1812
+ return applyEnvOverlay(envVars);
1813
+ }), () => Effect.try({
1814
+ try: () => loadExpoConfigModule().getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp,
1815
+ catch: (cause) => new ProjectNotLinkedError({ message: `Failed to load Expo config from ${projectRoot}: ${formatCause(cause)}` })
1816
+ }), (previous) => Effect.sync(() => {
1817
+ restoreEnv(previous);
1818
+ }));
1819
+ const getConfigFilePaths = (projectRoot) => Effect.try({
1820
+ try: () => loadExpoConfigModule().getConfigFilePaths(projectRoot),
1821
+ catch: (cause) => new ProjectNotLinkedError({ message: `Failed to inspect Expo config paths in ${projectRoot}: ${formatCause(cause)}` })
1822
+ });
1823
+ const extractProjectId = (config) => Effect.gen(function* () {
1824
+ const projectId = config.extra?.betterUpdate?.projectId;
1825
+ if (typeof projectId !== "string") return yield* new ProjectNotLinkedError({ message: "Project not linked. Run `better-update link` to connect this project, or set extra.betterUpdate.projectId in your Expo config." });
1826
+ return projectId;
1827
+ });
1828
+ const extractSlug = (config) => Effect.gen(function* () {
1829
+ if (typeof config.slug !== "string") return yield* new ProjectNotLinkedError({ message: "Missing slug in your Expo config. Required to identify the project." });
1830
+ return config.slug;
1831
+ });
1832
+ /** Convenience reader for command code: resolves projectRoot from CliRuntime. */
1833
+ const readProjectId = Effect.gen(function* () {
1834
+ return yield* extractProjectId(yield* readExpoConfig(yield* (yield* CliRuntime).cwd));
1835
+ });
1836
+ const buildManualPasteHint = (id, message) => {
1837
+ return [
1838
+ `Cannot write projectId to a dynamic Expo config${message ? ` (${message})` : ""}.`,
1839
+ "Add this to your config manually:",
1840
+ "",
1841
+ ` extra: { betterUpdate: { projectId: "${id}" } }`
1842
+ ].join("\n");
1843
+ };
1844
+ const writeProjectId = (projectRoot, id) => Effect.gen(function* () {
1845
+ const result = yield* Effect.tryPromise({
1846
+ try: async () => loadExpoConfigModule().modifyConfigAsync(projectRoot, { extra: { betterUpdate: { projectId: id } } }, { skipSDKVersionRequirement: true }),
1847
+ catch: (cause) => new ProjectNotLinkedError({ message: `Failed to write projectId to Expo config: ${formatCause(cause)}` })
1848
+ });
1849
+ if (result.type === "fail" || result.type === "warn" && result.config === null) return yield* new ProjectNotLinkedError({ message: buildManualPasteHint(id, result.message) });
1850
+ const paths = yield* getConfigFilePaths(projectRoot);
1851
+ return {
1852
+ type: result.type,
1853
+ configPath: paths.staticConfigPath,
1854
+ ...result.message === void 0 ? {} : { message: result.message }
1855
+ };
1856
+ });
1857
+ const extractBuildNumber = (config, platform) => {
1858
+ if (platform === "ios") return config.ios?.buildNumber;
1859
+ if (config.android?.versionCode === void 0) return;
1860
+ return String(config.android.versionCode);
1861
+ };
1862
+ const extractRawRuntimeVersion = (config) => {
1863
+ const raw = config.runtimeVersion;
1864
+ if (typeof raw === "string") return raw;
1865
+ if (typeof raw === "object" && raw !== null && typeof raw.policy === "string") return { policy: raw.policy };
1866
+ };
1867
+ /**
1868
+ * Extract AppMeta from a resolved ExpoConfig (from `@expo/config`).
1869
+ */
1870
+ const readAppMeta = (config, platform) => Effect.gen(function* () {
1871
+ if (platform === "ios" && !config.ios) return yield* new BuildProfileError({ message: "Missing ios section in your Expo config. Required for iOS builds (bundleIdentifier)." });
1872
+ if (platform === "android" && !config.android) return yield* new BuildProfileError({ message: "Missing android section in your Expo config. Required for Android builds (package)." });
1873
+ return {
1874
+ bundleId: config.ios?.bundleIdentifier,
1875
+ androidPackage: config.android?.package,
1876
+ appVersion: config.version,
1877
+ buildNumber: extractBuildNumber(config, platform),
1878
+ rawRuntimeVersion: extractRawRuntimeVersion(config)
1879
+ };
1880
+ });
1881
+
1767
1882
  //#endregion
1768
1883
  //#region src/lib/output.ts
1769
1884
  const printTable = (headers, rows) => Effect.gen(function* () {
@@ -2925,9 +3040,6 @@ const reserveAndUpload = (api, input) => Effect.gen(function* () {
2925
3040
  //#endregion
2926
3041
  //#region src/lib/build-profile.ts
2927
3042
  const asString$1 = (value) => typeof value === "string" ? value : void 0;
2928
- const getBetterUpdateExtra = (appJson) => {
2929
- return asRecord(asRecord(asRecord(appJson["expo"])?.["extra"])?.["betterUpdate"]);
2930
- };
2931
3043
  const VALID_IOS_DISTRIBUTIONS = [
2932
3044
  "app-store",
2933
3045
  "ad-hoc",
@@ -2970,11 +3082,11 @@ const readAndroidProfile = (raw) => {
2970
3082
  ...flavor === void 0 ? {} : { flavor }
2971
3083
  };
2972
3084
  };
2973
- const readBuildProfile = (appJson, profileName) => Effect.gen(function* () {
2974
- const profiles = asRecord(getBetterUpdateExtra(appJson)?.["profiles"]);
2975
- if (!profiles) return yield* new BuildProfileError({ message: "No build profiles defined. Add expo.extra.betterUpdate.profiles to app.json." });
3085
+ const readBuildProfile = (config, profileName) => Effect.gen(function* () {
3086
+ const profiles = asRecord(config.extra?.betterUpdate?.profiles);
3087
+ if (!profiles) return yield* new BuildProfileError({ message: "No build profiles defined. Add extra.betterUpdate.profiles to your Expo config." });
2976
3088
  const profileRaw = asRecord(profiles[profileName]);
2977
- if (!profileRaw) return yield* new BuildProfileError({ message: `Build profile "${profileName}" not found in app.json.` });
3089
+ if (!profileRaw) return yield* new BuildProfileError({ message: `Build profile "${profileName}" not found in your Expo config.` });
2978
3090
  const environment = asString$1(profileRaw["environment"]) ?? "production";
2979
3091
  const ios = readIosProfile(profileRaw["ios"]);
2980
3092
  const android = readAndroidProfile(profileRaw["android"]);
@@ -2985,35 +3097,10 @@ const readBuildProfile = (appJson, profileName) => Effect.gen(function* () {
2985
3097
  ...android === void 0 ? {} : { android }
2986
3098
  };
2987
3099
  });
2988
- const readRuntimeVersionMeta = (appJson) => Effect.gen(function* () {
2989
- const expo = asRecord(appJson["expo"]);
2990
- if (!expo) return yield* new BuildProfileError({ message: "Missing expo section in app.json." });
2991
- return {
2992
- appVersion: asString$1(expo["version"]),
2993
- rawRuntimeVersion: readRawRuntimeVersion(expo["runtimeVersion"])
2994
- };
3100
+ const readRuntimeVersionMeta = (config) => ({
3101
+ appVersion: config.version,
3102
+ rawRuntimeVersion: readRawRuntimeVersion(config.runtimeVersion)
2995
3103
  });
2996
- const readAppMeta = (appJson, platform) => Effect.gen(function* () {
2997
- const expo = asRecord(appJson["expo"]);
2998
- if (!expo) return yield* new BuildProfileError({ message: "Missing expo section in app.json." });
2999
- if (platform === "ios") {
3000
- if (!asRecord(expo["ios"])) return yield* new BuildProfileError({ message: "Missing expo.ios section in app.json. Required for iOS builds (bundleIdentifier)." });
3001
- } else if (!asRecord(expo["android"])) return yield* new BuildProfileError({ message: "Missing expo.android section in app.json. Required for Android builds (package)." });
3002
- const iosSection = asRecord(expo["ios"]);
3003
- const androidSection = asRecord(expo["android"]);
3004
- const buildNumber = platform === "ios" ? asString$1(iosSection?.["buildNumber"]) : asStringOrNumber(androidSection?.["versionCode"]);
3005
- return {
3006
- bundleId: asString$1(iosSection?.["bundleIdentifier"]),
3007
- androidPackage: asString$1(androidSection?.["package"]),
3008
- appVersion: asString$1(expo["version"]),
3009
- buildNumber,
3010
- rawRuntimeVersion: readRawRuntimeVersion(expo["runtimeVersion"])
3011
- };
3012
- });
3013
- const asStringOrNumber = (value) => {
3014
- if (typeof value === "string") return value;
3015
- if (typeof value === "number" && Number.isFinite(value)) return String(value);
3016
- };
3017
3104
  const readRawRuntimeVersion = (value) => {
3018
3105
  if (typeof value === "string") return value;
3019
3106
  const policy = asString$1(asRecord(value)?.["policy"]);
@@ -3031,62 +3118,6 @@ const pullEnvVars = (api, { projectId, environment }) => api["env-vars"].export(
3031
3118
  environment
3032
3119
  } }).pipe(Effect.map((result) => Object.fromEntries(result.items.map((item) => [item.key, item.value]))), Effect.mapError((cause) => new EnvExportError({ message: `Failed to export environment variables for "${environment}": ${String(cause)}` })));
3033
3120
 
3034
- //#endregion
3035
- //#region src/lib/expo-config.ts
3036
- /**
3037
- * Resolve the full Expo config using `@expo/config`, which handles
3038
- * `app.json`, `app.config.js`, and `app.config.ts` with plugin evaluation.
3039
- *
3040
- * `envVars` are applied as a scoped overlay on `process.env` for the duration
3041
- * of the call (restored afterwards) so dynamic configs (`app.config.js`)
3042
- * can read them without leaking server-side secrets to child processes.
3043
- *
3044
- * Falls back to undefined if `@expo/config` is not available or fails.
3045
- */
3046
- const readExpoConfig = (projectRoot, envVars = {}) => Effect.acquireUseRelease(Effect.sync(() => {
3047
- const previous = {};
3048
- for (const [key, value] of Object.entries(envVars)) {
3049
- previous[key] = process$1.env[key];
3050
- process$1.env[key] = value;
3051
- }
3052
- return previous;
3053
- }), () => Effect.try({
3054
- try: () => {
3055
- const { getConfig } = __require("@expo/config");
3056
- const { exp } = getConfig(projectRoot, { skipSDKVersionRequirement: true });
3057
- return exp;
3058
- },
3059
- catch: () => void 0
3060
- }).pipe(Effect.catchAll(() => Effect.succeed(void 0))), (previous) => Effect.sync(() => {
3061
- for (const [key, value] of Object.entries(previous)) if (value === void 0) delete process$1.env[key];
3062
- else process$1.env[key] = value;
3063
- }));
3064
- const extractBuildNumber = (config, platform) => {
3065
- if (platform === "ios") return config.ios?.buildNumber;
3066
- if (config.android?.versionCode === void 0) return;
3067
- return String(config.android.versionCode);
3068
- };
3069
- const extractRawRuntimeVersion = (config) => {
3070
- if (typeof config.runtimeVersion === "string") return config.runtimeVersion;
3071
- if (typeof config.runtimeVersion === "object") return config.runtimeVersion;
3072
- };
3073
- /**
3074
- * Extract AppMeta from a resolved ExpoConfig (from `@expo/config`).
3075
- * Mirrors `readAppMeta` from build-profile.ts but uses the resolved config
3076
- * which handles dynamic configs (`app.config.js`, `app.config.ts`).
3077
- */
3078
- const readAppMetaFromConfig = (config, platform) => Effect.gen(function* () {
3079
- if (platform === "ios" && !config.ios) return yield* new BuildProfileError({ message: "Missing expo.ios section in config. Required for iOS builds (bundleIdentifier)." });
3080
- if (platform === "android" && !config.android) return yield* new BuildProfileError({ message: "Missing expo.android section in config. Required for Android builds (package)." });
3081
- return {
3082
- bundleId: config.ios?.bundleIdentifier,
3083
- androidPackage: config.android?.package,
3084
- appVersion: config.version,
3085
- buildNumber: extractBuildNumber(config, platform),
3086
- rawRuntimeVersion: extractRawRuntimeVersion(config)
3087
- };
3088
- });
3089
-
3090
3121
  //#endregion
3091
3122
  //#region src/lib/git-context.ts
3092
3123
  const runString = (cmd, cwd) => Command.string(Command.workingDirectory(cmd, cwd));
@@ -3200,7 +3231,7 @@ const resolveRuntimeVersion = ({ raw, appVersion, projectRoot }) => Effect.gen(f
3200
3231
  return appVersion;
3201
3232
  }
3202
3233
  if (policy === "fingerprint") return yield* runFingerprintFull(projectRoot).pipe(Effect.map((result) => result.hash), Effect.mapError((cause) => new RuntimeVersionError({ message: cause.message })));
3203
- if (policy === "nativeVersion") return yield* new RuntimeVersionError({ message: "runtimeVersion policy \"nativeVersion\" is not supported. Set a static runtimeVersion string in app.json." });
3234
+ if (policy === "nativeVersion") return yield* new RuntimeVersionError({ message: "runtimeVersion policy \"nativeVersion\" is not supported. Set a static runtimeVersion string in your Expo config." });
3204
3235
  return yield* new RuntimeVersionError({ message: `Unsupported runtimeVersion policy "${policy}". Use a static string, "appVersion", or "fingerprint".` });
3205
3236
  });
3206
3237
 
@@ -3225,7 +3256,7 @@ const runIosPlatformBuild = (input) => Effect.gen(function* () {
3225
3256
  if (!profile.ios) return yield* new BuildProfileError({ message: `Profile "${profile.name}" has no ios section.` });
3226
3257
  const iosProfile = profile.ios;
3227
3258
  const iosBundleId = appMeta.bundleId;
3228
- if (!iosBundleId) return yield* new BuildProfileError({ message: "Missing expo.ios.bundleIdentifier in app.json." });
3259
+ if (!iosBundleId) return yield* new BuildProfileError({ message: "Missing ios.bundleIdentifier in your Expo config." });
3229
3260
  return {
3230
3261
  build: yield* runIosBuild({
3231
3262
  api,
@@ -3250,7 +3281,7 @@ const runAndroidPlatformBuild = (input) => Effect.gen(function* () {
3250
3281
  if (!profile.android) return yield* new BuildProfileError({ message: `Profile "${profile.name}" has no android section.` });
3251
3282
  const androidProfile = profile.android;
3252
3283
  const androidBundleId = appMeta.androidPackage;
3253
- if (!androidBundleId) return yield* new BuildProfileError({ message: "Missing expo.android.package in app.json." });
3284
+ if (!androidBundleId) return yield* new BuildProfileError({ message: "Missing android.package in your Expo config." });
3254
3285
  const gradleConfig = yield* readGradleConfig(`${projectRoot}/android`);
3255
3286
  yield* warnOnGradleMismatch(gradleConfig, androidBundleId);
3256
3287
  const applicationIdentifier = gradleConfig?.applicationId ?? androidBundleId;
@@ -3280,15 +3311,15 @@ const runPlatformBuild = (input) => input.options.platform === "ios" ? runIosPla
3280
3311
  const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
3281
3312
  const api = yield* apiClient;
3282
3313
  const projectRoot = yield* (yield* CliRuntime).cwd;
3283
- const appJson = yield* readAppJson;
3284
- const projectId = yield* readProjectId;
3285
- const profile = yield* readBuildProfile(appJson, options.profileName);
3314
+ const baseConfig = yield* readExpoConfig(projectRoot);
3315
+ const projectId = yield* extractProjectId(baseConfig);
3286
3316
  const envVars = yield* pullEnvVars(api, {
3287
3317
  projectId,
3288
- environment: profile.environment
3318
+ environment: (yield* readBuildProfile(baseConfig, options.profileName)).environment
3289
3319
  });
3290
3320
  const expoConfig = yield* readExpoConfig(projectRoot, envVars);
3291
- const appMeta = expoConfig ? yield* readAppMetaFromConfig(expoConfig, options.platform).pipe(Effect.tap(() => Console.log("Resolved app config via @expo/config")), Effect.catchAll(() => readAppMeta(appJson, options.platform))) : yield* readAppMeta(appJson, options.platform);
3321
+ const profile = yield* readBuildProfile(expoConfig, options.profileName);
3322
+ const appMeta = yield* readAppMeta(expoConfig, options.platform);
3292
3323
  const runtimeVersion = yield* resolveRuntimeVersion({
3293
3324
  raw: appMeta.rawRuntimeVersion,
3294
3325
  appVersion: appMeta.appVersion,
@@ -3572,7 +3603,7 @@ const listCommand$5 = defineCommand({
3572
3603
  //#region src/application/upload-workflow.ts
3573
3604
  const resolveIosTarget = (profile, appMeta) => Effect.gen(function* () {
3574
3605
  if (!profile.ios) return yield* new BuildProfileError({ message: `Profile "${profile.name}" has no ios section.` });
3575
- if (!appMeta.bundleId) return yield* new BuildProfileError({ message: "Missing expo.ios.bundleIdentifier in app.json." });
3606
+ if (!appMeta.bundleId) return yield* new BuildProfileError({ message: "Missing ios.bundleIdentifier in your Expo config." });
3576
3607
  return {
3577
3608
  target: {
3578
3609
  platform: "ios",
@@ -3584,7 +3615,7 @@ const resolveIosTarget = (profile, appMeta) => Effect.gen(function* () {
3584
3615
  });
3585
3616
  const resolveAndroidTarget = (profile, appMeta, projectRoot) => Effect.gen(function* () {
3586
3617
  if (!profile.android) return yield* new BuildProfileError({ message: `Profile "${profile.name}" has no android section.` });
3587
- if (!appMeta.androidPackage) return yield* new BuildProfileError({ message: "Missing expo.android.package in app.json." });
3618
+ if (!appMeta.androidPackage) return yield* new BuildProfileError({ message: "Missing android.package in your Expo config." });
3588
3619
  const gradleConfig = yield* readGradleConfig(`${projectRoot}/android`);
3589
3620
  yield* warnOnGradleMismatch(gradleConfig, appMeta.androidPackage);
3590
3621
  const bundleId = gradleConfig?.applicationId ?? appMeta.androidPackage;
@@ -3605,14 +3636,13 @@ const runUploadWorkflow = (options) => Effect.gen(function* () {
3605
3636
  const api = yield* apiClient;
3606
3637
  const projectRoot = yield* (yield* CliRuntime).cwd;
3607
3638
  if (!(yield* (yield* FileSystem.FileSystem).exists(options.artifactPath).pipe(Effect.orElseSucceed(() => false)))) yield* new ArtifactNotFoundError({ message: `Artifact not found at ${options.artifactPath}.` });
3608
- const appJson = yield* readAppJson;
3609
- const projectId = yield* readProjectId;
3610
- const profile = yield* readBuildProfile(appJson, options.profileName);
3611
- const expoConfig = yield* readExpoConfig(projectRoot, yield* pullEnvVars(api, {
3639
+ const baseConfig = yield* readExpoConfig(projectRoot);
3640
+ const projectId = yield* extractProjectId(baseConfig);
3641
+ const profile = yield* readBuildProfile(baseConfig, options.profileName);
3642
+ const appMeta = yield* readAppMeta(yield* readExpoConfig(projectRoot, yield* pullEnvVars(api, {
3612
3643
  projectId,
3613
3644
  environment: profile.environment
3614
- }));
3615
- const appMeta = expoConfig ? yield* readAppMetaFromConfig(expoConfig, options.platform).pipe(Effect.tap(() => Console.log("Resolved app config via @expo/config")), Effect.catchAll(() => readAppMeta(appJson, options.platform))) : yield* readAppMeta(appJson, options.platform);
3645
+ })), options.platform);
3616
3646
  const runtimeVersion = yield* resolveRuntimeVersion({
3617
3647
  raw: appMeta.rawRuntimeVersion,
3618
3648
  appVersion: appMeta.appVersion,
@@ -4787,9 +4817,10 @@ const initCommand = defineCommand({
4787
4817
  description: "Link the local Expo project to a better-update project"
4788
4818
  },
4789
4819
  run: async () => runEffect(Effect.gen(function* () {
4790
- const expo = asRecord((yield* readAppJson)["expo"]);
4791
- const name = asString$1(expo?.["name"]) ?? asString$1(expo?.["slug"]) ?? "untitled";
4792
- const slug = yield* readSlug;
4820
+ const projectRoot = yield* (yield* CliRuntime).cwd;
4821
+ const config = yield* readExpoConfig(projectRoot);
4822
+ const name = config.name ?? config.slug ?? "untitled";
4823
+ const slug = yield* extractSlug(config);
4793
4824
  yield* Console.log(`Linking project: ${name} (${slug})`);
4794
4825
  const api = yield* apiClient;
4795
4826
  const { items } = yield* api.projects.list({ urlParams: {
@@ -4797,19 +4828,22 @@ const initCommand = defineCommand({
4797
4828
  limit: 100
4798
4829
  } });
4799
4830
  const existing = items.find((project) => project.slug === slug);
4800
- if (existing) {
4801
- yield* Console.log(`Found existing project: ${existing.name} (${existing.id})`);
4802
- yield* writeProjectId(existing.id);
4803
- } else {
4831
+ const writeResult = yield* writeProjectId(projectRoot, yield* Effect.gen(function* () {
4832
+ if (existing) {
4833
+ yield* Console.log(`Found existing project: ${existing.name} (${existing.id})`);
4834
+ return existing.id;
4835
+ }
4804
4836
  yield* Console.log("No existing project found. Creating new project...");
4805
- const project = yield* api.projects.create({ payload: {
4837
+ const created = yield* api.projects.create({ payload: {
4806
4838
  name,
4807
4839
  slug
4808
4840
  } });
4809
- yield* Console.log(`Created project: ${project.name} (${project.id})`);
4810
- yield* writeProjectId(project.id);
4811
- }
4812
- yield* Console.log("Project linked successfully. ID saved to app.json.");
4841
+ yield* Console.log(`Created project: ${created.name} (${created.id})`);
4842
+ return created.id;
4843
+ }));
4844
+ const target = writeResult.configPath ? path.relative(projectRoot, writeResult.configPath) : "your Expo config";
4845
+ yield* Console.log(`Project linked successfully. ID saved to ${target}.`);
4846
+ if (writeResult.type === "warn" && writeResult.message) yield* Console.log(`Note: ${writeResult.message}`);
4813
4847
  }))
4814
4848
  });
4815
4849
 
@@ -5244,6 +5278,7 @@ const updateErrorExtras = {
5244
5278
  UpdateCommandError: 2,
5245
5279
  BuildProfileError: 2,
5246
5280
  RuntimeVersionError: 2,
5281
+ EnvExportError: 7,
5247
5282
  UpdateRollbackError: 2,
5248
5283
  UpdatePromoteError: 2
5249
5284
  };
@@ -5552,10 +5587,9 @@ const readExpoExportAssets = ({ exportDir, platform }) => Effect.gen(function* (
5552
5587
 
5553
5588
  //#endregion
5554
5589
  //#region src/lib/update-platforms.ts
5555
- const resolveUpdatePlatforms = (appJson, requestedPlatform) => {
5590
+ const resolveUpdatePlatforms = (config, requestedPlatform) => {
5556
5591
  if (requestedPlatform !== "all") return [requestedPlatform];
5557
- const expo = asRecord(appJson["expo"]);
5558
- return ["ios", "android"].filter((platform) => asRecord(expo?.[platform]) !== void 0);
5592
+ return ["ios", "android"].filter((platform) => typeof config[platform] === "object" && config[platform] !== null);
5559
5593
  };
5560
5594
 
5561
5595
  //#endregion
@@ -5581,7 +5615,7 @@ const preparePlatformAssets = ({ exportDir, platform }) => Effect.gen(function*
5581
5615
  const publishPlatform = (params) => Effect.gen(function* () {
5582
5616
  const api = yield* apiClient;
5583
5617
  const assetUploader = yield* UpdateAssetUploader;
5584
- const runtimeVersionMeta = yield* readRuntimeVersionMeta(params.appJson);
5618
+ const runtimeVersionMeta = readRuntimeVersionMeta(params.expoConfig);
5585
5619
  const runtimeVersion = yield* resolveRuntimeVersion({
5586
5620
  raw: runtimeVersionMeta.rawRuntimeVersion,
5587
5621
  appVersion: runtimeVersionMeta.appVersion,
@@ -5654,15 +5688,15 @@ const publishPlatform = (params) => Effect.gen(function* () {
5654
5688
  const runUpdatePublish = (options) => Effect.scoped(Effect.gen(function* () {
5655
5689
  const projectRoot = yield* (yield* CliRuntime).cwd;
5656
5690
  const api = yield* apiClient;
5657
- const projectId = yield* readProjectId;
5658
- const slug = yield* readSlug;
5659
- const appJson = yield* readAppJson;
5660
- const platforms = resolveUpdatePlatforms(appJson, options.platform);
5661
- if (platforms.length === 0) return yield* new UpdatePublishError({ message: "No publishable platforms found in app.json. Add an \"expo.ios\" or \"expo.android\" section, or pass --platform explicitly." });
5691
+ const projectId = yield* extractProjectId(yield* readExpoConfig(projectRoot));
5662
5692
  const environmentVars = yield* pullEnvVars(api, {
5663
5693
  projectId,
5664
5694
  environment: options.environment
5665
5695
  });
5696
+ const expoConfig = yield* readExpoConfig(projectRoot, environmentVars);
5697
+ const slug = yield* extractSlug(expoConfig);
5698
+ const platforms = resolveUpdatePlatforms(expoConfig, options.platform);
5699
+ if (platforms.length === 0) return yield* new UpdatePublishError({ message: "No publishable platforms found in your Expo config. Add an \"ios\" or \"android\" section, or pass --platform explicitly." });
5666
5700
  const expoClientConfig = yield* readExpoPublicConfig({
5667
5701
  projectRoot,
5668
5702
  envVars: environmentVars
@@ -5718,7 +5752,7 @@ const runUpdatePublish = (options) => Effect.scoped(Effect.gen(function* () {
5718
5752
  environmentVars,
5719
5753
  expoClientConfig,
5720
5754
  clear: options.clear,
5721
- appJson,
5755
+ expoConfig,
5722
5756
  platform,
5723
5757
  signedPayload: signedPayloads[platform] ?? null,
5724
5758
  rolloutPercentage: options.rolloutPercentage
@@ -5889,12 +5923,14 @@ const createRollbackForPlatform = (params) => Effect.gen(function* () {
5889
5923
  });
5890
5924
  const runUpdateRollback = (options) => Effect.gen(function* () {
5891
5925
  const projectRoot = yield* (yield* CliRuntime).cwd;
5892
- yield* readProjectId;
5893
- const projectSlug = yield* readSlug;
5894
- const appJson = yield* readAppJson;
5895
- const platforms = resolveUpdatePlatforms(appJson, options.platform);
5896
- if (platforms.length === 0) return yield* new UpdateRollbackError({ message: "No publishable platforms found in app.json. Add an \"expo.ios\" or \"expo.android\" section, or pass --platform explicitly." });
5897
- const { appVersion, rawRuntimeVersion } = yield* readRuntimeVersionMeta(appJson);
5926
+ const config = yield* readExpoConfig(projectRoot, yield* pullEnvVars(yield* apiClient, {
5927
+ projectId: yield* extractProjectId(yield* readExpoConfig(projectRoot)),
5928
+ environment: options.environment
5929
+ }));
5930
+ const projectSlug = yield* extractSlug(config);
5931
+ const platforms = resolveUpdatePlatforms(config, options.platform);
5932
+ if (platforms.length === 0) return yield* new UpdateRollbackError({ message: "No publishable platforms found in your Expo config. Add an \"ios\" or \"android\" section, or pass --platform explicitly." });
5933
+ const { appVersion, rawRuntimeVersion } = readRuntimeVersionMeta(config);
5898
5934
  const runtimeVersion = yield* resolveRuntimeVersion({
5899
5935
  raw: rawRuntimeVersion,
5900
5936
  appVersion,
@@ -5951,6 +5987,11 @@ const rollbackCommand = defineCommand({
5951
5987
  description: "Platform(s) to roll back"
5952
5988
  },
5953
5989
  message: { type: "string" },
5990
+ environment: {
5991
+ type: "string",
5992
+ default: "production",
5993
+ description: "Env vars scope"
5994
+ },
5954
5995
  "commit-time": { type: "string" },
5955
5996
  "directive-body-file": { type: "string" },
5956
5997
  "signature-file": { type: "string" },
@@ -5960,6 +6001,7 @@ const rollbackCommand = defineCommand({
5960
6001
  const result = yield* runUpdateRollback({
5961
6002
  branch: args.branch,
5962
6003
  platform: args.platform,
6004
+ environment: args.environment,
5963
6005
  message: args.message,
5964
6006
  commitTime: args["commit-time"],
5965
6007
  directiveBodyFile: args["directive-body-file"],
@@ -6076,14 +6118,88 @@ const updateCommand = defineCommand({
6076
6118
  }
6077
6119
  });
6078
6120
 
6121
+ //#endregion
6122
+ //#region src/lib/detect-installer.ts
6123
+ const detectInstaller = (modulePath) => {
6124
+ const normalized = modulePath.replaceAll("\\", "/").toLowerCase();
6125
+ if (normalized.includes("/.bun/")) return "bun";
6126
+ if (normalized.includes("/pnpm/")) return "pnpm";
6127
+ if (normalized.includes("/.yarn/") || normalized.includes("/yarn/")) return "yarn";
6128
+ return "npm";
6129
+ };
6130
+ const INSTALL_COMMANDS = {
6131
+ bun: "bun add -g @better-update/cli@latest",
6132
+ pnpm: "pnpm add -g @better-update/cli@latest",
6133
+ yarn: "yarn global add @better-update/cli@latest",
6134
+ npm: "npm install -g @better-update/cli@latest"
6135
+ };
6136
+ const installCommand = (installer) => INSTALL_COMMANDS[installer];
6137
+ const detectInstallerFromImportMetaUrl = (importMetaUrl) => detectInstaller(fileURLToPath(importMetaUrl));
6138
+
6139
+ //#endregion
6140
+ //#region src/lib/semver-compare.ts
6141
+ const stripPrerelease = (version) => {
6142
+ return version.replace(/-.*$/u, "").split(".").map((part) => Number.parseInt(part, 10) || 0);
6143
+ };
6144
+ const isNewerVersion = (latest, current) => {
6145
+ const [la = 0, lb = 0, lc = 0] = stripPrerelease(latest);
6146
+ const [ca = 0, cb = 0, cc = 0] = stripPrerelease(current);
6147
+ if (la !== ca) return la > ca;
6148
+ if (lb !== cb) return lb > cb;
6149
+ return lc > cc;
6150
+ };
6151
+
6152
+ //#endregion
6153
+ //#region src/lib/version-notifier.ts
6154
+ const formatNotice = (current, latest, command) => [
6155
+ "",
6156
+ `╭─ Update available: @better-update/cli ${current} → ${latest}`,
6157
+ `│ Run: ${command}`,
6158
+ "╰─",
6159
+ ""
6160
+ ].join("\n");
6161
+ const isOptedOut = Effect.gen(function* () {
6162
+ const value = yield* (yield* CliRuntime).getEnv("BETTER_UPDATE_DISABLE_UPDATE_NOTIFIER");
6163
+ return value === "1" || value === "true";
6164
+ });
6165
+ const bootstrapVersionCheck = (currentVersion, installerHint, spawnRefresh) => Effect.gen(function* () {
6166
+ if (yield* isOptedOut) return;
6167
+ const versionCheck = yield* VersionCheck;
6168
+ const cached = yield* versionCheck.cachedLatest;
6169
+ if (cached && isNewerVersion(cached, currentVersion)) {
6170
+ const installer = detectInstallerFromImportMetaUrl(installerHint);
6171
+ yield* Console.error(formatNotice(currentVersion, cached, installCommand(installer)));
6172
+ }
6173
+ if (yield* versionCheck.cacheStale) spawnRefresh();
6174
+ });
6175
+ const refreshVersionCacheIfStale = Effect.gen(function* () {
6176
+ if (yield* isOptedOut) return;
6177
+ const versionCheck = yield* VersionCheck;
6178
+ if (yield* versionCheck.cacheStale) yield* versionCheck.refreshCache;
6179
+ });
6180
+
6079
6181
  //#endregion
6080
6182
  //#region src/index.ts
6183
+ const REFRESH_VERSION_CACHE_FLAG = "__refresh-version-cache";
6184
+ if (process.argv[2] === REFRESH_VERSION_CACHE_FLAG) {
6185
+ await Effect.runPromise(refreshVersionCacheIfStale.pipe(Effect.provide(CliLive)));
6186
+ process.exit(0);
6187
+ }
6188
+ const spawnDetachedRefresh = () => {
6189
+ spawn(process.execPath, [import.meta.filename, REFRESH_VERSION_CACHE_FLAG], {
6190
+ detached: true,
6191
+ stdio: "ignore"
6192
+ }).unref();
6193
+ };
6081
6194
  await runMain(defineCommand({
6082
6195
  meta: {
6083
6196
  name: "better-update",
6084
6197
  version,
6085
6198
  description: "Publish OTA updates and builds for Expo apps"
6086
6199
  },
6200
+ setup: async () => {
6201
+ await Effect.runPromise(bootstrapVersionCheck(version, import.meta.url, spawnDetachedRefresh).pipe(Effect.provide(CliLive)));
6202
+ },
6087
6203
  subCommands: {
6088
6204
  login: loginCommand,
6089
6205
  logout: logoutCommand,