@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 +331 -215
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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.
|
|
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://
|
|
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
|
|
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 = (
|
|
2974
|
-
const profiles = asRecord(
|
|
2975
|
-
if (!profiles) return yield* new BuildProfileError({ message: "No build profiles defined. Add
|
|
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
|
|
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 = (
|
|
2989
|
-
|
|
2990
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3284
|
-
const projectId = yield*
|
|
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:
|
|
3318
|
+
environment: (yield* readBuildProfile(baseConfig, options.profileName)).environment
|
|
3289
3319
|
});
|
|
3290
3320
|
const expoConfig = yield* readExpoConfig(projectRoot, envVars);
|
|
3291
|
-
const
|
|
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
|
|
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
|
|
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
|
|
3609
|
-
const projectId = yield*
|
|
3610
|
-
const profile = yield* readBuildProfile(
|
|
3611
|
-
const
|
|
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
|
|
4791
|
-
const
|
|
4792
|
-
const
|
|
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
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
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
|
|
4837
|
+
const created = yield* api.projects.create({ payload: {
|
|
4806
4838
|
name,
|
|
4807
4839
|
slug
|
|
4808
4840
|
} });
|
|
4809
|
-
yield* Console.log(`Created project: ${
|
|
4810
|
-
|
|
4811
|
-
}
|
|
4812
|
-
|
|
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 = (
|
|
5590
|
+
const resolveUpdatePlatforms = (config, requestedPlatform) => {
|
|
5556
5591
|
if (requestedPlatform !== "all") return [requestedPlatform];
|
|
5557
|
-
|
|
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 =
|
|
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*
|
|
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
|
-
|
|
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*
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
const
|
|
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,
|