@better-update/cli 0.34.0 → 0.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +653 -500
- 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 plistMod 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.1";
|
|
38
39
|
|
|
39
40
|
//#endregion
|
|
40
41
|
//#region src/lib/interactive-mode.ts
|
|
@@ -3471,8 +3472,11 @@ const UpdateAssetUploaderLive = Layer.effect(UpdateAssetUploader, Effect.gen(fun
|
|
|
3471
3472
|
* private key — so the blast radius of a leaked keychain entry is one vault
|
|
3472
3473
|
* version's credentials, and only until the TTL lapses.
|
|
3473
3474
|
*/
|
|
3474
|
-
/**
|
|
3475
|
+
/** Default for how long a cached vault key stays valid before a fresh passphrase is required. */
|
|
3475
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;
|
|
3476
3480
|
/** Keychain service name; the account is the recipient's public key. */
|
|
3477
3481
|
const KEYCHAIN_SERVICE = "better-update-vault";
|
|
3478
3482
|
const isCachedVaultEntry = (value) => isRecord$1(value) && typeof value["vaultKey"] === "string" && typeof value["vaultVersion"] === "number" && typeof value["keyId"] === "string" && typeof value["exp"] === "number";
|
|
@@ -3524,9 +3528,9 @@ const VaultCacheLive = Layer.effect(VaultCache, Effect.gen(function* () {
|
|
|
3524
3528
|
}
|
|
3525
3529
|
return decoded;
|
|
3526
3530
|
}),
|
|
3527
|
-
set: (publicKey, vault) => Effect.gen(function* () {
|
|
3531
|
+
set: (publicKey, vault, ttlMs) => Effect.gen(function* () {
|
|
3528
3532
|
if (yield* cacheDisabled) return;
|
|
3529
|
-
yield* writeRaw(publicKey, encodeCacheEntry(vault, yield* Clock.currentTimeMillis));
|
|
3533
|
+
yield* writeRaw(publicKey, encodeCacheEntry(vault, yield* Clock.currentTimeMillis, ttlMs));
|
|
3530
3534
|
}),
|
|
3531
3535
|
clear: (publicKey) => deleteRaw(publicKey)
|
|
3532
3536
|
};
|
|
@@ -4130,53 +4134,401 @@ const printHumanList = (headers, rows, emptyMessage) => Effect.gen(function* ()
|
|
|
4130
4134
|
});
|
|
4131
4135
|
|
|
4132
4136
|
//#endregion
|
|
4133
|
-
//#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
|
+
*/
|
|
4134
4479
|
/**
|
|
4135
4480
|
* Environment variable that overrides project-id resolution. Highest precedence
|
|
4136
4481
|
* so CI and ephemeral checkouts can target a project without writing any file.
|
|
4137
4482
|
*/
|
|
4138
4483
|
const BETTER_UPDATE_PROJECT_ID_ENV = "BETTER_UPDATE_PROJECT_ID";
|
|
4139
4484
|
/**
|
|
4140
|
-
*
|
|
4141
|
-
*
|
|
4142
|
-
*
|
|
4143
|
-
*/
|
|
4144
|
-
const BETTER_UPDATE_CONFIG_FILENAME = "better-update.json";
|
|
4145
|
-
const configPath = (projectRoot) => path.join(projectRoot, BETTER_UPDATE_CONFIG_FILENAME);
|
|
4146
|
-
/**
|
|
4147
|
-
* Read the project-local `better-update.json` if present. Returns `undefined`
|
|
4148
|
-
* when the file is missing or holds invalid JSON — an unlinked project is a
|
|
4149
|
-
* normal state, not an error. Mirrors {@link file://./../services/config-store.ts}'s
|
|
4150
|
-
* 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).
|
|
4151
4488
|
*/
|
|
4152
|
-
const
|
|
4153
|
-
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(() => ""));
|
|
4154
4491
|
if (content.length === 0) return;
|
|
4155
4492
|
return yield* Effect.try(() => JSON.parse(content)).pipe(Effect.map((parsed) => isRecord$1(parsed) ? parsed : void 0), Effect.orElseSucceed(() => void 0));
|
|
4156
4493
|
});
|
|
4157
4494
|
/**
|
|
4158
|
-
*
|
|
4159
|
-
* the file
|
|
4160
|
-
|
|
4161
|
-
const readLinkedProjectId = (projectRoot) => readBetterUpdateConfig(projectRoot).pipe(Effect.map((config) => {
|
|
4162
|
-
const id = config?.["projectId"];
|
|
4163
|
-
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
4164
|
-
}));
|
|
4165
|
-
/**
|
|
4166
|
-
* Merge `patch` into the existing `better-update.json` (creating it if absent)
|
|
4167
|
-
* and write it back, returning the absolute file path. Used by `init` to persist
|
|
4168
|
-
* 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.
|
|
4169
4498
|
*/
|
|
4170
|
-
const
|
|
4499
|
+
const writeEasJsonPatch = (projectRoot, patch) => Effect.gen(function* () {
|
|
4171
4500
|
const fs = yield* FileSystem.FileSystem;
|
|
4172
|
-
const filePath =
|
|
4501
|
+
const filePath = easJsonPath(projectRoot);
|
|
4173
4502
|
const merged = {
|
|
4174
|
-
...yield*
|
|
4503
|
+
...yield* readEasJsonRaw(projectRoot),
|
|
4175
4504
|
...patch
|
|
4176
4505
|
};
|
|
4177
|
-
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)}` })));
|
|
4178
4507
|
return filePath;
|
|
4179
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
|
+
});
|
|
4180
4532
|
|
|
4181
4533
|
//#endregion
|
|
4182
4534
|
//#region ../../node_modules/.bun/structured-headers@2.0.2/node_modules/structured-headers/dist/util.js
|
|
@@ -4341,10 +4693,10 @@ const isExpoConfigInstalled = () => {
|
|
|
4341
4693
|
};
|
|
4342
4694
|
/**
|
|
4343
4695
|
* Build-system-neutral "project not linked" guidance, listing every supported
|
|
4344
|
-
* project-id source (env override,
|
|
4696
|
+
* project-id source (env override, eas.json, Expo config) so the
|
|
4345
4697
|
* message is correct for Expo and non-Expo projects alike.
|
|
4346
4698
|
*/
|
|
4347
|
-
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.`;
|
|
4348
4700
|
const clearDynamicConfigCache = (projectRoot) => {
|
|
4349
4701
|
const { dynamicConfigPath } = loadExpoConfigModule().getConfigFilePaths(projectRoot);
|
|
4350
4702
|
if (dynamicConfigPath) delete __require.cache[dynamicConfigPath];
|
|
@@ -4553,19 +4905,19 @@ const readAppMeta = (config, platform) => Effect.gen(function* () {
|
|
|
4553
4905
|
/**
|
|
4554
4906
|
* Resolve the active better-update project id, build-system-agnostically.
|
|
4555
4907
|
*
|
|
4556
|
-
* Precedence: `BETTER_UPDATE_PROJECT_ID` env > `
|
|
4557
|
-
* `@expo/config` (`extra.betterUpdate.projectId`). The Expo branch is taken
|
|
4558
|
-
* when `@expo/config` is installed, so a non-Expo project (no app.json,
|
|
4559
|
-
* installed) never crashes at module load — it simply falls through to
|
|
4560
|
-
* `ProjectNotLinkedError`. Every `env`/`credentials`/`status` command
|
|
4561
|
-
* 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.
|
|
4562
4914
|
*/
|
|
4563
4915
|
const readProjectId = Effect.gen(function* () {
|
|
4564
4916
|
const runtime = yield* CliRuntime;
|
|
4565
4917
|
const fromEnv = yield* runtime.getEnv(BETTER_UPDATE_PROJECT_ID_ENV);
|
|
4566
4918
|
if (fromEnv !== void 0 && fromEnv.length > 0) return fromEnv;
|
|
4567
4919
|
const projectRoot = yield* runtime.cwd;
|
|
4568
|
-
const fromConfig = yield*
|
|
4920
|
+
const fromConfig = yield* readEasLinkedProjectId(projectRoot);
|
|
4569
4921
|
if (fromConfig !== void 0) return fromConfig;
|
|
4570
4922
|
if (isExpoConfigInstalled()) {
|
|
4571
4923
|
const fromExpo = yield* readExpoConfig(projectRoot).pipe(Effect.flatMap(extractProjectId), Effect.option);
|
|
@@ -4577,7 +4929,7 @@ const readProjectId = Effect.gen(function* () {
|
|
|
4577
4929
|
* Read the Expo config without failing: `Option.some(config)` when an Expo
|
|
4578
4930
|
* project is present and loads, otherwise `Option.none()` (no `@expo/config`
|
|
4579
4931
|
* installed, no config file, or a config that errors). Lets `init` decide
|
|
4580
|
-
* whether to write the link into the Expo config vs `
|
|
4932
|
+
* whether to write the link into the Expo config vs `eas.json`.
|
|
4581
4933
|
*/
|
|
4582
4934
|
const readExpoConfigOptional = (projectRoot) => Effect.suspend(() => {
|
|
4583
4935
|
if (!isExpoConfigInstalled()) return Effect.succeed(Option.none());
|
|
@@ -18212,15 +18564,18 @@ const grantRecipient = (args) => Effect.gen(function* () {
|
|
|
18212
18564
|
* operation: download/build-resolve reads, seal-for-upload + generate writes, and
|
|
18213
18565
|
* rotation. There is no read-only cache: an unlock makes the next write seamless
|
|
18214
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.
|
|
18215
18570
|
*/
|
|
18216
|
-
const unlockVaultKeyInteractive = (api) => Effect.gen(function* () {
|
|
18571
|
+
const unlockVaultKeyInteractive = (api, options) => Effect.gen(function* () {
|
|
18217
18572
|
const recipient = yield* activeRecipient;
|
|
18218
18573
|
if (recipient.source !== "file") return yield* unlockVaultKey(api, void 0);
|
|
18219
18574
|
const cache = yield* VaultCache;
|
|
18220
18575
|
const cached = yield* cache.get(recipient.publicKey);
|
|
18221
18576
|
if (cached !== void 0) return cached.vault;
|
|
18222
18577
|
const vault = yield* unlockVaultKey(api, yield* promptPassword("Passphrase to unlock this device's identity:"));
|
|
18223
|
-
yield* cache.set(recipient.publicKey, vault);
|
|
18578
|
+
yield* cache.set(recipient.publicKey, vault, options?.cacheTtlMs);
|
|
18224
18579
|
return vault;
|
|
18225
18580
|
}).pipe(Effect.provide(VaultCacheLive));
|
|
18226
18581
|
/**
|
|
@@ -19649,6 +20004,131 @@ const loadLocalAndroidCredentials = (options) => Effect.gen(function* () {
|
|
|
19649
20004
|
};
|
|
19650
20005
|
});
|
|
19651
20006
|
|
|
20007
|
+
//#endregion
|
|
20008
|
+
//#region src/lib/plist.ts
|
|
20009
|
+
const plist = typeof plistMod.parse === "function" ? plistMod : plistMod.default;
|
|
20010
|
+
/**
|
|
20011
|
+
* Parse an XML plist string into a typed object.
|
|
20012
|
+
* Throws on malformed XML — callers should wrap in Effect.try.
|
|
20013
|
+
*/
|
|
20014
|
+
const parsePlistXml = (xml) => plist.parse(xml);
|
|
20015
|
+
/**
|
|
20016
|
+
* Serialize an object into XML plist text.
|
|
20017
|
+
*/
|
|
20018
|
+
const buildPlistXml = (value) => plist.build(value);
|
|
20019
|
+
/**
|
|
20020
|
+
* Parse a binary plist buffer into a typed object.
|
|
20021
|
+
* Uses bplist-parser for Apple's binary plist format.
|
|
20022
|
+
*/
|
|
20023
|
+
const parsePlistBinary = (buffer) => {
|
|
20024
|
+
const [result] = __require("bplist-parser").parseBuffer(buffer);
|
|
20025
|
+
return result;
|
|
20026
|
+
};
|
|
20027
|
+
const BPLIST_MAGIC = Buffer.from("bplist00");
|
|
20028
|
+
/**
|
|
20029
|
+
* Auto-detect plist format (binary vs XML) and parse accordingly.
|
|
20030
|
+
*/
|
|
20031
|
+
const parsePlist = (data) => data.subarray(0, 8).equals(BPLIST_MAGIC) ? parsePlistBinary(data) : parsePlistXml(data.toString("utf8"));
|
|
20032
|
+
|
|
20033
|
+
//#endregion
|
|
20034
|
+
//#region src/lib/update-channel-native.ts
|
|
20035
|
+
/**
|
|
20036
|
+
* EAS parity: after `expo prebuild`, bake the build profile's `channel` into
|
|
20037
|
+
* the generated native projects as the `expo-channel-name` request header —
|
|
20038
|
+
* exactly what EAS Build does (`androidSetChannelNativelyAsync` /
|
|
20039
|
+
* `iosSetChannelNativelyAsync` in eas-build). Writing the native files instead
|
|
20040
|
+
* of app.json works for dynamic configs (`app.config.ts`) too, which
|
|
20041
|
+
* `modifyConfigAsync` cannot patch.
|
|
20042
|
+
*/
|
|
20043
|
+
/** AndroidManifest meta-data key expo-updates reads extra request headers from. */
|
|
20044
|
+
const ANDROID_REQUEST_HEADERS_META_KEY = "expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY";
|
|
20045
|
+
/** Expo.plist key holding the same request-header map on iOS. */
|
|
20046
|
+
const IOS_REQUEST_HEADERS_PLIST_KEY = "EXUpdatesRequestHeaders";
|
|
20047
|
+
/** Request header expo-updates sends to select the OTA channel. */
|
|
20048
|
+
const EXPO_CHANNEL_HEADER = "expo-channel-name";
|
|
20049
|
+
const asStringRecord = (value) => {
|
|
20050
|
+
if (!isRecord$1(value)) return {};
|
|
20051
|
+
const out = {};
|
|
20052
|
+
for (const [key, raw] of Object.entries(value)) if (typeof raw === "string") out[key] = raw;
|
|
20053
|
+
return out;
|
|
20054
|
+
};
|
|
20055
|
+
/** Merge the channel header into an existing request-header map (channel wins). */
|
|
20056
|
+
const withChannelHeader = (existing, channel) => ({
|
|
20057
|
+
...asStringRecord(existing),
|
|
20058
|
+
[EXPO_CHANNEL_HEADER]: channel
|
|
20059
|
+
});
|
|
20060
|
+
/**
|
|
20061
|
+
* Whether the app declares `expo-updates` as a (dev)dependency. Channel
|
|
20062
|
+
* injection only makes sense when the updates module ships in the binary —
|
|
20063
|
+
* mirrors eas-build's `isExpoUpdatesInstalledAsync` gate.
|
|
20064
|
+
*/
|
|
20065
|
+
const isExpoUpdatesInstalled = (projectRoot) => Effect.gen(function* () {
|
|
20066
|
+
const parsed = safeJsonParse(yield* (yield* FileSystem.FileSystem).readFileString(path.join(projectRoot, "package.json")).pipe(Effect.orElseSucceed(() => "")));
|
|
20067
|
+
if (!isRecord$1(parsed)) return false;
|
|
20068
|
+
const dependencies = isRecord$1(parsed["dependencies"]) ? parsed["dependencies"] : {};
|
|
20069
|
+
const devDependencies = isRecord$1(parsed["devDependencies"]) ? parsed["devDependencies"] : {};
|
|
20070
|
+
return "expo-updates" in dependencies || "expo-updates" in devDependencies;
|
|
20071
|
+
});
|
|
20072
|
+
/**
|
|
20073
|
+
* Write the channel into the prebuilt `android/` project: merge it into the
|
|
20074
|
+
* request-headers JSON carried by the manifest meta-data entry (creating the
|
|
20075
|
+
* entry when prebuild emitted none).
|
|
20076
|
+
*/
|
|
20077
|
+
const setAndroidUpdateChannel = (params) => Effect.tryPromise({
|
|
20078
|
+
try: async () => {
|
|
20079
|
+
const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(params.projectRoot);
|
|
20080
|
+
const manifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
|
|
20081
|
+
const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(manifest);
|
|
20082
|
+
const existing = AndroidConfig.Manifest.getMainApplicationMetaDataValue(manifest, ANDROID_REQUEST_HEADERS_META_KEY);
|
|
20083
|
+
const headers = withChannelHeader(existing === null ? void 0 : safeJsonParse(existing), params.channel);
|
|
20084
|
+
AndroidConfig.Manifest.addMetaDataItemToMainApplication(mainApplication, ANDROID_REQUEST_HEADERS_META_KEY, JSON.stringify(headers));
|
|
20085
|
+
await AndroidConfig.Manifest.writeAndroidManifestAsync(manifestPath, manifest);
|
|
20086
|
+
},
|
|
20087
|
+
catch: (cause) => new BuildFailedError({
|
|
20088
|
+
step: "set android update channel",
|
|
20089
|
+
exitCode: 1,
|
|
20090
|
+
message: `Failed to write the update channel into AndroidManifest.xml: ${formatCause(cause)}`
|
|
20091
|
+
})
|
|
20092
|
+
});
|
|
20093
|
+
/** Locate `ios/<target>/Supporting/Expo.plist` in a prebuilt iOS project. */
|
|
20094
|
+
const findExpoPlist = (iosDir) => Effect.gen(function* () {
|
|
20095
|
+
const fs = yield* FileSystem.FileSystem;
|
|
20096
|
+
const entries = yield* fs.readDirectory(iosDir).pipe(Effect.orElseSucceed(() => []));
|
|
20097
|
+
for (const entry of entries) {
|
|
20098
|
+
const candidate = path.join(iosDir, entry, "Supporting", "Expo.plist");
|
|
20099
|
+
if (yield* fs.exists(candidate).pipe(Effect.orElseSucceed(() => false))) return candidate;
|
|
20100
|
+
}
|
|
20101
|
+
return yield* new BuildFailedError({
|
|
20102
|
+
step: "set ios update channel",
|
|
20103
|
+
exitCode: 1,
|
|
20104
|
+
message: `No Supporting/Expo.plist found under ${iosDir} after prebuild — is "expo-updates" installed?`
|
|
20105
|
+
});
|
|
20106
|
+
});
|
|
20107
|
+
/**
|
|
20108
|
+
* Write the channel into the prebuilt `ios/` project: merge it into the
|
|
20109
|
+
* `EXUpdatesRequestHeaders` dict of the generated Expo.plist.
|
|
20110
|
+
*/
|
|
20111
|
+
const setIosUpdateChannel = (params) => Effect.gen(function* () {
|
|
20112
|
+
const fs = yield* FileSystem.FileSystem;
|
|
20113
|
+
const plistPath = yield* findExpoPlist(params.iosDir);
|
|
20114
|
+
const failure = (cause) => new BuildFailedError({
|
|
20115
|
+
step: "set ios update channel",
|
|
20116
|
+
exitCode: 1,
|
|
20117
|
+
message: `Failed to write the update channel into ${plistPath}: ${formatCause(cause)}`
|
|
20118
|
+
});
|
|
20119
|
+
const content = yield* fs.readFileString(plistPath).pipe(Effect.mapError(failure));
|
|
20120
|
+
const parsed = yield* Effect.try({
|
|
20121
|
+
try: () => parsePlistXml(content),
|
|
20122
|
+
catch: failure
|
|
20123
|
+
});
|
|
20124
|
+
if (!isRecord$1(parsed)) return yield* failure("Expo.plist is not a plist dictionary.");
|
|
20125
|
+
const rendered = buildPlistXml({
|
|
20126
|
+
...parsed,
|
|
20127
|
+
[IOS_REQUEST_HEADERS_PLIST_KEY]: withChannelHeader(parsed[IOS_REQUEST_HEADERS_PLIST_KEY], params.channel)
|
|
20128
|
+
});
|
|
20129
|
+
yield* fs.writeFileString(plistPath, `${rendered}\n`).pipe(Effect.mapError(failure));
|
|
20130
|
+
});
|
|
20131
|
+
|
|
19652
20132
|
//#endregion
|
|
19653
20133
|
//#region src/lib/pty-runner.ts
|
|
19654
20134
|
const ptyDimensions = () => {
|
|
@@ -19929,7 +20409,7 @@ const runAndroidCustom = (input, commandEnv) => Effect.gen(function* () {
|
|
|
19929
20409
|
if (custom.artifactPath === void 0) return yield* new BuildFailedError({
|
|
19930
20410
|
step: "custom android build",
|
|
19931
20411
|
exitCode: 1,
|
|
19932
|
-
message: "Custom Android build requires \"artifactPath\" (e.g. \"**/*.aab\") in
|
|
20412
|
+
message: "Custom Android build requires \"artifactPath\" (e.g. \"**/*.aab\") in eas.json."
|
|
19933
20413
|
});
|
|
19934
20414
|
const credentials = yield* resolveAndroidCredentials(input);
|
|
19935
20415
|
const credEnv = credentials === void 0 ? {} : {
|
|
@@ -19963,18 +20443,24 @@ const runAndroidCustom = (input, commandEnv) => Effect.gen(function* () {
|
|
|
19963
20443
|
});
|
|
19964
20444
|
const runAndroidBuild = (input) => Effect.gen(function* () {
|
|
19965
20445
|
const commandEnv = yield* (yield* CliRuntime).commandEnvironment(input.envVars);
|
|
19966
|
-
if (input.strategy === "expo")
|
|
19967
|
-
|
|
19968
|
-
|
|
19969
|
-
|
|
19970
|
-
|
|
19971
|
-
|
|
19972
|
-
|
|
19973
|
-
|
|
19974
|
-
|
|
19975
|
-
|
|
19976
|
-
|
|
19977
|
-
|
|
20446
|
+
if (input.strategy === "expo") {
|
|
20447
|
+
yield* runStep({
|
|
20448
|
+
command: "bunx",
|
|
20449
|
+
args: [
|
|
20450
|
+
"expo",
|
|
20451
|
+
"prebuild",
|
|
20452
|
+
"--platform",
|
|
20453
|
+
"android",
|
|
20454
|
+
"--clean"
|
|
20455
|
+
],
|
|
20456
|
+
cwd: input.projectRoot,
|
|
20457
|
+
env: commandEnv
|
|
20458
|
+
}, "expo prebuild android");
|
|
20459
|
+
if (input.updateChannel !== void 0) yield* setAndroidUpdateChannel({
|
|
20460
|
+
projectRoot: input.projectRoot,
|
|
20461
|
+
channel: input.updateChannel
|
|
20462
|
+
});
|
|
20463
|
+
}
|
|
19978
20464
|
return input.strategy === "custom" ? yield* runAndroidCustom(input, commandEnv) : yield* runGradleBuild(input, commandEnv);
|
|
19979
20465
|
});
|
|
19980
20466
|
|
|
@@ -20852,28 +21338,6 @@ const acquireKeychain = ({ tempDir, p12Path, p12Password }) => {
|
|
|
20852
21338
|
})).pipe(Effect.map(({ handle }) => handle));
|
|
20853
21339
|
};
|
|
20854
21340
|
|
|
20855
|
-
//#endregion
|
|
20856
|
-
//#region src/lib/plist.ts
|
|
20857
|
-
const plist = typeof plistMod.parse === "function" ? plistMod : plistMod.default;
|
|
20858
|
-
/**
|
|
20859
|
-
* Parse an XML plist string into a typed object.
|
|
20860
|
-
* Throws on malformed XML — callers should wrap in Effect.try.
|
|
20861
|
-
*/
|
|
20862
|
-
const parsePlistXml = (xml) => plist.parse(xml);
|
|
20863
|
-
/**
|
|
20864
|
-
* Parse a binary plist buffer into a typed object.
|
|
20865
|
-
* Uses bplist-parser for Apple's binary plist format.
|
|
20866
|
-
*/
|
|
20867
|
-
const parsePlistBinary = (buffer) => {
|
|
20868
|
-
const [result] = __require("bplist-parser").parseBuffer(buffer);
|
|
20869
|
-
return result;
|
|
20870
|
-
};
|
|
20871
|
-
const BPLIST_MAGIC = Buffer.from("bplist00");
|
|
20872
|
-
/**
|
|
20873
|
-
* Auto-detect plist format (binary vs XML) and parse accordingly.
|
|
20874
|
-
*/
|
|
20875
|
-
const parsePlist = (data) => data.subarray(0, 8).equals(BPLIST_MAGIC) ? parsePlistBinary(data) : parsePlistXml(data.toString("utf8"));
|
|
20876
|
-
|
|
20877
21341
|
//#endregion
|
|
20878
21342
|
//#region src/lib/ios-provisioning.ts
|
|
20879
21343
|
const getString = (obj, key) => {
|
|
@@ -21157,7 +21621,7 @@ const resolveXcodeContainer = (projectRoot, iosDir, iosProfile) => Effect.gen(fu
|
|
|
21157
21621
|
return yield* new BuildFailedError({
|
|
21158
21622
|
step: "resolve Xcode container",
|
|
21159
21623
|
exitCode: 1,
|
|
21160
|
-
message: `No .xcworkspace or .xcodeproj found under ${iosDir}. Set ios.workspace / ios.project in
|
|
21624
|
+
message: `No .xcworkspace or .xcodeproj found under ${iosDir}. Set ios.workspace / ios.project in eas.json.`
|
|
21161
21625
|
});
|
|
21162
21626
|
});
|
|
21163
21627
|
/**
|
|
@@ -21179,6 +21643,10 @@ const prepareIosNative = (params) => Effect.gen(function* () {
|
|
|
21179
21643
|
cwd: params.projectRoot,
|
|
21180
21644
|
env: params.commandEnv
|
|
21181
21645
|
}, "expo prebuild ios");
|
|
21646
|
+
if (params.updateChannel !== void 0) yield* setIosUpdateChannel({
|
|
21647
|
+
iosDir: params.iosDir,
|
|
21648
|
+
channel: params.updateChannel
|
|
21649
|
+
});
|
|
21182
21650
|
yield* runStep({
|
|
21183
21651
|
command: "pod",
|
|
21184
21652
|
args: ["install"],
|
|
@@ -21228,7 +21696,8 @@ const runIosSimulatorBuild = (input) => Effect.gen(function* () {
|
|
|
21228
21696
|
projectRoot,
|
|
21229
21697
|
iosDir,
|
|
21230
21698
|
iosProfile,
|
|
21231
|
-
commandEnv
|
|
21699
|
+
commandEnv,
|
|
21700
|
+
updateChannel: input.updateChannel
|
|
21232
21701
|
});
|
|
21233
21702
|
const container = yield* resolveXcodeContainer(projectRoot, iosDir, iosProfile);
|
|
21234
21703
|
const scheme = iosProfile.scheme ?? container.schemeBase;
|
|
@@ -21331,7 +21800,8 @@ const runIosDeviceBuild = (input) => Effect.gen(function* () {
|
|
|
21331
21800
|
projectRoot,
|
|
21332
21801
|
iosDir,
|
|
21333
21802
|
iosProfile,
|
|
21334
|
-
commandEnv
|
|
21803
|
+
commandEnv,
|
|
21804
|
+
updateChannel: input.updateChannel
|
|
21335
21805
|
});
|
|
21336
21806
|
const container = yield* resolveXcodeContainer(projectRoot, iosDir, iosProfile);
|
|
21337
21807
|
const scheme = iosProfile.scheme ?? container.schemeBase;
|
|
@@ -21463,7 +21933,7 @@ const runIosCustom = (input) => Effect.gen(function* () {
|
|
|
21463
21933
|
if (custom.artifactPath === void 0) return yield* new BuildFailedError({
|
|
21464
21934
|
step: "custom ios build",
|
|
21465
21935
|
exitCode: 1,
|
|
21466
|
-
message: "Custom iOS build requires \"artifactPath\" (e.g. \"build/*.ipa\") in
|
|
21936
|
+
message: "Custom iOS build requires \"artifactPath\" (e.g. \"build/*.ipa\") in eas.json."
|
|
21467
21937
|
});
|
|
21468
21938
|
const credentials = yield* fetchAllCredentials({
|
|
21469
21939
|
api: input.api,
|
|
@@ -21652,354 +22122,6 @@ const applyAutoIncrement = (input) => Effect.gen(function* () {
|
|
|
21652
22122
|
return bumps;
|
|
21653
22123
|
});
|
|
21654
22124
|
|
|
21655
|
-
//#endregion
|
|
21656
|
-
//#region src/lib/eas-profile-extends.ts
|
|
21657
|
-
const asStringValue = (value) => typeof value === "string" ? value : void 0;
|
|
21658
|
-
const asBooleanValue = (value) => typeof value === "boolean" ? value : void 0;
|
|
21659
|
-
const asNumberValue = (raw) => typeof raw === "number" && Number.isFinite(raw) ? raw : void 0;
|
|
21660
|
-
const shallowMerge = (base, overlay) => {
|
|
21661
|
-
if (!base) return overlay;
|
|
21662
|
-
if (!overlay) return base;
|
|
21663
|
-
return {
|
|
21664
|
-
...base,
|
|
21665
|
-
...overlay
|
|
21666
|
-
};
|
|
21667
|
-
};
|
|
21668
|
-
const stripExtends = (profile) => {
|
|
21669
|
-
if (profile.extends === void 0) return profile;
|
|
21670
|
-
const { extends: _omit, ...rest } = profile;
|
|
21671
|
-
return rest;
|
|
21672
|
-
};
|
|
21673
|
-
const resolveExtendsChain = (params) => Effect.gen(function* () {
|
|
21674
|
-
const { profiles, profileName, label, maxDepth, makeError } = params;
|
|
21675
|
-
const sourceLabel = params.sourceLabel ?? "eas.json";
|
|
21676
|
-
const noun = label === "build" ? "Build" : "Submit";
|
|
21677
|
-
const chain = [];
|
|
21678
|
-
const visited = /* @__PURE__ */ new Set();
|
|
21679
|
-
let current = profileName;
|
|
21680
|
-
let depth = 0;
|
|
21681
|
-
while (current !== void 0) {
|
|
21682
|
-
if (visited.has(current)) return yield* Effect.fail(makeError(`Cycle detected in ${sourceLabel} ${label}.${profileName} extends chain at "${current}".`));
|
|
21683
|
-
visited.add(current);
|
|
21684
|
-
const profile = profiles[current];
|
|
21685
|
-
if (!profile) return yield* Effect.fail(makeError(current === profileName ? `${noun} profile "${profileName}" not found in ${sourceLabel}.` : `${noun} profile "${profileName}" extends missing profile "${current}".`));
|
|
21686
|
-
chain.unshift(profile);
|
|
21687
|
-
current = profile.extends;
|
|
21688
|
-
depth += 1;
|
|
21689
|
-
if (depth > maxDepth) return yield* Effect.fail(makeError(`Too many "extends" levels (max ${String(maxDepth)}) in ${sourceLabel} ${label}.${profileName}.`));
|
|
21690
|
-
}
|
|
21691
|
-
return chain;
|
|
21692
|
-
});
|
|
21693
|
-
|
|
21694
|
-
//#endregion
|
|
21695
|
-
//#region src/lib/eas-submit-config.ts
|
|
21696
|
-
const MAX_SUBMIT_EXTENDS_DEPTH = 10;
|
|
21697
|
-
const asStringArray = (raw) => {
|
|
21698
|
-
if (!Array.isArray(raw)) return;
|
|
21699
|
-
const items = raw.filter((item) => typeof item === "string");
|
|
21700
|
-
return items.length === 0 ? void 0 : items;
|
|
21701
|
-
};
|
|
21702
|
-
const asAndroidReleaseStatus = (raw) => {
|
|
21703
|
-
const value = asStringValue(raw);
|
|
21704
|
-
return value === "completed" || value === "draft" || value === "halted" || value === "inProgress" ? value : void 0;
|
|
21705
|
-
};
|
|
21706
|
-
const parseIosSubmitProfile = (raw) => {
|
|
21707
|
-
const record = asRecord(raw);
|
|
21708
|
-
if (!record) return;
|
|
21709
|
-
return compact({
|
|
21710
|
-
appleId: asStringValue(record["appleId"]),
|
|
21711
|
-
ascAppId: asStringValue(record["ascAppId"]),
|
|
21712
|
-
appleTeamId: asStringValue(record["appleTeamId"]),
|
|
21713
|
-
ascApiKeyPath: asStringValue(record["ascApiKeyPath"]),
|
|
21714
|
-
ascApiKeyId: asStringValue(record["ascApiKeyId"]),
|
|
21715
|
-
ascApiKeyIssuerId: asStringValue(record["ascApiKeyIssuerId"]),
|
|
21716
|
-
sku: asStringValue(record["sku"]),
|
|
21717
|
-
language: asStringValue(record["language"]),
|
|
21718
|
-
companyName: asStringValue(record["companyName"]),
|
|
21719
|
-
appName: asStringValue(record["appName"]),
|
|
21720
|
-
bundleIdentifier: asStringValue(record["bundleIdentifier"]),
|
|
21721
|
-
metadataPath: asStringValue(record["metadataPath"]),
|
|
21722
|
-
groups: asStringArray(record["groups"])
|
|
21723
|
-
});
|
|
21724
|
-
};
|
|
21725
|
-
const parseAndroidSubmitProfile = (raw) => {
|
|
21726
|
-
const record = asRecord(raw);
|
|
21727
|
-
if (!record) return;
|
|
21728
|
-
return compact({
|
|
21729
|
-
serviceAccountKeyPath: asStringValue(record["serviceAccountKeyPath"]),
|
|
21730
|
-
serviceAccountKeyId: asStringValue(record["serviceAccountKeyId"]),
|
|
21731
|
-
track: asStringValue(record["track"]),
|
|
21732
|
-
releaseStatus: asAndroidReleaseStatus(record["releaseStatus"]),
|
|
21733
|
-
changesNotSentForReview: asBooleanValue(record["changesNotSentForReview"]),
|
|
21734
|
-
rollout: asNumberValue(record["rollout"]),
|
|
21735
|
-
applicationId: asStringValue(record["applicationId"])
|
|
21736
|
-
});
|
|
21737
|
-
};
|
|
21738
|
-
const parseSubmitProfile = (raw) => {
|
|
21739
|
-
const record = asRecord(raw);
|
|
21740
|
-
if (!record) return;
|
|
21741
|
-
return compact({
|
|
21742
|
-
extends: asStringValue(record["extends"]),
|
|
21743
|
-
ios: parseIosSubmitProfile(record["ios"]),
|
|
21744
|
-
android: parseAndroidSubmitProfile(record["android"])
|
|
21745
|
-
});
|
|
21746
|
-
};
|
|
21747
|
-
const mergeSubmitProfile = (base, overlay) => {
|
|
21748
|
-
const ios = shallowMerge(base.ios, overlay.ios);
|
|
21749
|
-
const android = shallowMerge(base.android, overlay.android);
|
|
21750
|
-
return compact({
|
|
21751
|
-
extends: overlay.extends,
|
|
21752
|
-
ios,
|
|
21753
|
-
android
|
|
21754
|
-
});
|
|
21755
|
-
};
|
|
21756
|
-
const resolveEasSubmitProfile = (profiles, profileName, sourceLabel = "eas.json") => Effect.gen(function* () {
|
|
21757
|
-
if (!profiles) return yield* new BuildProfileError({ message: `${sourceLabel} has no "submit" section. Add at least one submit profile.` });
|
|
21758
|
-
return stripExtends((yield* resolveExtendsChain({
|
|
21759
|
-
profiles,
|
|
21760
|
-
profileName,
|
|
21761
|
-
label: "submit",
|
|
21762
|
-
maxDepth: MAX_SUBMIT_EXTENDS_DEPTH,
|
|
21763
|
-
sourceLabel,
|
|
21764
|
-
makeError: (message) => new BuildProfileError({ message })
|
|
21765
|
-
})).reduce((acc, next, index) => index === 0 ? next : mergeSubmitProfile(acc, next), {}));
|
|
21766
|
-
});
|
|
21767
|
-
|
|
21768
|
-
//#endregion
|
|
21769
|
-
//#region src/lib/eas-config.ts
|
|
21770
|
-
const MAX_EXTENDS_DEPTH = 10;
|
|
21771
|
-
const asEnv = (value) => {
|
|
21772
|
-
const record = asRecord(value);
|
|
21773
|
-
if (!record) return;
|
|
21774
|
-
const env = {};
|
|
21775
|
-
for (const [key, raw] of Object.entries(record)) if (typeof raw === "string") env[key] = raw;
|
|
21776
|
-
return Object.keys(env).length === 0 ? void 0 : env;
|
|
21777
|
-
};
|
|
21778
|
-
const asIosDistribution = (raw) => {
|
|
21779
|
-
const value = asStringValue(raw);
|
|
21780
|
-
if (value === "app-store" || value === "ad-hoc" || value === "development" || value === "enterprise") return value;
|
|
21781
|
-
};
|
|
21782
|
-
const asEnterpriseProvisioning = (raw) => {
|
|
21783
|
-
const value = asStringValue(raw);
|
|
21784
|
-
return value === "adhoc" || value === "universal" ? value : void 0;
|
|
21785
|
-
};
|
|
21786
|
-
const asAndroidBuildType = (raw) => {
|
|
21787
|
-
const value = asStringValue(raw);
|
|
21788
|
-
return value === "debug" || value === "release" ? value : void 0;
|
|
21789
|
-
};
|
|
21790
|
-
const asAndroidFormat = (raw) => {
|
|
21791
|
-
const value = asStringValue(raw);
|
|
21792
|
-
return value === "apk" || value === "aab" ? value : void 0;
|
|
21793
|
-
};
|
|
21794
|
-
const asAndroidDistribution = (raw) => {
|
|
21795
|
-
const value = asStringValue(raw);
|
|
21796
|
-
return value === "play-store" || value === "direct" ? value : void 0;
|
|
21797
|
-
};
|
|
21798
|
-
const asIosAutoIncrement = (raw) => {
|
|
21799
|
-
if (typeof raw === "boolean") return raw;
|
|
21800
|
-
const value = asStringValue(raw);
|
|
21801
|
-
return value === "buildNumber" || value === "version" ? value : void 0;
|
|
21802
|
-
};
|
|
21803
|
-
const asAndroidAutoIncrement = (raw) => {
|
|
21804
|
-
if (typeof raw === "boolean") return raw;
|
|
21805
|
-
const value = asStringValue(raw);
|
|
21806
|
-
return value === "versionCode" || value === "version" ? value : void 0;
|
|
21807
|
-
};
|
|
21808
|
-
const asAutoIncrement = (raw) => {
|
|
21809
|
-
if (typeof raw === "boolean") return raw;
|
|
21810
|
-
const value = asStringValue(raw);
|
|
21811
|
-
return value === "buildNumber" || value === "versionCode" || value === "version" ? value : void 0;
|
|
21812
|
-
};
|
|
21813
|
-
const asEasDistribution = (raw) => {
|
|
21814
|
-
const value = asStringValue(raw);
|
|
21815
|
-
return value === "internal" || value === "store" ? value : void 0;
|
|
21816
|
-
};
|
|
21817
|
-
const asCredentialsSource = (raw) => {
|
|
21818
|
-
const value = asStringValue(raw);
|
|
21819
|
-
return value === "remote" || value === "local" ? value : void 0;
|
|
21820
|
-
};
|
|
21821
|
-
const parseIosProfile = (raw) => {
|
|
21822
|
-
const record = asRecord(raw);
|
|
21823
|
-
if (!record) return;
|
|
21824
|
-
return compact({
|
|
21825
|
-
distribution: asIosDistribution(record["distribution"]),
|
|
21826
|
-
buildConfiguration: asStringValue(record["buildConfiguration"]),
|
|
21827
|
-
scheme: asStringValue(record["scheme"]),
|
|
21828
|
-
simulator: asBooleanValue(record["simulator"]),
|
|
21829
|
-
enterpriseProvisioning: asEnterpriseProvisioning(record["enterpriseProvisioning"]),
|
|
21830
|
-
autoIncrement: asIosAutoIncrement(record["autoIncrement"]),
|
|
21831
|
-
workspace: asStringValue(record["workspace"]),
|
|
21832
|
-
project: asStringValue(record["project"]),
|
|
21833
|
-
podInstall: asBooleanValue(record["podInstall"]),
|
|
21834
|
-
bundleIdentifier: asStringValue(record["bundleIdentifier"]),
|
|
21835
|
-
version: asStringValue(record["version"]),
|
|
21836
|
-
buildNumber: asStringValue(record["buildNumber"])
|
|
21837
|
-
});
|
|
21838
|
-
};
|
|
21839
|
-
const parseAndroidProfile = (raw) => {
|
|
21840
|
-
const record = asRecord(raw);
|
|
21841
|
-
if (!record) return;
|
|
21842
|
-
return compact({
|
|
21843
|
-
buildType: asAndroidBuildType(record["buildType"]),
|
|
21844
|
-
flavor: asStringValue(record["flavor"]),
|
|
21845
|
-
gradleCommand: asStringValue(record["gradleCommand"]),
|
|
21846
|
-
format: asAndroidFormat(record["format"]),
|
|
21847
|
-
distribution: asAndroidDistribution(record["distribution"]),
|
|
21848
|
-
autoIncrement: asAndroidAutoIncrement(record["autoIncrement"]),
|
|
21849
|
-
module: asStringValue(record["module"]),
|
|
21850
|
-
gradleTask: asStringValue(record["gradleTask"]),
|
|
21851
|
-
applicationId: asStringValue(record["applicationId"]),
|
|
21852
|
-
version: asStringValue(record["version"]),
|
|
21853
|
-
versionCode: asStringValue(record["versionCode"])
|
|
21854
|
-
});
|
|
21855
|
-
};
|
|
21856
|
-
const parseCustomCommandSpec = (raw) => {
|
|
21857
|
-
const record = asRecord(raw);
|
|
21858
|
-
if (!record) return;
|
|
21859
|
-
const command = asStringValue(record["command"]);
|
|
21860
|
-
if (command === void 0) return;
|
|
21861
|
-
return compact({
|
|
21862
|
-
command,
|
|
21863
|
-
cwd: asStringValue(record["cwd"]),
|
|
21864
|
-
env: asEnv(record["env"]),
|
|
21865
|
-
artifactPath: asStringValue(record["artifactPath"])
|
|
21866
|
-
});
|
|
21867
|
-
};
|
|
21868
|
-
const parseCustomCommandProfile = (raw) => {
|
|
21869
|
-
const record = asRecord(raw);
|
|
21870
|
-
if (!record) return;
|
|
21871
|
-
const result = compact({
|
|
21872
|
-
ios: parseCustomCommandSpec(record["ios"]),
|
|
21873
|
-
android: parseCustomCommandSpec(record["android"])
|
|
21874
|
-
});
|
|
21875
|
-
return Object.keys(result).length === 0 ? void 0 : result;
|
|
21876
|
-
};
|
|
21877
|
-
const parseBuildProfile = (raw) => {
|
|
21878
|
-
const record = asRecord(raw);
|
|
21879
|
-
if (!record) return;
|
|
21880
|
-
return compact({
|
|
21881
|
-
extends: asStringValue(record["extends"]),
|
|
21882
|
-
developmentClient: asBooleanValue(record["developmentClient"]),
|
|
21883
|
-
distribution: asEasDistribution(record["distribution"]),
|
|
21884
|
-
channel: asStringValue(record["channel"]),
|
|
21885
|
-
environment: asStringValue(record["environment"]),
|
|
21886
|
-
env: asEnv(record["env"]),
|
|
21887
|
-
ios: parseIosProfile(record["ios"]),
|
|
21888
|
-
android: parseAndroidProfile(record["android"]),
|
|
21889
|
-
credentialsSource: asCredentialsSource(record["credentialsSource"]),
|
|
21890
|
-
autoIncrement: asAutoIncrement(record["autoIncrement"]),
|
|
21891
|
-
withoutCredentials: asBooleanValue(record["withoutCredentials"]),
|
|
21892
|
-
custom: parseCustomCommandProfile(record["custom"])
|
|
21893
|
-
});
|
|
21894
|
-
};
|
|
21895
|
-
/**
|
|
21896
|
-
* Parse an already-decoded JSON object into an {@link EasConfig}. Shared by the
|
|
21897
|
-
* `eas.json` reader and the `better-update.json` build-config reader — both hold
|
|
21898
|
-
* the same `build`/`submit`/`cli` shape, only the source file differs.
|
|
21899
|
-
*/
|
|
21900
|
-
const parseConfigFromRecord = (root) => {
|
|
21901
|
-
const buildRecord = asRecord(root["build"]);
|
|
21902
|
-
if (!buildRecord) return asRecord(root["cli"]) ? { cli: parseCli(root["cli"]) } : {};
|
|
21903
|
-
const profiles = {};
|
|
21904
|
-
for (const [name, value] of Object.entries(buildRecord)) {
|
|
21905
|
-
const profile = parseBuildProfile(value);
|
|
21906
|
-
if (profile) profiles[name] = profile;
|
|
21907
|
-
}
|
|
21908
|
-
const submitRecord = asRecord(root["submit"]);
|
|
21909
|
-
const submit = {};
|
|
21910
|
-
if (submitRecord) for (const [name, value] of Object.entries(submitRecord)) {
|
|
21911
|
-
const profile = parseSubmitProfile(value);
|
|
21912
|
-
if (profile !== void 0) submit[name] = profile;
|
|
21913
|
-
}
|
|
21914
|
-
return {
|
|
21915
|
-
...asRecord(root["cli"]) ? { cli: parseCli(root["cli"]) } : {},
|
|
21916
|
-
build: profiles,
|
|
21917
|
-
...Object.keys(submit).length === 0 ? {} : { submit }
|
|
21918
|
-
};
|
|
21919
|
-
};
|
|
21920
|
-
const parseEasConfig = (text) => Effect.gen(function* () {
|
|
21921
|
-
const root = asRecord(yield* Effect.try({
|
|
21922
|
-
try: () => JSON.parse(text),
|
|
21923
|
-
catch: (cause) => new BuildProfileError({ message: `eas.json is not valid JSON: ${formatCause(cause)}` })
|
|
21924
|
-
}));
|
|
21925
|
-
if (!root) return yield* new BuildProfileError({ message: "eas.json must be a JSON object at the top level." });
|
|
21926
|
-
return parseConfigFromRecord(root);
|
|
21927
|
-
});
|
|
21928
|
-
const parseCli = (raw) => {
|
|
21929
|
-
const record = asRecord(raw);
|
|
21930
|
-
if (!record) return {};
|
|
21931
|
-
return compact({ version: asStringValue(record["version"]) });
|
|
21932
|
-
};
|
|
21933
|
-
const easJsonPath = (projectRoot) => Effect.gen(function* () {
|
|
21934
|
-
return (yield* Path.Path).join(projectRoot, "eas.json");
|
|
21935
|
-
});
|
|
21936
|
-
const readEasJson = (projectRoot) => Effect.gen(function* () {
|
|
21937
|
-
const fs = yield* FileSystem.FileSystem;
|
|
21938
|
-
const filePath = yield* easJsonPath(projectRoot);
|
|
21939
|
-
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}` }))));
|
|
21940
|
-
});
|
|
21941
|
-
const mergeCustom = (base, overlay) => {
|
|
21942
|
-
const merged = shallowMerge(base, overlay);
|
|
21943
|
-
return merged === void 0 || Object.keys(merged).length === 0 ? void 0 : merged;
|
|
21944
|
-
};
|
|
21945
|
-
const mergeProfile = (base, overlay) => {
|
|
21946
|
-
const ios = shallowMerge(base.ios, overlay.ios);
|
|
21947
|
-
const android = shallowMerge(base.android, overlay.android);
|
|
21948
|
-
const env = shallowMerge(base.env, overlay.env);
|
|
21949
|
-
const custom = mergeCustom(base.custom, overlay.custom);
|
|
21950
|
-
const developmentClient = overlay.developmentClient ?? base.developmentClient;
|
|
21951
|
-
const distribution = overlay.distribution ?? base.distribution;
|
|
21952
|
-
const channel = overlay.channel ?? base.channel;
|
|
21953
|
-
const environment = overlay.environment ?? base.environment;
|
|
21954
|
-
const credentialsSource = overlay.credentialsSource ?? base.credentialsSource;
|
|
21955
|
-
const autoIncrement = overlay.autoIncrement ?? base.autoIncrement;
|
|
21956
|
-
const withoutCredentials = overlay.withoutCredentials ?? base.withoutCredentials;
|
|
21957
|
-
return compact({
|
|
21958
|
-
extends: overlay.extends,
|
|
21959
|
-
developmentClient,
|
|
21960
|
-
distribution,
|
|
21961
|
-
channel,
|
|
21962
|
-
environment,
|
|
21963
|
-
env,
|
|
21964
|
-
ios,
|
|
21965
|
-
android,
|
|
21966
|
-
credentialsSource,
|
|
21967
|
-
autoIncrement,
|
|
21968
|
-
withoutCredentials,
|
|
21969
|
-
custom
|
|
21970
|
-
});
|
|
21971
|
-
};
|
|
21972
|
-
const resolveEasBuildProfile = (config, profileName, sourceLabel = "eas.json") => Effect.gen(function* () {
|
|
21973
|
-
const profiles = config.build;
|
|
21974
|
-
if (!profiles) return yield* new BuildProfileError({ message: `${sourceLabel} has no "build" section. Add at least one profile.` });
|
|
21975
|
-
return stripExtends((yield* resolveExtendsChain({
|
|
21976
|
-
profiles,
|
|
21977
|
-
profileName,
|
|
21978
|
-
label: "build",
|
|
21979
|
-
maxDepth: MAX_EXTENDS_DEPTH,
|
|
21980
|
-
sourceLabel,
|
|
21981
|
-
makeError: (message) => new BuildProfileError({ message })
|
|
21982
|
-
})).reduce((acc, next, index) => index === 0 ? next : mergeProfile(acc, next), {}));
|
|
21983
|
-
});
|
|
21984
|
-
|
|
21985
|
-
//#endregion
|
|
21986
|
-
//#region src/lib/better-update-build-config.ts
|
|
21987
|
-
/** Label used in profile-resolution error copy when config comes from this file. */
|
|
21988
|
-
const BETTER_UPDATE_SOURCE_LABEL = "better-update.json";
|
|
21989
|
-
/**
|
|
21990
|
-
* Read the `build`/`submit`/`cli` config from `better-update.json`. Returns an
|
|
21991
|
-
* empty config (no `build` key) when the file is absent or carries no build
|
|
21992
|
-
* section. Shares the parser with {@link file://./eas-config.ts}; only the
|
|
21993
|
-
* source file and the error `sourceLabel` differ.
|
|
21994
|
-
*/
|
|
21995
|
-
const readBuildConfig = (projectRoot) => readBetterUpdateConfig(projectRoot).pipe(Effect.map((config) => config === void 0 ? {} : parseConfigFromRecord(config)));
|
|
21996
|
-
/** List available build-profile names declared in `better-update.json`. */
|
|
21997
|
-
const listBuildProfileNames = (projectRoot) => readBuildConfig(projectRoot).pipe(Effect.map((config) => Object.keys(config.build ?? {})));
|
|
21998
|
-
/** Resolve a submit profile from `better-update.json`'s `submit` section. */
|
|
21999
|
-
const readSubmitProfile = (projectRoot, profileName) => Effect.gen(function* () {
|
|
22000
|
-
return yield* resolveEasSubmitProfile((yield* readBuildConfig(projectRoot)).submit, profileName, BETTER_UPDATE_SOURCE_LABEL);
|
|
22001
|
-
});
|
|
22002
|
-
|
|
22003
22125
|
//#endregion
|
|
22004
22126
|
//#region src/lib/build-profile.ts
|
|
22005
22127
|
const deriveIosDistribution = (eas) => {
|
|
@@ -22105,9 +22227,9 @@ const fromGenericProfile = (eas, profileName) => {
|
|
|
22105
22227
|
customCommand: eas.custom
|
|
22106
22228
|
});
|
|
22107
22229
|
};
|
|
22108
|
-
/** Resolve a build profile from `
|
|
22230
|
+
/** Resolve a build profile from `eas.json`'s `build` section (all build systems). */
|
|
22109
22231
|
const readBuildProfile = (projectRoot, profileName) => Effect.gen(function* () {
|
|
22110
|
-
return fromGenericProfile(yield* resolveEasBuildProfile(yield*
|
|
22232
|
+
return fromGenericProfile(yield* resolveEasBuildProfile(yield* readEasJson(projectRoot), profileName, "eas.json"), profileName);
|
|
22111
22233
|
});
|
|
22112
22234
|
const readRuntimeVersionMeta = (config, platform) => ({
|
|
22113
22235
|
platform,
|
|
@@ -22164,7 +22286,7 @@ const PROJECT_TYPES = [
|
|
|
22164
22286
|
"native",
|
|
22165
22287
|
"custom"
|
|
22166
22288
|
];
|
|
22167
|
-
/** Narrow an arbitrary `projectType` override (e.g. from
|
|
22289
|
+
/** Narrow an arbitrary `projectType` override (e.g. from eas.json) to a valid value. */
|
|
22168
22290
|
const asProjectType = (raw) => PROJECT_TYPES.find((type) => type === raw);
|
|
22169
22291
|
const exists = (filePath) => Effect.gen(function* () {
|
|
22170
22292
|
return yield* (yield* FileSystem.FileSystem).exists(filePath).pipe(Effect.orElseSucceed(() => false));
|
|
@@ -22595,7 +22717,7 @@ const detectPlatformGeneric = (explicit, context) => Effect.gen(function* () {
|
|
|
22595
22717
|
const wantsAndroid = context.profile.android !== void 0 || context.profile.customCommand?.android !== void 0;
|
|
22596
22718
|
if (wantsIos && (context.hasIosDir || context.profile.customCommand?.ios !== void 0)) candidates.push("ios");
|
|
22597
22719
|
if (wantsAndroid && (context.hasAndroidDir || context.profile.customCommand?.android !== void 0)) candidates.push("android");
|
|
22598
|
-
if (candidates.length === 0) return yield* new BuildProfileError({ message: "Cannot infer build platform. Add an `ios` or `android` section to the build profile in
|
|
22720
|
+
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." });
|
|
22599
22721
|
const [only] = candidates;
|
|
22600
22722
|
if (candidates.length === 1 && only !== void 0) return only;
|
|
22601
22723
|
if (!(yield* InteractiveMode).allow) return yield* new BuildProfileError({ message: `Multiple platforms available (${candidates.join(", ")}). Pass --platform explicitly when running non-interactively.` });
|
|
@@ -23477,13 +23599,13 @@ const EMPTY = {
|
|
|
23477
23599
|
buildNumber: void 0,
|
|
23478
23600
|
rawRuntimeVersion: void 0
|
|
23479
23601
|
};
|
|
23480
|
-
const warnIfMismatch = (label, override, native) => override !== void 0 && native !== void 0 && override !== native ? printWarn(`${label} override "${override}" differs from the native value "${native}". The
|
|
23602
|
+
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;
|
|
23481
23603
|
const resolveAndroidMeta = (projectRoot, profile) => Effect.gen(function* () {
|
|
23482
23604
|
const gradle = yield* readGradleConfig(path.join(projectRoot, "android"));
|
|
23483
23605
|
const override = profile.android?.metaOverride;
|
|
23484
23606
|
yield* warnIfMismatch("android.applicationId", override?.applicationId, gradle?.applicationId);
|
|
23485
23607
|
const androidPackage = override?.applicationId ?? gradle?.applicationId;
|
|
23486
|
-
if (androidPackage === void 0) return yield* new BuildProfileError({ message: "Could not determine the Android applicationId. Set android.applicationId under this build profile in
|
|
23608
|
+
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." });
|
|
23487
23609
|
const versionCode = override?.versionCode ?? (gradle?.versionCode === void 0 ? void 0 : String(gradle.versionCode));
|
|
23488
23610
|
return {
|
|
23489
23611
|
...EMPTY,
|
|
@@ -23501,7 +23623,7 @@ const resolveIosMeta = (projectRoot, profile) => Effect.gen(function* () {
|
|
|
23501
23623
|
const override = profile.ios?.metaOverride;
|
|
23502
23624
|
yield* warnIfMismatch("ios.bundleIdentifier", override?.bundleIdentifier, native.bundleId);
|
|
23503
23625
|
const bundleId = override?.bundleIdentifier ?? native.bundleId;
|
|
23504
|
-
if (bundleId === void 0) return yield* new BuildProfileError({ message: "Could not determine the iOS bundle identifier. Set ios.bundleIdentifier under this build profile in
|
|
23626
|
+
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." });
|
|
23505
23627
|
return {
|
|
23506
23628
|
...EMPTY,
|
|
23507
23629
|
bundleId,
|
|
@@ -23541,6 +23663,27 @@ const resolveAppMeta = (params) => {
|
|
|
23541
23663
|
return params.platform === "ios" ? resolveIosMeta(params.projectRoot, params.profile) : resolveAndroidMeta(params.projectRoot, params.profile);
|
|
23542
23664
|
};
|
|
23543
23665
|
|
|
23666
|
+
//#endregion
|
|
23667
|
+
//#region src/application/resolve-update-channel.ts
|
|
23668
|
+
/**
|
|
23669
|
+
* EAS parity: bake the profile's channel into the native app as the
|
|
23670
|
+
* expo-channel-name request header during prebuild. Only the managed ("expo")
|
|
23671
|
+
* strategy prebuilds, and only apps shipping expo-updates read the header — a
|
|
23672
|
+
* binary built without it silently falls back to the server's default channel,
|
|
23673
|
+
* so a missing `channel` on an updates-enabled profile fails fast instead.
|
|
23674
|
+
* Returns the channel to inject, or `undefined` to skip injection.
|
|
23675
|
+
*/
|
|
23676
|
+
const resolveUpdateChannel = (params) => Effect.gen(function* () {
|
|
23677
|
+
const { userCwd, platform, profile, projectType } = params;
|
|
23678
|
+
if ((platform === "ios" ? resolveIosStrategy(profile, projectType) : resolveAndroidStrategy(profile, projectType)) !== "expo") return;
|
|
23679
|
+
if (!(yield* isExpoUpdatesInstalled(userCwd))) {
|
|
23680
|
+
yield* printHuman("expo-updates is not installed — skipping update-channel injection.");
|
|
23681
|
+
return;
|
|
23682
|
+
}
|
|
23683
|
+
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.` });
|
|
23684
|
+
return profile.channel;
|
|
23685
|
+
});
|
|
23686
|
+
|
|
23544
23687
|
//#endregion
|
|
23545
23688
|
//#region src/application/build-workflow.ts
|
|
23546
23689
|
const runIosPlatformBuild = (input) => Effect.gen(function* () {
|
|
@@ -23570,6 +23713,7 @@ const runIosPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
23570
23713
|
strategy,
|
|
23571
23714
|
rawOutput: options.rawOutput,
|
|
23572
23715
|
freezeCredentials: options.freezeCredentials ?? false,
|
|
23716
|
+
updateChannel: input.updateChannel,
|
|
23573
23717
|
...compact({ customCommand: profile.customCommand?.ios })
|
|
23574
23718
|
}),
|
|
23575
23719
|
target: isSimulator ? {
|
|
@@ -23613,6 +23757,7 @@ const runAndroidPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
23613
23757
|
profileName: profile.name,
|
|
23614
23758
|
skipCredentials,
|
|
23615
23759
|
strategy,
|
|
23760
|
+
updateChannel: input.updateChannel,
|
|
23616
23761
|
...compact({ customCommand: profile.customCommand?.android })
|
|
23617
23762
|
}),
|
|
23618
23763
|
target: androidProfile.format === "aab" ? {
|
|
@@ -23669,7 +23814,7 @@ const resolveProfileName = (projectRoot, requested) => Effect.gen(function* () {
|
|
|
23669
23814
|
const available = yield* listBuildProfileNames(projectRoot);
|
|
23670
23815
|
if (available.includes(requested)) return requested;
|
|
23671
23816
|
if (!(yield* InteractiveMode).allow || available.length === 0) return requested;
|
|
23672
|
-
yield* printHuman(`Build profile "${requested}" not found in
|
|
23817
|
+
yield* printHuman(`Build profile "${requested}" not found in eas.json.`);
|
|
23673
23818
|
return yield* promptSelect("Pick a build profile:", available.map((name) => ({
|
|
23674
23819
|
value: name,
|
|
23675
23820
|
label: name
|
|
@@ -23685,7 +23830,7 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23685
23830
|
});
|
|
23686
23831
|
const projectType = yield* detectProjectType({
|
|
23687
23832
|
projectRoot: userCwd,
|
|
23688
|
-
override: asProjectType(
|
|
23833
|
+
override: asProjectType(yield* readEasProjectType(userCwd))
|
|
23689
23834
|
});
|
|
23690
23835
|
const isExpo = projectType === "expo";
|
|
23691
23836
|
const projectId = yield* readProjectId;
|
|
@@ -23703,6 +23848,12 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23703
23848
|
hasAndroidDir: yield* dirExists(userCwd, "android"),
|
|
23704
23849
|
hasIosDir: yield* dirExists(userCwd, "ios")
|
|
23705
23850
|
});
|
|
23851
|
+
const updateChannel = yield* resolveUpdateChannel({
|
|
23852
|
+
userCwd,
|
|
23853
|
+
platform,
|
|
23854
|
+
profile,
|
|
23855
|
+
projectType
|
|
23856
|
+
});
|
|
23706
23857
|
const { appMeta, runtimeVersion } = isExpo ? yield* resolveExpoBuildMeta({
|
|
23707
23858
|
userCwd,
|
|
23708
23859
|
platform,
|
|
@@ -23725,7 +23876,8 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23725
23876
|
envVars,
|
|
23726
23877
|
projectType
|
|
23727
23878
|
});
|
|
23728
|
-
|
|
23879
|
+
const buildDetails = [...runtimeVersion === void 0 ? [] : [`runtimeVersion=${runtimeVersion}`], ...updateChannel === void 0 ? [] : [`channel=${updateChannel}`]];
|
|
23880
|
+
yield* printHuman(`Building ${platform} artifact for profile "${profile.name}"${buildDetails.length === 0 ? "" : ` (${buildDetails.join(", ")})`}`);
|
|
23729
23881
|
const { build, target, bundleId } = yield* runPlatformBuild({
|
|
23730
23882
|
api,
|
|
23731
23883
|
options,
|
|
@@ -23736,7 +23888,8 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23736
23888
|
envVars,
|
|
23737
23889
|
projectId,
|
|
23738
23890
|
projectRoot: staging.projectRoot,
|
|
23739
|
-
tempDir
|
|
23891
|
+
tempDir,
|
|
23892
|
+
updateChannel
|
|
23740
23893
|
});
|
|
23741
23894
|
yield* printHuman(`Artifact produced: ${build.artifactPath}`);
|
|
23742
23895
|
let exportedArtifactPath = void 0;
|
|
@@ -24757,7 +24910,7 @@ const runUploadWorkflow = (options) => Effect.gen(function* () {
|
|
|
24757
24910
|
if (!(yield* (yield* FileSystem.FileSystem).exists(options.artifactPath).pipe(Effect.orElseSucceed(() => false)))) return yield* new ArtifactNotFoundError({ message: `Artifact not found at ${options.artifactPath}.` });
|
|
24758
24911
|
const projectType = yield* detectProjectType({
|
|
24759
24912
|
projectRoot,
|
|
24760
|
-
override: asProjectType(
|
|
24913
|
+
override: asProjectType(yield* readEasProjectType(projectRoot))
|
|
24761
24914
|
});
|
|
24762
24915
|
const projectId = yield* readProjectId;
|
|
24763
24916
|
const profile = yield* readBuildProfile(projectRoot, options.profileName);
|
|
@@ -28547,16 +28700,54 @@ const revokeCommand = defineCommand({
|
|
|
28547
28700
|
}
|
|
28548
28701
|
});
|
|
28549
28702
|
|
|
28703
|
+
//#endregion
|
|
28704
|
+
//#region src/lib/duration.ts
|
|
28705
|
+
/**
|
|
28706
|
+
* Parse a human-readable duration flag ("90", "45m", "2h", "1h30m") into
|
|
28707
|
+
* milliseconds. A bare number means minutes. Returns `undefined` for anything
|
|
28708
|
+
* unparseable or non-positive — range policy stays with the caller.
|
|
28709
|
+
*/
|
|
28710
|
+
const parseDurationMs = (input) => {
|
|
28711
|
+
const groups = /^(?:(?<hours>\d+)h)?(?:(?<minutes>\d+)m?)?$/u.exec(input.trim().toLowerCase())?.groups;
|
|
28712
|
+
if (!groups || groups["hours"] === void 0 && groups["minutes"] === void 0) return;
|
|
28713
|
+
const hours = Number(groups["hours"] ?? 0);
|
|
28714
|
+
const minutes = Number(groups["minutes"] ?? 0);
|
|
28715
|
+
const ms = (hours * 60 + minutes) * 6e4;
|
|
28716
|
+
return ms > 0 ? ms : void 0;
|
|
28717
|
+
};
|
|
28718
|
+
/**
|
|
28719
|
+
* Approximate human form of a duration, rounded up to whole minutes so
|
|
28720
|
+
* "<1 min remaining" still reads as 1 — "45 min", "2 h", "1 h 30 min".
|
|
28721
|
+
*/
|
|
28722
|
+
const formatDurationApprox = (ms) => {
|
|
28723
|
+
const totalMinutes = Math.max(1, Math.ceil(ms / 6e4));
|
|
28724
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
28725
|
+
const minutes = totalMinutes % 60;
|
|
28726
|
+
if (hours === 0) return `${minutes} min`;
|
|
28727
|
+
return minutes === 0 ? `${hours} h` : `${hours} h ${minutes} min`;
|
|
28728
|
+
};
|
|
28729
|
+
|
|
28550
28730
|
//#endregion
|
|
28551
28731
|
//#region src/commands/credentials/session.ts
|
|
28552
|
-
/**
|
|
28553
|
-
const
|
|
28732
|
+
/** Parse + bound the `--duration` flag; `undefined` (flag absent) keeps the 15-minute default. */
|
|
28733
|
+
const resolveUnlockTtlMs = (flag) => Effect.gen(function* () {
|
|
28734
|
+
if (flag === void 0) return;
|
|
28735
|
+
const ms = parseDurationMs(flag);
|
|
28736
|
+
if (ms === void 0) return yield* new InvalidArgumentError({ message: `Could not parse --duration "${flag}" — use minutes ("90") or h/m units ("45m", "2h", "1h30m").` });
|
|
28737
|
+
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}".` });
|
|
28738
|
+
return ms;
|
|
28739
|
+
});
|
|
28554
28740
|
const unlockCommand = defineCommand({
|
|
28555
28741
|
meta: {
|
|
28556
28742
|
name: "unlock",
|
|
28557
28743
|
description: "Unlock the credential vault and cache the key in your OS keychain, so later commands don't re-prompt"
|
|
28558
28744
|
},
|
|
28559
|
-
|
|
28745
|
+
args: { duration: {
|
|
28746
|
+
type: "string",
|
|
28747
|
+
description: "How long to stay unlocked — minutes (\"90\") or h/m units (\"45m\", \"2h\", \"1h30m\"); default 15m, max 24h"
|
|
28748
|
+
} },
|
|
28749
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
28750
|
+
const cacheTtlMs = yield* resolveUnlockTtlMs(args.duration);
|
|
28560
28751
|
const recipient = yield* activeRecipient;
|
|
28561
28752
|
if (recipient.source !== "file") {
|
|
28562
28753
|
yield* printHuman("Active identity is the BETTER_UPDATE_IDENTITY (CI) key — it has no passphrase and isn't cached.");
|
|
@@ -28565,9 +28756,9 @@ const unlockCommand = defineCommand({
|
|
|
28565
28756
|
const api = yield* apiClient;
|
|
28566
28757
|
const cache = yield* VaultCache;
|
|
28567
28758
|
yield* cache.clear(recipient.publicKey);
|
|
28568
|
-
yield* unlockVaultKeyInteractive(api);
|
|
28759
|
+
yield* unlockVaultKeyInteractive(api, { cacheTtlMs });
|
|
28569
28760
|
const cached = yield* cache.get(recipient.publicKey);
|
|
28570
|
-
yield* printHuman(`Vault unlocked${cached === void 0 ? " (no OS keychain available — commands will keep prompting)" : ` for ~${
|
|
28761
|
+
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`}.`);
|
|
28571
28762
|
}))
|
|
28572
28763
|
});
|
|
28573
28764
|
const lockCommand = defineCommand({
|
|
@@ -28593,7 +28784,7 @@ const statusCommand$1 = defineCommand({
|
|
|
28593
28784
|
return;
|
|
28594
28785
|
}
|
|
28595
28786
|
const cached = yield* (yield* VaultCache).get(recipient.publicKey);
|
|
28596
|
-
yield* printHuman(cached === void 0 ? "Locked — the next credential command will prompt for your passphrase." : `Unlocked — cached vault key expires in ~${
|
|
28787
|
+
yield* printHuman(cached === void 0 ? "Locked — the next credential command will prompt for your passphrase." : `Unlocked — cached vault key expires in ~${formatDurationApprox(cached.remainingMs)}.`);
|
|
28597
28788
|
}))
|
|
28598
28789
|
});
|
|
28599
28790
|
|
|
@@ -30044,21 +30235,22 @@ const checkProjectLink = Effect.gen(function* () {
|
|
|
30044
30235
|
if (fromEnv !== void 0 && fromEnv.length > 0) return pass("project-linked", "Project linked", `projectId=${fromEnv} (via ${BETTER_UPDATE_PROJECT_ID_ENV})`);
|
|
30045
30236
|
const resolved = yield* readProjectId.pipe(Effect.either);
|
|
30046
30237
|
if (resolved._tag === "Left") return warn("project-linked", "Project linked", resolved.left.message);
|
|
30047
|
-
const source = (yield*
|
|
30238
|
+
const source = (yield* readEasLinkedProjectId(root)) === void 0 ? "Expo config" : "eas.json";
|
|
30048
30239
|
return pass("project-linked", "Project linked", `projectId=${resolved.right} (via ${source})`);
|
|
30049
30240
|
});
|
|
30050
30241
|
const checkProjectType = Effect.gen(function* () {
|
|
30051
30242
|
const root = yield* (yield* CliRuntime).cwd;
|
|
30052
|
-
const override = asProjectType(
|
|
30243
|
+
const override = asProjectType(yield* readEasProjectType(root));
|
|
30053
30244
|
return pass("project-type", "Project type", `${yield* detectProjectType({
|
|
30054
30245
|
projectRoot: root,
|
|
30055
30246
|
override
|
|
30056
|
-
})} (${override === void 0 ? "auto-detected" : "
|
|
30247
|
+
})} (${override === void 0 ? "auto-detected" : "eas.json override"})`);
|
|
30057
30248
|
});
|
|
30058
30249
|
const checkBuildConfig = Effect.gen(function* () {
|
|
30059
|
-
const names = yield* listBuildProfileNames(yield* (yield* CliRuntime).cwd);
|
|
30060
|
-
if (names.
|
|
30061
|
-
return
|
|
30250
|
+
const names = yield* listBuildProfileNames(yield* (yield* CliRuntime).cwd).pipe(Effect.either);
|
|
30251
|
+
if (names._tag === "Left") return warn("build-config", "Build config", names.left.message);
|
|
30252
|
+
if (names.right.length === 0) return warn("build-config", "Build config", "No build profiles found. Add a \"build\" section to eas.json.");
|
|
30253
|
+
return pass("build-config", "Build config", `${names.right.length} profile(s) defined`);
|
|
30062
30254
|
});
|
|
30063
30255
|
const runChecks = Effect.gen(function* () {
|
|
30064
30256
|
const xcode = (yield* CliRuntime).platform === "darwin" ? [yield* checkCommand("xcode", "Xcode CLI tools", "xcode-select", ["-p"])] : [];
|
|
@@ -31573,7 +31765,8 @@ const resolveNameAndSlug = (args, projectRoot, expoConfig) => Effect.gen(functio
|
|
|
31573
31765
|
});
|
|
31574
31766
|
/**
|
|
31575
31767
|
* Persist the resolved project id: into the Expo config (`extra.betterUpdate`)
|
|
31576
|
-
* when an Expo project is present, otherwise
|
|
31768
|
+
* when an Expo project is present, otherwise as `eas.json`'s top-level
|
|
31769
|
+
* `projectId` extension key.
|
|
31577
31770
|
*/
|
|
31578
31771
|
const persistLink = (projectRoot, projectId, hasExpoConfig) => Effect.gen(function* () {
|
|
31579
31772
|
if (hasExpoConfig) {
|
|
@@ -31585,7 +31778,7 @@ const persistLink = (projectRoot, projectId, hasExpoConfig) => Effect.gen(functi
|
|
|
31585
31778
|
...compact({ configPath: writeResult.configPath })
|
|
31586
31779
|
};
|
|
31587
31780
|
}
|
|
31588
|
-
const filePath = yield*
|
|
31781
|
+
const filePath = yield* writeEasJsonPatch(projectRoot, { projectId });
|
|
31589
31782
|
yield* printHuman(`Project linked successfully. ID saved to ${path.relative(projectRoot, filePath)}.`);
|
|
31590
31783
|
return {
|
|
31591
31784
|
projectId,
|
|
@@ -31694,45 +31887,6 @@ const logoutCommand = defineCommand({
|
|
|
31694
31887
|
}), { json: "value" })
|
|
31695
31888
|
});
|
|
31696
31889
|
|
|
31697
|
-
//#endregion
|
|
31698
|
-
//#region src/commands/migrate-config.ts
|
|
31699
|
-
const migrateConfigCommand = defineCommand({
|
|
31700
|
-
meta: {
|
|
31701
|
-
name: "migrate-config",
|
|
31702
|
-
description: "Migrate `build`/`submit` profiles from a legacy eas.json into better-update.json"
|
|
31703
|
-
},
|
|
31704
|
-
args: { yes: {
|
|
31705
|
-
type: "boolean",
|
|
31706
|
-
description: "Skip the confirmation prompt"
|
|
31707
|
-
} },
|
|
31708
|
-
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31709
|
-
const runtime = yield* CliRuntime;
|
|
31710
|
-
const fs = yield* FileSystem.FileSystem;
|
|
31711
|
-
const root = yield* runtime.cwd;
|
|
31712
|
-
const easPath = path.join(root, "eas.json");
|
|
31713
|
-
if (!(yield* fs.exists(easPath).pipe(Effect.orElseSucceed(() => false)))) return yield* new InvalidArgumentError({ message: `No eas.json found at ${root}.` });
|
|
31714
|
-
const config = yield* readEasJson(root);
|
|
31715
|
-
const patch = compact({
|
|
31716
|
-
build: config.build,
|
|
31717
|
-
submit: config.submit,
|
|
31718
|
-
cli: config.cli
|
|
31719
|
-
});
|
|
31720
|
-
if (Object.keys(patch).length === 0) {
|
|
31721
|
-
yield* printHuman("eas.json has no build/submit/cli sections — nothing to migrate.");
|
|
31722
|
-
return;
|
|
31723
|
-
}
|
|
31724
|
-
const existingBuild = (yield* readBetterUpdateConfig(root))?.["build"];
|
|
31725
|
-
if (typeof existingBuild === "object" && existingBuild !== null && config.build !== void 0 && !args.yes) {
|
|
31726
|
-
if (!(yield* promptConfirm("better-update.json already has a build section — overwrite it from eas.json?", { initialValue: false }))) {
|
|
31727
|
-
yield* printHuman("Cancelled.");
|
|
31728
|
-
return;
|
|
31729
|
-
}
|
|
31730
|
-
}
|
|
31731
|
-
yield* writeBetterUpdateConfig(root, patch);
|
|
31732
|
-
yield* printHuman("Merged eas.json build/submit into better-update.json. You can now delete eas.json.");
|
|
31733
|
-
}))
|
|
31734
|
-
});
|
|
31735
|
-
|
|
31736
31890
|
//#endregion
|
|
31737
31891
|
//#region src/commands/open.ts
|
|
31738
31892
|
const RESOURCE_PATHS = {
|
|
@@ -35432,7 +35586,6 @@ const commandRegistry = {
|
|
|
35432
35586
|
devices: devicesCommand,
|
|
35433
35587
|
webhooks: webhooksCommand,
|
|
35434
35588
|
autocomplete: autocompleteCommand,
|
|
35435
|
-
"migrate-config": migrateConfigCommand,
|
|
35436
35589
|
apple: appleCommand,
|
|
35437
35590
|
submit: submitCommand
|
|
35438
35591
|
};
|