@better-update/cli 0.33.2 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +661 -494
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import { execFile, spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import { defineCommand, runMain } from "citty";
|
|
5
5
|
import { Clock, Console, Context, Data, Deferred, Duration, Effect, Either, Layer, Match, Option, ParseResult, Schedule, Schema } from "effect";
|
|
6
|
-
import { Command, FetchHttpClient, FileSystem, Headers as Headers$1, HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, HttpApiSecurity, HttpClient, HttpClientRequest, OpenApi
|
|
6
|
+
import { Command, FetchHttpClient, FileSystem, Headers as Headers$1, HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, HttpApiSecurity, HttpClient, HttpClientRequest, OpenApi } from "@effect/platform";
|
|
7
7
|
import { NodeContext } from "@effect/platform-node";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import process$1 from "node:process";
|
|
@@ -17,10 +17,11 @@ import { once } from "node:events";
|
|
|
17
17
|
import { createServer } from "node:http";
|
|
18
18
|
import { maxBy, uniqBy } from "es-toolkit";
|
|
19
19
|
import forge from "node-forge";
|
|
20
|
+
import { AndroidConfig } from "@expo/config-plugins";
|
|
21
|
+
import plist from "@expo/plist";
|
|
20
22
|
import { spawn as spawn$1 } from "node-pty";
|
|
21
23
|
import chalk from "chalk";
|
|
22
24
|
import os, { tmpdir } from "node:os";
|
|
23
|
-
import plistMod from "@expo/plist";
|
|
24
25
|
import { ExpoRunFormatter } from "@expo/xcpretty";
|
|
25
26
|
import { promisify } from "node:util";
|
|
26
27
|
import ignore from "ignore";
|
|
@@ -34,7 +35,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
34
35
|
|
|
35
36
|
//#endregion
|
|
36
37
|
//#region package.json
|
|
37
|
-
var version = "0.
|
|
38
|
+
var version = "0.35.0";
|
|
38
39
|
|
|
39
40
|
//#endregion
|
|
40
41
|
//#region src/lib/interactive-mode.ts
|
|
@@ -355,7 +356,15 @@ var OrgVault = class extends Schema.Class("OrgVault")({
|
|
|
355
356
|
organizationId: Id,
|
|
356
357
|
vaultVersion: VaultVersion,
|
|
357
358
|
createdAt: DateTimeString,
|
|
358
|
-
updatedAt: DateTimeString
|
|
359
|
+
updatedAt: DateTimeString,
|
|
360
|
+
/**
|
|
361
|
+
* A recipient was dropped out-of-band (member removed/downgraded); the live key
|
|
362
|
+
* is considered compromised and must be rotated. While true, credential-download
|
|
363
|
+
* paths fail closed (409). `rotate` clears it.
|
|
364
|
+
*/
|
|
365
|
+
rotationPending: Schema.Boolean,
|
|
366
|
+
rotationPendingSince: Schema.NullOr(DateTimeString),
|
|
367
|
+
rotationPendingReason: Schema.NullOr(Schema.String)
|
|
359
368
|
}) {};
|
|
360
369
|
/**
|
|
361
370
|
* One wrap of the org vault key to a recipient's public key — an `age` blob the
|
|
@@ -1045,7 +1054,7 @@ const projectIdParam$5 = HttpApiSchema.param("projectId", Schema.String);
|
|
|
1045
1054
|
var BuildCredentialsGroup = class extends HttpApiGroup.make("buildCredentials").add(HttpApiEndpoint.post("resolve")`/api/projects/${projectIdParam$5}/build-credentials/resolve`.setPayload(ResolveBuildCredentialsBody).addSuccess(ResolveBuildCredentialsResult).annotateContext(OpenApi.annotations({
|
|
1046
1055
|
title: "Resolve build credentials",
|
|
1047
1056
|
description: "Return decrypted signing assets for a project build. Regenerates the iOS provisioning profile via Apple ASC when the registered device roster has changed since the profile was last generated."
|
|
1048
|
-
}))).addError(NotFound).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
|
|
1057
|
+
}))).addError(NotFound).addError(BadRequest).addError(Forbidden).addError(Conflict).annotateContext(OpenApi.annotations({
|
|
1049
1058
|
title: "Build Credentials",
|
|
1050
1059
|
description: "Materialize signing assets needed by a CLI build run"
|
|
1051
1060
|
})) {};
|
|
@@ -3463,8 +3472,11 @@ const UpdateAssetUploaderLive = Layer.effect(UpdateAssetUploader, Effect.gen(fun
|
|
|
3463
3472
|
* private key — so the blast radius of a leaked keychain entry is one vault
|
|
3464
3473
|
* version's credentials, and only until the TTL lapses.
|
|
3465
3474
|
*/
|
|
3466
|
-
/**
|
|
3475
|
+
/** Default for how long a cached vault key stays valid before a fresh passphrase is required. */
|
|
3467
3476
|
const VAULT_CACHE_TTL_MS = 900 * 1e3;
|
|
3477
|
+
/** Bounds for a user-chosen TTL (`credentials unlock --duration`). */
|
|
3478
|
+
const VAULT_CACHE_TTL_MIN_MS = 60 * 1e3;
|
|
3479
|
+
const VAULT_CACHE_TTL_MAX_MS = 1440 * 60 * 1e3;
|
|
3468
3480
|
/** Keychain service name; the account is the recipient's public key. */
|
|
3469
3481
|
const KEYCHAIN_SERVICE = "better-update-vault";
|
|
3470
3482
|
const isCachedVaultEntry = (value) => isRecord$1(value) && typeof value["vaultKey"] === "string" && typeof value["vaultVersion"] === "number" && typeof value["keyId"] === "string" && typeof value["exp"] === "number";
|
|
@@ -3516,9 +3528,9 @@ const VaultCacheLive = Layer.effect(VaultCache, Effect.gen(function* () {
|
|
|
3516
3528
|
}
|
|
3517
3529
|
return decoded;
|
|
3518
3530
|
}),
|
|
3519
|
-
set: (publicKey, vault) => Effect.gen(function* () {
|
|
3531
|
+
set: (publicKey, vault, ttlMs) => Effect.gen(function* () {
|
|
3520
3532
|
if (yield* cacheDisabled) return;
|
|
3521
|
-
yield* writeRaw(publicKey, encodeCacheEntry(vault, yield* Clock.currentTimeMillis));
|
|
3533
|
+
yield* writeRaw(publicKey, encodeCacheEntry(vault, yield* Clock.currentTimeMillis, ttlMs));
|
|
3522
3534
|
}),
|
|
3523
3535
|
clear: (publicKey) => deleteRaw(publicKey)
|
|
3524
3536
|
};
|
|
@@ -4122,53 +4134,401 @@ const printHumanList = (headers, rows, emptyMessage) => Effect.gen(function* ()
|
|
|
4122
4134
|
});
|
|
4123
4135
|
|
|
4124
4136
|
//#endregion
|
|
4125
|
-
//#region src/lib/
|
|
4137
|
+
//#region src/lib/eas-profile-extends.ts
|
|
4138
|
+
const asStringValue = (value) => typeof value === "string" ? value : void 0;
|
|
4139
|
+
const asBooleanValue = (value) => typeof value === "boolean" ? value : void 0;
|
|
4140
|
+
const asNumberValue = (raw) => typeof raw === "number" && Number.isFinite(raw) ? raw : void 0;
|
|
4141
|
+
const shallowMerge = (base, overlay) => {
|
|
4142
|
+
if (!base) return overlay;
|
|
4143
|
+
if (!overlay) return base;
|
|
4144
|
+
return {
|
|
4145
|
+
...base,
|
|
4146
|
+
...overlay
|
|
4147
|
+
};
|
|
4148
|
+
};
|
|
4149
|
+
const stripExtends = (profile) => {
|
|
4150
|
+
if (profile.extends === void 0) return profile;
|
|
4151
|
+
const { extends: _omit, ...rest } = profile;
|
|
4152
|
+
return rest;
|
|
4153
|
+
};
|
|
4154
|
+
const resolveExtendsChain = (params) => Effect.gen(function* () {
|
|
4155
|
+
const { profiles, profileName, label, maxDepth, makeError } = params;
|
|
4156
|
+
const sourceLabel = params.sourceLabel ?? "eas.json";
|
|
4157
|
+
const noun = label === "build" ? "Build" : "Submit";
|
|
4158
|
+
const chain = [];
|
|
4159
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4160
|
+
let current = profileName;
|
|
4161
|
+
let depth = 0;
|
|
4162
|
+
while (current !== void 0) {
|
|
4163
|
+
if (visited.has(current)) return yield* Effect.fail(makeError(`Cycle detected in ${sourceLabel} ${label}.${profileName} extends chain at "${current}".`));
|
|
4164
|
+
visited.add(current);
|
|
4165
|
+
const profile = profiles[current];
|
|
4166
|
+
if (!profile) return yield* Effect.fail(makeError(current === profileName ? `${noun} profile "${profileName}" not found in ${sourceLabel}.` : `${noun} profile "${profileName}" extends missing profile "${current}".`));
|
|
4167
|
+
chain.unshift(profile);
|
|
4168
|
+
current = profile.extends;
|
|
4169
|
+
depth += 1;
|
|
4170
|
+
if (depth > maxDepth) return yield* Effect.fail(makeError(`Too many "extends" levels (max ${String(maxDepth)}) in ${sourceLabel} ${label}.${profileName}.`));
|
|
4171
|
+
}
|
|
4172
|
+
return chain;
|
|
4173
|
+
});
|
|
4174
|
+
|
|
4175
|
+
//#endregion
|
|
4176
|
+
//#region src/lib/eas-submit-config.ts
|
|
4177
|
+
const MAX_SUBMIT_EXTENDS_DEPTH = 10;
|
|
4178
|
+
const asStringArray = (raw) => {
|
|
4179
|
+
if (!Array.isArray(raw)) return;
|
|
4180
|
+
const items = raw.filter((item) => typeof item === "string");
|
|
4181
|
+
return items.length === 0 ? void 0 : items;
|
|
4182
|
+
};
|
|
4183
|
+
const asAndroidReleaseStatus = (raw) => {
|
|
4184
|
+
const value = asStringValue(raw);
|
|
4185
|
+
return value === "completed" || value === "draft" || value === "halted" || value === "inProgress" ? value : void 0;
|
|
4186
|
+
};
|
|
4187
|
+
const parseIosSubmitProfile = (raw) => {
|
|
4188
|
+
const record = asRecord(raw);
|
|
4189
|
+
if (!record) return;
|
|
4190
|
+
return compact({
|
|
4191
|
+
appleId: asStringValue(record["appleId"]),
|
|
4192
|
+
ascAppId: asStringValue(record["ascAppId"]),
|
|
4193
|
+
appleTeamId: asStringValue(record["appleTeamId"]),
|
|
4194
|
+
ascApiKeyPath: asStringValue(record["ascApiKeyPath"]),
|
|
4195
|
+
ascApiKeyId: asStringValue(record["ascApiKeyId"]),
|
|
4196
|
+
ascApiKeyIssuerId: asStringValue(record["ascApiKeyIssuerId"]),
|
|
4197
|
+
sku: asStringValue(record["sku"]),
|
|
4198
|
+
language: asStringValue(record["language"]),
|
|
4199
|
+
companyName: asStringValue(record["companyName"]),
|
|
4200
|
+
appName: asStringValue(record["appName"]),
|
|
4201
|
+
bundleIdentifier: asStringValue(record["bundleIdentifier"]),
|
|
4202
|
+
metadataPath: asStringValue(record["metadataPath"]),
|
|
4203
|
+
groups: asStringArray(record["groups"])
|
|
4204
|
+
});
|
|
4205
|
+
};
|
|
4206
|
+
const parseAndroidSubmitProfile = (raw) => {
|
|
4207
|
+
const record = asRecord(raw);
|
|
4208
|
+
if (!record) return;
|
|
4209
|
+
return compact({
|
|
4210
|
+
serviceAccountKeyPath: asStringValue(record["serviceAccountKeyPath"]),
|
|
4211
|
+
serviceAccountKeyId: asStringValue(record["serviceAccountKeyId"]),
|
|
4212
|
+
track: asStringValue(record["track"]),
|
|
4213
|
+
releaseStatus: asAndroidReleaseStatus(record["releaseStatus"]),
|
|
4214
|
+
changesNotSentForReview: asBooleanValue(record["changesNotSentForReview"]),
|
|
4215
|
+
rollout: asNumberValue(record["rollout"]),
|
|
4216
|
+
applicationId: asStringValue(record["applicationId"])
|
|
4217
|
+
});
|
|
4218
|
+
};
|
|
4219
|
+
const parseSubmitProfile = (raw) => {
|
|
4220
|
+
const record = asRecord(raw);
|
|
4221
|
+
if (!record) return;
|
|
4222
|
+
return compact({
|
|
4223
|
+
extends: asStringValue(record["extends"]),
|
|
4224
|
+
ios: parseIosSubmitProfile(record["ios"]),
|
|
4225
|
+
android: parseAndroidSubmitProfile(record["android"])
|
|
4226
|
+
});
|
|
4227
|
+
};
|
|
4228
|
+
const mergeSubmitProfile = (base, overlay) => {
|
|
4229
|
+
const ios = shallowMerge(base.ios, overlay.ios);
|
|
4230
|
+
const android = shallowMerge(base.android, overlay.android);
|
|
4231
|
+
return compact({
|
|
4232
|
+
extends: overlay.extends,
|
|
4233
|
+
ios,
|
|
4234
|
+
android
|
|
4235
|
+
});
|
|
4236
|
+
};
|
|
4237
|
+
const resolveEasSubmitProfile = (profiles, profileName, sourceLabel = "eas.json") => Effect.gen(function* () {
|
|
4238
|
+
if (!profiles) return yield* new BuildProfileError({ message: `${sourceLabel} has no "submit" section. Add at least one submit profile.` });
|
|
4239
|
+
return stripExtends((yield* resolveExtendsChain({
|
|
4240
|
+
profiles,
|
|
4241
|
+
profileName,
|
|
4242
|
+
label: "submit",
|
|
4243
|
+
maxDepth: MAX_SUBMIT_EXTENDS_DEPTH,
|
|
4244
|
+
sourceLabel,
|
|
4245
|
+
makeError: (message) => new BuildProfileError({ message })
|
|
4246
|
+
})).reduce((acc, next, index) => index === 0 ? next : mergeSubmitProfile(acc, next), {}));
|
|
4247
|
+
});
|
|
4248
|
+
|
|
4249
|
+
//#endregion
|
|
4250
|
+
//#region src/lib/eas-config.ts
|
|
4251
|
+
const MAX_EXTENDS_DEPTH = 10;
|
|
4252
|
+
const asEnv = (value) => {
|
|
4253
|
+
const record = asRecord(value);
|
|
4254
|
+
if (!record) return;
|
|
4255
|
+
const env = {};
|
|
4256
|
+
for (const [key, raw] of Object.entries(record)) if (typeof raw === "string") env[key] = raw;
|
|
4257
|
+
return Object.keys(env).length === 0 ? void 0 : env;
|
|
4258
|
+
};
|
|
4259
|
+
const asIosDistribution = (raw) => {
|
|
4260
|
+
const value = asStringValue(raw);
|
|
4261
|
+
if (value === "app-store" || value === "ad-hoc" || value === "development" || value === "enterprise") return value;
|
|
4262
|
+
};
|
|
4263
|
+
const asEnterpriseProvisioning = (raw) => {
|
|
4264
|
+
const value = asStringValue(raw);
|
|
4265
|
+
return value === "adhoc" || value === "universal" ? value : void 0;
|
|
4266
|
+
};
|
|
4267
|
+
const asAndroidBuildType = (raw) => {
|
|
4268
|
+
const value = asStringValue(raw);
|
|
4269
|
+
return value === "debug" || value === "release" ? value : void 0;
|
|
4270
|
+
};
|
|
4271
|
+
const asAndroidFormat = (raw) => {
|
|
4272
|
+
const value = asStringValue(raw);
|
|
4273
|
+
return value === "apk" || value === "aab" ? value : void 0;
|
|
4274
|
+
};
|
|
4275
|
+
const asAndroidDistribution = (raw) => {
|
|
4276
|
+
const value = asStringValue(raw);
|
|
4277
|
+
return value === "play-store" || value === "direct" ? value : void 0;
|
|
4278
|
+
};
|
|
4279
|
+
const asIosAutoIncrement = (raw) => {
|
|
4280
|
+
if (typeof raw === "boolean") return raw;
|
|
4281
|
+
const value = asStringValue(raw);
|
|
4282
|
+
return value === "buildNumber" || value === "version" ? value : void 0;
|
|
4283
|
+
};
|
|
4284
|
+
const asAndroidAutoIncrement = (raw) => {
|
|
4285
|
+
if (typeof raw === "boolean") return raw;
|
|
4286
|
+
const value = asStringValue(raw);
|
|
4287
|
+
return value === "versionCode" || value === "version" ? value : void 0;
|
|
4288
|
+
};
|
|
4289
|
+
const asAutoIncrement = (raw) => {
|
|
4290
|
+
if (typeof raw === "boolean") return raw;
|
|
4291
|
+
const value = asStringValue(raw);
|
|
4292
|
+
return value === "buildNumber" || value === "versionCode" || value === "version" ? value : void 0;
|
|
4293
|
+
};
|
|
4294
|
+
const asEasDistribution = (raw) => {
|
|
4295
|
+
const value = asStringValue(raw);
|
|
4296
|
+
return value === "internal" || value === "store" ? value : void 0;
|
|
4297
|
+
};
|
|
4298
|
+
const asCredentialsSource = (raw) => {
|
|
4299
|
+
const value = asStringValue(raw);
|
|
4300
|
+
return value === "remote" || value === "local" ? value : void 0;
|
|
4301
|
+
};
|
|
4302
|
+
const parseIosProfile = (raw) => {
|
|
4303
|
+
const record = asRecord(raw);
|
|
4304
|
+
if (!record) return;
|
|
4305
|
+
return compact({
|
|
4306
|
+
distribution: asIosDistribution(record["distribution"]),
|
|
4307
|
+
buildConfiguration: asStringValue(record["buildConfiguration"]),
|
|
4308
|
+
scheme: asStringValue(record["scheme"]),
|
|
4309
|
+
simulator: asBooleanValue(record["simulator"]),
|
|
4310
|
+
enterpriseProvisioning: asEnterpriseProvisioning(record["enterpriseProvisioning"]),
|
|
4311
|
+
autoIncrement: asIosAutoIncrement(record["autoIncrement"]),
|
|
4312
|
+
workspace: asStringValue(record["workspace"]),
|
|
4313
|
+
project: asStringValue(record["project"]),
|
|
4314
|
+
podInstall: asBooleanValue(record["podInstall"]),
|
|
4315
|
+
bundleIdentifier: asStringValue(record["bundleIdentifier"]),
|
|
4316
|
+
version: asStringValue(record["version"]),
|
|
4317
|
+
buildNumber: asStringValue(record["buildNumber"])
|
|
4318
|
+
});
|
|
4319
|
+
};
|
|
4320
|
+
const parseAndroidProfile = (raw) => {
|
|
4321
|
+
const record = asRecord(raw);
|
|
4322
|
+
if (!record) return;
|
|
4323
|
+
return compact({
|
|
4324
|
+
buildType: asAndroidBuildType(record["buildType"]),
|
|
4325
|
+
flavor: asStringValue(record["flavor"]),
|
|
4326
|
+
gradleCommand: asStringValue(record["gradleCommand"]),
|
|
4327
|
+
format: asAndroidFormat(record["format"]),
|
|
4328
|
+
distribution: asAndroidDistribution(record["distribution"]),
|
|
4329
|
+
autoIncrement: asAndroidAutoIncrement(record["autoIncrement"]),
|
|
4330
|
+
module: asStringValue(record["module"]),
|
|
4331
|
+
gradleTask: asStringValue(record["gradleTask"]),
|
|
4332
|
+
applicationId: asStringValue(record["applicationId"]),
|
|
4333
|
+
version: asStringValue(record["version"]),
|
|
4334
|
+
versionCode: asStringValue(record["versionCode"])
|
|
4335
|
+
});
|
|
4336
|
+
};
|
|
4337
|
+
const parseCustomCommandSpec = (raw) => {
|
|
4338
|
+
const record = asRecord(raw);
|
|
4339
|
+
if (!record) return;
|
|
4340
|
+
const command = asStringValue(record["command"]);
|
|
4341
|
+
if (command === void 0) return;
|
|
4342
|
+
return compact({
|
|
4343
|
+
command,
|
|
4344
|
+
cwd: asStringValue(record["cwd"]),
|
|
4345
|
+
env: asEnv(record["env"]),
|
|
4346
|
+
artifactPath: asStringValue(record["artifactPath"])
|
|
4347
|
+
});
|
|
4348
|
+
};
|
|
4349
|
+
const parseCustomCommandProfile = (raw) => {
|
|
4350
|
+
const record = asRecord(raw);
|
|
4351
|
+
if (!record) return;
|
|
4352
|
+
const result = compact({
|
|
4353
|
+
ios: parseCustomCommandSpec(record["ios"]),
|
|
4354
|
+
android: parseCustomCommandSpec(record["android"])
|
|
4355
|
+
});
|
|
4356
|
+
return Object.keys(result).length === 0 ? void 0 : result;
|
|
4357
|
+
};
|
|
4358
|
+
const parseBuildProfile = (raw) => {
|
|
4359
|
+
const record = asRecord(raw);
|
|
4360
|
+
if (!record) return;
|
|
4361
|
+
return compact({
|
|
4362
|
+
extends: asStringValue(record["extends"]),
|
|
4363
|
+
developmentClient: asBooleanValue(record["developmentClient"]),
|
|
4364
|
+
distribution: asEasDistribution(record["distribution"]),
|
|
4365
|
+
channel: asStringValue(record["channel"]),
|
|
4366
|
+
environment: asStringValue(record["environment"]),
|
|
4367
|
+
env: asEnv(record["env"]),
|
|
4368
|
+
ios: parseIosProfile(record["ios"]),
|
|
4369
|
+
android: parseAndroidProfile(record["android"]),
|
|
4370
|
+
credentialsSource: asCredentialsSource(record["credentialsSource"]),
|
|
4371
|
+
autoIncrement: asAutoIncrement(record["autoIncrement"]),
|
|
4372
|
+
withoutCredentials: asBooleanValue(record["withoutCredentials"]),
|
|
4373
|
+
custom: parseCustomCommandProfile(record["custom"])
|
|
4374
|
+
});
|
|
4375
|
+
};
|
|
4376
|
+
/**
|
|
4377
|
+
* Parse an already-decoded JSON object into an {@link EasConfig}. Shared by the
|
|
4378
|
+
* `eas.json` reader and any legacy build-config reader — both hold
|
|
4379
|
+
* the same `build`/`submit`/`cli` shape, only the source file differs.
|
|
4380
|
+
*/
|
|
4381
|
+
const parseConfigFromRecord = (root) => {
|
|
4382
|
+
const buildRecord = asRecord(root["build"]);
|
|
4383
|
+
if (!buildRecord) return asRecord(root["cli"]) ? { cli: parseCli(root["cli"]) } : {};
|
|
4384
|
+
const profiles = {};
|
|
4385
|
+
for (const [name, value] of Object.entries(buildRecord)) {
|
|
4386
|
+
const profile = parseBuildProfile(value);
|
|
4387
|
+
if (profile) profiles[name] = profile;
|
|
4388
|
+
}
|
|
4389
|
+
const submitRecord = asRecord(root["submit"]);
|
|
4390
|
+
const submit = {};
|
|
4391
|
+
if (submitRecord) for (const [name, value] of Object.entries(submitRecord)) {
|
|
4392
|
+
const profile = parseSubmitProfile(value);
|
|
4393
|
+
if (profile !== void 0) submit[name] = profile;
|
|
4394
|
+
}
|
|
4395
|
+
return {
|
|
4396
|
+
...asRecord(root["cli"]) ? { cli: parseCli(root["cli"]) } : {},
|
|
4397
|
+
build: profiles,
|
|
4398
|
+
...Object.keys(submit).length === 0 ? {} : { submit }
|
|
4399
|
+
};
|
|
4400
|
+
};
|
|
4401
|
+
const parseEasConfig = (text) => Effect.gen(function* () {
|
|
4402
|
+
const root = asRecord(yield* Effect.try({
|
|
4403
|
+
try: () => JSON.parse(text),
|
|
4404
|
+
catch: (cause) => new BuildProfileError({ message: `eas.json is not valid JSON: ${formatCause(cause)}` })
|
|
4405
|
+
}));
|
|
4406
|
+
if (!root) return yield* new BuildProfileError({ message: "eas.json must be a JSON object at the top level." });
|
|
4407
|
+
return parseConfigFromRecord(root);
|
|
4408
|
+
});
|
|
4409
|
+
const parseCli = (raw) => {
|
|
4410
|
+
const record = asRecord(raw);
|
|
4411
|
+
if (!record) return {};
|
|
4412
|
+
return compact({ version: asStringValue(record["version"]) });
|
|
4413
|
+
};
|
|
4414
|
+
const easJsonPath = (projectRoot) => path.join(projectRoot, "eas.json");
|
|
4415
|
+
const readEasJson = (projectRoot) => Effect.gen(function* () {
|
|
4416
|
+
const fs = yield* FileSystem.FileSystem;
|
|
4417
|
+
const filePath = easJsonPath(projectRoot);
|
|
4418
|
+
return yield* parseEasConfig(yield* fs.readFileString(filePath).pipe(Effect.mapError((cause) => 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}` }))));
|
|
4419
|
+
});
|
|
4420
|
+
const mergeCustom = (base, overlay) => {
|
|
4421
|
+
const merged = shallowMerge(base, overlay);
|
|
4422
|
+
return merged === void 0 || Object.keys(merged).length === 0 ? void 0 : merged;
|
|
4423
|
+
};
|
|
4424
|
+
const mergeProfile = (base, overlay) => {
|
|
4425
|
+
const ios = shallowMerge(base.ios, overlay.ios);
|
|
4426
|
+
const android = shallowMerge(base.android, overlay.android);
|
|
4427
|
+
const env = shallowMerge(base.env, overlay.env);
|
|
4428
|
+
const custom = mergeCustom(base.custom, overlay.custom);
|
|
4429
|
+
const developmentClient = overlay.developmentClient ?? base.developmentClient;
|
|
4430
|
+
const distribution = overlay.distribution ?? base.distribution;
|
|
4431
|
+
const channel = overlay.channel ?? base.channel;
|
|
4432
|
+
const environment = overlay.environment ?? base.environment;
|
|
4433
|
+
const credentialsSource = overlay.credentialsSource ?? base.credentialsSource;
|
|
4434
|
+
const autoIncrement = overlay.autoIncrement ?? base.autoIncrement;
|
|
4435
|
+
const withoutCredentials = overlay.withoutCredentials ?? base.withoutCredentials;
|
|
4436
|
+
return compact({
|
|
4437
|
+
extends: overlay.extends,
|
|
4438
|
+
developmentClient,
|
|
4439
|
+
distribution,
|
|
4440
|
+
channel,
|
|
4441
|
+
environment,
|
|
4442
|
+
env,
|
|
4443
|
+
ios,
|
|
4444
|
+
android,
|
|
4445
|
+
credentialsSource,
|
|
4446
|
+
autoIncrement,
|
|
4447
|
+
withoutCredentials,
|
|
4448
|
+
custom
|
|
4449
|
+
});
|
|
4450
|
+
};
|
|
4451
|
+
const resolveEasBuildProfile = (config, profileName, sourceLabel = "eas.json") => Effect.gen(function* () {
|
|
4452
|
+
const profiles = config.build;
|
|
4453
|
+
if (!profiles) return yield* new BuildProfileError({ message: `${sourceLabel} has no "build" section. Add at least one profile.` });
|
|
4454
|
+
return stripExtends((yield* resolveExtendsChain({
|
|
4455
|
+
profiles,
|
|
4456
|
+
profileName,
|
|
4457
|
+
label: "build",
|
|
4458
|
+
maxDepth: MAX_EXTENDS_DEPTH,
|
|
4459
|
+
sourceLabel,
|
|
4460
|
+
makeError: (message) => new BuildProfileError({ message })
|
|
4461
|
+
})).reduce((acc, next, index) => index === 0 ? next : mergeProfile(acc, next), {}));
|
|
4462
|
+
});
|
|
4463
|
+
|
|
4464
|
+
//#endregion
|
|
4465
|
+
//#region src/lib/eas-json.ts
|
|
4466
|
+
/**
|
|
4467
|
+
* `eas.json` is better-update's single project config file — for every build
|
|
4468
|
+
* system, not just Expo. Besides the EAS-shaped `cli`/`build`/`submit`
|
|
4469
|
+
* sections it carries two CLI-owned top-level extension keys:
|
|
4470
|
+
*
|
|
4471
|
+
* - `projectId` — the better-update project link (non-Expo projects; Expo
|
|
4472
|
+
* projects may keep it in app.json `extra.betterUpdate`).
|
|
4473
|
+
* - `projectType` — build-system override ("expo" | "bare" | "kmp" | "native"
|
|
4474
|
+
* | "custom") for projects auto-detection gets wrong.
|
|
4475
|
+
*
|
|
4476
|
+
* Helpers here read/write the file as a raw record so unknown keys (and the
|
|
4477
|
+
* extension keys) survive round-trips untouched.
|
|
4478
|
+
*/
|
|
4126
4479
|
/**
|
|
4127
4480
|
* Environment variable that overrides project-id resolution. Highest precedence
|
|
4128
4481
|
* so CI and ephemeral checkouts can target a project without writing any file.
|
|
4129
4482
|
*/
|
|
4130
4483
|
const BETTER_UPDATE_PROJECT_ID_ENV = "BETTER_UPDATE_PROJECT_ID";
|
|
4131
4484
|
/**
|
|
4132
|
-
*
|
|
4133
|
-
*
|
|
4134
|
-
*
|
|
4135
|
-
*/
|
|
4136
|
-
const BETTER_UPDATE_CONFIG_FILENAME = "better-update.json";
|
|
4137
|
-
const configPath = (projectRoot) => path.join(projectRoot, BETTER_UPDATE_CONFIG_FILENAME);
|
|
4138
|
-
/**
|
|
4139
|
-
* Read the project-local `better-update.json` if present. Returns `undefined`
|
|
4140
|
-
* when the file is missing or holds invalid JSON — an unlinked project is a
|
|
4141
|
-
* normal state, not an error. Mirrors {@link file://./../services/config-store.ts}'s
|
|
4142
|
-
* graceful `readConfig`.
|
|
4485
|
+
* Read `eas.json` as a raw record if present. Returns `undefined` when the
|
|
4486
|
+
* file is missing or holds invalid JSON — an unlinked project is a normal
|
|
4487
|
+
* state, not an error (profile readers surface parse errors separately).
|
|
4143
4488
|
*/
|
|
4144
|
-
const
|
|
4145
|
-
const content = yield* (yield* FileSystem.FileSystem).readFileString(
|
|
4489
|
+
const readEasJsonRaw = (projectRoot) => Effect.gen(function* () {
|
|
4490
|
+
const content = yield* (yield* FileSystem.FileSystem).readFileString(easJsonPath(projectRoot)).pipe(Effect.orElseSucceed(() => ""));
|
|
4146
4491
|
if (content.length === 0) return;
|
|
4147
4492
|
return yield* Effect.try(() => JSON.parse(content)).pipe(Effect.map((parsed) => isRecord$1(parsed) ? parsed : void 0), Effect.orElseSucceed(() => void 0));
|
|
4148
4493
|
});
|
|
4149
4494
|
/**
|
|
4150
|
-
*
|
|
4151
|
-
* the file
|
|
4152
|
-
|
|
4153
|
-
const readLinkedProjectId = (projectRoot) => readBetterUpdateConfig(projectRoot).pipe(Effect.map((config) => {
|
|
4154
|
-
const id = config?.["projectId"];
|
|
4155
|
-
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
4156
|
-
}));
|
|
4157
|
-
/**
|
|
4158
|
-
* Merge `patch` into the existing `better-update.json` (creating it if absent)
|
|
4159
|
-
* and write it back, returning the absolute file path. Used by `init` to persist
|
|
4160
|
-
* a project link for build-system-neutral projects.
|
|
4495
|
+
* Merge `patch` into the existing `eas.json` (creating it if absent) and write
|
|
4496
|
+
* it back, returning the absolute file path. Shallow merge: patched keys win,
|
|
4497
|
+
* all other keys are preserved verbatim.
|
|
4161
4498
|
*/
|
|
4162
|
-
const
|
|
4499
|
+
const writeEasJsonPatch = (projectRoot, patch) => Effect.gen(function* () {
|
|
4163
4500
|
const fs = yield* FileSystem.FileSystem;
|
|
4164
|
-
const filePath =
|
|
4501
|
+
const filePath = easJsonPath(projectRoot);
|
|
4165
4502
|
const merged = {
|
|
4166
|
-
...yield*
|
|
4503
|
+
...yield* readEasJsonRaw(projectRoot),
|
|
4167
4504
|
...patch
|
|
4168
4505
|
};
|
|
4169
|
-
yield* fs.writeFileString(filePath, `${JSON.stringify(merged, null, 2)}\n`).pipe(Effect.mapError((cause) => new ProjectNotLinkedError({ message: `Failed to write
|
|
4506
|
+
yield* fs.writeFileString(filePath, `${JSON.stringify(merged, null, 2)}\n`).pipe(Effect.mapError((cause) => new ProjectNotLinkedError({ message: `Failed to write eas.json: ${formatCause(cause)}` })));
|
|
4170
4507
|
return filePath;
|
|
4171
4508
|
});
|
|
4509
|
+
/**
|
|
4510
|
+
* Resolve the linked project id from `eas.json`'s top-level `projectId`, or
|
|
4511
|
+
* `undefined` when the file is absent / has no usable value.
|
|
4512
|
+
*/
|
|
4513
|
+
const readEasLinkedProjectId = (projectRoot) => readEasJsonRaw(projectRoot).pipe(Effect.map((config) => {
|
|
4514
|
+
const id = config?.["projectId"];
|
|
4515
|
+
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
4516
|
+
}));
|
|
4517
|
+
/**
|
|
4518
|
+
* Raw `projectType` override from `eas.json`, for `detectProjectType`.
|
|
4519
|
+
* Callers narrow it via `asProjectType`.
|
|
4520
|
+
*/
|
|
4521
|
+
const readEasProjectType = (projectRoot) => readEasJsonRaw(projectRoot).pipe(Effect.map((config) => config?.["projectType"]));
|
|
4522
|
+
/** List available build-profile names; `[]` when no eas.json exists. */
|
|
4523
|
+
const listBuildProfileNames = (projectRoot) => Effect.gen(function* () {
|
|
4524
|
+
if (!(yield* (yield* FileSystem.FileSystem).exists(easJsonPath(projectRoot)).pipe(Effect.orElseSucceed(() => false)))) return [];
|
|
4525
|
+
const config = yield* readEasJson(projectRoot);
|
|
4526
|
+
return Object.keys(config.build ?? {});
|
|
4527
|
+
});
|
|
4528
|
+
/** Resolve a submit profile from `eas.json`'s `submit` section. */
|
|
4529
|
+
const readSubmitProfile = (projectRoot, profileName) => Effect.gen(function* () {
|
|
4530
|
+
return yield* resolveEasSubmitProfile((yield* readEasJson(projectRoot)).submit, profileName, "eas.json");
|
|
4531
|
+
});
|
|
4172
4532
|
|
|
4173
4533
|
//#endregion
|
|
4174
4534
|
//#region ../../node_modules/.bun/structured-headers@2.0.2/node_modules/structured-headers/dist/util.js
|
|
@@ -4333,10 +4693,10 @@ const isExpoConfigInstalled = () => {
|
|
|
4333
4693
|
};
|
|
4334
4694
|
/**
|
|
4335
4695
|
* Build-system-neutral "project not linked" guidance, listing every supported
|
|
4336
|
-
* project-id source (env override,
|
|
4696
|
+
* project-id source (env override, eas.json, Expo config) so the
|
|
4337
4697
|
* message is correct for Expo and non-Expo projects alike.
|
|
4338
4698
|
*/
|
|
4339
|
-
const PROJECT_NOT_LINKED_MESSAGE = `Project not linked. Run \`better-update init\` to link this project, set the ${BETTER_UPDATE_PROJECT_ID_ENV} environment variable, add a "projectId" to
|
|
4699
|
+
const PROJECT_NOT_LINKED_MESSAGE = `Project not linked. Run \`better-update init\` to link this project, set the ${BETTER_UPDATE_PROJECT_ID_ENV} environment variable, add a top-level "projectId" to eas.json, or set extra.betterUpdate.projectId in your Expo config.`;
|
|
4340
4700
|
const clearDynamicConfigCache = (projectRoot) => {
|
|
4341
4701
|
const { dynamicConfigPath } = loadExpoConfigModule().getConfigFilePaths(projectRoot);
|
|
4342
4702
|
if (dynamicConfigPath) delete __require.cache[dynamicConfigPath];
|
|
@@ -4545,19 +4905,19 @@ const readAppMeta = (config, platform) => Effect.gen(function* () {
|
|
|
4545
4905
|
/**
|
|
4546
4906
|
* Resolve the active better-update project id, build-system-agnostically.
|
|
4547
4907
|
*
|
|
4548
|
-
* Precedence: `BETTER_UPDATE_PROJECT_ID` env > `
|
|
4549
|
-
* `@expo/config` (`extra.betterUpdate.projectId`). The Expo branch is taken
|
|
4550
|
-
* when `@expo/config` is installed, so a non-Expo project (no app.json,
|
|
4551
|
-
* installed) never crashes at module load — it simply falls through to
|
|
4552
|
-
* `ProjectNotLinkedError`. Every `env`/`credentials`/`status` command
|
|
4553
|
-
* vault through this single resolver.
|
|
4908
|
+
* Precedence: `BETTER_UPDATE_PROJECT_ID` env > `eas.json` top-level `projectId`
|
|
4909
|
+
* > `@expo/config` (`extra.betterUpdate.projectId`). The Expo branch is taken
|
|
4910
|
+
* only when `@expo/config` is installed, so a non-Expo project (no app.json,
|
|
4911
|
+
* Expo not installed) never crashes at module load — it simply falls through to
|
|
4912
|
+
* the `ProjectNotLinkedError`. Every `env`/`credentials`/`status` command
|
|
4913
|
+
* reaches the vault through this single resolver.
|
|
4554
4914
|
*/
|
|
4555
4915
|
const readProjectId = Effect.gen(function* () {
|
|
4556
4916
|
const runtime = yield* CliRuntime;
|
|
4557
4917
|
const fromEnv = yield* runtime.getEnv(BETTER_UPDATE_PROJECT_ID_ENV);
|
|
4558
4918
|
if (fromEnv !== void 0 && fromEnv.length > 0) return fromEnv;
|
|
4559
4919
|
const projectRoot = yield* runtime.cwd;
|
|
4560
|
-
const fromConfig = yield*
|
|
4920
|
+
const fromConfig = yield* readEasLinkedProjectId(projectRoot);
|
|
4561
4921
|
if (fromConfig !== void 0) return fromConfig;
|
|
4562
4922
|
if (isExpoConfigInstalled()) {
|
|
4563
4923
|
const fromExpo = yield* readExpoConfig(projectRoot).pipe(Effect.flatMap(extractProjectId), Effect.option);
|
|
@@ -4569,7 +4929,7 @@ const readProjectId = Effect.gen(function* () {
|
|
|
4569
4929
|
* Read the Expo config without failing: `Option.some(config)` when an Expo
|
|
4570
4930
|
* project is present and loads, otherwise `Option.none()` (no `@expo/config`
|
|
4571
4931
|
* installed, no config file, or a config that errors). Lets `init` decide
|
|
4572
|
-
* whether to write the link into the Expo config vs `
|
|
4932
|
+
* whether to write the link into the Expo config vs `eas.json`.
|
|
4573
4933
|
*/
|
|
4574
4934
|
const readExpoConfigOptional = (projectRoot) => Effect.suspend(() => {
|
|
4575
4935
|
if (!isExpoConfigInstalled()) return Effect.succeed(Option.none());
|
|
@@ -18204,15 +18564,18 @@ const grantRecipient = (args) => Effect.gen(function* () {
|
|
|
18204
18564
|
* operation: download/build-resolve reads, seal-for-upload + generate writes, and
|
|
18205
18565
|
* rotation. There is no read-only cache: an unlock makes the next write seamless
|
|
18206
18566
|
* too.
|
|
18567
|
+
*
|
|
18568
|
+
* `cacheTtlMs` overrides how long the unlocked key stays cached (default 15 min)
|
|
18569
|
+
* — `credentials unlock --duration` is the one caller that sets it.
|
|
18207
18570
|
*/
|
|
18208
|
-
const unlockVaultKeyInteractive = (api) => Effect.gen(function* () {
|
|
18571
|
+
const unlockVaultKeyInteractive = (api, options) => Effect.gen(function* () {
|
|
18209
18572
|
const recipient = yield* activeRecipient;
|
|
18210
18573
|
if (recipient.source !== "file") return yield* unlockVaultKey(api, void 0);
|
|
18211
18574
|
const cache = yield* VaultCache;
|
|
18212
18575
|
const cached = yield* cache.get(recipient.publicKey);
|
|
18213
18576
|
if (cached !== void 0) return cached.vault;
|
|
18214
18577
|
const vault = yield* unlockVaultKey(api, yield* promptPassword("Passphrase to unlock this device's identity:"));
|
|
18215
|
-
yield* cache.set(recipient.publicKey, vault);
|
|
18578
|
+
yield* cache.set(recipient.publicKey, vault, options?.cacheTtlMs);
|
|
18216
18579
|
return vault;
|
|
18217
18580
|
}).pipe(Effect.provide(VaultCacheLive));
|
|
18218
18581
|
/**
|
|
@@ -19641,6 +20004,106 @@ const loadLocalAndroidCredentials = (options) => Effect.gen(function* () {
|
|
|
19641
20004
|
};
|
|
19642
20005
|
});
|
|
19643
20006
|
|
|
20007
|
+
//#endregion
|
|
20008
|
+
//#region src/lib/update-channel-native.ts
|
|
20009
|
+
/**
|
|
20010
|
+
* EAS parity: after `expo prebuild`, bake the build profile's `channel` into
|
|
20011
|
+
* the generated native projects as the `expo-channel-name` request header —
|
|
20012
|
+
* exactly what EAS Build does (`androidSetChannelNativelyAsync` /
|
|
20013
|
+
* `iosSetChannelNativelyAsync` in eas-build). Writing the native files instead
|
|
20014
|
+
* of app.json works for dynamic configs (`app.config.ts`) too, which
|
|
20015
|
+
* `modifyConfigAsync` cannot patch.
|
|
20016
|
+
*/
|
|
20017
|
+
/** AndroidManifest meta-data key expo-updates reads extra request headers from. */
|
|
20018
|
+
const ANDROID_REQUEST_HEADERS_META_KEY = "expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY";
|
|
20019
|
+
/** Expo.plist key holding the same request-header map on iOS. */
|
|
20020
|
+
const IOS_REQUEST_HEADERS_PLIST_KEY = "EXUpdatesRequestHeaders";
|
|
20021
|
+
/** Request header expo-updates sends to select the OTA channel. */
|
|
20022
|
+
const EXPO_CHANNEL_HEADER = "expo-channel-name";
|
|
20023
|
+
const asStringRecord = (value) => {
|
|
20024
|
+
if (!isRecord$1(value)) return {};
|
|
20025
|
+
const out = {};
|
|
20026
|
+
for (const [key, raw] of Object.entries(value)) if (typeof raw === "string") out[key] = raw;
|
|
20027
|
+
return out;
|
|
20028
|
+
};
|
|
20029
|
+
/** Merge the channel header into an existing request-header map (channel wins). */
|
|
20030
|
+
const withChannelHeader = (existing, channel) => ({
|
|
20031
|
+
...asStringRecord(existing),
|
|
20032
|
+
[EXPO_CHANNEL_HEADER]: channel
|
|
20033
|
+
});
|
|
20034
|
+
/**
|
|
20035
|
+
* Whether the app declares `expo-updates` as a (dev)dependency. Channel
|
|
20036
|
+
* injection only makes sense when the updates module ships in the binary —
|
|
20037
|
+
* mirrors eas-build's `isExpoUpdatesInstalledAsync` gate.
|
|
20038
|
+
*/
|
|
20039
|
+
const isExpoUpdatesInstalled = (projectRoot) => Effect.gen(function* () {
|
|
20040
|
+
const parsed = safeJsonParse(yield* (yield* FileSystem.FileSystem).readFileString(path.join(projectRoot, "package.json")).pipe(Effect.orElseSucceed(() => "")));
|
|
20041
|
+
if (!isRecord$1(parsed)) return false;
|
|
20042
|
+
const dependencies = isRecord$1(parsed["dependencies"]) ? parsed["dependencies"] : {};
|
|
20043
|
+
const devDependencies = isRecord$1(parsed["devDependencies"]) ? parsed["devDependencies"] : {};
|
|
20044
|
+
return "expo-updates" in dependencies || "expo-updates" in devDependencies;
|
|
20045
|
+
});
|
|
20046
|
+
/**
|
|
20047
|
+
* Write the channel into the prebuilt `android/` project: merge it into the
|
|
20048
|
+
* request-headers JSON carried by the manifest meta-data entry (creating the
|
|
20049
|
+
* entry when prebuild emitted none).
|
|
20050
|
+
*/
|
|
20051
|
+
const setAndroidUpdateChannel = (params) => Effect.tryPromise({
|
|
20052
|
+
try: async () => {
|
|
20053
|
+
const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(params.projectRoot);
|
|
20054
|
+
const manifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
|
|
20055
|
+
const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(manifest);
|
|
20056
|
+
const existing = AndroidConfig.Manifest.getMainApplicationMetaDataValue(manifest, ANDROID_REQUEST_HEADERS_META_KEY);
|
|
20057
|
+
const headers = withChannelHeader(existing === null ? void 0 : safeJsonParse(existing), params.channel);
|
|
20058
|
+
AndroidConfig.Manifest.addMetaDataItemToMainApplication(mainApplication, ANDROID_REQUEST_HEADERS_META_KEY, JSON.stringify(headers));
|
|
20059
|
+
await AndroidConfig.Manifest.writeAndroidManifestAsync(manifestPath, manifest);
|
|
20060
|
+
},
|
|
20061
|
+
catch: (cause) => new BuildFailedError({
|
|
20062
|
+
step: "set android update channel",
|
|
20063
|
+
exitCode: 1,
|
|
20064
|
+
message: `Failed to write the update channel into AndroidManifest.xml: ${formatCause(cause)}`
|
|
20065
|
+
})
|
|
20066
|
+
});
|
|
20067
|
+
/** Locate `ios/<target>/Supporting/Expo.plist` in a prebuilt iOS project. */
|
|
20068
|
+
const findExpoPlist = (iosDir) => Effect.gen(function* () {
|
|
20069
|
+
const fs = yield* FileSystem.FileSystem;
|
|
20070
|
+
const entries = yield* fs.readDirectory(iosDir).pipe(Effect.orElseSucceed(() => []));
|
|
20071
|
+
for (const entry of entries) {
|
|
20072
|
+
const candidate = path.join(iosDir, entry, "Supporting", "Expo.plist");
|
|
20073
|
+
if (yield* fs.exists(candidate).pipe(Effect.orElseSucceed(() => false))) return candidate;
|
|
20074
|
+
}
|
|
20075
|
+
return yield* new BuildFailedError({
|
|
20076
|
+
step: "set ios update channel",
|
|
20077
|
+
exitCode: 1,
|
|
20078
|
+
message: `No Supporting/Expo.plist found under ${iosDir} after prebuild — is "expo-updates" installed?`
|
|
20079
|
+
});
|
|
20080
|
+
});
|
|
20081
|
+
/**
|
|
20082
|
+
* Write the channel into the prebuilt `ios/` project: merge it into the
|
|
20083
|
+
* `EXUpdatesRequestHeaders` dict of the generated Expo.plist.
|
|
20084
|
+
*/
|
|
20085
|
+
const setIosUpdateChannel = (params) => Effect.gen(function* () {
|
|
20086
|
+
const fs = yield* FileSystem.FileSystem;
|
|
20087
|
+
const plistPath = yield* findExpoPlist(params.iosDir);
|
|
20088
|
+
const failure = (cause) => new BuildFailedError({
|
|
20089
|
+
step: "set ios update channel",
|
|
20090
|
+
exitCode: 1,
|
|
20091
|
+
message: `Failed to write the update channel into ${plistPath}: ${formatCause(cause)}`
|
|
20092
|
+
});
|
|
20093
|
+
const content = yield* fs.readFileString(plistPath).pipe(Effect.mapError(failure));
|
|
20094
|
+
const parsed = yield* Effect.try({
|
|
20095
|
+
try: () => plist.parse(content),
|
|
20096
|
+
catch: failure
|
|
20097
|
+
});
|
|
20098
|
+
if (!isRecord$1(parsed)) return yield* failure("Expo.plist is not a plist dictionary.");
|
|
20099
|
+
const next = {
|
|
20100
|
+
...parsed,
|
|
20101
|
+
[IOS_REQUEST_HEADERS_PLIST_KEY]: withChannelHeader(parsed[IOS_REQUEST_HEADERS_PLIST_KEY], params.channel)
|
|
20102
|
+
};
|
|
20103
|
+
const rendered = plist.build(next);
|
|
20104
|
+
yield* fs.writeFileString(plistPath, `${rendered}\n`).pipe(Effect.mapError(failure));
|
|
20105
|
+
});
|
|
20106
|
+
|
|
19644
20107
|
//#endregion
|
|
19645
20108
|
//#region src/lib/pty-runner.ts
|
|
19646
20109
|
const ptyDimensions = () => {
|
|
@@ -19921,7 +20384,7 @@ const runAndroidCustom = (input, commandEnv) => Effect.gen(function* () {
|
|
|
19921
20384
|
if (custom.artifactPath === void 0) return yield* new BuildFailedError({
|
|
19922
20385
|
step: "custom android build",
|
|
19923
20386
|
exitCode: 1,
|
|
19924
|
-
message: "Custom Android build requires \"artifactPath\" (e.g. \"**/*.aab\") in
|
|
20387
|
+
message: "Custom Android build requires \"artifactPath\" (e.g. \"**/*.aab\") in eas.json."
|
|
19925
20388
|
});
|
|
19926
20389
|
const credentials = yield* resolveAndroidCredentials(input);
|
|
19927
20390
|
const credEnv = credentials === void 0 ? {} : {
|
|
@@ -19955,18 +20418,24 @@ const runAndroidCustom = (input, commandEnv) => Effect.gen(function* () {
|
|
|
19955
20418
|
});
|
|
19956
20419
|
const runAndroidBuild = (input) => Effect.gen(function* () {
|
|
19957
20420
|
const commandEnv = yield* (yield* CliRuntime).commandEnvironment(input.envVars);
|
|
19958
|
-
if (input.strategy === "expo")
|
|
19959
|
-
|
|
19960
|
-
|
|
19961
|
-
|
|
19962
|
-
|
|
19963
|
-
|
|
19964
|
-
|
|
19965
|
-
|
|
19966
|
-
|
|
19967
|
-
|
|
19968
|
-
|
|
19969
|
-
|
|
20421
|
+
if (input.strategy === "expo") {
|
|
20422
|
+
yield* runStep({
|
|
20423
|
+
command: "bunx",
|
|
20424
|
+
args: [
|
|
20425
|
+
"expo",
|
|
20426
|
+
"prebuild",
|
|
20427
|
+
"--platform",
|
|
20428
|
+
"android",
|
|
20429
|
+
"--clean"
|
|
20430
|
+
],
|
|
20431
|
+
cwd: input.projectRoot,
|
|
20432
|
+
env: commandEnv
|
|
20433
|
+
}, "expo prebuild android");
|
|
20434
|
+
if (input.updateChannel !== void 0) yield* setAndroidUpdateChannel({
|
|
20435
|
+
projectRoot: input.projectRoot,
|
|
20436
|
+
channel: input.updateChannel
|
|
20437
|
+
});
|
|
20438
|
+
}
|
|
19970
20439
|
return input.strategy === "custom" ? yield* runAndroidCustom(input, commandEnv) : yield* runGradleBuild(input, commandEnv);
|
|
19971
20440
|
});
|
|
19972
20441
|
|
|
@@ -20846,12 +21315,12 @@ const acquireKeychain = ({ tempDir, p12Path, p12Password }) => {
|
|
|
20846
21315
|
|
|
20847
21316
|
//#endregion
|
|
20848
21317
|
//#region src/lib/plist.ts
|
|
20849
|
-
const plist = typeof
|
|
21318
|
+
const plist$1 = typeof plist.parse === "function" ? plist : plist.default;
|
|
20850
21319
|
/**
|
|
20851
21320
|
* Parse an XML plist string into a typed object.
|
|
20852
21321
|
* Throws on malformed XML — callers should wrap in Effect.try.
|
|
20853
21322
|
*/
|
|
20854
|
-
const parsePlistXml = (xml) => plist.parse(xml);
|
|
21323
|
+
const parsePlistXml = (xml) => plist$1.parse(xml);
|
|
20855
21324
|
/**
|
|
20856
21325
|
* Parse a binary plist buffer into a typed object.
|
|
20857
21326
|
* Uses bplist-parser for Apple's binary plist format.
|
|
@@ -21149,7 +21618,7 @@ const resolveXcodeContainer = (projectRoot, iosDir, iosProfile) => Effect.gen(fu
|
|
|
21149
21618
|
return yield* new BuildFailedError({
|
|
21150
21619
|
step: "resolve Xcode container",
|
|
21151
21620
|
exitCode: 1,
|
|
21152
|
-
message: `No .xcworkspace or .xcodeproj found under ${iosDir}. Set ios.workspace / ios.project in
|
|
21621
|
+
message: `No .xcworkspace or .xcodeproj found under ${iosDir}. Set ios.workspace / ios.project in eas.json.`
|
|
21153
21622
|
});
|
|
21154
21623
|
});
|
|
21155
21624
|
/**
|
|
@@ -21171,6 +21640,10 @@ const prepareIosNative = (params) => Effect.gen(function* () {
|
|
|
21171
21640
|
cwd: params.projectRoot,
|
|
21172
21641
|
env: params.commandEnv
|
|
21173
21642
|
}, "expo prebuild ios");
|
|
21643
|
+
if (params.updateChannel !== void 0) yield* setIosUpdateChannel({
|
|
21644
|
+
iosDir: params.iosDir,
|
|
21645
|
+
channel: params.updateChannel
|
|
21646
|
+
});
|
|
21174
21647
|
yield* runStep({
|
|
21175
21648
|
command: "pod",
|
|
21176
21649
|
args: ["install"],
|
|
@@ -21220,7 +21693,8 @@ const runIosSimulatorBuild = (input) => Effect.gen(function* () {
|
|
|
21220
21693
|
projectRoot,
|
|
21221
21694
|
iosDir,
|
|
21222
21695
|
iosProfile,
|
|
21223
|
-
commandEnv
|
|
21696
|
+
commandEnv,
|
|
21697
|
+
updateChannel: input.updateChannel
|
|
21224
21698
|
});
|
|
21225
21699
|
const container = yield* resolveXcodeContainer(projectRoot, iosDir, iosProfile);
|
|
21226
21700
|
const scheme = iosProfile.scheme ?? container.schemeBase;
|
|
@@ -21323,7 +21797,8 @@ const runIosDeviceBuild = (input) => Effect.gen(function* () {
|
|
|
21323
21797
|
projectRoot,
|
|
21324
21798
|
iosDir,
|
|
21325
21799
|
iosProfile,
|
|
21326
|
-
commandEnv
|
|
21800
|
+
commandEnv,
|
|
21801
|
+
updateChannel: input.updateChannel
|
|
21327
21802
|
});
|
|
21328
21803
|
const container = yield* resolveXcodeContainer(projectRoot, iosDir, iosProfile);
|
|
21329
21804
|
const scheme = iosProfile.scheme ?? container.schemeBase;
|
|
@@ -21455,7 +21930,7 @@ const runIosCustom = (input) => Effect.gen(function* () {
|
|
|
21455
21930
|
if (custom.artifactPath === void 0) return yield* new BuildFailedError({
|
|
21456
21931
|
step: "custom ios build",
|
|
21457
21932
|
exitCode: 1,
|
|
21458
|
-
message: "Custom iOS build requires \"artifactPath\" (e.g. \"build/*.ipa\") in
|
|
21933
|
+
message: "Custom iOS build requires \"artifactPath\" (e.g. \"build/*.ipa\") in eas.json."
|
|
21459
21934
|
});
|
|
21460
21935
|
const credentials = yield* fetchAllCredentials({
|
|
21461
21936
|
api: input.api,
|
|
@@ -21644,354 +22119,6 @@ const applyAutoIncrement = (input) => Effect.gen(function* () {
|
|
|
21644
22119
|
return bumps;
|
|
21645
22120
|
});
|
|
21646
22121
|
|
|
21647
|
-
//#endregion
|
|
21648
|
-
//#region src/lib/eas-profile-extends.ts
|
|
21649
|
-
const asStringValue = (value) => typeof value === "string" ? value : void 0;
|
|
21650
|
-
const asBooleanValue = (value) => typeof value === "boolean" ? value : void 0;
|
|
21651
|
-
const asNumberValue = (raw) => typeof raw === "number" && Number.isFinite(raw) ? raw : void 0;
|
|
21652
|
-
const shallowMerge = (base, overlay) => {
|
|
21653
|
-
if (!base) return overlay;
|
|
21654
|
-
if (!overlay) return base;
|
|
21655
|
-
return {
|
|
21656
|
-
...base,
|
|
21657
|
-
...overlay
|
|
21658
|
-
};
|
|
21659
|
-
};
|
|
21660
|
-
const stripExtends = (profile) => {
|
|
21661
|
-
if (profile.extends === void 0) return profile;
|
|
21662
|
-
const { extends: _omit, ...rest } = profile;
|
|
21663
|
-
return rest;
|
|
21664
|
-
};
|
|
21665
|
-
const resolveExtendsChain = (params) => Effect.gen(function* () {
|
|
21666
|
-
const { profiles, profileName, label, maxDepth, makeError } = params;
|
|
21667
|
-
const sourceLabel = params.sourceLabel ?? "eas.json";
|
|
21668
|
-
const noun = label === "build" ? "Build" : "Submit";
|
|
21669
|
-
const chain = [];
|
|
21670
|
-
const visited = /* @__PURE__ */ new Set();
|
|
21671
|
-
let current = profileName;
|
|
21672
|
-
let depth = 0;
|
|
21673
|
-
while (current !== void 0) {
|
|
21674
|
-
if (visited.has(current)) return yield* Effect.fail(makeError(`Cycle detected in ${sourceLabel} ${label}.${profileName} extends chain at "${current}".`));
|
|
21675
|
-
visited.add(current);
|
|
21676
|
-
const profile = profiles[current];
|
|
21677
|
-
if (!profile) return yield* Effect.fail(makeError(current === profileName ? `${noun} profile "${profileName}" not found in ${sourceLabel}.` : `${noun} profile "${profileName}" extends missing profile "${current}".`));
|
|
21678
|
-
chain.unshift(profile);
|
|
21679
|
-
current = profile.extends;
|
|
21680
|
-
depth += 1;
|
|
21681
|
-
if (depth > maxDepth) return yield* Effect.fail(makeError(`Too many "extends" levels (max ${String(maxDepth)}) in ${sourceLabel} ${label}.${profileName}.`));
|
|
21682
|
-
}
|
|
21683
|
-
return chain;
|
|
21684
|
-
});
|
|
21685
|
-
|
|
21686
|
-
//#endregion
|
|
21687
|
-
//#region src/lib/eas-submit-config.ts
|
|
21688
|
-
const MAX_SUBMIT_EXTENDS_DEPTH = 10;
|
|
21689
|
-
const asStringArray = (raw) => {
|
|
21690
|
-
if (!Array.isArray(raw)) return;
|
|
21691
|
-
const items = raw.filter((item) => typeof item === "string");
|
|
21692
|
-
return items.length === 0 ? void 0 : items;
|
|
21693
|
-
};
|
|
21694
|
-
const asAndroidReleaseStatus = (raw) => {
|
|
21695
|
-
const value = asStringValue(raw);
|
|
21696
|
-
return value === "completed" || value === "draft" || value === "halted" || value === "inProgress" ? value : void 0;
|
|
21697
|
-
};
|
|
21698
|
-
const parseIosSubmitProfile = (raw) => {
|
|
21699
|
-
const record = asRecord(raw);
|
|
21700
|
-
if (!record) return;
|
|
21701
|
-
return compact({
|
|
21702
|
-
appleId: asStringValue(record["appleId"]),
|
|
21703
|
-
ascAppId: asStringValue(record["ascAppId"]),
|
|
21704
|
-
appleTeamId: asStringValue(record["appleTeamId"]),
|
|
21705
|
-
ascApiKeyPath: asStringValue(record["ascApiKeyPath"]),
|
|
21706
|
-
ascApiKeyId: asStringValue(record["ascApiKeyId"]),
|
|
21707
|
-
ascApiKeyIssuerId: asStringValue(record["ascApiKeyIssuerId"]),
|
|
21708
|
-
sku: asStringValue(record["sku"]),
|
|
21709
|
-
language: asStringValue(record["language"]),
|
|
21710
|
-
companyName: asStringValue(record["companyName"]),
|
|
21711
|
-
appName: asStringValue(record["appName"]),
|
|
21712
|
-
bundleIdentifier: asStringValue(record["bundleIdentifier"]),
|
|
21713
|
-
metadataPath: asStringValue(record["metadataPath"]),
|
|
21714
|
-
groups: asStringArray(record["groups"])
|
|
21715
|
-
});
|
|
21716
|
-
};
|
|
21717
|
-
const parseAndroidSubmitProfile = (raw) => {
|
|
21718
|
-
const record = asRecord(raw);
|
|
21719
|
-
if (!record) return;
|
|
21720
|
-
return compact({
|
|
21721
|
-
serviceAccountKeyPath: asStringValue(record["serviceAccountKeyPath"]),
|
|
21722
|
-
serviceAccountKeyId: asStringValue(record["serviceAccountKeyId"]),
|
|
21723
|
-
track: asStringValue(record["track"]),
|
|
21724
|
-
releaseStatus: asAndroidReleaseStatus(record["releaseStatus"]),
|
|
21725
|
-
changesNotSentForReview: asBooleanValue(record["changesNotSentForReview"]),
|
|
21726
|
-
rollout: asNumberValue(record["rollout"]),
|
|
21727
|
-
applicationId: asStringValue(record["applicationId"])
|
|
21728
|
-
});
|
|
21729
|
-
};
|
|
21730
|
-
const parseSubmitProfile = (raw) => {
|
|
21731
|
-
const record = asRecord(raw);
|
|
21732
|
-
if (!record) return;
|
|
21733
|
-
return compact({
|
|
21734
|
-
extends: asStringValue(record["extends"]),
|
|
21735
|
-
ios: parseIosSubmitProfile(record["ios"]),
|
|
21736
|
-
android: parseAndroidSubmitProfile(record["android"])
|
|
21737
|
-
});
|
|
21738
|
-
};
|
|
21739
|
-
const mergeSubmitProfile = (base, overlay) => {
|
|
21740
|
-
const ios = shallowMerge(base.ios, overlay.ios);
|
|
21741
|
-
const android = shallowMerge(base.android, overlay.android);
|
|
21742
|
-
return compact({
|
|
21743
|
-
extends: overlay.extends,
|
|
21744
|
-
ios,
|
|
21745
|
-
android
|
|
21746
|
-
});
|
|
21747
|
-
};
|
|
21748
|
-
const resolveEasSubmitProfile = (profiles, profileName, sourceLabel = "eas.json") => Effect.gen(function* () {
|
|
21749
|
-
if (!profiles) return yield* new BuildProfileError({ message: `${sourceLabel} has no "submit" section. Add at least one submit profile.` });
|
|
21750
|
-
return stripExtends((yield* resolveExtendsChain({
|
|
21751
|
-
profiles,
|
|
21752
|
-
profileName,
|
|
21753
|
-
label: "submit",
|
|
21754
|
-
maxDepth: MAX_SUBMIT_EXTENDS_DEPTH,
|
|
21755
|
-
sourceLabel,
|
|
21756
|
-
makeError: (message) => new BuildProfileError({ message })
|
|
21757
|
-
})).reduce((acc, next, index) => index === 0 ? next : mergeSubmitProfile(acc, next), {}));
|
|
21758
|
-
});
|
|
21759
|
-
|
|
21760
|
-
//#endregion
|
|
21761
|
-
//#region src/lib/eas-config.ts
|
|
21762
|
-
const MAX_EXTENDS_DEPTH = 10;
|
|
21763
|
-
const asEnv = (value) => {
|
|
21764
|
-
const record = asRecord(value);
|
|
21765
|
-
if (!record) return;
|
|
21766
|
-
const env = {};
|
|
21767
|
-
for (const [key, raw] of Object.entries(record)) if (typeof raw === "string") env[key] = raw;
|
|
21768
|
-
return Object.keys(env).length === 0 ? void 0 : env;
|
|
21769
|
-
};
|
|
21770
|
-
const asIosDistribution = (raw) => {
|
|
21771
|
-
const value = asStringValue(raw);
|
|
21772
|
-
if (value === "app-store" || value === "ad-hoc" || value === "development" || value === "enterprise") return value;
|
|
21773
|
-
};
|
|
21774
|
-
const asEnterpriseProvisioning = (raw) => {
|
|
21775
|
-
const value = asStringValue(raw);
|
|
21776
|
-
return value === "adhoc" || value === "universal" ? value : void 0;
|
|
21777
|
-
};
|
|
21778
|
-
const asAndroidBuildType = (raw) => {
|
|
21779
|
-
const value = asStringValue(raw);
|
|
21780
|
-
return value === "debug" || value === "release" ? value : void 0;
|
|
21781
|
-
};
|
|
21782
|
-
const asAndroidFormat = (raw) => {
|
|
21783
|
-
const value = asStringValue(raw);
|
|
21784
|
-
return value === "apk" || value === "aab" ? value : void 0;
|
|
21785
|
-
};
|
|
21786
|
-
const asAndroidDistribution = (raw) => {
|
|
21787
|
-
const value = asStringValue(raw);
|
|
21788
|
-
return value === "play-store" || value === "direct" ? value : void 0;
|
|
21789
|
-
};
|
|
21790
|
-
const asIosAutoIncrement = (raw) => {
|
|
21791
|
-
if (typeof raw === "boolean") return raw;
|
|
21792
|
-
const value = asStringValue(raw);
|
|
21793
|
-
return value === "buildNumber" || value === "version" ? value : void 0;
|
|
21794
|
-
};
|
|
21795
|
-
const asAndroidAutoIncrement = (raw) => {
|
|
21796
|
-
if (typeof raw === "boolean") return raw;
|
|
21797
|
-
const value = asStringValue(raw);
|
|
21798
|
-
return value === "versionCode" || value === "version" ? value : void 0;
|
|
21799
|
-
};
|
|
21800
|
-
const asAutoIncrement = (raw) => {
|
|
21801
|
-
if (typeof raw === "boolean") return raw;
|
|
21802
|
-
const value = asStringValue(raw);
|
|
21803
|
-
return value === "buildNumber" || value === "versionCode" || value === "version" ? value : void 0;
|
|
21804
|
-
};
|
|
21805
|
-
const asEasDistribution = (raw) => {
|
|
21806
|
-
const value = asStringValue(raw);
|
|
21807
|
-
return value === "internal" || value === "store" ? value : void 0;
|
|
21808
|
-
};
|
|
21809
|
-
const asCredentialsSource = (raw) => {
|
|
21810
|
-
const value = asStringValue(raw);
|
|
21811
|
-
return value === "remote" || value === "local" ? value : void 0;
|
|
21812
|
-
};
|
|
21813
|
-
const parseIosProfile = (raw) => {
|
|
21814
|
-
const record = asRecord(raw);
|
|
21815
|
-
if (!record) return;
|
|
21816
|
-
return compact({
|
|
21817
|
-
distribution: asIosDistribution(record["distribution"]),
|
|
21818
|
-
buildConfiguration: asStringValue(record["buildConfiguration"]),
|
|
21819
|
-
scheme: asStringValue(record["scheme"]),
|
|
21820
|
-
simulator: asBooleanValue(record["simulator"]),
|
|
21821
|
-
enterpriseProvisioning: asEnterpriseProvisioning(record["enterpriseProvisioning"]),
|
|
21822
|
-
autoIncrement: asIosAutoIncrement(record["autoIncrement"]),
|
|
21823
|
-
workspace: asStringValue(record["workspace"]),
|
|
21824
|
-
project: asStringValue(record["project"]),
|
|
21825
|
-
podInstall: asBooleanValue(record["podInstall"]),
|
|
21826
|
-
bundleIdentifier: asStringValue(record["bundleIdentifier"]),
|
|
21827
|
-
version: asStringValue(record["version"]),
|
|
21828
|
-
buildNumber: asStringValue(record["buildNumber"])
|
|
21829
|
-
});
|
|
21830
|
-
};
|
|
21831
|
-
const parseAndroidProfile = (raw) => {
|
|
21832
|
-
const record = asRecord(raw);
|
|
21833
|
-
if (!record) return;
|
|
21834
|
-
return compact({
|
|
21835
|
-
buildType: asAndroidBuildType(record["buildType"]),
|
|
21836
|
-
flavor: asStringValue(record["flavor"]),
|
|
21837
|
-
gradleCommand: asStringValue(record["gradleCommand"]),
|
|
21838
|
-
format: asAndroidFormat(record["format"]),
|
|
21839
|
-
distribution: asAndroidDistribution(record["distribution"]),
|
|
21840
|
-
autoIncrement: asAndroidAutoIncrement(record["autoIncrement"]),
|
|
21841
|
-
module: asStringValue(record["module"]),
|
|
21842
|
-
gradleTask: asStringValue(record["gradleTask"]),
|
|
21843
|
-
applicationId: asStringValue(record["applicationId"]),
|
|
21844
|
-
version: asStringValue(record["version"]),
|
|
21845
|
-
versionCode: asStringValue(record["versionCode"])
|
|
21846
|
-
});
|
|
21847
|
-
};
|
|
21848
|
-
const parseCustomCommandSpec = (raw) => {
|
|
21849
|
-
const record = asRecord(raw);
|
|
21850
|
-
if (!record) return;
|
|
21851
|
-
const command = asStringValue(record["command"]);
|
|
21852
|
-
if (command === void 0) return;
|
|
21853
|
-
return compact({
|
|
21854
|
-
command,
|
|
21855
|
-
cwd: asStringValue(record["cwd"]),
|
|
21856
|
-
env: asEnv(record["env"]),
|
|
21857
|
-
artifactPath: asStringValue(record["artifactPath"])
|
|
21858
|
-
});
|
|
21859
|
-
};
|
|
21860
|
-
const parseCustomCommandProfile = (raw) => {
|
|
21861
|
-
const record = asRecord(raw);
|
|
21862
|
-
if (!record) return;
|
|
21863
|
-
const result = compact({
|
|
21864
|
-
ios: parseCustomCommandSpec(record["ios"]),
|
|
21865
|
-
android: parseCustomCommandSpec(record["android"])
|
|
21866
|
-
});
|
|
21867
|
-
return Object.keys(result).length === 0 ? void 0 : result;
|
|
21868
|
-
};
|
|
21869
|
-
const parseBuildProfile = (raw) => {
|
|
21870
|
-
const record = asRecord(raw);
|
|
21871
|
-
if (!record) return;
|
|
21872
|
-
return compact({
|
|
21873
|
-
extends: asStringValue(record["extends"]),
|
|
21874
|
-
developmentClient: asBooleanValue(record["developmentClient"]),
|
|
21875
|
-
distribution: asEasDistribution(record["distribution"]),
|
|
21876
|
-
channel: asStringValue(record["channel"]),
|
|
21877
|
-
environment: asStringValue(record["environment"]),
|
|
21878
|
-
env: asEnv(record["env"]),
|
|
21879
|
-
ios: parseIosProfile(record["ios"]),
|
|
21880
|
-
android: parseAndroidProfile(record["android"]),
|
|
21881
|
-
credentialsSource: asCredentialsSource(record["credentialsSource"]),
|
|
21882
|
-
autoIncrement: asAutoIncrement(record["autoIncrement"]),
|
|
21883
|
-
withoutCredentials: asBooleanValue(record["withoutCredentials"]),
|
|
21884
|
-
custom: parseCustomCommandProfile(record["custom"])
|
|
21885
|
-
});
|
|
21886
|
-
};
|
|
21887
|
-
/**
|
|
21888
|
-
* Parse an already-decoded JSON object into an {@link EasConfig}. Shared by the
|
|
21889
|
-
* `eas.json` reader and the `better-update.json` build-config reader — both hold
|
|
21890
|
-
* the same `build`/`submit`/`cli` shape, only the source file differs.
|
|
21891
|
-
*/
|
|
21892
|
-
const parseConfigFromRecord = (root) => {
|
|
21893
|
-
const buildRecord = asRecord(root["build"]);
|
|
21894
|
-
if (!buildRecord) return asRecord(root["cli"]) ? { cli: parseCli(root["cli"]) } : {};
|
|
21895
|
-
const profiles = {};
|
|
21896
|
-
for (const [name, value] of Object.entries(buildRecord)) {
|
|
21897
|
-
const profile = parseBuildProfile(value);
|
|
21898
|
-
if (profile) profiles[name] = profile;
|
|
21899
|
-
}
|
|
21900
|
-
const submitRecord = asRecord(root["submit"]);
|
|
21901
|
-
const submit = {};
|
|
21902
|
-
if (submitRecord) for (const [name, value] of Object.entries(submitRecord)) {
|
|
21903
|
-
const profile = parseSubmitProfile(value);
|
|
21904
|
-
if (profile !== void 0) submit[name] = profile;
|
|
21905
|
-
}
|
|
21906
|
-
return {
|
|
21907
|
-
...asRecord(root["cli"]) ? { cli: parseCli(root["cli"]) } : {},
|
|
21908
|
-
build: profiles,
|
|
21909
|
-
...Object.keys(submit).length === 0 ? {} : { submit }
|
|
21910
|
-
};
|
|
21911
|
-
};
|
|
21912
|
-
const parseEasConfig = (text) => Effect.gen(function* () {
|
|
21913
|
-
const root = asRecord(yield* Effect.try({
|
|
21914
|
-
try: () => JSON.parse(text),
|
|
21915
|
-
catch: (cause) => new BuildProfileError({ message: `eas.json is not valid JSON: ${formatCause(cause)}` })
|
|
21916
|
-
}));
|
|
21917
|
-
if (!root) return yield* new BuildProfileError({ message: "eas.json must be a JSON object at the top level." });
|
|
21918
|
-
return parseConfigFromRecord(root);
|
|
21919
|
-
});
|
|
21920
|
-
const parseCli = (raw) => {
|
|
21921
|
-
const record = asRecord(raw);
|
|
21922
|
-
if (!record) return {};
|
|
21923
|
-
return compact({ version: asStringValue(record["version"]) });
|
|
21924
|
-
};
|
|
21925
|
-
const easJsonPath = (projectRoot) => Effect.gen(function* () {
|
|
21926
|
-
return (yield* Path.Path).join(projectRoot, "eas.json");
|
|
21927
|
-
});
|
|
21928
|
-
const readEasJson = (projectRoot) => Effect.gen(function* () {
|
|
21929
|
-
const fs = yield* FileSystem.FileSystem;
|
|
21930
|
-
const filePath = yield* easJsonPath(projectRoot);
|
|
21931
|
-
return yield* parseEasConfig(yield* fs.readFileString(filePath).pipe(Effect.mapError((cause) => 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}` }))));
|
|
21932
|
-
});
|
|
21933
|
-
const mergeCustom = (base, overlay) => {
|
|
21934
|
-
const merged = shallowMerge(base, overlay);
|
|
21935
|
-
return merged === void 0 || Object.keys(merged).length === 0 ? void 0 : merged;
|
|
21936
|
-
};
|
|
21937
|
-
const mergeProfile = (base, overlay) => {
|
|
21938
|
-
const ios = shallowMerge(base.ios, overlay.ios);
|
|
21939
|
-
const android = shallowMerge(base.android, overlay.android);
|
|
21940
|
-
const env = shallowMerge(base.env, overlay.env);
|
|
21941
|
-
const custom = mergeCustom(base.custom, overlay.custom);
|
|
21942
|
-
const developmentClient = overlay.developmentClient ?? base.developmentClient;
|
|
21943
|
-
const distribution = overlay.distribution ?? base.distribution;
|
|
21944
|
-
const channel = overlay.channel ?? base.channel;
|
|
21945
|
-
const environment = overlay.environment ?? base.environment;
|
|
21946
|
-
const credentialsSource = overlay.credentialsSource ?? base.credentialsSource;
|
|
21947
|
-
const autoIncrement = overlay.autoIncrement ?? base.autoIncrement;
|
|
21948
|
-
const withoutCredentials = overlay.withoutCredentials ?? base.withoutCredentials;
|
|
21949
|
-
return compact({
|
|
21950
|
-
extends: overlay.extends,
|
|
21951
|
-
developmentClient,
|
|
21952
|
-
distribution,
|
|
21953
|
-
channel,
|
|
21954
|
-
environment,
|
|
21955
|
-
env,
|
|
21956
|
-
ios,
|
|
21957
|
-
android,
|
|
21958
|
-
credentialsSource,
|
|
21959
|
-
autoIncrement,
|
|
21960
|
-
withoutCredentials,
|
|
21961
|
-
custom
|
|
21962
|
-
});
|
|
21963
|
-
};
|
|
21964
|
-
const resolveEasBuildProfile = (config, profileName, sourceLabel = "eas.json") => Effect.gen(function* () {
|
|
21965
|
-
const profiles = config.build;
|
|
21966
|
-
if (!profiles) return yield* new BuildProfileError({ message: `${sourceLabel} has no "build" section. Add at least one profile.` });
|
|
21967
|
-
return stripExtends((yield* resolveExtendsChain({
|
|
21968
|
-
profiles,
|
|
21969
|
-
profileName,
|
|
21970
|
-
label: "build",
|
|
21971
|
-
maxDepth: MAX_EXTENDS_DEPTH,
|
|
21972
|
-
sourceLabel,
|
|
21973
|
-
makeError: (message) => new BuildProfileError({ message })
|
|
21974
|
-
})).reduce((acc, next, index) => index === 0 ? next : mergeProfile(acc, next), {}));
|
|
21975
|
-
});
|
|
21976
|
-
|
|
21977
|
-
//#endregion
|
|
21978
|
-
//#region src/lib/better-update-build-config.ts
|
|
21979
|
-
/** Label used in profile-resolution error copy when config comes from this file. */
|
|
21980
|
-
const BETTER_UPDATE_SOURCE_LABEL = "better-update.json";
|
|
21981
|
-
/**
|
|
21982
|
-
* Read the `build`/`submit`/`cli` config from `better-update.json`. Returns an
|
|
21983
|
-
* empty config (no `build` key) when the file is absent or carries no build
|
|
21984
|
-
* section. Shares the parser with {@link file://./eas-config.ts}; only the
|
|
21985
|
-
* source file and the error `sourceLabel` differ.
|
|
21986
|
-
*/
|
|
21987
|
-
const readBuildConfig = (projectRoot) => readBetterUpdateConfig(projectRoot).pipe(Effect.map((config) => config === void 0 ? {} : parseConfigFromRecord(config)));
|
|
21988
|
-
/** List available build-profile names declared in `better-update.json`. */
|
|
21989
|
-
const listBuildProfileNames = (projectRoot) => readBuildConfig(projectRoot).pipe(Effect.map((config) => Object.keys(config.build ?? {})));
|
|
21990
|
-
/** Resolve a submit profile from `better-update.json`'s `submit` section. */
|
|
21991
|
-
const readSubmitProfile = (projectRoot, profileName) => Effect.gen(function* () {
|
|
21992
|
-
return yield* resolveEasSubmitProfile((yield* readBuildConfig(projectRoot)).submit, profileName, BETTER_UPDATE_SOURCE_LABEL);
|
|
21993
|
-
});
|
|
21994
|
-
|
|
21995
22122
|
//#endregion
|
|
21996
22123
|
//#region src/lib/build-profile.ts
|
|
21997
22124
|
const deriveIosDistribution = (eas) => {
|
|
@@ -22097,9 +22224,9 @@ const fromGenericProfile = (eas, profileName) => {
|
|
|
22097
22224
|
customCommand: eas.custom
|
|
22098
22225
|
});
|
|
22099
22226
|
};
|
|
22100
|
-
/** Resolve a build profile from `
|
|
22227
|
+
/** Resolve a build profile from `eas.json`'s `build` section (all build systems). */
|
|
22101
22228
|
const readBuildProfile = (projectRoot, profileName) => Effect.gen(function* () {
|
|
22102
|
-
return fromGenericProfile(yield* resolveEasBuildProfile(yield*
|
|
22229
|
+
return fromGenericProfile(yield* resolveEasBuildProfile(yield* readEasJson(projectRoot), profileName, "eas.json"), profileName);
|
|
22103
22230
|
});
|
|
22104
22231
|
const readRuntimeVersionMeta = (config, platform) => ({
|
|
22105
22232
|
platform,
|
|
@@ -22156,7 +22283,7 @@ const PROJECT_TYPES = [
|
|
|
22156
22283
|
"native",
|
|
22157
22284
|
"custom"
|
|
22158
22285
|
];
|
|
22159
|
-
/** Narrow an arbitrary `projectType` override (e.g. from
|
|
22286
|
+
/** Narrow an arbitrary `projectType` override (e.g. from eas.json) to a valid value. */
|
|
22160
22287
|
const asProjectType = (raw) => PROJECT_TYPES.find((type) => type === raw);
|
|
22161
22288
|
const exists = (filePath) => Effect.gen(function* () {
|
|
22162
22289
|
return yield* (yield* FileSystem.FileSystem).exists(filePath).pipe(Effect.orElseSucceed(() => false));
|
|
@@ -22279,12 +22406,14 @@ const exportDecryptedEnvVars = (api, projectId, environment) => Effect.gen(funct
|
|
|
22279
22406
|
});
|
|
22280
22407
|
/**
|
|
22281
22408
|
* Pull + decrypt environment variables flattened into a key/value map for
|
|
22282
|
-
* injection into a build/subprocess.
|
|
22409
|
+
* injection into a build/subprocess. Reports which variables were loaded (names
|
|
22410
|
+
* only — values stay secret) so users can see what the server contributed.
|
|
22283
22411
|
*/
|
|
22284
22412
|
const pullEnvVars = (api, { projectId, environment }) => Effect.gen(function* () {
|
|
22285
22413
|
const validated = coerceEnvironment(environment);
|
|
22286
22414
|
if (!validated) return yield* new EnvExportError({ message: `Invalid environment "${environment}": must be lowercase letters, digits, and hyphens, starting with a letter.` });
|
|
22287
22415
|
const items = yield* exportDecryptedEnvVars(api, projectId, validated);
|
|
22416
|
+
yield* printHuman(items.length === 0 ? `No environment variables found for the "${validated}" environment.` : `Environment variables loaded from the "${validated}" environment: ${items.map((item) => item.key).join(", ")}`);
|
|
22288
22417
|
return Object.fromEntries(items.map((item) => [item.key, item.value]));
|
|
22289
22418
|
});
|
|
22290
22419
|
/**
|
|
@@ -22552,13 +22681,14 @@ const inferPlatforms = (config) => {
|
|
|
22552
22681
|
};
|
|
22553
22682
|
/**
|
|
22554
22683
|
* Resolve a build platform from an explicit flag, or fall back to the Expo
|
|
22555
|
-
* config (`expo.platforms` or the presence of `ios`/`android` sections).
|
|
22556
|
-
*
|
|
22557
|
-
*
|
|
22684
|
+
* config (`expo.platforms` or the presence of `ios`/`android` sections). The
|
|
22685
|
+
* config is loaded lazily so an explicit `--platform` skips evaluating
|
|
22686
|
+
* `app.config.js`/`.ts` entirely. Prompts when the config declares both
|
|
22687
|
+
* platforms; fails when ambiguous and prompts are disallowed.
|
|
22558
22688
|
*/
|
|
22559
|
-
const detectPlatform = (explicit,
|
|
22689
|
+
const detectPlatform = (explicit, loadConfig) => Effect.gen(function* () {
|
|
22560
22690
|
if (explicit !== void 0) return explicit;
|
|
22561
|
-
const candidates = inferPlatforms(
|
|
22691
|
+
const candidates = inferPlatforms(yield* loadConfig);
|
|
22562
22692
|
if (candidates.length === 0) return yield* new BuildProfileError({ message: "Cannot infer build platform. Add an `ios` or `android` section to your Expo config, or pass --platform." });
|
|
22563
22693
|
if (candidates.length === 1) {
|
|
22564
22694
|
const [only] = candidates;
|
|
@@ -22584,7 +22714,7 @@ const detectPlatformGeneric = (explicit, context) => Effect.gen(function* () {
|
|
|
22584
22714
|
const wantsAndroid = context.profile.android !== void 0 || context.profile.customCommand?.android !== void 0;
|
|
22585
22715
|
if (wantsIos && (context.hasIosDir || context.profile.customCommand?.ios !== void 0)) candidates.push("ios");
|
|
22586
22716
|
if (wantsAndroid && (context.hasAndroidDir || context.profile.customCommand?.android !== void 0)) candidates.push("android");
|
|
22587
|
-
if (candidates.length === 0) return yield* new BuildProfileError({ message: "Cannot infer build platform. Add an `ios` or `android` section to the build profile in
|
|
22717
|
+
if (candidates.length === 0) return yield* new BuildProfileError({ message: "Cannot infer build platform. Add an `ios` or `android` section to the build profile in eas.json, or pass --platform." });
|
|
22588
22718
|
const [only] = candidates;
|
|
22589
22719
|
if (candidates.length === 1 && only !== void 0) return only;
|
|
22590
22720
|
if (!(yield* InteractiveMode).allow) return yield* new BuildProfileError({ message: `Multiple platforms available (${candidates.join(", ")}). Pass --platform explicitly when running non-interactively.` });
|
|
@@ -23466,13 +23596,13 @@ const EMPTY = {
|
|
|
23466
23596
|
buildNumber: void 0,
|
|
23467
23597
|
rawRuntimeVersion: void 0
|
|
23468
23598
|
};
|
|
23469
|
-
const warnIfMismatch = (label, override, native) => override !== void 0 && native !== void 0 && override !== native ? printWarn(`${label} override "${override}" differs from the native value "${native}". The
|
|
23599
|
+
const warnIfMismatch = (label, override, native) => override !== void 0 && native !== void 0 && override !== native ? printWarn(`${label} override "${override}" differs from the native value "${native}". The eas.json value will be used for build metadata.`) : Effect.void;
|
|
23470
23600
|
const resolveAndroidMeta = (projectRoot, profile) => Effect.gen(function* () {
|
|
23471
23601
|
const gradle = yield* readGradleConfig(path.join(projectRoot, "android"));
|
|
23472
23602
|
const override = profile.android?.metaOverride;
|
|
23473
23603
|
yield* warnIfMismatch("android.applicationId", override?.applicationId, gradle?.applicationId);
|
|
23474
23604
|
const androidPackage = override?.applicationId ?? gradle?.applicationId;
|
|
23475
|
-
if (androidPackage === void 0) return yield* new BuildProfileError({ message: "Could not determine the Android applicationId. Set android.applicationId under this build profile in
|
|
23605
|
+
if (androidPackage === void 0) return yield* new BuildProfileError({ message: "Could not determine the Android applicationId. Set android.applicationId under this build profile in eas.json, or ensure android/app/build.gradle defines it." });
|
|
23476
23606
|
const versionCode = override?.versionCode ?? (gradle?.versionCode === void 0 ? void 0 : String(gradle.versionCode));
|
|
23477
23607
|
return {
|
|
23478
23608
|
...EMPTY,
|
|
@@ -23490,7 +23620,7 @@ const resolveIosMeta = (projectRoot, profile) => Effect.gen(function* () {
|
|
|
23490
23620
|
const override = profile.ios?.metaOverride;
|
|
23491
23621
|
yield* warnIfMismatch("ios.bundleIdentifier", override?.bundleIdentifier, native.bundleId);
|
|
23492
23622
|
const bundleId = override?.bundleIdentifier ?? native.bundleId;
|
|
23493
|
-
if (bundleId === void 0) return yield* new BuildProfileError({ message: "Could not determine the iOS bundle identifier. Set ios.bundleIdentifier under this build profile in
|
|
23623
|
+
if (bundleId === void 0) return yield* new BuildProfileError({ message: "Could not determine the iOS bundle identifier. Set ios.bundleIdentifier under this build profile in eas.json, or ensure the Xcode project defines PRODUCT_BUNDLE_IDENTIFIER for the build configuration." });
|
|
23494
23624
|
return {
|
|
23495
23625
|
...EMPTY,
|
|
23496
23626
|
bundleId,
|
|
@@ -23530,6 +23660,27 @@ const resolveAppMeta = (params) => {
|
|
|
23530
23660
|
return params.platform === "ios" ? resolveIosMeta(params.projectRoot, params.profile) : resolveAndroidMeta(params.projectRoot, params.profile);
|
|
23531
23661
|
};
|
|
23532
23662
|
|
|
23663
|
+
//#endregion
|
|
23664
|
+
//#region src/application/resolve-update-channel.ts
|
|
23665
|
+
/**
|
|
23666
|
+
* EAS parity: bake the profile's channel into the native app as the
|
|
23667
|
+
* expo-channel-name request header during prebuild. Only the managed ("expo")
|
|
23668
|
+
* strategy prebuilds, and only apps shipping expo-updates read the header — a
|
|
23669
|
+
* binary built without it silently falls back to the server's default channel,
|
|
23670
|
+
* so a missing `channel` on an updates-enabled profile fails fast instead.
|
|
23671
|
+
* Returns the channel to inject, or `undefined` to skip injection.
|
|
23672
|
+
*/
|
|
23673
|
+
const resolveUpdateChannel = (params) => Effect.gen(function* () {
|
|
23674
|
+
const { userCwd, platform, profile, projectType } = params;
|
|
23675
|
+
if ((platform === "ios" ? resolveIosStrategy(profile, projectType) : resolveAndroidStrategy(profile, projectType)) !== "expo") return;
|
|
23676
|
+
if (!(yield* isExpoUpdatesInstalled(userCwd))) {
|
|
23677
|
+
yield* printHuman("expo-updates is not installed — skipping update-channel injection.");
|
|
23678
|
+
return;
|
|
23679
|
+
}
|
|
23680
|
+
if (profile.channel === void 0) return yield* new BuildProfileError({ message: `Build profile "${profile.name}" has no "channel". The channel is baked into the binary as the expo-channel-name update request header; add e.g. "channel": "production" to the profile.` });
|
|
23681
|
+
return profile.channel;
|
|
23682
|
+
});
|
|
23683
|
+
|
|
23533
23684
|
//#endregion
|
|
23534
23685
|
//#region src/application/build-workflow.ts
|
|
23535
23686
|
const runIosPlatformBuild = (input) => Effect.gen(function* () {
|
|
@@ -23559,6 +23710,7 @@ const runIosPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
23559
23710
|
strategy,
|
|
23560
23711
|
rawOutput: options.rawOutput,
|
|
23561
23712
|
freezeCredentials: options.freezeCredentials ?? false,
|
|
23713
|
+
updateChannel: input.updateChannel,
|
|
23562
23714
|
...compact({ customCommand: profile.customCommand?.ios })
|
|
23563
23715
|
}),
|
|
23564
23716
|
target: isSimulator ? {
|
|
@@ -23602,6 +23754,7 @@ const runAndroidPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
23602
23754
|
profileName: profile.name,
|
|
23603
23755
|
skipCredentials,
|
|
23604
23756
|
strategy,
|
|
23757
|
+
updateChannel: input.updateChannel,
|
|
23605
23758
|
...compact({ customCommand: profile.customCommand?.android })
|
|
23606
23759
|
}),
|
|
23607
23760
|
target: androidProfile.format === "aab" ? {
|
|
@@ -23658,7 +23811,7 @@ const resolveProfileName = (projectRoot, requested) => Effect.gen(function* () {
|
|
|
23658
23811
|
const available = yield* listBuildProfileNames(projectRoot);
|
|
23659
23812
|
if (available.includes(requested)) return requested;
|
|
23660
23813
|
if (!(yield* InteractiveMode).allow || available.length === 0) return requested;
|
|
23661
|
-
yield* printHuman(`Build profile "${requested}" not found in
|
|
23814
|
+
yield* printHuman(`Build profile "${requested}" not found in eas.json.`);
|
|
23662
23815
|
return yield* promptSelect("Pick a build profile:", available.map((name) => ({
|
|
23663
23816
|
value: name,
|
|
23664
23817
|
label: name
|
|
@@ -23674,17 +23827,12 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23674
23827
|
});
|
|
23675
23828
|
const projectType = yield* detectProjectType({
|
|
23676
23829
|
projectRoot: userCwd,
|
|
23677
|
-
override: asProjectType(
|
|
23830
|
+
override: asProjectType(yield* readEasProjectType(userCwd))
|
|
23678
23831
|
});
|
|
23679
23832
|
const isExpo = projectType === "expo";
|
|
23680
23833
|
const projectId = yield* readProjectId;
|
|
23681
23834
|
const profile = yield* readBuildProfile(userCwd, yield* resolveProfileName(userCwd, options.profileName));
|
|
23682
23835
|
if (profile.developmentClient === true) yield* warnIfDevClientMissing(userCwd);
|
|
23683
|
-
const platform = isExpo ? yield* detectPlatform(options.platform, yield* readExpoConfig(userCwd)) : yield* detectPlatformGeneric(options.platform, {
|
|
23684
|
-
profile,
|
|
23685
|
-
hasAndroidDir: yield* dirExists(userCwd, "android"),
|
|
23686
|
-
hasIosDir: yield* dirExists(userCwd, "ios")
|
|
23687
|
-
});
|
|
23688
23836
|
const envVars = {
|
|
23689
23837
|
...yield* pullEnvVars(api, {
|
|
23690
23838
|
projectId,
|
|
@@ -23692,6 +23840,17 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23692
23840
|
}),
|
|
23693
23841
|
...profile.env
|
|
23694
23842
|
};
|
|
23843
|
+
const platform = isExpo ? yield* detectPlatform(options.platform, readExpoConfig(userCwd, envVars)) : yield* detectPlatformGeneric(options.platform, {
|
|
23844
|
+
profile,
|
|
23845
|
+
hasAndroidDir: yield* dirExists(userCwd, "android"),
|
|
23846
|
+
hasIosDir: yield* dirExists(userCwd, "ios")
|
|
23847
|
+
});
|
|
23848
|
+
const updateChannel = yield* resolveUpdateChannel({
|
|
23849
|
+
userCwd,
|
|
23850
|
+
platform,
|
|
23851
|
+
profile,
|
|
23852
|
+
projectType
|
|
23853
|
+
});
|
|
23695
23854
|
const { appMeta, runtimeVersion } = isExpo ? yield* resolveExpoBuildMeta({
|
|
23696
23855
|
userCwd,
|
|
23697
23856
|
platform,
|
|
@@ -23714,7 +23873,8 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23714
23873
|
envVars,
|
|
23715
23874
|
projectType
|
|
23716
23875
|
});
|
|
23717
|
-
|
|
23876
|
+
const buildDetails = [...runtimeVersion === void 0 ? [] : [`runtimeVersion=${runtimeVersion}`], ...updateChannel === void 0 ? [] : [`channel=${updateChannel}`]];
|
|
23877
|
+
yield* printHuman(`Building ${platform} artifact for profile "${profile.name}"${buildDetails.length === 0 ? "" : ` (${buildDetails.join(", ")})`}`);
|
|
23718
23878
|
const { build, target, bundleId } = yield* runPlatformBuild({
|
|
23719
23879
|
api,
|
|
23720
23880
|
options,
|
|
@@ -23725,7 +23885,8 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23725
23885
|
envVars,
|
|
23726
23886
|
projectId,
|
|
23727
23887
|
projectRoot: staging.projectRoot,
|
|
23728
|
-
tempDir
|
|
23888
|
+
tempDir,
|
|
23889
|
+
updateChannel
|
|
23729
23890
|
});
|
|
23730
23891
|
yield* printHuman(`Artifact produced: ${build.artifactPath}`);
|
|
23731
23892
|
let exportedArtifactPath = void 0;
|
|
@@ -24746,7 +24907,7 @@ const runUploadWorkflow = (options) => Effect.gen(function* () {
|
|
|
24746
24907
|
if (!(yield* (yield* FileSystem.FileSystem).exists(options.artifactPath).pipe(Effect.orElseSucceed(() => false)))) return yield* new ArtifactNotFoundError({ message: `Artifact not found at ${options.artifactPath}.` });
|
|
24747
24908
|
const projectType = yield* detectProjectType({
|
|
24748
24909
|
projectRoot,
|
|
24749
|
-
override: asProjectType(
|
|
24910
|
+
override: asProjectType(yield* readEasProjectType(projectRoot))
|
|
24750
24911
|
});
|
|
24751
24912
|
const projectId = yield* readProjectId;
|
|
24752
24913
|
const profile = yield* readBuildProfile(projectRoot, options.profileName);
|
|
@@ -26842,9 +27003,14 @@ const listCommand$6 = defineCommand({
|
|
|
26842
27003
|
},
|
|
26843
27004
|
run: async () => runEffect(Effect.gen(function* () {
|
|
26844
27005
|
const api = yield* apiClient;
|
|
26845
|
-
const [{ recipients, vaultVersion }, { items }] = yield* Effect.all([
|
|
27006
|
+
const [{ recipients, vaultVersion }, { items }, vault] = yield* Effect.all([
|
|
27007
|
+
api.orgVault.listWraps(),
|
|
27008
|
+
api.userEncryptionKeys.list(),
|
|
27009
|
+
api.orgVault.get()
|
|
27010
|
+
]);
|
|
26846
27011
|
const byId = new Map(items.map((key) => [key.id, key]));
|
|
26847
27012
|
yield* printHuman(`Vault version ${vaultVersion}`);
|
|
27013
|
+
if (vault.rotationPending) yield* printHuman(`⚠ Rotation pending — a recipient was removed (${vault.rotationPendingReason ?? "vault access revoked"}). Credential downloads are blocked until you run \`credentials access rotate\`.`);
|
|
26848
27014
|
yield* printHumanList([
|
|
26849
27015
|
"Key ID",
|
|
26850
27016
|
"Kind",
|
|
@@ -26861,6 +27027,7 @@ const listCommand$6 = defineCommand({
|
|
|
26861
27027
|
}), "No recipients hold the vault key yet.");
|
|
26862
27028
|
return {
|
|
26863
27029
|
vaultVersion,
|
|
27030
|
+
rotationPending: vault.rotationPending,
|
|
26864
27031
|
recipients: recipients.map((recipient) => toRecipientView(recipient.userEncryptionKeyId, byId.get(recipient.userEncryptionKeyId)))
|
|
26865
27032
|
};
|
|
26866
27033
|
}), { json: "value" })
|
|
@@ -28530,16 +28697,54 @@ const revokeCommand = defineCommand({
|
|
|
28530
28697
|
}
|
|
28531
28698
|
});
|
|
28532
28699
|
|
|
28700
|
+
//#endregion
|
|
28701
|
+
//#region src/lib/duration.ts
|
|
28702
|
+
/**
|
|
28703
|
+
* Parse a human-readable duration flag ("90", "45m", "2h", "1h30m") into
|
|
28704
|
+
* milliseconds. A bare number means minutes. Returns `undefined` for anything
|
|
28705
|
+
* unparseable or non-positive — range policy stays with the caller.
|
|
28706
|
+
*/
|
|
28707
|
+
const parseDurationMs = (input) => {
|
|
28708
|
+
const groups = /^(?:(?<hours>\d+)h)?(?:(?<minutes>\d+)m?)?$/u.exec(input.trim().toLowerCase())?.groups;
|
|
28709
|
+
if (!groups || groups["hours"] === void 0 && groups["minutes"] === void 0) return;
|
|
28710
|
+
const hours = Number(groups["hours"] ?? 0);
|
|
28711
|
+
const minutes = Number(groups["minutes"] ?? 0);
|
|
28712
|
+
const ms = (hours * 60 + minutes) * 6e4;
|
|
28713
|
+
return ms > 0 ? ms : void 0;
|
|
28714
|
+
};
|
|
28715
|
+
/**
|
|
28716
|
+
* Approximate human form of a duration, rounded up to whole minutes so
|
|
28717
|
+
* "<1 min remaining" still reads as 1 — "45 min", "2 h", "1 h 30 min".
|
|
28718
|
+
*/
|
|
28719
|
+
const formatDurationApprox = (ms) => {
|
|
28720
|
+
const totalMinutes = Math.max(1, Math.ceil(ms / 6e4));
|
|
28721
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
28722
|
+
const minutes = totalMinutes % 60;
|
|
28723
|
+
if (hours === 0) return `${minutes} min`;
|
|
28724
|
+
return minutes === 0 ? `${hours} h` : `${hours} h ${minutes} min`;
|
|
28725
|
+
};
|
|
28726
|
+
|
|
28533
28727
|
//#endregion
|
|
28534
28728
|
//#region src/commands/credentials/session.ts
|
|
28535
|
-
/**
|
|
28536
|
-
const
|
|
28729
|
+
/** Parse + bound the `--duration` flag; `undefined` (flag absent) keeps the 15-minute default. */
|
|
28730
|
+
const resolveUnlockTtlMs = (flag) => Effect.gen(function* () {
|
|
28731
|
+
if (flag === void 0) return;
|
|
28732
|
+
const ms = parseDurationMs(flag);
|
|
28733
|
+
if (ms === void 0) return yield* new InvalidArgumentError({ message: `Could not parse --duration "${flag}" — use minutes ("90") or h/m units ("45m", "2h", "1h30m").` });
|
|
28734
|
+
if (ms < 6e4 || ms > 864e5) return yield* new InvalidArgumentError({ message: `--duration must be between ${formatDurationApprox(VAULT_CACHE_TTL_MIN_MS)} and ${formatDurationApprox(VAULT_CACHE_TTL_MAX_MS)}, got "${flag}".` });
|
|
28735
|
+
return ms;
|
|
28736
|
+
});
|
|
28537
28737
|
const unlockCommand = defineCommand({
|
|
28538
28738
|
meta: {
|
|
28539
28739
|
name: "unlock",
|
|
28540
28740
|
description: "Unlock the credential vault and cache the key in your OS keychain, so later commands don't re-prompt"
|
|
28541
28741
|
},
|
|
28542
|
-
|
|
28742
|
+
args: { duration: {
|
|
28743
|
+
type: "string",
|
|
28744
|
+
description: "How long to stay unlocked — minutes (\"90\") or h/m units (\"45m\", \"2h\", \"1h30m\"); default 15m, max 24h"
|
|
28745
|
+
} },
|
|
28746
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
28747
|
+
const cacheTtlMs = yield* resolveUnlockTtlMs(args.duration);
|
|
28543
28748
|
const recipient = yield* activeRecipient;
|
|
28544
28749
|
if (recipient.source !== "file") {
|
|
28545
28750
|
yield* printHuman("Active identity is the BETTER_UPDATE_IDENTITY (CI) key — it has no passphrase and isn't cached.");
|
|
@@ -28548,9 +28753,9 @@ const unlockCommand = defineCommand({
|
|
|
28548
28753
|
const api = yield* apiClient;
|
|
28549
28754
|
const cache = yield* VaultCache;
|
|
28550
28755
|
yield* cache.clear(recipient.publicKey);
|
|
28551
|
-
yield* unlockVaultKeyInteractive(api);
|
|
28756
|
+
yield* unlockVaultKeyInteractive(api, { cacheTtlMs });
|
|
28552
28757
|
const cached = yield* cache.get(recipient.publicKey);
|
|
28553
|
-
yield* printHuman(`Vault unlocked${cached === void 0 ? " (no OS keychain available — commands will keep prompting)" : ` for ~${
|
|
28758
|
+
yield* printHuman(`Vault unlocked${cached === void 0 ? " (no OS keychain available — commands will keep prompting)" : ` for ~${formatDurationApprox(cached.remainingMs)}; run \`better-update credentials lock\` to clear it`}.`);
|
|
28554
28759
|
}))
|
|
28555
28760
|
});
|
|
28556
28761
|
const lockCommand = defineCommand({
|
|
@@ -28576,7 +28781,7 @@ const statusCommand$1 = defineCommand({
|
|
|
28576
28781
|
return;
|
|
28577
28782
|
}
|
|
28578
28783
|
const cached = yield* (yield* VaultCache).get(recipient.publicKey);
|
|
28579
|
-
yield* printHuman(cached === void 0 ? "Locked — the next credential command will prompt for your passphrase." : `Unlocked — cached vault key expires in ~${
|
|
28784
|
+
yield* printHuman(cached === void 0 ? "Locked — the next credential command will prompt for your passphrase." : `Unlocked — cached vault key expires in ~${formatDurationApprox(cached.remainingMs)}.`);
|
|
28580
28785
|
}))
|
|
28581
28786
|
});
|
|
28582
28787
|
|
|
@@ -30027,21 +30232,22 @@ const checkProjectLink = Effect.gen(function* () {
|
|
|
30027
30232
|
if (fromEnv !== void 0 && fromEnv.length > 0) return pass("project-linked", "Project linked", `projectId=${fromEnv} (via ${BETTER_UPDATE_PROJECT_ID_ENV})`);
|
|
30028
30233
|
const resolved = yield* readProjectId.pipe(Effect.either);
|
|
30029
30234
|
if (resolved._tag === "Left") return warn("project-linked", "Project linked", resolved.left.message);
|
|
30030
|
-
const source = (yield*
|
|
30235
|
+
const source = (yield* readEasLinkedProjectId(root)) === void 0 ? "Expo config" : "eas.json";
|
|
30031
30236
|
return pass("project-linked", "Project linked", `projectId=${resolved.right} (via ${source})`);
|
|
30032
30237
|
});
|
|
30033
30238
|
const checkProjectType = Effect.gen(function* () {
|
|
30034
30239
|
const root = yield* (yield* CliRuntime).cwd;
|
|
30035
|
-
const override = asProjectType(
|
|
30240
|
+
const override = asProjectType(yield* readEasProjectType(root));
|
|
30036
30241
|
return pass("project-type", "Project type", `${yield* detectProjectType({
|
|
30037
30242
|
projectRoot: root,
|
|
30038
30243
|
override
|
|
30039
|
-
})} (${override === void 0 ? "auto-detected" : "
|
|
30244
|
+
})} (${override === void 0 ? "auto-detected" : "eas.json override"})`);
|
|
30040
30245
|
});
|
|
30041
30246
|
const checkBuildConfig = Effect.gen(function* () {
|
|
30042
|
-
const names = yield* listBuildProfileNames(yield* (yield* CliRuntime).cwd);
|
|
30043
|
-
if (names.
|
|
30044
|
-
return
|
|
30247
|
+
const names = yield* listBuildProfileNames(yield* (yield* CliRuntime).cwd).pipe(Effect.either);
|
|
30248
|
+
if (names._tag === "Left") return warn("build-config", "Build config", names.left.message);
|
|
30249
|
+
if (names.right.length === 0) return warn("build-config", "Build config", "No build profiles found. Add a \"build\" section to eas.json.");
|
|
30250
|
+
return pass("build-config", "Build config", `${names.right.length} profile(s) defined`);
|
|
30045
30251
|
});
|
|
30046
30252
|
const runChecks = Effect.gen(function* () {
|
|
30047
30253
|
const xcode = (yield* CliRuntime).platform === "darwin" ? [yield* checkCommand("xcode", "Xcode CLI tools", "xcode-select", ["-p"])] : [];
|
|
@@ -31556,7 +31762,8 @@ const resolveNameAndSlug = (args, projectRoot, expoConfig) => Effect.gen(functio
|
|
|
31556
31762
|
});
|
|
31557
31763
|
/**
|
|
31558
31764
|
* Persist the resolved project id: into the Expo config (`extra.betterUpdate`)
|
|
31559
|
-
* when an Expo project is present, otherwise
|
|
31765
|
+
* when an Expo project is present, otherwise as `eas.json`'s top-level
|
|
31766
|
+
* `projectId` extension key.
|
|
31560
31767
|
*/
|
|
31561
31768
|
const persistLink = (projectRoot, projectId, hasExpoConfig) => Effect.gen(function* () {
|
|
31562
31769
|
if (hasExpoConfig) {
|
|
@@ -31568,7 +31775,7 @@ const persistLink = (projectRoot, projectId, hasExpoConfig) => Effect.gen(functi
|
|
|
31568
31775
|
...compact({ configPath: writeResult.configPath })
|
|
31569
31776
|
};
|
|
31570
31777
|
}
|
|
31571
|
-
const filePath = yield*
|
|
31778
|
+
const filePath = yield* writeEasJsonPatch(projectRoot, { projectId });
|
|
31572
31779
|
yield* printHuman(`Project linked successfully. ID saved to ${path.relative(projectRoot, filePath)}.`);
|
|
31573
31780
|
return {
|
|
31574
31781
|
projectId,
|
|
@@ -31677,45 +31884,6 @@ const logoutCommand = defineCommand({
|
|
|
31677
31884
|
}), { json: "value" })
|
|
31678
31885
|
});
|
|
31679
31886
|
|
|
31680
|
-
//#endregion
|
|
31681
|
-
//#region src/commands/migrate-config.ts
|
|
31682
|
-
const migrateConfigCommand = defineCommand({
|
|
31683
|
-
meta: {
|
|
31684
|
-
name: "migrate-config",
|
|
31685
|
-
description: "Migrate `build`/`submit` profiles from a legacy eas.json into better-update.json"
|
|
31686
|
-
},
|
|
31687
|
-
args: { yes: {
|
|
31688
|
-
type: "boolean",
|
|
31689
|
-
description: "Skip the confirmation prompt"
|
|
31690
|
-
} },
|
|
31691
|
-
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31692
|
-
const runtime = yield* CliRuntime;
|
|
31693
|
-
const fs = yield* FileSystem.FileSystem;
|
|
31694
|
-
const root = yield* runtime.cwd;
|
|
31695
|
-
const easPath = path.join(root, "eas.json");
|
|
31696
|
-
if (!(yield* fs.exists(easPath).pipe(Effect.orElseSucceed(() => false)))) return yield* new InvalidArgumentError({ message: `No eas.json found at ${root}.` });
|
|
31697
|
-
const config = yield* readEasJson(root);
|
|
31698
|
-
const patch = compact({
|
|
31699
|
-
build: config.build,
|
|
31700
|
-
submit: config.submit,
|
|
31701
|
-
cli: config.cli
|
|
31702
|
-
});
|
|
31703
|
-
if (Object.keys(patch).length === 0) {
|
|
31704
|
-
yield* printHuman("eas.json has no build/submit/cli sections — nothing to migrate.");
|
|
31705
|
-
return;
|
|
31706
|
-
}
|
|
31707
|
-
const existingBuild = (yield* readBetterUpdateConfig(root))?.["build"];
|
|
31708
|
-
if (typeof existingBuild === "object" && existingBuild !== null && config.build !== void 0 && !args.yes) {
|
|
31709
|
-
if (!(yield* promptConfirm("better-update.json already has a build section — overwrite it from eas.json?", { initialValue: false }))) {
|
|
31710
|
-
yield* printHuman("Cancelled.");
|
|
31711
|
-
return;
|
|
31712
|
-
}
|
|
31713
|
-
}
|
|
31714
|
-
yield* writeBetterUpdateConfig(root, patch);
|
|
31715
|
-
yield* printHuman("Merged eas.json build/submit into better-update.json. You can now delete eas.json.");
|
|
31716
|
-
}))
|
|
31717
|
-
});
|
|
31718
|
-
|
|
31719
31887
|
//#endregion
|
|
31720
31888
|
//#region src/commands/open.ts
|
|
31721
31889
|
const RESOURCE_PATHS = {
|
|
@@ -35415,7 +35583,6 @@ const commandRegistry = {
|
|
|
35415
35583
|
devices: devicesCommand,
|
|
35416
35584
|
webhooks: webhooksCommand,
|
|
35417
35585
|
autocomplete: autocompleteCommand,
|
|
35418
|
-
"migrate-config": migrateConfigCommand,
|
|
35419
35586
|
apple: appleCommand,
|
|
35420
35587
|
submit: submitCommand
|
|
35421
35588
|
};
|