@better-update/cli 0.24.2 → 0.26.1
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 +966 -219
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { execFile, spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import { defineCommand, runMain } from "citty";
|
|
5
|
-
import { Console, Context, Data, Deferred, Duration, Effect, Either, Layer, Match, Option, ParseResult, Schedule, Schema } from "effect";
|
|
5
|
+
import { Clock, Console, Context, Data, Deferred, Duration, Effect, Either, Layer, Match, Option, ParseResult, Schedule, Schema } from "effect";
|
|
6
6
|
import { Command, FetchHttpClient, FileSystem, Headers as Headers$1, HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, HttpApiSecurity, HttpClient, HttpClientRequest, OpenApi, Path } from "@effect/platform";
|
|
7
7
|
import { NodeContext } from "@effect/platform-node";
|
|
8
8
|
import path from "node:path";
|
|
@@ -11,7 +11,8 @@ import AppleUtils from "@expo/apple-utils";
|
|
|
11
11
|
import { autocomplete, cancel, confirm, isCancel, multiselect, password, select, text } from "@clack/prompts";
|
|
12
12
|
import { open, readFile, writeFile } from "node:fs/promises";
|
|
13
13
|
import { X509Certificate, createHash, createSign, createVerify, randomBytes, randomUUID } from "node:crypto";
|
|
14
|
-
import { accessSync, chmodSync, constants, createReadStream,
|
|
14
|
+
import { accessSync, chmodSync, constants, createReadStream, promises } from "node:fs";
|
|
15
|
+
import { Entry } from "@napi-rs/keyring";
|
|
15
16
|
import { once } from "node:events";
|
|
16
17
|
import { createServer } from "node:http";
|
|
17
18
|
import { maxBy, uniqBy } from "es-toolkit";
|
|
@@ -33,7 +34,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
33
34
|
|
|
34
35
|
//#endregion
|
|
35
36
|
//#region package.json
|
|
36
|
-
var version = "0.
|
|
37
|
+
var version = "0.26.1";
|
|
37
38
|
|
|
38
39
|
//#endregion
|
|
39
40
|
//#region src/lib/interactive-mode.ts
|
|
@@ -255,7 +256,7 @@ var AnalyticsGroup = class extends HttpApiGroup.make("analytics").add(HttpApiEnd
|
|
|
255
256
|
|
|
256
257
|
//#endregion
|
|
257
258
|
//#region ../../packages/api/src/domain/android-application-identifier.ts
|
|
258
|
-
const AndroidPackageName = Schema.String.pipe(Schema.pattern(/^[A-Za-z][A-Za-z0-9_]*(
|
|
259
|
+
const AndroidPackageName = Schema.String.pipe(Schema.pattern(/^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+$/u, { message: () => "Package name must be reverse-domain style (e.g., com.acme.app)" }));
|
|
259
260
|
var AndroidApplicationIdentifier = class extends Schema.Class("AndroidApplicationIdentifier")({
|
|
260
261
|
id: Id,
|
|
261
262
|
organizationId: Id,
|
|
@@ -2929,6 +2930,81 @@ const UpdateAssetUploaderLive = Layer.effect(UpdateAssetUploader, Effect.gen(fun
|
|
|
2929
2930
|
}) };
|
|
2930
2931
|
}));
|
|
2931
2932
|
|
|
2933
|
+
//#endregion
|
|
2934
|
+
//#region src/services/vault-cache.ts
|
|
2935
|
+
/**
|
|
2936
|
+
* "Unlock once, reuse" for the credential vault — the analog of macOS
|
|
2937
|
+
* `security unlock-keychain`. The first vault operation in a session prompts for
|
|
2938
|
+
* the device passphrase, unwraps the vault key, and stows it in the OS keychain
|
|
2939
|
+
* (`@napi-rs/keyring`: macOS Keychain / Windows Credential Manager / Linux
|
|
2940
|
+
* libsecret) with a short TTL; subsequent commands read it back and skip the
|
|
2941
|
+
* prompt + Argon2id derivation entirely until it expires.
|
|
2942
|
+
*
|
|
2943
|
+
* What is cached is the unwrapped **vault key**, never the passphrase or the age
|
|
2944
|
+
* private key — so the blast radius of a leaked keychain entry is one vault
|
|
2945
|
+
* version's credentials, and only until the TTL lapses.
|
|
2946
|
+
*/
|
|
2947
|
+
/** How long a cached vault key stays valid before a fresh passphrase is required. */
|
|
2948
|
+
const VAULT_CACHE_TTL_MS = 900 * 1e3;
|
|
2949
|
+
/** Keychain service name; the account is the recipient's public key. */
|
|
2950
|
+
const KEYCHAIN_SERVICE = "better-update-vault";
|
|
2951
|
+
const isCachedVaultEntry = (value) => isRecord(value) && typeof value["vaultKey"] === "string" && typeof value["vaultVersion"] === "number" && typeof value["keyId"] === "string" && typeof value["exp"] === "number";
|
|
2952
|
+
/** Serialize an unlocked vault into a keychain blob, stamping a TTL from `now`. */
|
|
2953
|
+
const encodeCacheEntry = (vault, now, ttlMs = VAULT_CACHE_TTL_MS) => JSON.stringify({
|
|
2954
|
+
vaultKey: toBase64(vault.vaultKey),
|
|
2955
|
+
vaultVersion: vault.vaultVersion,
|
|
2956
|
+
keyId: vault.keyId,
|
|
2957
|
+
exp: now + ttlMs
|
|
2958
|
+
});
|
|
2959
|
+
/**
|
|
2960
|
+
* Parse a keychain blob back into an unlocked vault, or `undefined` when it is
|
|
2961
|
+
* malformed or has expired as of `now` — so an expired entry reads exactly like
|
|
2962
|
+
* a missing one (and is evicted by the caller).
|
|
2963
|
+
*/
|
|
2964
|
+
const decodeCacheEntry = (raw, now) => {
|
|
2965
|
+
const parsed = safeJsonParse(raw);
|
|
2966
|
+
if (!isCachedVaultEntry(parsed) || now >= parsed.exp) return;
|
|
2967
|
+
return {
|
|
2968
|
+
vault: {
|
|
2969
|
+
vaultKey: fromBase64(parsed.vaultKey),
|
|
2970
|
+
vaultVersion: parsed.vaultVersion,
|
|
2971
|
+
keyId: parsed.keyId
|
|
2972
|
+
},
|
|
2973
|
+
remainingMs: parsed.exp - now
|
|
2974
|
+
};
|
|
2975
|
+
};
|
|
2976
|
+
var VaultCache = class extends Context.Tag("cli/VaultCache")() {};
|
|
2977
|
+
const VaultCacheLive = Layer.effect(VaultCache, Effect.gen(function* () {
|
|
2978
|
+
const runtime = yield* CliRuntime;
|
|
2979
|
+
const cacheDisabled = Effect.gen(function* () {
|
|
2980
|
+
const flag = yield* runtime.getEnv("BETTER_UPDATE_NO_CACHE");
|
|
2981
|
+
return flag !== void 0 && flag.length > 0 && flag !== "0" && flag !== "false";
|
|
2982
|
+
});
|
|
2983
|
+
const readRaw = (publicKey) => Effect.try(() => new Entry(KEYCHAIN_SERVICE, publicKey).getPassword()).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
2984
|
+
const writeRaw = (publicKey, blob) => Effect.try(() => {
|
|
2985
|
+
new Entry(KEYCHAIN_SERVICE, publicKey).setPassword(blob);
|
|
2986
|
+
}).pipe(Effect.ignore);
|
|
2987
|
+
const deleteRaw = (publicKey) => Effect.try(() => new Entry(KEYCHAIN_SERVICE, publicKey).deletePassword()).pipe(Effect.ignore);
|
|
2988
|
+
return {
|
|
2989
|
+
get: (publicKey) => Effect.gen(function* () {
|
|
2990
|
+
if (yield* cacheDisabled) return;
|
|
2991
|
+
const raw = yield* readRaw(publicKey);
|
|
2992
|
+
if (raw === null) return;
|
|
2993
|
+
const decoded = decodeCacheEntry(raw, yield* Clock.currentTimeMillis);
|
|
2994
|
+
if (decoded === void 0) {
|
|
2995
|
+
yield* deleteRaw(publicKey);
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
return decoded;
|
|
2999
|
+
}),
|
|
3000
|
+
set: (publicKey, vault) => Effect.gen(function* () {
|
|
3001
|
+
if (yield* cacheDisabled) return;
|
|
3002
|
+
yield* writeRaw(publicKey, encodeCacheEntry(vault, yield* Clock.currentTimeMillis));
|
|
3003
|
+
}),
|
|
3004
|
+
clear: (publicKey) => deleteRaw(publicKey)
|
|
3005
|
+
};
|
|
3006
|
+
}));
|
|
3007
|
+
|
|
2932
3008
|
//#endregion
|
|
2933
3009
|
//#region src/services/version-check.ts
|
|
2934
3010
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/@better-update/cli/latest";
|
|
@@ -2979,7 +3055,7 @@ const VersionCheckLive = Layer.effect(VersionCheck, Effect.gen(function* () {
|
|
|
2979
3055
|
//#endregion
|
|
2980
3056
|
//#region src/app-layer.ts
|
|
2981
3057
|
const CliPlatformLayer = Layer.mergeAll(CliRuntimeLive, NodeContext.layer, FetchHttpClient.layer);
|
|
2982
|
-
const CliStoreLayer = Layer.mergeAll(AuthStoreLive, ConfigStoreLive, AppleSessionStoreLive, IdentityStoreLive).pipe(Layer.provide(CliPlatformLayer));
|
|
3058
|
+
const CliStoreLayer = Layer.mergeAll(AuthStoreLive, ConfigStoreLive, AppleSessionStoreLive, IdentityStoreLive, VaultCacheLive).pipe(Layer.provide(CliPlatformLayer));
|
|
2983
3059
|
const CliAdapterDependencies = Layer.mergeAll(CliPlatformLayer, CliStoreLayer);
|
|
2984
3060
|
const ApiClientLayer = ApiClientLive.pipe(Layer.provide(CliAdapterDependencies));
|
|
2985
3061
|
const AppleAuthLayer = AppleAuthLive.pipe(Layer.provide(CliAdapterDependencies));
|
|
@@ -4675,8 +4751,31 @@ const findIosArtifact = ({ exportPath }) => Effect.gen(function* () {
|
|
|
4675
4751
|
if (!picked) return yield* new ArtifactNotFoundError({ message: `No .ipa file found under "${exportPath}".` });
|
|
4676
4752
|
return picked.path;
|
|
4677
4753
|
});
|
|
4678
|
-
|
|
4679
|
-
|
|
4754
|
+
/**
|
|
4755
|
+
* Resolve a custom-command build artifact from a user-supplied path. A pattern
|
|
4756
|
+
* without wildcards is treated as a literal path (relative to `baseDir`);
|
|
4757
|
+
* otherwise the fixed leading directory + file extension are extracted and the
|
|
4758
|
+
* newest matching file under that directory is returned.
|
|
4759
|
+
*/
|
|
4760
|
+
const findArtifactByGlob = ({ baseDir, pattern, minMtimeMs }) => Effect.gen(function* () {
|
|
4761
|
+
const fs = yield* FileSystem.FileSystem;
|
|
4762
|
+
if (!/[*?[]/u.test(pattern)) {
|
|
4763
|
+
const full = path.isAbsolute(pattern) ? pattern : path.join(baseDir, pattern);
|
|
4764
|
+
if (yield* fs.exists(full).pipe(Effect.orElseSucceed(() => false))) return full;
|
|
4765
|
+
return yield* new ArtifactNotFoundError({ message: `No artifact found at "${full}".` });
|
|
4766
|
+
}
|
|
4767
|
+
const extension = path.extname(pattern).toLowerCase();
|
|
4768
|
+
if (extension === "") return yield* new ArtifactNotFoundError({ message: `artifactPath "${pattern}" must end in a file extension (e.g. **/*.aab).` });
|
|
4769
|
+
const wildcardIndex = pattern.search(/[*?[]/u);
|
|
4770
|
+
const fixedPrefix = pattern.slice(0, wildcardIndex);
|
|
4771
|
+
const prefixDir = fixedPrefix.includes("/") ? fixedPrefix.slice(0, fixedPrefix.lastIndexOf("/")) : "";
|
|
4772
|
+
const searchRoot = prefixDir === "" ? baseDir : path.join(baseDir, prefixDir);
|
|
4773
|
+
const picked = newest(yield* walkAndFind(searchRoot, extension), minMtimeMs);
|
|
4774
|
+
if (!picked) return yield* new ArtifactNotFoundError({ message: `No file matching "${pattern}" found under "${searchRoot}".` });
|
|
4775
|
+
return picked.path;
|
|
4776
|
+
});
|
|
4777
|
+
const findAndroidArtifact = ({ projectRoot, format, flavor, buildType, minMtimeMs, module: gradleModule = "app" }) => Effect.gen(function* () {
|
|
4778
|
+
const outputsRoot = path.join(projectRoot, "android", gradleModule, "build", "outputs");
|
|
4680
4779
|
const subdir = format === "aab" ? "bundle" : "apk";
|
|
4681
4780
|
const variantDir = flavor ? `${flavor}${capitalize(buildType)}` : buildType;
|
|
4682
4781
|
const pickedDirect = newest(yield* walkAndFind(path.join(outputsRoot, subdir, variantDir), `.${format}`), minMtimeMs);
|
|
@@ -18373,6 +18472,25 @@ const grantRecipient = (args) => Effect.gen(function* () {
|
|
|
18373
18472
|
const resolveVaultPassphrase = Effect.gen(function* () {
|
|
18374
18473
|
return (yield* activeRecipient).source === "file" ? yield* promptPassword("Passphrase to unlock this device's identity:") : void 0;
|
|
18375
18474
|
});
|
|
18475
|
+
/**
|
|
18476
|
+
* Unlock the org vault key for an interactive command, reusing a cached vault key
|
|
18477
|
+
* from the OS keychain when one is present and unexpired — so the device
|
|
18478
|
+
* passphrase is prompted at most once per cache TTL rather than on every command
|
|
18479
|
+
* (`better-update credentials unlock` / `lock` drive that session explicitly).
|
|
18480
|
+
* The CI `BETTER_UPDATE_IDENTITY` key carries no passphrase and is never cached:
|
|
18481
|
+
* it skips straight to the raw unwrap. On a cache miss the full unlock runs —
|
|
18482
|
+
* prompt, Argon2id, fetch + unwrap — and the result is cached for next time.
|
|
18483
|
+
*/
|
|
18484
|
+
const unlockVaultKeyInteractive = (api) => Effect.gen(function* () {
|
|
18485
|
+
const recipient = yield* activeRecipient;
|
|
18486
|
+
if (recipient.source !== "file") return yield* unlockVaultKey(api, void 0);
|
|
18487
|
+
const cache = yield* VaultCache;
|
|
18488
|
+
const cached = yield* cache.get(recipient.publicKey);
|
|
18489
|
+
if (cached !== void 0) return cached.vault;
|
|
18490
|
+
const vault = yield* unlockVaultKey(api, yield* promptPassword("Passphrase to unlock this device's identity:"));
|
|
18491
|
+
yield* cache.set(recipient.publicKey, vault);
|
|
18492
|
+
return vault;
|
|
18493
|
+
}).pipe(Effect.provide(VaultCacheLive));
|
|
18376
18494
|
/** Look up a registered recipient by its key id or full `SHA256:` fingerprint. */
|
|
18377
18495
|
const findRecipient = (api, selector) => Effect.gen(function* () {
|
|
18378
18496
|
const { items } = yield* api.userEncryptionKeys.list();
|
|
@@ -18420,11 +18538,15 @@ const openVaultSession = (api, passphrase) => Effect.gen(function* () {
|
|
|
18420
18538
|
};
|
|
18421
18539
|
});
|
|
18422
18540
|
/**
|
|
18423
|
-
* {@link openVaultSession} that
|
|
18424
|
-
*
|
|
18541
|
+
* {@link openVaultSession} that unlocks the vault key interactively — reusing the
|
|
18542
|
+
* OS-keychain-cached key when one is live (no prompt), prompting for the device
|
|
18543
|
+
* passphrase only on a cache miss, and none at all for the CI env key.
|
|
18425
18544
|
*/
|
|
18426
18545
|
const openVaultSessionInteractive = (api) => Effect.gen(function* () {
|
|
18427
|
-
return
|
|
18546
|
+
return {
|
|
18547
|
+
orgId: yield* getActiveOrgId(api),
|
|
18548
|
+
vault: yield* unlockVaultKeyInteractive(api)
|
|
18549
|
+
};
|
|
18428
18550
|
});
|
|
18429
18551
|
/** Reshape a sealed envelope into the `{ id, …opaque fields }` an upload body carries. */
|
|
18430
18552
|
const toUploadEnvelope = (envelope) => ({
|
|
@@ -18841,7 +18963,7 @@ const stringField = (cert, name) => {
|
|
|
18841
18963
|
return typeof value === "string" ? value : null;
|
|
18842
18964
|
};
|
|
18843
18965
|
const matchTeamFromCommonName = (cn) => {
|
|
18844
|
-
const match = /\(([A-Z0-9]{10})\)/u.exec(cn);
|
|
18966
|
+
const match = /\((?<team>[A-Z0-9]{10})\)/u.exec(cn);
|
|
18845
18967
|
if (match === null) return null;
|
|
18846
18968
|
const [, captured] = match;
|
|
18847
18969
|
return captured === void 0 ? null : captured;
|
|
@@ -19926,56 +20048,44 @@ const gradleTaskName = (format, flavor, buildType) => {
|
|
|
19926
20048
|
const verb = format === "aab" ? "bundle" : "assemble";
|
|
19927
20049
|
return flavor ? `${verb}${capitalize(flavor)}${capitalize(buildType)}` : `${verb}${capitalize(buildType)}`;
|
|
19928
20050
|
};
|
|
19929
|
-
|
|
19930
|
-
|
|
19931
|
-
|
|
20051
|
+
/** Resolve the signing keystore (remote or local), or `undefined` when skipped. */
|
|
20052
|
+
const resolveAndroidCredentials = (input) => {
|
|
20053
|
+
if (input.skipCredentials) return Effect.succeed(void 0);
|
|
20054
|
+
return input.credentialsSource === "local" ? loadLocalAndroidCredentials({ projectRoot: input.projectRoot }) : downloadAndroidCredentials(input.api, {
|
|
20055
|
+
projectId: input.projectId,
|
|
20056
|
+
applicationIdentifier: input.applicationIdentifier,
|
|
20057
|
+
tempDir: input.tempDir,
|
|
20058
|
+
buildProfile: input.profileName
|
|
20059
|
+
});
|
|
20060
|
+
};
|
|
20061
|
+
/** Gradle build against the (already-prepared) `android/` dir. */
|
|
20062
|
+
const runGradleBuild = (input, commandEnv) => Effect.gen(function* () {
|
|
19932
20063
|
const buildStartMs = Date.now();
|
|
19933
|
-
const { format } = androidProfile;
|
|
19934
|
-
const
|
|
19935
|
-
const
|
|
19936
|
-
const androidDir = path.join(projectRoot, "android");
|
|
19937
|
-
const
|
|
19938
|
-
yield*
|
|
19939
|
-
command: "bunx",
|
|
19940
|
-
args: [
|
|
19941
|
-
"expo",
|
|
19942
|
-
"prebuild",
|
|
19943
|
-
"--platform",
|
|
19944
|
-
"android",
|
|
19945
|
-
"--clean"
|
|
19946
|
-
],
|
|
19947
|
-
cwd: projectRoot,
|
|
19948
|
-
env: commandEnv
|
|
19949
|
-
}, "expo prebuild android");
|
|
19950
|
-
const gradleArgs = yield* input.skipCredentials ? Effect.succeed([]) : Effect.gen(function* () {
|
|
19951
|
-
const credentials = input.credentialsSource === "local" ? yield* loadLocalAndroidCredentials({ projectRoot }) : yield* downloadAndroidCredentials(api, {
|
|
19952
|
-
projectId,
|
|
19953
|
-
applicationIdentifier,
|
|
19954
|
-
tempDir,
|
|
19955
|
-
buildProfile: input.profileName
|
|
19956
|
-
});
|
|
20064
|
+
const { format, flavor } = input.androidProfile;
|
|
20065
|
+
const buildType = input.androidProfile.buildType ?? "release";
|
|
20066
|
+
const moduleName = input.androidProfile.module ?? "app";
|
|
20067
|
+
const androidDir = path.join(input.projectRoot, "android");
|
|
20068
|
+
const credentials = yield* resolveAndroidCredentials(input);
|
|
20069
|
+
const gradleArgs = credentials === void 0 ? [] : yield* Effect.gen(function* () {
|
|
19957
20070
|
const fs = yield* FileSystem.FileSystem;
|
|
19958
|
-
const signingGradlePath = path.join(tempDir, "signing.gradle");
|
|
19959
|
-
yield* fs.writeFileString(signingGradlePath, renderSigningGradle(
|
|
19960
|
-
keystorePath: credentials.keystorePath,
|
|
19961
|
-
storePassword: credentials.storePassword,
|
|
19962
|
-
keyAlias: credentials.keyAlias,
|
|
19963
|
-
keyPassword: credentials.keyPassword
|
|
19964
|
-
}));
|
|
20071
|
+
const signingGradlePath = path.join(input.tempDir, "signing.gradle");
|
|
20072
|
+
yield* fs.writeFileString(signingGradlePath, renderSigningGradle(credentials));
|
|
19965
20073
|
return ["--init-script", signingGradlePath];
|
|
19966
20074
|
});
|
|
19967
|
-
const taskName = gradleTaskName(format, flavor, buildType);
|
|
20075
|
+
const taskName = input.androidProfile.gradleTask ?? gradleTaskName(format, flavor, buildType);
|
|
20076
|
+
const taskArg = taskName.startsWith(":") ? taskName : `:${moduleName}:${taskName}`;
|
|
19968
20077
|
yield* runStep({
|
|
19969
20078
|
command: "./gradlew",
|
|
19970
|
-
args: [...gradleArgs,
|
|
20079
|
+
args: [...gradleArgs, taskArg],
|
|
19971
20080
|
cwd: androidDir,
|
|
19972
20081
|
env: commandEnv
|
|
19973
20082
|
}, "gradlew");
|
|
19974
20083
|
const artifactPath = yield* findAndroidArtifact({
|
|
19975
|
-
projectRoot,
|
|
20084
|
+
projectRoot: input.projectRoot,
|
|
19976
20085
|
format,
|
|
19977
20086
|
buildType,
|
|
19978
20087
|
minMtimeMs: buildStartMs,
|
|
20088
|
+
module: moduleName,
|
|
19979
20089
|
...compact({ flavor })
|
|
19980
20090
|
});
|
|
19981
20091
|
const { sha256, byteSize } = yield* sha256File(artifactPath);
|
|
@@ -19985,6 +20095,71 @@ const runAndroidBuild = (input) => Effect.gen(function* () {
|
|
|
19985
20095
|
sha256
|
|
19986
20096
|
};
|
|
19987
20097
|
});
|
|
20098
|
+
/**
|
|
20099
|
+
* Custom-command build. We can't inject signing into an arbitrary build, so the
|
|
20100
|
+
* resolved keystore + passwords are exposed to the command as `BETTER_UPDATE_*`
|
|
20101
|
+
* env vars; the user's script consumes them. The artifact is located via the
|
|
20102
|
+
* profile's `artifactPath` glob.
|
|
20103
|
+
*/
|
|
20104
|
+
const runAndroidCustom = (input, commandEnv) => Effect.gen(function* () {
|
|
20105
|
+
const buildStartMs = Date.now();
|
|
20106
|
+
const custom = input.customCommand;
|
|
20107
|
+
if (custom === void 0) return yield* new BuildFailedError({
|
|
20108
|
+
step: "custom android build",
|
|
20109
|
+
exitCode: 1,
|
|
20110
|
+
message: "Internal: custom Android strategy selected without a custom command."
|
|
20111
|
+
});
|
|
20112
|
+
if (custom.artifactPath === void 0) return yield* new BuildFailedError({
|
|
20113
|
+
step: "custom android build",
|
|
20114
|
+
exitCode: 1,
|
|
20115
|
+
message: "Custom Android build requires \"artifactPath\" (e.g. \"**/*.aab\") in better-update.json."
|
|
20116
|
+
});
|
|
20117
|
+
const credentials = yield* resolveAndroidCredentials(input);
|
|
20118
|
+
const credEnv = credentials === void 0 ? {} : {
|
|
20119
|
+
BETTER_UPDATE_ANDROID_KEYSTORE_PATH: credentials.keystorePath,
|
|
20120
|
+
BETTER_UPDATE_ANDROID_KEYSTORE_PASSWORD: credentials.storePassword,
|
|
20121
|
+
BETTER_UPDATE_ANDROID_KEY_ALIAS: credentials.keyAlias,
|
|
20122
|
+
BETTER_UPDATE_ANDROID_KEY_PASSWORD: credentials.keyPassword
|
|
20123
|
+
};
|
|
20124
|
+
const cwd = custom.cwd === void 0 ? input.projectRoot : path.join(input.projectRoot, custom.cwd);
|
|
20125
|
+
yield* runStep({
|
|
20126
|
+
command: "sh",
|
|
20127
|
+
args: ["-c", custom.command],
|
|
20128
|
+
cwd,
|
|
20129
|
+
env: {
|
|
20130
|
+
...commandEnv,
|
|
20131
|
+
...credEnv,
|
|
20132
|
+
...custom.env
|
|
20133
|
+
}
|
|
20134
|
+
}, "custom android build");
|
|
20135
|
+
const artifactPath = yield* findArtifactByGlob({
|
|
20136
|
+
baseDir: cwd,
|
|
20137
|
+
pattern: custom.artifactPath,
|
|
20138
|
+
minMtimeMs: buildStartMs
|
|
20139
|
+
});
|
|
20140
|
+
const { sha256, byteSize } = yield* sha256File(artifactPath);
|
|
20141
|
+
return {
|
|
20142
|
+
artifactPath,
|
|
20143
|
+
byteSize,
|
|
20144
|
+
sha256
|
|
20145
|
+
};
|
|
20146
|
+
});
|
|
20147
|
+
const runAndroidBuild = (input) => Effect.gen(function* () {
|
|
20148
|
+
const commandEnv = yield* (yield* CliRuntime).commandEnvironment(input.envVars);
|
|
20149
|
+
if (input.strategy === "expo") yield* runStep({
|
|
20150
|
+
command: "bunx",
|
|
20151
|
+
args: [
|
|
20152
|
+
"expo",
|
|
20153
|
+
"prebuild",
|
|
20154
|
+
"--platform",
|
|
20155
|
+
"android",
|
|
20156
|
+
"--clean"
|
|
20157
|
+
],
|
|
20158
|
+
cwd: input.projectRoot,
|
|
20159
|
+
env: commandEnv
|
|
20160
|
+
}, "expo prebuild android");
|
|
20161
|
+
return input.strategy === "custom" ? yield* runAndroidCustom(input, commandEnv) : yield* runGradleBuild(input, commandEnv);
|
|
20162
|
+
});
|
|
19988
20163
|
|
|
19989
20164
|
//#endregion
|
|
19990
20165
|
//#region src/lib/credentials-generator-apple-id.ts
|
|
@@ -20699,7 +20874,7 @@ const listCurrentKeychains = Effect.gen(function* () {
|
|
|
20699
20874
|
const parseSigningIdentity = (output) => {
|
|
20700
20875
|
const lines = output.split("\n");
|
|
20701
20876
|
for (const line of lines) {
|
|
20702
|
-
const match = /"([^"]+)"/u.exec(line);
|
|
20877
|
+
const match = /"(?<identity>[^"]+)"/u.exec(line);
|
|
20703
20878
|
if (match?.[1]) return match[1];
|
|
20704
20879
|
}
|
|
20705
20880
|
};
|
|
@@ -21002,36 +21177,85 @@ const createXcodebuildFormatter = (projectRoot) => {
|
|
|
21002
21177
|
};
|
|
21003
21178
|
|
|
21004
21179
|
//#endregion
|
|
21005
|
-
//#region src/commands/build/ios.ts
|
|
21006
|
-
const
|
|
21007
|
-
|
|
21008
|
-
|
|
21009
|
-
|
|
21180
|
+
//#region src/commands/build/ios-prepare.ts
|
|
21181
|
+
const baseName = (entry) => entry.replace(/\.(?<ext>xcworkspace|xcodeproj)$/u, "");
|
|
21182
|
+
/**
|
|
21183
|
+
* Resolve the Xcode container to build: an explicit `workspace`/`project` from
|
|
21184
|
+
* the profile, else an auto-discovered `.xcworkspace` (CocoaPods), else the
|
|
21185
|
+
* `.xcodeproj` (pure-native apps without Pods).
|
|
21186
|
+
*/
|
|
21187
|
+
const resolveXcodeContainer = (projectRoot, iosDir, iosProfile) => Effect.gen(function* () {
|
|
21188
|
+
if (iosProfile.workspace !== void 0) {
|
|
21189
|
+
const containerPath = path.resolve(projectRoot, iosProfile.workspace);
|
|
21190
|
+
return {
|
|
21191
|
+
flag: "-workspace",
|
|
21192
|
+
containerPath,
|
|
21193
|
+
schemeBase: baseName(path.basename(containerPath))
|
|
21194
|
+
};
|
|
21195
|
+
}
|
|
21196
|
+
if (iosProfile.project !== void 0) {
|
|
21197
|
+
const containerPath = path.resolve(projectRoot, iosProfile.project);
|
|
21198
|
+
return {
|
|
21199
|
+
flag: "-project",
|
|
21200
|
+
containerPath,
|
|
21201
|
+
schemeBase: baseName(path.basename(containerPath))
|
|
21202
|
+
};
|
|
21203
|
+
}
|
|
21204
|
+
const entries = yield* (yield* FileSystem.FileSystem).readDirectory(iosDir).pipe(Effect.orElseSucceed(() => []));
|
|
21205
|
+
const workspace = entries.find((entry) => entry.endsWith(".xcworkspace"));
|
|
21206
|
+
if (workspace !== void 0) return {
|
|
21207
|
+
flag: "-workspace",
|
|
21208
|
+
containerPath: path.join(iosDir, workspace),
|
|
21209
|
+
schemeBase: baseName(workspace)
|
|
21210
|
+
};
|
|
21211
|
+
const project = entries.find((entry) => entry.endsWith(".xcodeproj"));
|
|
21212
|
+
if (project !== void 0) return {
|
|
21213
|
+
flag: "-project",
|
|
21214
|
+
containerPath: path.join(iosDir, project),
|
|
21215
|
+
schemeBase: baseName(project)
|
|
21216
|
+
};
|
|
21217
|
+
return yield* new BuildFailedError({
|
|
21218
|
+
step: "resolve Xcode container",
|
|
21010
21219
|
exitCode: 1,
|
|
21011
|
-
message: `No .xcworkspace found under ${iosDir}.
|
|
21220
|
+
message: `No .xcworkspace or .xcodeproj found under ${iosDir}. Set ios.workspace / ios.project in better-update.json.`
|
|
21012
21221
|
});
|
|
21013
|
-
return workspace;
|
|
21014
21222
|
});
|
|
21015
|
-
|
|
21016
|
-
|
|
21017
|
-
|
|
21018
|
-
|
|
21019
|
-
|
|
21020
|
-
|
|
21021
|
-
|
|
21022
|
-
|
|
21023
|
-
"
|
|
21024
|
-
|
|
21025
|
-
|
|
21026
|
-
|
|
21027
|
-
|
|
21028
|
-
|
|
21223
|
+
/**
|
|
21224
|
+
* Prepare the `ios/` dir for an xcodebuild. Expo regenerates it from app.json
|
|
21225
|
+
* via prebuild then runs `pod install`; bare/KMP/native build the committed dir
|
|
21226
|
+
* and only run `pod install` when a Podfile is present (unless disabled).
|
|
21227
|
+
*/
|
|
21228
|
+
const prepareIosNative = (params) => Effect.gen(function* () {
|
|
21229
|
+
if (params.strategy === "expo") {
|
|
21230
|
+
yield* runStep({
|
|
21231
|
+
command: "bunx",
|
|
21232
|
+
args: [
|
|
21233
|
+
"expo",
|
|
21234
|
+
"prebuild",
|
|
21235
|
+
"--platform",
|
|
21236
|
+
"ios",
|
|
21237
|
+
"--clean"
|
|
21238
|
+
],
|
|
21239
|
+
cwd: params.projectRoot,
|
|
21240
|
+
env: params.commandEnv
|
|
21241
|
+
}, "expo prebuild ios");
|
|
21242
|
+
yield* runStep({
|
|
21243
|
+
command: "pod",
|
|
21244
|
+
args: ["install"],
|
|
21245
|
+
cwd: params.iosDir,
|
|
21246
|
+
env: params.commandEnv
|
|
21247
|
+
}, "pod install");
|
|
21248
|
+
return;
|
|
21249
|
+
}
|
|
21250
|
+
if (params.iosProfile.podInstall === false) return;
|
|
21251
|
+
if (yield* (yield* FileSystem.FileSystem).exists(path.join(params.iosDir, "Podfile")).pipe(Effect.orElseSucceed(() => false))) yield* runStep({
|
|
21029
21252
|
command: "pod",
|
|
21030
21253
|
args: ["install"],
|
|
21031
21254
|
cwd: params.iosDir,
|
|
21032
21255
|
env: params.commandEnv
|
|
21033
21256
|
}, "pod install");
|
|
21034
21257
|
});
|
|
21258
|
+
/** Recursively locate the first `.app` bundle under `root` (simulator output). */
|
|
21035
21259
|
const findAppDirectory = (root) => Effect.gen(function* () {
|
|
21036
21260
|
const fs = yield* FileSystem.FileSystem;
|
|
21037
21261
|
const stack = [root];
|
|
@@ -21051,25 +21275,30 @@ const findAppDirectory = (root) => Effect.gen(function* () {
|
|
|
21051
21275
|
}
|
|
21052
21276
|
return yield* new ArtifactNotFoundError({ message: `No .app bundle found under "${root}".` });
|
|
21053
21277
|
});
|
|
21278
|
+
|
|
21279
|
+
//#endregion
|
|
21280
|
+
//#region src/commands/build/ios.ts
|
|
21054
21281
|
const runIosSimulatorBuild = (input) => Effect.gen(function* () {
|
|
21055
21282
|
const { projectRoot, iosProfile, envVars, tempDir } = input;
|
|
21056
21283
|
const runtime = yield* CliRuntime;
|
|
21057
21284
|
const iosDir = path.join(projectRoot, "ios");
|
|
21058
21285
|
const commandEnv = yield* runtime.commandEnvironment(envVars);
|
|
21059
|
-
yield*
|
|
21286
|
+
yield* prepareIosNative({
|
|
21287
|
+
strategy: input.strategy,
|
|
21060
21288
|
projectRoot,
|
|
21061
21289
|
iosDir,
|
|
21290
|
+
iosProfile,
|
|
21062
21291
|
commandEnv
|
|
21063
21292
|
});
|
|
21064
|
-
const
|
|
21065
|
-
const scheme = iosProfile.scheme ??
|
|
21293
|
+
const container = yield* resolveXcodeContainer(projectRoot, iosDir, iosProfile);
|
|
21294
|
+
const scheme = iosProfile.scheme ?? container.schemeBase;
|
|
21066
21295
|
const configuration = iosProfile.buildConfiguration ?? "Release";
|
|
21067
21296
|
const derivedDataPath = path.join(tempDir, "derived-data");
|
|
21068
21297
|
const buildCmd = {
|
|
21069
21298
|
command: "xcodebuild",
|
|
21070
21299
|
args: [
|
|
21071
|
-
|
|
21072
|
-
|
|
21300
|
+
container.flag,
|
|
21301
|
+
container.containerPath,
|
|
21073
21302
|
"-scheme",
|
|
21074
21303
|
scheme,
|
|
21075
21304
|
"-configuration",
|
|
@@ -21157,13 +21386,15 @@ const runIosDeviceBuild = (input) => Effect.gen(function* () {
|
|
|
21157
21386
|
const iosDir = path.join(projectRoot, "ios");
|
|
21158
21387
|
const { distribution } = iosProfile;
|
|
21159
21388
|
const commandEnv = yield* runtime.commandEnvironment(envVars);
|
|
21160
|
-
yield*
|
|
21389
|
+
yield* prepareIosNative({
|
|
21390
|
+
strategy: input.strategy,
|
|
21161
21391
|
projectRoot,
|
|
21162
21392
|
iosDir,
|
|
21393
|
+
iosProfile,
|
|
21163
21394
|
commandEnv
|
|
21164
21395
|
});
|
|
21165
|
-
const
|
|
21166
|
-
const scheme = iosProfile.scheme ??
|
|
21396
|
+
const container = yield* resolveXcodeContainer(projectRoot, iosDir, iosProfile);
|
|
21397
|
+
const scheme = iosProfile.scheme ?? container.schemeBase;
|
|
21167
21398
|
const configuration = iosProfile.buildConfiguration ?? "Release";
|
|
21168
21399
|
const signedTargets = yield* discoverSignedTargets({
|
|
21169
21400
|
iosDir,
|
|
@@ -21210,8 +21441,8 @@ const runIosDeviceBuild = (input) => Effect.gen(function* () {
|
|
|
21210
21441
|
const archiveCmd = {
|
|
21211
21442
|
command: "xcodebuild",
|
|
21212
21443
|
args: [
|
|
21213
|
-
|
|
21214
|
-
|
|
21444
|
+
container.flag,
|
|
21445
|
+
container.containerPath,
|
|
21215
21446
|
"-scheme",
|
|
21216
21447
|
scheme,
|
|
21217
21448
|
"-configuration",
|
|
@@ -21275,19 +21506,76 @@ const runIosDeviceBuild = (input) => Effect.gen(function* () {
|
|
|
21275
21506
|
sha256
|
|
21276
21507
|
};
|
|
21277
21508
|
});
|
|
21278
|
-
|
|
21509
|
+
/**
|
|
21510
|
+
* Custom-command iOS build. The resolved p12 + provisioning profiles are written
|
|
21511
|
+
* to `tempDir` and their paths exposed via `BETTER_UPDATE_IOS_*` env vars; the
|
|
21512
|
+
* user's command performs the actual signing/archive. The artifact is located
|
|
21513
|
+
* via the profile's `artifactPath` glob.
|
|
21514
|
+
*/
|
|
21515
|
+
const runIosCustom = (input) => Effect.gen(function* () {
|
|
21516
|
+
const commandEnv = yield* (yield* CliRuntime).commandEnvironment(input.envVars);
|
|
21517
|
+
const custom = input.customCommand;
|
|
21518
|
+
if (custom === void 0) return yield* new BuildFailedError({
|
|
21519
|
+
step: "custom ios build",
|
|
21520
|
+
exitCode: 1,
|
|
21521
|
+
message: "Internal: custom iOS strategy selected without a custom command."
|
|
21522
|
+
});
|
|
21523
|
+
if (custom.artifactPath === void 0) return yield* new BuildFailedError({
|
|
21524
|
+
step: "custom ios build",
|
|
21525
|
+
exitCode: 1,
|
|
21526
|
+
message: "Custom iOS build requires \"artifactPath\" (e.g. \"build/*.ipa\") in better-update.json."
|
|
21527
|
+
});
|
|
21528
|
+
const credentials = yield* fetchAllCredentials({
|
|
21529
|
+
api: input.api,
|
|
21530
|
+
input,
|
|
21531
|
+
mainBundleIdentifier: input.bundleId,
|
|
21532
|
+
allBundleIdentifiers: [input.bundleId]
|
|
21533
|
+
});
|
|
21534
|
+
const credEnv = {
|
|
21535
|
+
BETTER_UPDATE_IOS_P12_PATH: credentials.p12Path,
|
|
21536
|
+
BETTER_UPDATE_IOS_P12_PASSWORD: credentials.p12Password,
|
|
21537
|
+
BETTER_UPDATE_IOS_PROVISIONING_PROFILES: credentials.profiles.map((profile) => profile.profilePath).join(":")
|
|
21538
|
+
};
|
|
21539
|
+
const cwd = custom.cwd === void 0 ? input.projectRoot : path.join(input.projectRoot, custom.cwd);
|
|
21540
|
+
const buildStartMs = Date.now();
|
|
21541
|
+
yield* runStep({
|
|
21542
|
+
command: "sh",
|
|
21543
|
+
args: ["-c", custom.command],
|
|
21544
|
+
cwd,
|
|
21545
|
+
env: {
|
|
21546
|
+
...commandEnv,
|
|
21547
|
+
...credEnv,
|
|
21548
|
+
...custom.env
|
|
21549
|
+
}
|
|
21550
|
+
}, "custom ios build");
|
|
21551
|
+
const artifactPath = yield* findArtifactByGlob({
|
|
21552
|
+
baseDir: cwd,
|
|
21553
|
+
pattern: custom.artifactPath,
|
|
21554
|
+
minMtimeMs: buildStartMs
|
|
21555
|
+
});
|
|
21556
|
+
const { sha256, byteSize } = yield* sha256File(artifactPath);
|
|
21557
|
+
return {
|
|
21558
|
+
artifactPath,
|
|
21559
|
+
byteSize,
|
|
21560
|
+
sha256
|
|
21561
|
+
};
|
|
21562
|
+
});
|
|
21563
|
+
const runIosBuild = (input) => {
|
|
21564
|
+
if (input.strategy === "custom") return runIosCustom(input);
|
|
21565
|
+
return input.iosProfile.simulator === true ? runIosSimulatorBuild(input) : runIosDeviceBuild(input);
|
|
21566
|
+
};
|
|
21279
21567
|
|
|
21280
21568
|
//#endregion
|
|
21281
21569
|
//#region src/commands/build/reserve-and-upload.ts
|
|
21282
21570
|
const buildReserveCommon = (input) => ({
|
|
21283
21571
|
projectId: input.projectId,
|
|
21284
21572
|
profile: input.profileName,
|
|
21285
|
-
runtimeVersion: input.runtimeVersion,
|
|
21286
21573
|
bundleId: input.bundleId,
|
|
21287
21574
|
sha256: input.sha256,
|
|
21288
21575
|
byteSize: input.byteSize,
|
|
21289
21576
|
gitDirty: input.gitContext.dirty,
|
|
21290
21577
|
...compact({
|
|
21578
|
+
runtimeVersion: input.runtimeVersion,
|
|
21291
21579
|
appVersion: input.appVersion,
|
|
21292
21580
|
buildNumber: input.buildNumber,
|
|
21293
21581
|
gitRef: input.gitContext.ref,
|
|
@@ -21363,7 +21651,7 @@ const bumpVersionCode = (current) => Effect.gen(function* () {
|
|
|
21363
21651
|
if (!Number.isInteger(value) || value < 0) return yield* new BuildProfileError({ message: `Cannot autoIncrement android.versionCode: current value ${String(value)} is not a non-negative integer.` });
|
|
21364
21652
|
return value + 1;
|
|
21365
21653
|
});
|
|
21366
|
-
const SEMVER_PATCH = /^(
|
|
21654
|
+
const SEMVER_PATCH = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?<suffix>.*)$/u;
|
|
21367
21655
|
const bumpVersion = (current) => Effect.gen(function* () {
|
|
21368
21656
|
if (current === void 0) return yield* new BuildProfileError({ message: "Cannot autoIncrement version: no `version` field set in Expo config." });
|
|
21369
21657
|
const match = SEMVER_PATCH.exec(current);
|
|
@@ -21444,20 +21732,21 @@ const stripExtends = (profile) => {
|
|
|
21444
21732
|
};
|
|
21445
21733
|
const resolveExtendsChain = (params) => Effect.gen(function* () {
|
|
21446
21734
|
const { profiles, profileName, label, maxDepth, makeError } = params;
|
|
21735
|
+
const sourceLabel = params.sourceLabel ?? "eas.json";
|
|
21447
21736
|
const noun = label === "build" ? "Build" : "Submit";
|
|
21448
21737
|
const chain = [];
|
|
21449
21738
|
const visited = /* @__PURE__ */ new Set();
|
|
21450
21739
|
let current = profileName;
|
|
21451
21740
|
let depth = 0;
|
|
21452
21741
|
while (current !== void 0) {
|
|
21453
|
-
if (visited.has(current)) return yield* Effect.fail(makeError(`Cycle detected in
|
|
21742
|
+
if (visited.has(current)) return yield* Effect.fail(makeError(`Cycle detected in ${sourceLabel} ${label}.${profileName} extends chain at "${current}".`));
|
|
21454
21743
|
visited.add(current);
|
|
21455
21744
|
const profile = profiles[current];
|
|
21456
|
-
if (!profile) return yield* Effect.fail(makeError(current === profileName ? `${noun} profile "${profileName}" not found in
|
|
21745
|
+
if (!profile) return yield* Effect.fail(makeError(current === profileName ? `${noun} profile "${profileName}" not found in ${sourceLabel}.` : `${noun} profile "${profileName}" extends missing profile "${current}".`));
|
|
21457
21746
|
chain.unshift(profile);
|
|
21458
21747
|
current = profile.extends;
|
|
21459
21748
|
depth += 1;
|
|
21460
|
-
if (depth > maxDepth) return yield* Effect.fail(makeError(`Too many "extends" levels (max ${String(maxDepth)}) in
|
|
21749
|
+
if (depth > maxDepth) return yield* Effect.fail(makeError(`Too many "extends" levels (max ${String(maxDepth)}) in ${sourceLabel} ${label}.${profileName}.`));
|
|
21461
21750
|
}
|
|
21462
21751
|
return chain;
|
|
21463
21752
|
});
|
|
@@ -21524,13 +21813,14 @@ const mergeSubmitProfile = (base, overlay) => {
|
|
|
21524
21813
|
android
|
|
21525
21814
|
});
|
|
21526
21815
|
};
|
|
21527
|
-
const resolveEasSubmitProfile = (profiles, profileName) => Effect.gen(function* () {
|
|
21528
|
-
if (!profiles) return yield* new BuildProfileError({ message:
|
|
21816
|
+
const resolveEasSubmitProfile = (profiles, profileName, sourceLabel = "eas.json") => Effect.gen(function* () {
|
|
21817
|
+
if (!profiles) return yield* new BuildProfileError({ message: `${sourceLabel} has no "submit" section. Add at least one submit profile.` });
|
|
21529
21818
|
return stripExtends((yield* resolveExtendsChain({
|
|
21530
21819
|
profiles,
|
|
21531
21820
|
profileName,
|
|
21532
21821
|
label: "submit",
|
|
21533
21822
|
maxDepth: MAX_SUBMIT_EXTENDS_DEPTH,
|
|
21823
|
+
sourceLabel,
|
|
21534
21824
|
makeError: (message) => new BuildProfileError({ message })
|
|
21535
21825
|
})).reduce((acc, next, index) => index === 0 ? next : mergeSubmitProfile(acc, next), {}));
|
|
21536
21826
|
});
|
|
@@ -21597,7 +21887,13 @@ const parseIosProfile = (raw) => {
|
|
|
21597
21887
|
scheme: asStringValue(record["scheme"]),
|
|
21598
21888
|
simulator: asBooleanValue(record["simulator"]),
|
|
21599
21889
|
enterpriseProvisioning: asEnterpriseProvisioning(record["enterpriseProvisioning"]),
|
|
21600
|
-
autoIncrement: asIosAutoIncrement(record["autoIncrement"])
|
|
21890
|
+
autoIncrement: asIosAutoIncrement(record["autoIncrement"]),
|
|
21891
|
+
workspace: asStringValue(record["workspace"]),
|
|
21892
|
+
project: asStringValue(record["project"]),
|
|
21893
|
+
podInstall: asBooleanValue(record["podInstall"]),
|
|
21894
|
+
bundleIdentifier: asStringValue(record["bundleIdentifier"]),
|
|
21895
|
+
version: asStringValue(record["version"]),
|
|
21896
|
+
buildNumber: asStringValue(record["buildNumber"])
|
|
21601
21897
|
});
|
|
21602
21898
|
};
|
|
21603
21899
|
const parseAndroidProfile = (raw) => {
|
|
@@ -21609,9 +21905,35 @@ const parseAndroidProfile = (raw) => {
|
|
|
21609
21905
|
gradleCommand: asStringValue(record["gradleCommand"]),
|
|
21610
21906
|
format: asAndroidFormat(record["format"]),
|
|
21611
21907
|
distribution: asAndroidDistribution(record["distribution"]),
|
|
21612
|
-
autoIncrement: asAndroidAutoIncrement(record["autoIncrement"])
|
|
21908
|
+
autoIncrement: asAndroidAutoIncrement(record["autoIncrement"]),
|
|
21909
|
+
module: asStringValue(record["module"]),
|
|
21910
|
+
gradleTask: asStringValue(record["gradleTask"]),
|
|
21911
|
+
applicationId: asStringValue(record["applicationId"]),
|
|
21912
|
+
version: asStringValue(record["version"]),
|
|
21913
|
+
versionCode: asStringValue(record["versionCode"])
|
|
21613
21914
|
});
|
|
21614
21915
|
};
|
|
21916
|
+
const parseCustomCommandSpec = (raw) => {
|
|
21917
|
+
const record = asRecord(raw);
|
|
21918
|
+
if (!record) return;
|
|
21919
|
+
const command = asStringValue(record["command"]);
|
|
21920
|
+
if (command === void 0) return;
|
|
21921
|
+
return compact({
|
|
21922
|
+
command,
|
|
21923
|
+
cwd: asStringValue(record["cwd"]),
|
|
21924
|
+
env: asEnv(record["env"]),
|
|
21925
|
+
artifactPath: asStringValue(record["artifactPath"])
|
|
21926
|
+
});
|
|
21927
|
+
};
|
|
21928
|
+
const parseCustomCommandProfile = (raw) => {
|
|
21929
|
+
const record = asRecord(raw);
|
|
21930
|
+
if (!record) return;
|
|
21931
|
+
const result = compact({
|
|
21932
|
+
ios: parseCustomCommandSpec(record["ios"]),
|
|
21933
|
+
android: parseCustomCommandSpec(record["android"])
|
|
21934
|
+
});
|
|
21935
|
+
return Object.keys(result).length === 0 ? void 0 : result;
|
|
21936
|
+
};
|
|
21615
21937
|
const parseBuildProfile = (raw) => {
|
|
21616
21938
|
const record = asRecord(raw);
|
|
21617
21939
|
if (!record) return;
|
|
@@ -21626,15 +21948,16 @@ const parseBuildProfile = (raw) => {
|
|
|
21626
21948
|
android: parseAndroidProfile(record["android"]),
|
|
21627
21949
|
credentialsSource: asCredentialsSource(record["credentialsSource"]),
|
|
21628
21950
|
autoIncrement: asAutoIncrement(record["autoIncrement"]),
|
|
21629
|
-
withoutCredentials: asBooleanValue(record["withoutCredentials"])
|
|
21951
|
+
withoutCredentials: asBooleanValue(record["withoutCredentials"]),
|
|
21952
|
+
custom: parseCustomCommandProfile(record["custom"])
|
|
21630
21953
|
});
|
|
21631
21954
|
};
|
|
21632
|
-
|
|
21633
|
-
|
|
21634
|
-
|
|
21635
|
-
|
|
21636
|
-
|
|
21637
|
-
|
|
21955
|
+
/**
|
|
21956
|
+
* Parse an already-decoded JSON object into an {@link EasConfig}. Shared by the
|
|
21957
|
+
* `eas.json` reader and the `better-update.json` build-config reader — both hold
|
|
21958
|
+
* the same `build`/`submit`/`cli` shape, only the source file differs.
|
|
21959
|
+
*/
|
|
21960
|
+
const parseConfigFromRecord = (root) => {
|
|
21638
21961
|
const buildRecord = asRecord(root["build"]);
|
|
21639
21962
|
if (!buildRecord) return asRecord(root["cli"]) ? { cli: parseCli(root["cli"]) } : {};
|
|
21640
21963
|
const profiles = {};
|
|
@@ -21653,6 +21976,14 @@ const parseEasConfig = (text) => Effect.gen(function* () {
|
|
|
21653
21976
|
build: profiles,
|
|
21654
21977
|
...Object.keys(submit).length === 0 ? {} : { submit }
|
|
21655
21978
|
};
|
|
21979
|
+
};
|
|
21980
|
+
const parseEasConfig = (text) => Effect.gen(function* () {
|
|
21981
|
+
const root = asRecord(yield* Effect.try({
|
|
21982
|
+
try: () => JSON.parse(text),
|
|
21983
|
+
catch: (cause) => new BuildProfileError({ message: `eas.json is not valid JSON: ${formatCause(cause)}` })
|
|
21984
|
+
}));
|
|
21985
|
+
if (!root) return yield* new BuildProfileError({ message: "eas.json must be a JSON object at the top level." });
|
|
21986
|
+
return parseConfigFromRecord(root);
|
|
21656
21987
|
});
|
|
21657
21988
|
const parseCli = (raw) => {
|
|
21658
21989
|
const record = asRecord(raw);
|
|
@@ -21667,10 +21998,15 @@ const readEasJson = (projectRoot) => Effect.gen(function* () {
|
|
|
21667
21998
|
const filePath = yield* easJsonPath(projectRoot);
|
|
21668
21999
|
return yield* parseEasConfig(yield* fs.readFileString(filePath).pipe(Effect.catchAll((cause) => Effect.fail(new BuildProfileError({ message: cause._tag === "SystemError" && cause.reason === "NotFound" ? `No eas.json found at ${filePath}. Create one with a "build" section.` : `Failed to read eas.json: ${cause.message}` })))));
|
|
21669
22000
|
});
|
|
22001
|
+
const mergeCustom = (base, overlay) => {
|
|
22002
|
+
const merged = shallowMerge(base, overlay);
|
|
22003
|
+
return merged === void 0 || Object.keys(merged).length === 0 ? void 0 : merged;
|
|
22004
|
+
};
|
|
21670
22005
|
const mergeProfile = (base, overlay) => {
|
|
21671
22006
|
const ios = shallowMerge(base.ios, overlay.ios);
|
|
21672
22007
|
const android = shallowMerge(base.android, overlay.android);
|
|
21673
22008
|
const env = shallowMerge(base.env, overlay.env);
|
|
22009
|
+
const custom = mergeCustom(base.custom, overlay.custom);
|
|
21674
22010
|
const developmentClient = overlay.developmentClient ?? base.developmentClient;
|
|
21675
22011
|
const distribution = overlay.distribution ?? base.distribution;
|
|
21676
22012
|
const channel = overlay.channel ?? base.channel;
|
|
@@ -21689,21 +22025,41 @@ const mergeProfile = (base, overlay) => {
|
|
|
21689
22025
|
android,
|
|
21690
22026
|
credentialsSource,
|
|
21691
22027
|
autoIncrement,
|
|
21692
|
-
withoutCredentials
|
|
22028
|
+
withoutCredentials,
|
|
22029
|
+
custom
|
|
21693
22030
|
});
|
|
21694
22031
|
};
|
|
21695
|
-
const resolveEasBuildProfile = (config, profileName) => Effect.gen(function* () {
|
|
22032
|
+
const resolveEasBuildProfile = (config, profileName, sourceLabel = "eas.json") => Effect.gen(function* () {
|
|
21696
22033
|
const profiles = config.build;
|
|
21697
|
-
if (!profiles) return yield* new BuildProfileError({ message:
|
|
22034
|
+
if (!profiles) return yield* new BuildProfileError({ message: `${sourceLabel} has no "build" section. Add at least one profile.` });
|
|
21698
22035
|
return stripExtends((yield* resolveExtendsChain({
|
|
21699
22036
|
profiles,
|
|
21700
22037
|
profileName,
|
|
21701
22038
|
label: "build",
|
|
21702
22039
|
maxDepth: MAX_EXTENDS_DEPTH,
|
|
22040
|
+
sourceLabel,
|
|
21703
22041
|
makeError: (message) => new BuildProfileError({ message })
|
|
21704
22042
|
})).reduce((acc, next, index) => index === 0 ? next : mergeProfile(acc, next), {}));
|
|
21705
22043
|
});
|
|
21706
22044
|
|
|
22045
|
+
//#endregion
|
|
22046
|
+
//#region src/lib/better-update-build-config.ts
|
|
22047
|
+
/** Label used in profile-resolution error copy when config comes from this file. */
|
|
22048
|
+
const BETTER_UPDATE_SOURCE_LABEL = "better-update.json";
|
|
22049
|
+
/**
|
|
22050
|
+
* Read the `build`/`submit`/`cli` config from `better-update.json`. Returns an
|
|
22051
|
+
* empty config (no `build` key) when the file is absent or carries no build
|
|
22052
|
+
* section. Shares the parser with {@link file://./eas-config.ts}; only the
|
|
22053
|
+
* source file and the error `sourceLabel` differ.
|
|
22054
|
+
*/
|
|
22055
|
+
const readBuildConfig = (projectRoot) => readBetterUpdateConfig(projectRoot).pipe(Effect.map((config) => config === void 0 ? {} : parseConfigFromRecord(config)));
|
|
22056
|
+
/** List available build-profile names declared in `better-update.json`. */
|
|
22057
|
+
const listBuildProfileNames = (projectRoot) => readBuildConfig(projectRoot).pipe(Effect.map((config) => Object.keys(config.build ?? {})));
|
|
22058
|
+
/** Resolve a submit profile from `better-update.json`'s `submit` section. */
|
|
22059
|
+
const readSubmitProfile = (projectRoot, profileName) => Effect.gen(function* () {
|
|
22060
|
+
return yield* resolveEasSubmitProfile((yield* readBuildConfig(projectRoot)).submit, profileName, BETTER_UPDATE_SOURCE_LABEL);
|
|
22061
|
+
});
|
|
22062
|
+
|
|
21707
22063
|
//#endregion
|
|
21708
22064
|
//#region src/lib/build-profile.ts
|
|
21709
22065
|
const deriveIosDistribution = (eas) => {
|
|
@@ -21750,12 +22106,22 @@ const toIosProfile = (eas) => {
|
|
|
21750
22106
|
if (!distribution) return;
|
|
21751
22107
|
const ios = eas.ios ?? {};
|
|
21752
22108
|
const autoIncrement = resolveIosAutoIncrement(eas);
|
|
22109
|
+
const buildConfiguration = ios.buildConfiguration ?? (eas.developmentClient === true ? "Debug" : void 0);
|
|
22110
|
+
const metaOverride = compact({
|
|
22111
|
+
bundleIdentifier: ios.bundleIdentifier,
|
|
22112
|
+
version: ios.version,
|
|
22113
|
+
buildNumber: ios.buildNumber
|
|
22114
|
+
});
|
|
21753
22115
|
return compact({
|
|
21754
22116
|
distribution,
|
|
21755
|
-
buildConfiguration
|
|
22117
|
+
buildConfiguration,
|
|
21756
22118
|
scheme: ios.scheme,
|
|
21757
22119
|
simulator: ios.simulator,
|
|
21758
|
-
autoIncrement
|
|
22120
|
+
autoIncrement,
|
|
22121
|
+
workspace: ios.workspace,
|
|
22122
|
+
project: ios.project,
|
|
22123
|
+
podInstall: ios.podInstall,
|
|
22124
|
+
metaOverride: Object.keys(metaOverride).length === 0 ? void 0 : metaOverride
|
|
21759
22125
|
});
|
|
21760
22126
|
};
|
|
21761
22127
|
const toAndroidProfile = (eas) => {
|
|
@@ -21765,16 +22131,25 @@ const toAndroidProfile = (eas) => {
|
|
|
21765
22131
|
const android = eas.android ?? {};
|
|
21766
22132
|
const distribution = deriveAndroidDistribution(eas, format);
|
|
21767
22133
|
const autoIncrement = resolveAndroidAutoIncrement(eas);
|
|
22134
|
+
const buildType = android.buildType ?? (eas.developmentClient === true ? "debug" : void 0);
|
|
22135
|
+
const metaOverride = compact({
|
|
22136
|
+
applicationId: android.applicationId,
|
|
22137
|
+
version: android.version,
|
|
22138
|
+
versionCode: android.versionCode
|
|
22139
|
+
});
|
|
21768
22140
|
return compact({
|
|
21769
22141
|
format,
|
|
21770
22142
|
distribution,
|
|
21771
|
-
buildType
|
|
22143
|
+
buildType,
|
|
21772
22144
|
flavor: android.flavor,
|
|
21773
22145
|
gradleCommand: android.gradleCommand,
|
|
21774
|
-
autoIncrement
|
|
22146
|
+
autoIncrement,
|
|
22147
|
+
module: android.module,
|
|
22148
|
+
gradleTask: android.gradleTask,
|
|
22149
|
+
metaOverride: Object.keys(metaOverride).length === 0 ? void 0 : metaOverride
|
|
21775
22150
|
});
|
|
21776
22151
|
};
|
|
21777
|
-
const
|
|
22152
|
+
const fromGenericProfile = (eas, profileName) => {
|
|
21778
22153
|
const ios = toIosProfile(eas);
|
|
21779
22154
|
const android = toAndroidProfile(eas);
|
|
21780
22155
|
return compact({
|
|
@@ -21786,11 +22161,13 @@ const fromEasProfile = (eas, profileName) => {
|
|
|
21786
22161
|
android,
|
|
21787
22162
|
credentialsSource: eas.credentialsSource,
|
|
21788
22163
|
developmentClient: eas.developmentClient,
|
|
21789
|
-
withoutCredentials: eas.withoutCredentials
|
|
22164
|
+
withoutCredentials: eas.withoutCredentials,
|
|
22165
|
+
customCommand: eas.custom
|
|
21790
22166
|
});
|
|
21791
22167
|
};
|
|
22168
|
+
/** Resolve a build profile from `better-update.json`'s `build` section. */
|
|
21792
22169
|
const readBuildProfile = (projectRoot, profileName) => Effect.gen(function* () {
|
|
21793
|
-
return
|
|
22170
|
+
return fromGenericProfile(yield* resolveEasBuildProfile(yield* readBuildConfig(projectRoot), profileName, "better-update.json"), profileName);
|
|
21794
22171
|
});
|
|
21795
22172
|
const readRuntimeVersionMeta = (config, platform) => ({
|
|
21796
22173
|
platform,
|
|
@@ -21800,6 +22177,17 @@ const readRuntimeVersionMeta = (config, platform) => ({
|
|
|
21800
22177
|
rawRuntimeVersion: extractRawRuntimeVersion(config, platform)
|
|
21801
22178
|
});
|
|
21802
22179
|
|
|
22180
|
+
//#endregion
|
|
22181
|
+
//#region src/lib/build-strategy.ts
|
|
22182
|
+
const resolveAndroidStrategy = (profile, projectType) => {
|
|
22183
|
+
if (profile.customCommand?.android !== void 0) return "custom";
|
|
22184
|
+
return projectType === "expo" ? "expo" : "gradle";
|
|
22185
|
+
};
|
|
22186
|
+
const resolveIosStrategy = (profile, projectType) => {
|
|
22187
|
+
if (profile.customCommand?.ios !== void 0) return "custom";
|
|
22188
|
+
return projectType === "expo" ? "expo" : "xcode";
|
|
22189
|
+
};
|
|
22190
|
+
|
|
21803
22191
|
//#endregion
|
|
21804
22192
|
//#region src/lib/clear-cache.ts
|
|
21805
22193
|
/**
|
|
@@ -21827,6 +22215,64 @@ const clearBuildCaches = (projectRoot) => Effect.gen(function* () {
|
|
|
21827
22215
|
if (removed.length > 0) yield* Console.error(`Cleared caches: ${removed.join(", ")}`);
|
|
21828
22216
|
});
|
|
21829
22217
|
|
|
22218
|
+
//#endregion
|
|
22219
|
+
//#region src/lib/detect-project-type.ts
|
|
22220
|
+
const PROJECT_TYPES = [
|
|
22221
|
+
"expo",
|
|
22222
|
+
"bare",
|
|
22223
|
+
"kmp",
|
|
22224
|
+
"native",
|
|
22225
|
+
"custom"
|
|
22226
|
+
];
|
|
22227
|
+
/** Narrow an arbitrary `projectType` override (e.g. from better-update.json) to a valid value. */
|
|
22228
|
+
const asProjectType = (raw) => PROJECT_TYPES.find((type) => type === raw);
|
|
22229
|
+
const exists = (filePath) => Effect.gen(function* () {
|
|
22230
|
+
return yield* (yield* FileSystem.FileSystem).exists(filePath).pipe(Effect.orElseSucceed(() => false));
|
|
22231
|
+
});
|
|
22232
|
+
const readText = (filePath) => Effect.gen(function* () {
|
|
22233
|
+
return yield* (yield* FileSystem.FileSystem).readFileString(filePath).pipe(Effect.orElseSucceed(() => ""));
|
|
22234
|
+
});
|
|
22235
|
+
const hasExpoDependency = (projectRoot) => Effect.gen(function* () {
|
|
22236
|
+
const text = yield* readText(path.join(projectRoot, "package.json"));
|
|
22237
|
+
if (text.length === 0) return false;
|
|
22238
|
+
const pkg = asRecord(yield* Effect.try(() => JSON.parse(text)).pipe(Effect.orElseSucceed(() => void 0)));
|
|
22239
|
+
const deps = asRecord(pkg?.["dependencies"]);
|
|
22240
|
+
const devDeps = asRecord(pkg?.["devDependencies"]);
|
|
22241
|
+
return deps?.["expo"] !== void 0 || devDeps?.["expo"] !== void 0;
|
|
22242
|
+
});
|
|
22243
|
+
const hasAnyExpoConfigFile = (projectRoot) => Effect.gen(function* () {
|
|
22244
|
+
for (const name of [
|
|
22245
|
+
"app.json",
|
|
22246
|
+
"app.config.js",
|
|
22247
|
+
"app.config.ts"
|
|
22248
|
+
]) if (yield* exists(path.join(projectRoot, name))) return true;
|
|
22249
|
+
return false;
|
|
22250
|
+
});
|
|
22251
|
+
const looksKmp = (projectRoot) => Effect.gen(function* () {
|
|
22252
|
+
if (yield* exists(path.join(projectRoot, "composeApp"))) return true;
|
|
22253
|
+
for (const name of ["settings.gradle.kts", "settings.gradle"]) {
|
|
22254
|
+
const text = yield* readText(path.join(projectRoot, name));
|
|
22255
|
+
if (text.includes("composeApp") || text.includes(":shared")) return true;
|
|
22256
|
+
}
|
|
22257
|
+
return yield* exists(path.join(projectRoot, "android", "app", "build.gradle.kts"));
|
|
22258
|
+
});
|
|
22259
|
+
/**
|
|
22260
|
+
* Resolve a project's build-system family. An explicit override always wins;
|
|
22261
|
+
* otherwise the filesystem shape is inspected. `custom` is never auto-detected —
|
|
22262
|
+
* it is intent expressed via override or a profile `custom` block.
|
|
22263
|
+
*/
|
|
22264
|
+
const detectProjectType = (params) => Effect.gen(function* () {
|
|
22265
|
+
if (params.override !== void 0) return params.override;
|
|
22266
|
+
const { projectRoot } = params;
|
|
22267
|
+
if (isExpoConfigInstalled() && ((yield* hasExpoDependency(projectRoot)) || (yield* hasAnyExpoConfigFile(projectRoot)))) return "expo";
|
|
22268
|
+
if (yield* looksKmp(projectRoot)) return "kmp";
|
|
22269
|
+
const hasAndroid = yield* exists(path.join(projectRoot, "android"));
|
|
22270
|
+
const hasIos = yield* exists(path.join(projectRoot, "ios"));
|
|
22271
|
+
const hasPackageJson = yield* exists(path.join(projectRoot, "package.json"));
|
|
22272
|
+
if (hasAndroid && hasIos && hasPackageJson) return "bare";
|
|
22273
|
+
return "native";
|
|
22274
|
+
});
|
|
22275
|
+
|
|
21830
22276
|
//#endregion
|
|
21831
22277
|
//#region src/lib/dev-client-check.ts
|
|
21832
22278
|
const readDeps = (filePath) => Effect.gen(function* () {
|
|
@@ -22192,6 +22638,28 @@ const detectPlatform = (explicit, config) => Effect.gen(function* () {
|
|
|
22192
22638
|
label: entry
|
|
22193
22639
|
})));
|
|
22194
22640
|
});
|
|
22641
|
+
/**
|
|
22642
|
+
* Resolve a build platform for non-Expo projects from an explicit flag, or by
|
|
22643
|
+
* intersecting the profile's declared platform sections with the native dirs
|
|
22644
|
+
* present on disk. Prompts when both remain; fails when ambiguous and prompts
|
|
22645
|
+
* are disallowed.
|
|
22646
|
+
*/
|
|
22647
|
+
const detectPlatformGeneric = (explicit, context) => Effect.gen(function* () {
|
|
22648
|
+
if (explicit !== void 0) return explicit;
|
|
22649
|
+
const candidates = [];
|
|
22650
|
+
const wantsIos = context.profile.ios !== void 0 || context.profile.customCommand?.ios !== void 0;
|
|
22651
|
+
const wantsAndroid = context.profile.android !== void 0 || context.profile.customCommand?.android !== void 0;
|
|
22652
|
+
if (wantsIos && (context.hasIosDir || context.profile.customCommand?.ios !== void 0)) candidates.push("ios");
|
|
22653
|
+
if (wantsAndroid && (context.hasAndroidDir || context.profile.customCommand?.android !== void 0)) candidates.push("android");
|
|
22654
|
+
if (candidates.length === 0) return yield* new BuildProfileError({ message: "Cannot infer build platform. Add an `ios` or `android` section to the build profile in better-update.json, or pass --platform." });
|
|
22655
|
+
const [only] = candidates;
|
|
22656
|
+
if (candidates.length === 1 && only !== void 0) return only;
|
|
22657
|
+
if (!(yield* InteractiveMode).allow) return yield* new BuildProfileError({ message: `Multiple platforms available (${candidates.join(", ")}). Pass --platform explicitly when running non-interactively.` });
|
|
22658
|
+
return yield* promptSelect("Which platform to build?", candidates.map((entry) => ({
|
|
22659
|
+
value: entry,
|
|
22660
|
+
label: entry
|
|
22661
|
+
})));
|
|
22662
|
+
});
|
|
22195
22663
|
|
|
22196
22664
|
//#endregion
|
|
22197
22665
|
//#region src/lib/project-staging.ts
|
|
@@ -22204,24 +22672,30 @@ const LOCKFILES = [
|
|
|
22204
22672
|
["package-lock.json", "npm"]
|
|
22205
22673
|
];
|
|
22206
22674
|
/**
|
|
22207
|
-
*
|
|
22208
|
-
*
|
|
22675
|
+
* Generated native build outputs / dependency dirs that must never be copied —
|
|
22676
|
+
* they are regenerated in staging (Pods via `pod install`, build/ via gradle).
|
|
22209
22677
|
*/
|
|
22210
|
-
const
|
|
22211
|
-
"node_modules",
|
|
22212
|
-
".git",
|
|
22678
|
+
const NATIVE_BUILD_OUTPUTS = [
|
|
22213
22679
|
"ios/build",
|
|
22214
22680
|
"ios/Pods",
|
|
22215
22681
|
"ios/DerivedData",
|
|
22216
22682
|
"android/build",
|
|
22217
22683
|
"android/app/build",
|
|
22218
22684
|
"android/.gradle",
|
|
22219
|
-
"android/.kotlin"
|
|
22685
|
+
"android/.kotlin"
|
|
22686
|
+
];
|
|
22687
|
+
/**
|
|
22688
|
+
* Paths never copied into staging — covers generated native build outputs and
|
|
22689
|
+
* dependency dirs that must be reinstalled fresh in staging.
|
|
22690
|
+
*/
|
|
22691
|
+
const ALWAYS_IGNORE = [...[
|
|
22692
|
+
"node_modules",
|
|
22693
|
+
".git",
|
|
22220
22694
|
".expo",
|
|
22221
22695
|
".gradle",
|
|
22222
22696
|
".turbo",
|
|
22223
22697
|
"dist"
|
|
22224
|
-
];
|
|
22698
|
+
], ...NATIVE_BUILD_OUTPUTS];
|
|
22225
22699
|
const findLockfile = (fs, dir) => Effect.gen(function* () {
|
|
22226
22700
|
for (const [name, pm] of LOCKFILES) if (yield* fs.exists(path.join(dir, name)).pipe(Effect.catchAll(() => Effect.succeed(false)))) return pm;
|
|
22227
22701
|
});
|
|
@@ -22249,8 +22723,12 @@ const detectWorkspaceRoot = (cwd) => walkUpForLockfile(cwd, cwd);
|
|
|
22249
22723
|
* Build an `Ignore` matcher for the workspace root. `.easignore` REPLACES
|
|
22250
22724
|
* `.gitignore` when present (matches EAS semantics); otherwise `.gitignore`
|
|
22251
22725
|
* is layered on top of the always-ignore baseline.
|
|
22726
|
+
*
|
|
22727
|
+
* When `includeNativeSource` is set, the native source dirs are re-included
|
|
22728
|
+
* after the ignore files are applied, then their build outputs re-excluded, so
|
|
22729
|
+
* a committed `ios/`/`android/` reaches staging intact.
|
|
22252
22730
|
*/
|
|
22253
|
-
const buildIgnoreInstance = (workspaceRoot) => Effect.gen(function* () {
|
|
22731
|
+
const buildIgnoreInstance = (workspaceRoot, options = {}) => Effect.gen(function* () {
|
|
22254
22732
|
const fs = yield* FileSystem.FileSystem;
|
|
22255
22733
|
const ig = ignore();
|
|
22256
22734
|
ig.add([...ALWAYS_IGNORE]);
|
|
@@ -22258,12 +22736,17 @@ const buildIgnoreInstance = (workspaceRoot) => Effect.gen(function* () {
|
|
|
22258
22736
|
if (yield* fs.exists(easignorePath).pipe(Effect.catchAll(() => Effect.succeed(false)))) {
|
|
22259
22737
|
const content = yield* fs.readFileString(easignorePath).pipe(Effect.catchAll(() => Effect.succeed("")));
|
|
22260
22738
|
ig.add(content);
|
|
22261
|
-
|
|
22739
|
+
} else {
|
|
22740
|
+
const gitignorePath = path.join(workspaceRoot, ".gitignore");
|
|
22741
|
+
if (yield* fs.exists(gitignorePath).pipe(Effect.catchAll(() => Effect.succeed(false)))) {
|
|
22742
|
+
const content = yield* fs.readFileString(gitignorePath).pipe(Effect.catchAll(() => Effect.succeed("")));
|
|
22743
|
+
ig.add(content);
|
|
22744
|
+
}
|
|
22262
22745
|
}
|
|
22263
|
-
|
|
22264
|
-
|
|
22265
|
-
|
|
22266
|
-
ig.add(
|
|
22746
|
+
if (options.includeNativeSource === true) {
|
|
22747
|
+
const base = options.appRelPath === void 0 || options.appRelPath === "" ? "" : `${options.appRelPath}/`;
|
|
22748
|
+
ig.add([`!${base}android`, `!${base}ios`]);
|
|
22749
|
+
ig.add(NATIVE_BUILD_OUTPUTS.map((entry) => `${base}${entry}`));
|
|
22267
22750
|
}
|
|
22268
22751
|
return ig;
|
|
22269
22752
|
});
|
|
@@ -22311,6 +22794,7 @@ const runInstall = (params) => runStep({
|
|
|
22311
22794
|
* regardless of what `expo prebuild`, `pod install`, or `gradlew` write.
|
|
22312
22795
|
*/
|
|
22313
22796
|
const prepareStagingProject = (input) => Effect.gen(function* () {
|
|
22797
|
+
const fs = yield* FileSystem.FileSystem;
|
|
22314
22798
|
const runtime = yield* CliRuntime;
|
|
22315
22799
|
const { workspaceRoot, packageManager } = yield* detectWorkspaceRoot(input.userCwd);
|
|
22316
22800
|
const relAppPath = path.relative(workspaceRoot, input.userCwd);
|
|
@@ -22320,14 +22804,18 @@ const prepareStagingProject = (input) => Effect.gen(function* () {
|
|
|
22320
22804
|
yield* copyProjectTree({
|
|
22321
22805
|
source: workspaceRoot,
|
|
22322
22806
|
dest: stagingRoot,
|
|
22323
|
-
ig: yield* buildIgnoreInstance(workspaceRoot
|
|
22807
|
+
ig: yield* buildIgnoreInstance(workspaceRoot, {
|
|
22808
|
+
includeNativeSource: input.projectType !== void 0 && input.projectType !== "expo",
|
|
22809
|
+
appRelPath: relAppPath
|
|
22810
|
+
})
|
|
22324
22811
|
});
|
|
22325
22812
|
yield* initGitRepo(stagingRoot);
|
|
22326
|
-
yield* runInstall({
|
|
22813
|
+
if (yield* fs.exists(path.join(workspaceRoot, "package.json")).pipe(Effect.catchAll(() => Effect.succeed(false)))) yield* runInstall({
|
|
22327
22814
|
stagingRoot,
|
|
22328
22815
|
packageManager,
|
|
22329
22816
|
env: yield* runtime.commandEnvironment(input.envVars)
|
|
22330
22817
|
});
|
|
22818
|
+
else yield* printHuman("No package.json at the staging root — skipping dependency install.");
|
|
22331
22819
|
return {
|
|
22332
22820
|
stagingRoot,
|
|
22333
22821
|
projectRoot,
|
|
@@ -22927,7 +23415,7 @@ const runAndroidGooglePlayUpload = (inputs) => Effect.gen(function* () {
|
|
|
22927
23415
|
});
|
|
22928
23416
|
|
|
22929
23417
|
//#endregion
|
|
22930
|
-
//#region src/application/build-
|
|
23418
|
+
//#region src/application/build-auto-submit.ts
|
|
22931
23419
|
const buildAutoSubmitIosConfig = (iosProfile, whatToTest) => {
|
|
22932
23420
|
if (iosProfile?.bundleIdentifier === void 0) return;
|
|
22933
23421
|
return compact({
|
|
@@ -22953,9 +23441,10 @@ const buildAutoSubmitAndroidConfig = (androidProfile) => {
|
|
|
22953
23441
|
rollout: androidProfile.rollout
|
|
22954
23442
|
});
|
|
22955
23443
|
};
|
|
23444
|
+
/** Submit a freshly-built artifact to the store using the profile's submit config. */
|
|
22956
23445
|
const runAutoSubmit = (input) => Effect.gen(function* () {
|
|
22957
23446
|
yield* printHuman(`\nAuto-submitting build ${input.buildId} (profile ${input.profileName})...`);
|
|
22958
|
-
const easProfile = yield*
|
|
23447
|
+
const easProfile = yield* readSubmitProfile(yield* (yield* CliRuntime).cwd, input.profileName);
|
|
22959
23448
|
const archiveUrl = (yield* input.api.builds.getInstallLink({ path: { id: input.buildId } })).artifactUrl;
|
|
22960
23449
|
const iosConfig = input.platform === "ios" ? buildAutoSubmitIosConfig(easProfile.ios, input.whatToTest) : void 0;
|
|
22961
23450
|
const androidConfig = input.platform === "android" ? buildAutoSubmitAndroidConfig(easProfile.android) : void 0;
|
|
@@ -22983,15 +23472,146 @@ const runAutoSubmit = (input) => Effect.gen(function* () {
|
|
|
22983
23472
|
}
|
|
22984
23473
|
yield* printHuman(`Submission final status: ${(yield* pollSubmissionUntilTerminal(input.api, submission.id)).status}`);
|
|
22985
23474
|
});
|
|
23475
|
+
|
|
23476
|
+
//#endregion
|
|
23477
|
+
//#region src/lib/ios-native-meta.ts
|
|
23478
|
+
const APPLICATION_PRODUCT_TYPE = "com.apple.product-type.application";
|
|
23479
|
+
/**
|
|
23480
|
+
* A build setting that is an unresolved reference (`$(MARKETING_VERSION)`) or a
|
|
23481
|
+
* variable interpolation tells us nothing concrete — treat it as absent so the
|
|
23482
|
+
* profile `metaOverride` can supply a real value.
|
|
23483
|
+
*/
|
|
23484
|
+
const concreteSetting = (raw) => {
|
|
23485
|
+
if (typeof raw === "number") return String(raw);
|
|
23486
|
+
if (typeof raw !== "string") return;
|
|
23487
|
+
const value = unquote$1(raw);
|
|
23488
|
+
return value.length === 0 || value.includes("$(") ? void 0 : value;
|
|
23489
|
+
};
|
|
23490
|
+
const findApplicationTarget = (project) => {
|
|
23491
|
+
const nativeTargets = project.pbxNativeTargetSection();
|
|
23492
|
+
for (const [uuid, entry] of Object.entries(nativeTargets)) {
|
|
23493
|
+
if (uuid.endsWith("_comment") || typeof entry === "string") continue;
|
|
23494
|
+
if (unquote$1(entry.productType) === APPLICATION_PRODUCT_TYPE) return entry;
|
|
23495
|
+
}
|
|
23496
|
+
};
|
|
23497
|
+
const configUuidForName = (project, target, configurationName) => {
|
|
23498
|
+
const configList = project.pbxXCConfigurationList()[target.buildConfigurationList];
|
|
23499
|
+
if (!configList || typeof configList === "string") return;
|
|
23500
|
+
const buildConfigSection = project.pbxXCBuildConfigurationSection();
|
|
23501
|
+
return configList.buildConfigurations.map((entry) => entry.value).find((uuid) => {
|
|
23502
|
+
const cfg = buildConfigSection[uuid];
|
|
23503
|
+
return cfg !== void 0 && typeof cfg !== "string" && unquote$1(cfg.name) === configurationName;
|
|
23504
|
+
});
|
|
23505
|
+
};
|
|
23506
|
+
/**
|
|
23507
|
+
* Read app metadata (bundle id, marketing version, build number) for the main
|
|
23508
|
+
* application target of the single `.xcodeproj` under `iosDir`, for a given build
|
|
23509
|
+
* configuration. Used for non-Expo (bare/native) projects where there is no
|
|
23510
|
+
* `app.json`. Missing or unresolved settings come back `undefined` so the caller
|
|
23511
|
+
* can fall back to profile `metaOverride`.
|
|
23512
|
+
*/
|
|
23513
|
+
const readIosNativeMeta = (params) => Effect.gen(function* () {
|
|
23514
|
+
const projectDir = yield* findXcodeProjectDir(params.iosDir);
|
|
23515
|
+
const project = yield* parseProject(path.join(projectDir, "project.pbxproj"));
|
|
23516
|
+
const target = findApplicationTarget(project);
|
|
23517
|
+
if (target === void 0) return {};
|
|
23518
|
+
const configUuid = configUuidForName(project, target, params.configurationName);
|
|
23519
|
+
if (configUuid === void 0) return {};
|
|
23520
|
+
const cfg = project.pbxXCBuildConfigurationSection()[configUuid];
|
|
23521
|
+
if (cfg === void 0 || typeof cfg === "string") return {};
|
|
23522
|
+
const settings = cfg.buildSettings;
|
|
23523
|
+
return compact({
|
|
23524
|
+
bundleId: concreteSetting(settings["PRODUCT_BUNDLE_IDENTIFIER"]),
|
|
23525
|
+
marketingVersion: concreteSetting(settings["MARKETING_VERSION"]),
|
|
23526
|
+
currentProjectVersion: concreteSetting(settings["CURRENT_PROJECT_VERSION"])
|
|
23527
|
+
});
|
|
23528
|
+
});
|
|
23529
|
+
|
|
23530
|
+
//#endregion
|
|
23531
|
+
//#region src/application/resolve-app-meta.ts
|
|
23532
|
+
const EMPTY = {
|
|
23533
|
+
bundleId: void 0,
|
|
23534
|
+
androidPackage: void 0,
|
|
23535
|
+
appVersion: void 0,
|
|
23536
|
+
buildNumber: void 0,
|
|
23537
|
+
rawRuntimeVersion: void 0
|
|
23538
|
+
};
|
|
23539
|
+
const warnIfMismatch = (label, override, native) => override !== void 0 && native !== void 0 && override !== native ? printWarn(`${label} override "${override}" differs from the native value "${native}". The better-update.json value will be used for build metadata.`) : Effect.void;
|
|
23540
|
+
const resolveAndroidMeta = (projectRoot, profile) => Effect.gen(function* () {
|
|
23541
|
+
const gradle = yield* readGradleConfig(path.join(projectRoot, "android"));
|
|
23542
|
+
const override = profile.android?.metaOverride;
|
|
23543
|
+
yield* warnIfMismatch("android.applicationId", override?.applicationId, gradle?.applicationId);
|
|
23544
|
+
const androidPackage = override?.applicationId ?? gradle?.applicationId;
|
|
23545
|
+
if (androidPackage === void 0) return yield* new BuildProfileError({ message: "Could not determine the Android applicationId. Set android.applicationId under this build profile in better-update.json, or ensure android/app/build.gradle defines it." });
|
|
23546
|
+
const versionCode = override?.versionCode ?? (gradle?.versionCode === void 0 ? void 0 : String(gradle.versionCode));
|
|
23547
|
+
return {
|
|
23548
|
+
...EMPTY,
|
|
23549
|
+
androidPackage,
|
|
23550
|
+
appVersion: override?.version ?? gradle?.versionName,
|
|
23551
|
+
buildNumber: versionCode
|
|
23552
|
+
};
|
|
23553
|
+
});
|
|
23554
|
+
const resolveIosMeta = (projectRoot, profile) => Effect.gen(function* () {
|
|
23555
|
+
const configurationName = profile.ios?.buildConfiguration ?? "Release";
|
|
23556
|
+
const native = yield* readIosNativeMeta({
|
|
23557
|
+
iosDir: path.join(projectRoot, "ios"),
|
|
23558
|
+
configurationName
|
|
23559
|
+
}).pipe(Effect.orElseSucceed(() => ({})));
|
|
23560
|
+
const override = profile.ios?.metaOverride;
|
|
23561
|
+
yield* warnIfMismatch("ios.bundleIdentifier", override?.bundleIdentifier, native.bundleId);
|
|
23562
|
+
const bundleId = override?.bundleIdentifier ?? native.bundleId;
|
|
23563
|
+
if (bundleId === void 0) return yield* new BuildProfileError({ message: "Could not determine the iOS bundle identifier. Set ios.bundleIdentifier under this build profile in better-update.json, or ensure the Xcode project defines PRODUCT_BUNDLE_IDENTIFIER for the build configuration." });
|
|
23564
|
+
return {
|
|
23565
|
+
...EMPTY,
|
|
23566
|
+
bundleId,
|
|
23567
|
+
appVersion: override?.version ?? native.marketingVersion,
|
|
23568
|
+
buildNumber: override?.buildNumber ?? native.currentProjectVersion
|
|
23569
|
+
};
|
|
23570
|
+
});
|
|
23571
|
+
const overlayExpoOverride = (meta, platform, profile) => {
|
|
23572
|
+
if (platform === "ios") {
|
|
23573
|
+
const override = profile.ios?.metaOverride;
|
|
23574
|
+
return {
|
|
23575
|
+
...meta,
|
|
23576
|
+
bundleId: override?.bundleIdentifier ?? meta.bundleId,
|
|
23577
|
+
appVersion: override?.version ?? meta.appVersion,
|
|
23578
|
+
buildNumber: override?.buildNumber ?? meta.buildNumber
|
|
23579
|
+
};
|
|
23580
|
+
}
|
|
23581
|
+
const override = profile.android?.metaOverride;
|
|
23582
|
+
return {
|
|
23583
|
+
...meta,
|
|
23584
|
+
androidPackage: override?.applicationId ?? meta.androidPackage,
|
|
23585
|
+
appVersion: override?.version ?? meta.appVersion,
|
|
23586
|
+
buildNumber: override?.versionCode ?? meta.buildNumber
|
|
23587
|
+
};
|
|
23588
|
+
};
|
|
23589
|
+
/**
|
|
23590
|
+
* Resolve app metadata (bundle id / package, version, build number) in a
|
|
23591
|
+
* project-type-aware way. Expo reads from app.json; bare/native read from native
|
|
23592
|
+
* files (build.gradle / pbxproj); KMP/custom rely on profile `metaOverride`,
|
|
23593
|
+
* which also overrides native values when both are present.
|
|
23594
|
+
*/
|
|
23595
|
+
const resolveAppMeta = (params) => {
|
|
23596
|
+
if (params.projectType === "expo") {
|
|
23597
|
+
if (params.expoAppMeta === void 0) return Effect.fail(new BuildProfileError({ message: "Internal: missing Expo app metadata for expo project." }));
|
|
23598
|
+
return Effect.succeed(overlayExpoOverride(params.expoAppMeta, params.platform, params.profile));
|
|
23599
|
+
}
|
|
23600
|
+
return params.platform === "ios" ? resolveIosMeta(params.projectRoot, params.profile) : resolveAndroidMeta(params.projectRoot, params.profile);
|
|
23601
|
+
};
|
|
23602
|
+
|
|
23603
|
+
//#endregion
|
|
23604
|
+
//#region src/application/build-workflow.ts
|
|
22986
23605
|
const runIosPlatformBuild = (input) => Effect.gen(function* () {
|
|
22987
23606
|
const { api, appMeta, envVars, options, profile, projectId, projectRoot, tempDir } = input;
|
|
22988
23607
|
if (!profile.ios) return yield* new BuildProfileError({ message: `Profile "${profile.name}" has no ios section.` });
|
|
22989
23608
|
const iosProfile = profile.ios;
|
|
22990
23609
|
const iosBundleId = appMeta.bundleId;
|
|
22991
|
-
if (!iosBundleId) return yield* new BuildProfileError({ message: "Missing ios.bundleIdentifier
|
|
23610
|
+
if (!iosBundleId) return yield* new BuildProfileError({ message: "Missing iOS bundle identifier (set ios.bundleIdentifier or your Expo config)." });
|
|
23611
|
+
const strategy = resolveIosStrategy(profile, input.projectType);
|
|
22992
23612
|
const isSimulator = iosProfile.simulator === true;
|
|
22993
23613
|
const credentialsSource = profile.credentialsSource ?? "remote";
|
|
22994
|
-
if (!isSimulator && credentialsSource === "remote") yield* ensureIosCredentials(api, {
|
|
23614
|
+
if (strategy !== "custom" && !isSimulator && credentialsSource === "remote") yield* ensureIosCredentials(api, {
|
|
22995
23615
|
projectId,
|
|
22996
23616
|
bundleIdentifier: iosBundleId,
|
|
22997
23617
|
distribution: iosProfile.distribution
|
|
@@ -23006,8 +23626,10 @@ const runIosPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
23006
23626
|
envVars,
|
|
23007
23627
|
projectId,
|
|
23008
23628
|
credentialsSource,
|
|
23629
|
+
strategy,
|
|
23009
23630
|
rawOutput: options.rawOutput,
|
|
23010
|
-
freezeCredentials: options.freezeCredentials ?? false
|
|
23631
|
+
freezeCredentials: options.freezeCredentials ?? false,
|
|
23632
|
+
...compact({ customCommand: profile.customCommand?.ios })
|
|
23011
23633
|
}),
|
|
23012
23634
|
target: isSimulator ? {
|
|
23013
23635
|
platform: "ios",
|
|
@@ -23026,7 +23648,8 @@ const runAndroidPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
23026
23648
|
if (!profile.android) return yield* new BuildProfileError({ message: `Profile "${profile.name}" has no android section.` });
|
|
23027
23649
|
const androidProfile = profile.android;
|
|
23028
23650
|
const androidBundleId = appMeta.androidPackage;
|
|
23029
|
-
if (!androidBundleId) return yield* new BuildProfileError({ message: "Missing android.
|
|
23651
|
+
if (!androidBundleId) return yield* new BuildProfileError({ message: "Missing Android applicationId (set android.applicationId or your Expo config)." });
|
|
23652
|
+
const strategy = resolveAndroidStrategy(profile, input.projectType);
|
|
23030
23653
|
const gradleConfig = yield* readGradleConfig(`${projectRoot}/android`);
|
|
23031
23654
|
yield* warnOnGradleMismatch(gradleConfig, androidBundleId);
|
|
23032
23655
|
const applicationIdentifier = gradleConfig?.applicationId ?? androidBundleId;
|
|
@@ -23047,7 +23670,9 @@ const runAndroidPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
23047
23670
|
projectId,
|
|
23048
23671
|
credentialsSource,
|
|
23049
23672
|
profileName: profile.name,
|
|
23050
|
-
skipCredentials
|
|
23673
|
+
skipCredentials,
|
|
23674
|
+
strategy,
|
|
23675
|
+
...compact({ customCommand: profile.customCommand?.android })
|
|
23051
23676
|
}),
|
|
23052
23677
|
target: androidProfile.format === "aab" ? {
|
|
23053
23678
|
platform: "android",
|
|
@@ -23062,12 +23687,48 @@ const runAndroidPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
23062
23687
|
};
|
|
23063
23688
|
});
|
|
23064
23689
|
const runPlatformBuild = (input) => input.platform === "ios" ? runIosPlatformBuild(input) : runAndroidPlatformBuild(input);
|
|
23690
|
+
const dirExists = (root, name) => Effect.gen(function* () {
|
|
23691
|
+
return yield* (yield* FileSystem.FileSystem).exists(path.join(root, name)).pipe(Effect.orElseSucceed(() => false));
|
|
23692
|
+
});
|
|
23693
|
+
/**
|
|
23694
|
+
* Expo metadata path: read app.json (with the env overlay so dynamic configs
|
|
23695
|
+
* resolve), apply autoIncrement to the user's tree, re-read, then derive the OTA
|
|
23696
|
+
* runtimeVersion. Mirrors the original managed flow.
|
|
23697
|
+
*/
|
|
23698
|
+
const resolveExpoBuildMeta = (params) => Effect.gen(function* () {
|
|
23699
|
+
const { userCwd, platform, profile, envVars } = params;
|
|
23700
|
+
yield* applyAutoIncrement({
|
|
23701
|
+
projectRoot: userCwd,
|
|
23702
|
+
platform,
|
|
23703
|
+
config: yield* readExpoConfig(userCwd, envVars),
|
|
23704
|
+
...platform === "ios" && profile.ios?.autoIncrement !== void 0 ? { iosMode: profile.ios.autoIncrement } : {},
|
|
23705
|
+
...platform === "android" && profile.android?.autoIncrement !== void 0 ? { androidMode: profile.android.autoIncrement } : {}
|
|
23706
|
+
});
|
|
23707
|
+
const bumpedConfig = yield* readExpoConfig(userCwd, envVars);
|
|
23708
|
+
const appMeta = yield* resolveAppMeta({
|
|
23709
|
+
projectType: "expo",
|
|
23710
|
+
platform,
|
|
23711
|
+
projectRoot: userCwd,
|
|
23712
|
+
profile,
|
|
23713
|
+
expoAppMeta: yield* readAppMeta(bumpedConfig, platform)
|
|
23714
|
+
});
|
|
23715
|
+
return {
|
|
23716
|
+
appMeta,
|
|
23717
|
+
runtimeVersion: yield* resolveRuntimeVersion({
|
|
23718
|
+
raw: appMeta.rawRuntimeVersion,
|
|
23719
|
+
appVersion: appMeta.appVersion,
|
|
23720
|
+
projectRoot: userCwd,
|
|
23721
|
+
platform,
|
|
23722
|
+
buildNumber: appMeta.buildNumber,
|
|
23723
|
+
sdkVersion: bumpedConfig.sdkVersion
|
|
23724
|
+
})
|
|
23725
|
+
};
|
|
23726
|
+
});
|
|
23065
23727
|
const resolveProfileName = (projectRoot, requested) => Effect.gen(function* () {
|
|
23066
|
-
const
|
|
23067
|
-
const available = Object.keys(easConfig.build ?? {});
|
|
23728
|
+
const available = yield* listBuildProfileNames(projectRoot);
|
|
23068
23729
|
if (available.includes(requested)) return requested;
|
|
23069
23730
|
if (!(yield* InteractiveMode).allow || available.length === 0) return requested;
|
|
23070
|
-
yield* printHuman(`Build profile "${requested}" not found in
|
|
23731
|
+
yield* printHuman(`Build profile "${requested}" not found in better-update.json.`);
|
|
23071
23732
|
return yield* promptSelect("Pick a build profile:", available.map((name) => ({
|
|
23072
23733
|
value: name,
|
|
23073
23734
|
label: name
|
|
@@ -23081,11 +23742,19 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23081
23742
|
allowDirty: options.allowDirty ?? false,
|
|
23082
23743
|
label: "build"
|
|
23083
23744
|
});
|
|
23084
|
-
const
|
|
23085
|
-
|
|
23086
|
-
|
|
23745
|
+
const projectType = yield* detectProjectType({
|
|
23746
|
+
projectRoot: userCwd,
|
|
23747
|
+
override: asProjectType((yield* readBetterUpdateConfig(userCwd))?.["projectType"])
|
|
23748
|
+
});
|
|
23749
|
+
const isExpo = projectType === "expo";
|
|
23750
|
+
const projectId = yield* readProjectId;
|
|
23087
23751
|
const profile = yield* readBuildProfile(userCwd, yield* resolveProfileName(userCwd, options.profileName));
|
|
23088
23752
|
if (profile.developmentClient === true) yield* warnIfDevClientMissing(userCwd);
|
|
23753
|
+
const platform = isExpo ? yield* detectPlatform(options.platform, yield* readExpoConfig(userCwd)) : yield* detectPlatformGeneric(options.platform, {
|
|
23754
|
+
profile,
|
|
23755
|
+
hasAndroidDir: yield* dirExists(userCwd, "android"),
|
|
23756
|
+
hasIosDir: yield* dirExists(userCwd, "ios")
|
|
23757
|
+
});
|
|
23089
23758
|
const envVars = {
|
|
23090
23759
|
...yield* pullEnvVars(api, {
|
|
23091
23760
|
projectId,
|
|
@@ -23093,36 +23762,35 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23093
23762
|
}),
|
|
23094
23763
|
...profile.env
|
|
23095
23764
|
};
|
|
23096
|
-
yield*
|
|
23097
|
-
|
|
23098
|
-
platform,
|
|
23099
|
-
config: yield* readExpoConfig(userCwd, envVars),
|
|
23100
|
-
...platform === "ios" && profile.ios?.autoIncrement !== void 0 ? { iosMode: profile.ios.autoIncrement } : {},
|
|
23101
|
-
...platform === "android" && profile.android?.autoIncrement !== void 0 ? { androidMode: profile.android.autoIncrement } : {}
|
|
23102
|
-
});
|
|
23103
|
-
const bumpedConfig = yield* readExpoConfig(userCwd, envVars);
|
|
23104
|
-
const appMeta = yield* readAppMeta(bumpedConfig, platform);
|
|
23105
|
-
const runtimeVersion = yield* resolveRuntimeVersion({
|
|
23106
|
-
raw: appMeta.rawRuntimeVersion,
|
|
23107
|
-
appVersion: appMeta.appVersion,
|
|
23108
|
-
projectRoot: userCwd,
|
|
23765
|
+
const { appMeta, runtimeVersion } = isExpo ? yield* resolveExpoBuildMeta({
|
|
23766
|
+
userCwd,
|
|
23109
23767
|
platform,
|
|
23110
|
-
|
|
23111
|
-
|
|
23112
|
-
})
|
|
23768
|
+
profile,
|
|
23769
|
+
envVars
|
|
23770
|
+
}) : {
|
|
23771
|
+
appMeta: yield* resolveAppMeta({
|
|
23772
|
+
projectType,
|
|
23773
|
+
platform,
|
|
23774
|
+
projectRoot: userCwd,
|
|
23775
|
+
profile
|
|
23776
|
+
}),
|
|
23777
|
+
runtimeVersion: void 0
|
|
23778
|
+
};
|
|
23113
23779
|
if (options.clearCache) yield* clearBuildCaches(userCwd);
|
|
23114
23780
|
const tempDir = yield* acquireBuildTempDir;
|
|
23115
23781
|
const staging = yield* prepareStagingProject({
|
|
23116
23782
|
userCwd,
|
|
23117
23783
|
tempDir,
|
|
23118
|
-
envVars
|
|
23784
|
+
envVars,
|
|
23785
|
+
projectType
|
|
23119
23786
|
});
|
|
23120
|
-
yield* printHuman(`Building ${platform} artifact for profile "${profile.name}" (runtimeVersion=${runtimeVersion})`);
|
|
23787
|
+
yield* printHuman(`Building ${platform} artifact for profile "${profile.name}"${runtimeVersion === void 0 ? "" : ` (runtimeVersion=${runtimeVersion})`}`);
|
|
23121
23788
|
const { build, target, bundleId } = yield* runPlatformBuild({
|
|
23122
23789
|
api,
|
|
23123
23790
|
options,
|
|
23124
23791
|
platform,
|
|
23125
23792
|
profile,
|
|
23793
|
+
projectType,
|
|
23126
23794
|
appMeta,
|
|
23127
23795
|
envVars,
|
|
23128
23796
|
projectId,
|
|
@@ -23156,18 +23824,18 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23156
23824
|
commit: rawGitContext.commit,
|
|
23157
23825
|
dirty: rawGitContext.dirty
|
|
23158
23826
|
});
|
|
23159
|
-
const fingerprintHash = yield* runFingerprintForPlatform(userCwd, platform).pipe(Effect.map((entry) => entry.hash), Effect.catchAll(() => Effect.succeed(void 0)));
|
|
23827
|
+
const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(userCwd, platform).pipe(Effect.map((entry) => entry.hash), Effect.catchAll(() => Effect.succeed(void 0))) : void 0;
|
|
23160
23828
|
const result = yield* reserveAndUpload(api, {
|
|
23161
23829
|
target,
|
|
23162
23830
|
projectId,
|
|
23163
23831
|
profileName: profile.name,
|
|
23164
|
-
runtimeVersion,
|
|
23165
23832
|
bundleId,
|
|
23166
23833
|
gitContext,
|
|
23167
23834
|
artifactPath: build.artifactPath,
|
|
23168
23835
|
sha256: build.sha256,
|
|
23169
23836
|
byteSize: build.byteSize,
|
|
23170
23837
|
...compact({
|
|
23838
|
+
runtimeVersion,
|
|
23171
23839
|
appVersion: appMeta.appVersion,
|
|
23172
23840
|
buildNumber: appMeta.buildNumber,
|
|
23173
23841
|
message: options.message,
|
|
@@ -23180,7 +23848,7 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23180
23848
|
["Status", result.status],
|
|
23181
23849
|
["Platform", platform],
|
|
23182
23850
|
["Profile", profile.name],
|
|
23183
|
-
["Runtime version", runtimeVersion],
|
|
23851
|
+
...runtimeVersion === void 0 ? [] : [["Runtime version", runtimeVersion]],
|
|
23184
23852
|
["Artifact", build.artifactPath],
|
|
23185
23853
|
["SHA-256", build.sha256],
|
|
23186
23854
|
["Bytes", String(build.byteSize)]
|
|
@@ -23225,7 +23893,7 @@ const DEFAULT_PROFILES = [
|
|
|
23225
23893
|
"preview",
|
|
23226
23894
|
"production"
|
|
23227
23895
|
];
|
|
23228
|
-
const writeEasJson
|
|
23896
|
+
const writeEasJson = (filePath, value) => Effect.gen(function* () {
|
|
23229
23897
|
yield* (yield* FileSystem.FileSystem).writeFileString(filePath, `${JSON.stringify(value, null, 2)}\n`).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to write eas.json: ${cause.message}` })));
|
|
23230
23898
|
});
|
|
23231
23899
|
const configureBuildCommand = defineCommand({
|
|
@@ -23243,7 +23911,7 @@ const configureBuildCommand = defineCommand({
|
|
|
23243
23911
|
const easJsonPath = path.join(projectRoot, "eas.json");
|
|
23244
23912
|
const fs = yield* FileSystem.FileSystem;
|
|
23245
23913
|
if (!(yield* fs.exists(easJsonPath))) {
|
|
23246
|
-
yield* writeEasJson
|
|
23914
|
+
yield* writeEasJson(easJsonPath, DEFAULT_EAS_JSON);
|
|
23247
23915
|
yield* printHuman(`Wrote eas.json with default profiles to ${easJsonPath}.`);
|
|
23248
23916
|
yield* printHumanKeyValue([["Profiles", DEFAULT_PROFILES.join(", ")], ["Path", easJsonPath]]);
|
|
23249
23917
|
return {
|
|
@@ -23260,7 +23928,7 @@ const configureBuildCommand = defineCommand({
|
|
|
23260
23928
|
path: easJsonPath
|
|
23261
23929
|
};
|
|
23262
23930
|
}
|
|
23263
|
-
yield* writeEasJson
|
|
23931
|
+
yield* writeEasJson(easJsonPath, DEFAULT_EAS_JSON);
|
|
23264
23932
|
yield* printHuman(`Overwrote eas.json with default profiles.`);
|
|
23265
23933
|
return {
|
|
23266
23934
|
action: "overwritten",
|
|
@@ -23288,7 +23956,7 @@ const configureBuildCommand = defineCommand({
|
|
|
23288
23956
|
};
|
|
23289
23957
|
}
|
|
23290
23958
|
const additions = Object.fromEntries(missing.map((name) => [name, DEFAULT_EAS_JSON.build[name]]));
|
|
23291
|
-
yield* writeEasJson
|
|
23959
|
+
yield* writeEasJson(easJsonPath, {
|
|
23292
23960
|
build: {
|
|
23293
23961
|
...config.build,
|
|
23294
23962
|
...additions
|
|
@@ -23888,7 +24556,7 @@ const tryReadApkPackageWith = (bin, apkPath) => Effect.gen(function* () {
|
|
|
23888
24556
|
if (!(yield* which(bin).pipe(Effect.catchAll(() => Effect.succeed(null))))) return;
|
|
23889
24557
|
const raw = yield* execCapture(`${bin} dump`, bin, "dump", "badging", apkPath).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
23890
24558
|
if (!raw) return;
|
|
23891
|
-
return /package: name='([^']+)'/u.exec(raw)?.[1];
|
|
24559
|
+
return /package: name='(?<packageName>[^']+)'/u.exec(raw)?.[1];
|
|
23892
24560
|
});
|
|
23893
24561
|
const readApkPackageName = (apkPath) => Effect.gen(function* () {
|
|
23894
24562
|
return yield* Effect.reduce(["aapt2", "aapt"], void 0, (acc, bin) => acc === void 0 ? tryReadApkPackageWith(bin, apkPath) : Effect.succeed(acc));
|
|
@@ -24101,7 +24769,7 @@ const runCommand$1 = defineCommand({
|
|
|
24101
24769
|
//#region src/application/upload-workflow.ts
|
|
24102
24770
|
const resolveIosTarget = (profile, appMeta) => Effect.gen(function* () {
|
|
24103
24771
|
if (!profile.ios) return yield* new BuildProfileError({ message: `Profile "${profile.name}" has no ios section.` });
|
|
24104
|
-
if (!appMeta.bundleId) return yield* new BuildProfileError({ message: "Missing ios.bundleIdentifier
|
|
24772
|
+
if (!appMeta.bundleId) return yield* new BuildProfileError({ message: "Missing iOS bundle identifier (set ios.bundleIdentifier or your Expo config)." });
|
|
24105
24773
|
return {
|
|
24106
24774
|
target: {
|
|
24107
24775
|
platform: "ios",
|
|
@@ -24113,7 +24781,7 @@ const resolveIosTarget = (profile, appMeta) => Effect.gen(function* () {
|
|
|
24113
24781
|
});
|
|
24114
24782
|
const resolveAndroidTarget = (profile, appMeta, projectRoot) => Effect.gen(function* () {
|
|
24115
24783
|
if (!profile.android) return yield* new BuildProfileError({ message: `Profile "${profile.name}" has no android section.` });
|
|
24116
|
-
if (!appMeta.androidPackage) return yield* new BuildProfileError({ message: "Missing android.
|
|
24784
|
+
if (!appMeta.androidPackage) return yield* new BuildProfileError({ message: "Missing Android applicationId (set android.applicationId or your Expo config)." });
|
|
24117
24785
|
const gradleConfig = yield* readGradleConfig(`${projectRoot}/android`);
|
|
24118
24786
|
yield* warnOnGradleMismatch(gradleConfig, appMeta.androidPackage);
|
|
24119
24787
|
const bundleId = gradleConfig?.applicationId ?? appMeta.androidPackage;
|
|
@@ -24130,27 +24798,56 @@ const resolveAndroidTarget = (profile, appMeta, projectRoot) => Effect.gen(funct
|
|
|
24130
24798
|
bundleId
|
|
24131
24799
|
};
|
|
24132
24800
|
});
|
|
24801
|
+
/** Resolve app metadata + OTA runtimeVersion for an upload (project-type aware). */
|
|
24802
|
+
const resolveUploadMeta = (params) => Effect.gen(function* () {
|
|
24803
|
+
const { projectType, platform, projectRoot, profile, envVars } = params;
|
|
24804
|
+
const expoConfig = projectType === "expo" ? yield* readExpoConfig(projectRoot, envVars) : void 0;
|
|
24805
|
+
const appMeta = yield* resolveAppMeta({
|
|
24806
|
+
projectType,
|
|
24807
|
+
platform,
|
|
24808
|
+
projectRoot,
|
|
24809
|
+
profile,
|
|
24810
|
+
...compact({
|
|
24811
|
+
expoConfig,
|
|
24812
|
+
expoAppMeta: expoConfig === void 0 ? void 0 : yield* readAppMeta(expoConfig, platform)
|
|
24813
|
+
})
|
|
24814
|
+
});
|
|
24815
|
+
return {
|
|
24816
|
+
appMeta,
|
|
24817
|
+
runtimeVersion: expoConfig === void 0 ? void 0 : yield* resolveRuntimeVersion({
|
|
24818
|
+
raw: appMeta.rawRuntimeVersion,
|
|
24819
|
+
appVersion: appMeta.appVersion,
|
|
24820
|
+
projectRoot,
|
|
24821
|
+
platform,
|
|
24822
|
+
buildNumber: appMeta.buildNumber,
|
|
24823
|
+
sdkVersion: expoConfig.sdkVersion
|
|
24824
|
+
}),
|
|
24825
|
+
isExpo: expoConfig !== void 0
|
|
24826
|
+
};
|
|
24827
|
+
});
|
|
24133
24828
|
const runUploadWorkflow = (options) => Effect.gen(function* () {
|
|
24134
24829
|
const api = yield* apiClient;
|
|
24135
24830
|
const projectRoot = yield* (yield* CliRuntime).cwd;
|
|
24136
24831
|
if (!(yield* (yield* FileSystem.FileSystem).exists(options.artifactPath).pipe(Effect.orElseSucceed(() => false)))) yield* new ArtifactNotFoundError({ message: `Artifact not found at ${options.artifactPath}.` });
|
|
24137
|
-
const
|
|
24832
|
+
const projectType = yield* detectProjectType({
|
|
24833
|
+
projectRoot,
|
|
24834
|
+
override: asProjectType((yield* readBetterUpdateConfig(projectRoot))?.["projectType"])
|
|
24835
|
+
});
|
|
24836
|
+
const projectId = yield* readProjectId;
|
|
24138
24837
|
const profile = yield* readBuildProfile(projectRoot, options.profileName);
|
|
24139
|
-
const
|
|
24838
|
+
const envVars = {
|
|
24140
24839
|
...yield* pullEnvVars(api, {
|
|
24141
24840
|
projectId,
|
|
24142
24841
|
environment: profile.environment
|
|
24143
24842
|
}),
|
|
24144
24843
|
...profile.env
|
|
24145
|
-
}
|
|
24146
|
-
const appMeta = yield*
|
|
24147
|
-
|
|
24148
|
-
raw: appMeta.rawRuntimeVersion,
|
|
24149
|
-
appVersion: appMeta.appVersion,
|
|
24150
|
-
projectRoot,
|
|
24844
|
+
};
|
|
24845
|
+
const { appMeta, runtimeVersion, isExpo } = yield* resolveUploadMeta({
|
|
24846
|
+
projectType,
|
|
24151
24847
|
platform: options.platform,
|
|
24152
|
-
|
|
24153
|
-
|
|
24848
|
+
projectRoot,
|
|
24849
|
+
profile,
|
|
24850
|
+
envVars
|
|
24154
24851
|
});
|
|
24155
24852
|
const { target, bundleId } = options.platform === "ios" ? yield* resolveIosTarget(profile, appMeta) : yield* resolveAndroidTarget(profile, appMeta, projectRoot);
|
|
24156
24853
|
yield* printHuman(`Hashing ${options.artifactPath}...`);
|
|
@@ -24161,7 +24858,7 @@ const runUploadWorkflow = (options) => Effect.gen(function* () {
|
|
|
24161
24858
|
commit: rawGitContext.commit,
|
|
24162
24859
|
dirty: rawGitContext.dirty
|
|
24163
24860
|
});
|
|
24164
|
-
const fingerprintHash = yield* runFingerprintForPlatform(projectRoot, options.platform).pipe(Effect.map((entry) => entry.hash), Effect.catchAll(() => Effect.succeed(void 0)));
|
|
24861
|
+
const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(projectRoot, options.platform).pipe(Effect.map((entry) => entry.hash), Effect.catchAll(() => Effect.succeed(void 0))) : void 0;
|
|
24165
24862
|
const result = yield* reserveAndUpload(api, compact({
|
|
24166
24863
|
target,
|
|
24167
24864
|
projectId,
|
|
@@ -24183,7 +24880,7 @@ const runUploadWorkflow = (options) => Effect.gen(function* () {
|
|
|
24183
24880
|
["Status", result.status],
|
|
24184
24881
|
["Platform", options.platform],
|
|
24185
24882
|
["Profile", profile.name],
|
|
24186
|
-
["Runtime version", runtimeVersion],
|
|
24883
|
+
...runtimeVersion === void 0 ? [] : [["Runtime version", runtimeVersion]],
|
|
24187
24884
|
["Artifact", options.artifactPath],
|
|
24188
24885
|
["SHA-256", sha256],
|
|
24189
24886
|
["Bytes", String(byteSize)]
|
|
@@ -24792,7 +25489,7 @@ const parseGoogleServiceAccountKey = (jsonText) => Effect.gen(function* () {
|
|
|
24792
25489
|
const APPLE_TEAM_ID_RE = /^[A-Z0-9]{10}$/u;
|
|
24793
25490
|
const extractTeamId = (params) => {
|
|
24794
25491
|
if (params.orgUnit && APPLE_TEAM_ID_RE.test(params.orgUnit)) return params.orgUnit;
|
|
24795
|
-
return /\(([A-Z0-9]{10})\)\s*$/u.exec(params.signingIdentity)?.[1];
|
|
25492
|
+
return /\((?<team>[A-Z0-9]{10})\)\s*$/u.exec(params.signingIdentity)?.[1];
|
|
24796
25493
|
};
|
|
24797
25494
|
/**
|
|
24798
25495
|
* Parse a PKCS#12 (.p12) buffer and extract certificate metadata.
|
|
@@ -26102,13 +26799,11 @@ const currentRecipients = (api) => Effect.gen(function* () {
|
|
|
26102
26799
|
//#endregion
|
|
26103
26800
|
//#region src/commands/credentials/vault-session.ts
|
|
26104
26801
|
/**
|
|
26105
|
-
* Unlock the vault key
|
|
26106
|
-
*
|
|
26107
|
-
*
|
|
26802
|
+
* Unlock the vault key for an interactive command: reuse the OS-keychain-cached
|
|
26803
|
+
* key when live, prompt for the device passphrase only on a cache miss, and none
|
|
26804
|
+
* at all for the CI `BETTER_UPDATE_IDENTITY` env key.
|
|
26108
26805
|
*/
|
|
26109
|
-
const unlockVaultInteractively = (api) =>
|
|
26110
|
-
return yield* unlockVaultKey(api, yield* resolveVaultPassphrase);
|
|
26111
|
-
});
|
|
26806
|
+
const unlockVaultInteractively = (api) => unlockVaultKeyInteractive(api);
|
|
26112
26807
|
/** Resolve a recipient selector (key id or fingerprint) from a flag, prompting if absent. */
|
|
26113
26808
|
const resolveSelector = (flag, message) => Effect.gen(function* () {
|
|
26114
26809
|
if (flag && flag.trim().length > 0) return flag.trim();
|
|
@@ -27740,6 +28435,56 @@ const revokeCommand = defineCommand({
|
|
|
27740
28435
|
subCommands: { "distribution-certificate": distributionCertificateCommand }
|
|
27741
28436
|
});
|
|
27742
28437
|
|
|
28438
|
+
//#endregion
|
|
28439
|
+
//#region src/commands/credentials/session.ts
|
|
28440
|
+
/** Whole minutes left, rounded up so "<1 min remaining" still reads as 1. */
|
|
28441
|
+
const remainingMinutes = (remainingMs) => Math.max(1, Math.ceil(remainingMs / 6e4));
|
|
28442
|
+
const unlockCommand = defineCommand({
|
|
28443
|
+
meta: {
|
|
28444
|
+
name: "unlock",
|
|
28445
|
+
description: "Unlock the credential vault and cache the key in your OS keychain, so later commands don't re-prompt"
|
|
28446
|
+
},
|
|
28447
|
+
run: async () => runEffect(Effect.gen(function* () {
|
|
28448
|
+
const recipient = yield* activeRecipient;
|
|
28449
|
+
if (recipient.source !== "file") {
|
|
28450
|
+
yield* printHuman("Active identity is the BETTER_UPDATE_IDENTITY (CI) key — it has no passphrase and isn't cached.");
|
|
28451
|
+
return;
|
|
28452
|
+
}
|
|
28453
|
+
const api = yield* apiClient;
|
|
28454
|
+
const cache = yield* VaultCache;
|
|
28455
|
+
yield* cache.clear(recipient.publicKey);
|
|
28456
|
+
yield* unlockVaultKeyInteractive(api);
|
|
28457
|
+
const cached = yield* cache.get(recipient.publicKey);
|
|
28458
|
+
yield* printHuman(`Vault unlocked${cached === void 0 ? " (no OS keychain available — commands will keep prompting)" : ` for ~${remainingMinutes(cached.remainingMs)} min; run \`better-update credentials lock\` to clear it`}.`);
|
|
28459
|
+
}))
|
|
28460
|
+
});
|
|
28461
|
+
const lockCommand = defineCommand({
|
|
28462
|
+
meta: {
|
|
28463
|
+
name: "lock",
|
|
28464
|
+
description: "Forget the cached vault key — the next credential command will prompt again"
|
|
28465
|
+
},
|
|
28466
|
+
run: async () => runEffect(Effect.gen(function* () {
|
|
28467
|
+
const recipient = yield* activeRecipient;
|
|
28468
|
+
yield* (yield* VaultCache).clear(recipient.publicKey);
|
|
28469
|
+
yield* printHuman("Vault locked — the cached key was cleared from your OS keychain.");
|
|
28470
|
+
}))
|
|
28471
|
+
});
|
|
28472
|
+
const statusCommand$1 = defineCommand({
|
|
28473
|
+
meta: {
|
|
28474
|
+
name: "status",
|
|
28475
|
+
description: "Show whether the vault is currently unlocked (cached) and for how much longer"
|
|
28476
|
+
},
|
|
28477
|
+
run: async () => runEffect(Effect.gen(function* () {
|
|
28478
|
+
const recipient = yield* activeRecipient;
|
|
28479
|
+
if (recipient.source !== "file") {
|
|
28480
|
+
yield* printHuman("Active identity is the BETTER_UPDATE_IDENTITY (CI) key — caching not used.");
|
|
28481
|
+
return;
|
|
28482
|
+
}
|
|
28483
|
+
const cached = yield* (yield* VaultCache).get(recipient.publicKey);
|
|
28484
|
+
yield* printHuman(cached === void 0 ? "Locked — the next credential command will prompt for your passphrase." : `Unlocked — cached vault key expires in ~${remainingMinutes(cached.remainingMs)} min.`);
|
|
28485
|
+
}))
|
|
28486
|
+
});
|
|
28487
|
+
|
|
27743
28488
|
//#endregion
|
|
27744
28489
|
//#region src/commands/credentials/sync/helpers.ts
|
|
27745
28490
|
const SYNC_EXIT_EXTRAS = {
|
|
@@ -28599,6 +29344,9 @@ const credentialsCommand = defineCommand({
|
|
|
28599
29344
|
identity: identityCommand,
|
|
28600
29345
|
access: accessCommand,
|
|
28601
29346
|
device: deviceCommand,
|
|
29347
|
+
unlock: unlockCommand,
|
|
29348
|
+
lock: lockCommand,
|
|
29349
|
+
status: statusCommand$1,
|
|
28602
29350
|
list: listCommand$3,
|
|
28603
29351
|
view: viewCommand$1,
|
|
28604
29352
|
download: downloadCommand,
|
|
@@ -28626,7 +29374,7 @@ const DEVICE_CLASS_VALUES = [
|
|
|
28626
29374
|
const isDeviceClass = (value) => DEVICE_CLASS_VALUES.includes(value);
|
|
28627
29375
|
const ttlHours = (value) => {
|
|
28628
29376
|
if (value === void 0) return;
|
|
28629
|
-
const match = /^([0-9]+)([hd])?$/u.exec(value);
|
|
29377
|
+
const match = /^(?<value>[0-9]+)(?<unit>[hd])?$/u.exec(value);
|
|
28630
29378
|
if (!match?.[1]) return;
|
|
28631
29379
|
const num = Number.parseInt(match[1], 10);
|
|
28632
29380
|
return match[2] === "d" ? num * 24 : num;
|
|
@@ -29036,11 +29784,18 @@ const checkProjectLink = Effect.gen(function* () {
|
|
|
29036
29784
|
const source = (yield* readLinkedProjectId(root)) === void 0 ? "Expo config" : "better-update.json";
|
|
29037
29785
|
return pass("project-linked", "Project linked", `projectId=${resolved.right} (via ${source})`);
|
|
29038
29786
|
});
|
|
29039
|
-
const
|
|
29040
|
-
const
|
|
29041
|
-
|
|
29042
|
-
|
|
29043
|
-
|
|
29787
|
+
const checkProjectType = Effect.gen(function* () {
|
|
29788
|
+
const root = yield* (yield* CliRuntime).cwd;
|
|
29789
|
+
const override = asProjectType((yield* readBetterUpdateConfig(root))?.["projectType"]);
|
|
29790
|
+
return pass("project-type", "Project type", `${yield* detectProjectType({
|
|
29791
|
+
projectRoot: root,
|
|
29792
|
+
override
|
|
29793
|
+
})} (${override === void 0 ? "auto-detected" : "better-update.json override"})`);
|
|
29794
|
+
});
|
|
29795
|
+
const checkBuildConfig = Effect.gen(function* () {
|
|
29796
|
+
const names = yield* listBuildProfileNames(yield* (yield* CliRuntime).cwd);
|
|
29797
|
+
if (names.length === 0) return warn("build-config", "Build config", "No build profiles found. Add a \"build\" section to better-update.json.");
|
|
29798
|
+
return pass("build-config", "Build config", `${names.length} profile(s) defined`);
|
|
29044
29799
|
});
|
|
29045
29800
|
const runChecks = Effect.gen(function* () {
|
|
29046
29801
|
const xcode = (yield* CliRuntime).platform === "darwin" ? [yield* checkCommand("xcode", "Xcode CLI tools", "xcode-select", ["-p"])] : [];
|
|
@@ -29051,7 +29806,8 @@ const runChecks = Effect.gen(function* () {
|
|
|
29051
29806
|
yield* checkServerHealth,
|
|
29052
29807
|
yield* checkAuth,
|
|
29053
29808
|
yield* checkProjectLink,
|
|
29054
|
-
yield*
|
|
29809
|
+
yield* checkProjectType,
|
|
29810
|
+
yield* checkBuildConfig
|
|
29055
29811
|
];
|
|
29056
29812
|
});
|
|
29057
29813
|
const statusIcon = (status) => {
|
|
@@ -29110,7 +29866,7 @@ const parseSingleEnvironmentArg = (raw) => Effect.gen(function* () {
|
|
|
29110
29866
|
return raw;
|
|
29111
29867
|
});
|
|
29112
29868
|
const formatEnvironments = (environments) => [...environments].toSorted((left, right) => left.localeCompare(right)).join(",");
|
|
29113
|
-
const DOTENV_LINE = /^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=\s*(
|
|
29869
|
+
const DOTENV_LINE = /^\s*(?:export\s+)?(?<key>[A-Z][A-Z0-9_]*)\s*=\s*(?<value>.*?)\s*$/u;
|
|
29114
29870
|
const stripQuotes = (raw) => {
|
|
29115
29871
|
if (raw.length < 2) return raw;
|
|
29116
29872
|
const [first] = raw;
|
|
@@ -30219,49 +30975,40 @@ const logoutCommand = defineCommand({
|
|
|
30219
30975
|
|
|
30220
30976
|
//#endregion
|
|
30221
30977
|
//#region src/commands/migrate-config.ts
|
|
30222
|
-
const readAppJson = (projectRoot) => {
|
|
30223
|
-
const path$1 = path.join(projectRoot, "app.json");
|
|
30224
|
-
if (!existsSync(path$1)) return null;
|
|
30225
|
-
const raw = readFileSync(path$1, "utf8");
|
|
30226
|
-
return JSON.parse(raw);
|
|
30227
|
-
};
|
|
30228
|
-
const writeAppJson = (projectRoot, content) => {
|
|
30229
|
-
writeFileSync(path.join(projectRoot, "app.json"), `${JSON.stringify(content, null, 2)}\n`);
|
|
30230
|
-
};
|
|
30231
|
-
const writeEasJson = (projectRoot, profiles) => {
|
|
30232
|
-
writeFileSync(path.join(projectRoot, "eas.json"), `${JSON.stringify({ build: profiles }, null, 2)}\n`);
|
|
30233
|
-
};
|
|
30234
30978
|
const migrateConfigCommand = defineCommand({
|
|
30235
30979
|
meta: {
|
|
30236
30980
|
name: "migrate-config",
|
|
30237
|
-
description: "Migrate
|
|
30981
|
+
description: "Migrate `build`/`submit` profiles from a legacy eas.json into better-update.json"
|
|
30238
30982
|
},
|
|
30239
30983
|
args: { yes: {
|
|
30240
30984
|
type: "boolean",
|
|
30241
30985
|
description: "Skip the confirmation prompt"
|
|
30242
30986
|
} },
|
|
30243
30987
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
30244
|
-
const
|
|
30245
|
-
const
|
|
30246
|
-
|
|
30247
|
-
const
|
|
30248
|
-
if (
|
|
30249
|
-
|
|
30988
|
+
const runtime = yield* CliRuntime;
|
|
30989
|
+
const fs = yield* FileSystem.FileSystem;
|
|
30990
|
+
const root = yield* runtime.cwd;
|
|
30991
|
+
const easPath = path.join(root, "eas.json");
|
|
30992
|
+
if (!(yield* fs.exists(easPath).pipe(Effect.orElseSucceed(() => false)))) return yield* new InvalidArgumentError({ message: `No eas.json found at ${root}.` });
|
|
30993
|
+
const config = yield* readEasJson(root);
|
|
30994
|
+
const patch = compact({
|
|
30995
|
+
build: config.build,
|
|
30996
|
+
submit: config.submit,
|
|
30997
|
+
cli: config.cli
|
|
30998
|
+
});
|
|
30999
|
+
if (Object.keys(patch).length === 0) {
|
|
31000
|
+
yield* printHuman("eas.json has no build/submit/cli sections — nothing to migrate.");
|
|
30250
31001
|
return;
|
|
30251
31002
|
}
|
|
30252
|
-
|
|
30253
|
-
if (!args.yes) {
|
|
30254
|
-
if (!(yield* promptConfirm(
|
|
31003
|
+
const existingBuild = (yield* readBetterUpdateConfig(root))?.["build"];
|
|
31004
|
+
if (typeof existingBuild === "object" && existingBuild !== null && config.build !== void 0 && !args.yes) {
|
|
31005
|
+
if (!(yield* promptConfirm("better-update.json already has a build section — overwrite it from eas.json?", { initialValue: false }))) {
|
|
30255
31006
|
yield* printHuman("Cancelled.");
|
|
30256
31007
|
return;
|
|
30257
31008
|
}
|
|
30258
31009
|
}
|
|
30259
|
-
|
|
30260
|
-
|
|
30261
|
-
const betterUpdate = (clone.expo?.extra)?.betterUpdate;
|
|
30262
|
-
if (betterUpdate) delete betterUpdate["profiles"];
|
|
30263
|
-
writeAppJson(root, clone);
|
|
30264
|
-
yield* printHuman("Migrated profiles into eas.json. Legacy field removed from app.json.");
|
|
31010
|
+
yield* writeBetterUpdateConfig(root, patch);
|
|
31011
|
+
yield* printHuman("Merged eas.json build/submit into better-update.json. You can now delete eas.json.");
|
|
30265
31012
|
}))
|
|
30266
31013
|
});
|
|
30267
31014
|
|
|
@@ -30706,7 +31453,7 @@ const submitCommand = defineCommand({
|
|
|
30706
31453
|
}
|
|
30707
31454
|
const projectId = yield* readProjectId;
|
|
30708
31455
|
const api = yield* apiClient;
|
|
30709
|
-
const easProfile = yield*
|
|
31456
|
+
const easProfile = yield* readSubmitProfile(yield* (yield* CliRuntime).cwd, args.profile);
|
|
30710
31457
|
const archive = yield* resolveArchive(api, projectId, platform, {
|
|
30711
31458
|
id: args.id,
|
|
30712
31459
|
path: args.path,
|