@better-update/cli 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -14,12 +14,13 @@ import { createServer } from "node:http";
14
14
  import { maxBy, uniqBy } from "es-toolkit";
15
15
  import { createHash, randomBytes, randomUUID } from "node:crypto";
16
16
  import forge from "node-forge";
17
- import { createReadStream, existsSync, readFileSync, writeFileSync } from "node:fs";
17
+ import { createReadStream, existsSync, promises, readFileSync, writeFileSync } from "node:fs";
18
18
  import { spawn as spawn$1 } from "node-pty";
19
19
  import chalk from "chalk";
20
20
  import os from "node:os";
21
21
  import plistMod from "@expo/plist";
22
22
  import { ExpoRunFormatter } from "@expo/xcpretty";
23
+ import ignore from "ignore";
23
24
  import { Buffer as Buffer$1 } from "node:buffer";
24
25
  import { getFormattedSerialNumber, getX509Certificate, parsePKCS12 } from "@expo/pkcs12";
25
26
  import qrcode from "qrcode-terminal";
@@ -30,7 +31,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
30
31
 
31
32
  //#endregion
32
33
  //#region package.json
33
- var version = "0.16.0";
34
+ var version = "0.18.0";
34
35
 
35
36
  //#endregion
36
37
  //#region src/lib/interactive-mode.ts
@@ -199,11 +200,11 @@ var NotAcceptable = class extends Schema.TaggedError()("NotAcceptable", { messag
199
200
  //#endregion
200
201
  //#region ../../packages/api/src/groups/android-application-identifiers.ts
201
202
  const idParam$16 = HttpApiSchema.param("id", Schema.String);
202
- const projectIdParam$3 = HttpApiSchema.param("projectId", Schema.String);
203
- var AndroidApplicationIdentifiersGroup = class extends HttpApiGroup.make("androidApplicationIdentifiers").add(HttpApiEndpoint.get("list")`/api/projects/${projectIdParam$3}/android-application-identifiers`.addSuccess(Schema.Struct({ items: Schema.Array(AndroidApplicationIdentifier) })).annotateContext(OpenApi.annotations({
203
+ const projectIdParam$4 = HttpApiSchema.param("projectId", Schema.String);
204
+ var AndroidApplicationIdentifiersGroup = class extends HttpApiGroup.make("androidApplicationIdentifiers").add(HttpApiEndpoint.get("list")`/api/projects/${projectIdParam$4}/android-application-identifiers`.addSuccess(Schema.Struct({ items: Schema.Array(AndroidApplicationIdentifier) })).annotateContext(OpenApi.annotations({
204
205
  title: "List Android application identifiers",
205
206
  description: "List all Android package identifiers for a project"
206
- }))).add(HttpApiEndpoint.post("create")`/api/projects/${projectIdParam$3}/android-application-identifiers`.setPayload(CreateAndroidApplicationIdentifierBody).addSuccess(AndroidApplicationIdentifier, { status: 201 }).annotateContext(OpenApi.annotations({
207
+ }))).add(HttpApiEndpoint.post("create")`/api/projects/${projectIdParam$4}/android-application-identifiers`.setPayload(CreateAndroidApplicationIdentifierBody).addSuccess(AndroidApplicationIdentifier, { status: 201 }).annotateContext(OpenApi.annotations({
207
208
  title: "Create Android application identifier",
208
209
  description: "Register an Android package name for a project"
209
210
  }))).add(HttpApiEndpoint.del("delete")`/api/android-application-identifiers/${idParam$16}`.addSuccess(DeleteAndroidApplicationIdentifierResult).annotateContext(OpenApi.annotations({
@@ -611,11 +612,11 @@ const AssetUploadResult = Schema.Struct({
611
612
 
612
613
  //#endregion
613
614
  //#region ../../packages/api/src/groups/assets.ts
614
- const hashParam = HttpApiSchema.param("hash", Schema.String);
615
+ const hashParam$1 = HttpApiSchema.param("hash", Schema.String);
615
616
  var AssetsGroup = class extends HttpApiGroup.make("assets").add(HttpApiEndpoint.post("upload", "/api/assets/upload").setPayload(AssetUploadBody).addSuccess(AssetUploadResult, { status: 201 }).annotateContext(OpenApi.annotations({
616
617
  title: "Upload assets",
617
618
  description: "Upload asset files to R2 storage (deduplicated by content hash)"
618
- }))).add(HttpApiEndpoint.post("finalize")`/api/assets/${hashParam}/finalize`.addSuccess(Asset).annotateContext(OpenApi.annotations({
619
+ }))).add(HttpApiEndpoint.post("finalize")`/api/assets/${hashParam$1}/finalize`.addSuccess(Asset).annotateContext(OpenApi.annotations({
619
620
  title: "Finalize asset upload",
620
621
  description: "Verify a directly uploaded asset in R2 and mark it available for updates"
621
622
  }))).addError(BadRequest).addError(NotFound).addError(Forbidden).annotateContext(OpenApi.annotations({
@@ -720,7 +721,8 @@ const ResolveBuildCredentialsIosBody = Schema.Struct({
720
721
  });
721
722
  const ResolveBuildCredentialsAndroidBody = Schema.Struct({
722
723
  platform: Schema.Literal("android"),
723
- applicationIdentifier: AndroidPackageName
724
+ applicationIdentifier: AndroidPackageName,
725
+ buildProfile: Schema.optional(Schema.String.pipe(Schema.minLength(1), Schema.maxLength(120)))
724
726
  });
725
727
  const ResolveBuildCredentialsBody = Schema.Union(ResolveBuildCredentialsIosBody, ResolveBuildCredentialsAndroidBody);
726
728
  const IosBuildDistributionCertificate = Schema.Struct({
@@ -776,8 +778,8 @@ const ResolveBuildCredentialsResult = Schema.Union(ResolveBuildCredentialsIosRes
776
778
 
777
779
  //#endregion
778
780
  //#region ../../packages/api/src/groups/build-credentials.ts
779
- const projectIdParam$2 = HttpApiSchema.param("projectId", Schema.String);
780
- var BuildCredentialsGroup = class extends HttpApiGroup.make("buildCredentials").add(HttpApiEndpoint.post("resolve")`/api/projects/${projectIdParam$2}/build-credentials/resolve`.setPayload(ResolveBuildCredentialsBody).addSuccess(ResolveBuildCredentialsResult).annotateContext(OpenApi.annotations({
781
+ const projectIdParam$3 = HttpApiSchema.param("projectId", Schema.String);
782
+ var BuildCredentialsGroup = class extends HttpApiGroup.make("buildCredentials").add(HttpApiEndpoint.post("resolve")`/api/projects/${projectIdParam$3}/build-credentials/resolve`.setPayload(ResolveBuildCredentialsBody).addSuccess(ResolveBuildCredentialsResult).annotateContext(OpenApi.annotations({
781
783
  title: "Resolve build credentials",
782
784
  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."
783
785
  }))).addError(NotFound).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
@@ -788,6 +790,7 @@ var BuildCredentialsGroup = class extends HttpApiGroup.make("buildCredentials").
788
790
  //#endregion
789
791
  //#region ../../packages/api/src/domain/build.ts
790
792
  const Distribution = Schema.Literal("app-store", "ad-hoc", "development", "enterprise", "simulator", "play-store", "direct");
793
+ const BuildAudience = Schema.Literal("internal", "store");
791
794
  const ArtifactFormat = Schema.Literal("ipa", "apk", "aab", "tar.gz");
792
795
  const Sha256Hex = Schema.String.pipe(Schema.pattern(/^[a-fA-F0-9]{64}$/u), Schema.maxLength(64));
793
796
  const CreateBuildCommonFields = {
@@ -804,6 +807,7 @@ const CreateBuildCommonFields = {
804
807
  key: Schema.String,
805
808
  value: Schema.Unknown
806
809
  })),
810
+ fingerprintHash: Schema.optional(Schema.String.pipe(Schema.minLength(1))),
807
811
  sha256: Sha256Hex,
808
812
  byteSize: Schema.Number.pipe(Schema.nonNegative())
809
813
  };
@@ -821,6 +825,7 @@ var Build = class extends Schema.Class("Build")({
821
825
  gitCommit: Schema.NullOr(Schema.String),
822
826
  message: Schema.NullOr(Schema.String),
823
827
  metadataJson: Schema.String,
828
+ fingerprintHash: Schema.NullOr(Schema.String),
824
829
  createdAt: DateTimeString
825
830
  }) {};
826
831
  var BuildArtifact = class extends Schema.Class("BuildArtifact")({
@@ -872,6 +877,7 @@ const ListBuildsParams = Schema.Struct({
872
877
  profile: Schema.optional(Schema.String),
873
878
  runtimeVersion: Schema.optional(Schema.String),
874
879
  distribution: Schema.optional(Distribution),
880
+ audience: Schema.optional(BuildAudience),
875
881
  ...PaginationParams.fields,
876
882
  sort: Schema.optional(BuildSort)
877
883
  });
@@ -1244,6 +1250,108 @@ var EnvVarsGroup = class extends HttpApiGroup.make("env-vars").add(HttpApiEndpoi
1244
1250
  description: "Manage environment variables for project builds and deployments"
1245
1251
  })) {};
1246
1252
 
1253
+ //#endregion
1254
+ //#region ../../packages/api/src/domain/update.ts
1255
+ var Update = class extends Schema.Class("Update")({
1256
+ id: Id,
1257
+ branchId: Id,
1258
+ runtimeVersion: Schema.String,
1259
+ platform: Platform,
1260
+ message: Schema.String,
1261
+ metadataJson: Schema.String,
1262
+ extraJson: Schema.NullOr(Schema.String),
1263
+ groupId: Schema.String,
1264
+ rolloutPercentage: Schema.Number,
1265
+ isRollback: Schema.Boolean,
1266
+ signature: Schema.NullOr(Schema.String),
1267
+ certificateChain: Schema.NullOr(Schema.String),
1268
+ manifestBody: Schema.NullOr(Schema.String),
1269
+ directiveBody: Schema.NullOr(Schema.String),
1270
+ fingerprintHash: Schema.NullOr(Schema.String),
1271
+ totalAssetSize: Schema.Number,
1272
+ createdAt: DateTimeString
1273
+ }) {};
1274
+ const UpdateSortColumn = Schema.Literal("createdAt", "runtimeVersion", "platform", "rolloutPercentage");
1275
+ /**
1276
+ * Sort param: column name optionally prefixed with `-` for descending.
1277
+ * Example: `runtimeVersion` (asc), `-createdAt` (desc).
1278
+ */
1279
+ const UpdateSort = Schema.Union(UpdateSortColumn, Schema.TemplateLiteral("-", UpdateSortColumn));
1280
+ const ListUpdatesParams = Schema.Struct({
1281
+ projectId: Id,
1282
+ branchId: Schema.optional(Id),
1283
+ platform: Schema.optional(Platform),
1284
+ runtimeVersion: Schema.optional(Schema.String),
1285
+ ...PaginationParams.fields,
1286
+ sort: Schema.optional(UpdateSort)
1287
+ });
1288
+ const AssetRef = Schema.Struct({
1289
+ hash: Schema.String,
1290
+ key: Schema.String,
1291
+ isLaunch: Schema.Boolean,
1292
+ contentChecksum: Schema.optional(Schema.String)
1293
+ });
1294
+ const UpdateAssetEntry = Schema.Struct({
1295
+ hash: Schema.String,
1296
+ key: Schema.String,
1297
+ isLaunch: Schema.Boolean,
1298
+ contentChecksum: Schema.NullOr(Schema.String)
1299
+ });
1300
+ const CreateUpdateBody = Schema.Struct({
1301
+ branch: Schema.String.pipe(Schema.minLength(1)),
1302
+ slug: Schema.String.pipe(Schema.minLength(1)),
1303
+ runtimeVersion: Schema.String.pipe(Schema.minLength(1)),
1304
+ platform: Platform,
1305
+ message: Schema.String,
1306
+ groupId: Schema.String.pipe(Schema.minLength(1)),
1307
+ metadata: Schema.Record({
1308
+ key: Schema.String,
1309
+ value: Schema.Unknown
1310
+ }),
1311
+ extra: Schema.optional(Schema.Record({
1312
+ key: Schema.String,
1313
+ value: Schema.Unknown
1314
+ })),
1315
+ assets: Schema.Array(AssetRef),
1316
+ manifestBody: Schema.optional(Schema.String),
1317
+ directiveBody: Schema.optional(Schema.String),
1318
+ isRollback: Schema.optional(Schema.Boolean),
1319
+ signature: Schema.optional(Schema.String),
1320
+ certificateChain: Schema.optional(Schema.String),
1321
+ rolloutPercentage: Schema.optional(Schema.Number.pipe(Schema.int(), Schema.between(1, 100))),
1322
+ fingerprintHash: Schema.optional(Schema.String.pipe(Schema.minLength(1)))
1323
+ });
1324
+ const RepublishBody = Schema.Struct({
1325
+ sourceUpdateId: Schema.optional(Id),
1326
+ sourceGroupId: Schema.optional(Schema.String.pipe(Schema.minLength(1))),
1327
+ destinationBranchId: Schema.optional(Id),
1328
+ destinationChannel: Schema.optional(Schema.String.pipe(Schema.minLength(1))),
1329
+ message: Schema.optional(Schema.String),
1330
+ signedUpdates: Schema.optional(Schema.Array(Schema.Struct({
1331
+ sourceUpdateId: Id,
1332
+ manifestBody: Schema.String.pipe(Schema.minLength(1)),
1333
+ signature: Schema.String.pipe(Schema.minLength(1)),
1334
+ certificateChain: Schema.String.pipe(Schema.minLength(1))
1335
+ })))
1336
+ });
1337
+ const RepublishResult = Schema.Struct({ updates: Schema.Array(Update) });
1338
+ const DeleteUpdateResult = Schema.Struct({ deleted: Schema.Number });
1339
+
1340
+ //#endregion
1341
+ //#region ../../packages/api/src/groups/fingerprints.ts
1342
+ const projectIdParam$2 = HttpApiSchema.param("projectId", Id);
1343
+ const hashParam = HttpApiSchema.param("hash", Schema.String.pipe(Schema.minLength(1)));
1344
+ const FingerprintDetail = Schema.Struct({
1345
+ hash: Schema.String,
1346
+ projectId: Id,
1347
+ builds: Schema.Array(BuildWithArtifact),
1348
+ updates: Schema.Array(Update)
1349
+ });
1350
+ var FingerprintsGroup = class extends HttpApiGroup.make("fingerprints").add(HttpApiEndpoint.get("get")`/api/projects/${projectIdParam$2}/fingerprints/${hashParam}`.addSuccess(FingerprintDetail).annotateContext(OpenApi.annotations({
1351
+ title: "Get fingerprint",
1352
+ description: "Fetch builds and updates compatible with a given fingerprint hash within a project."
1353
+ }))).addError(Forbidden, { status: 403 }).addError(NotFound, { status: 404 }).addError(BadRequest, { status: 400 }) {};
1354
+
1247
1355
  //#endregion
1248
1356
  //#region ../../packages/api/src/domain/google-service-account-key.ts
1249
1357
  var GoogleServiceAccountKey = class extends Schema.Class("GoogleServiceAccountKey")({
@@ -1432,83 +1540,6 @@ var ProjectsGroup = class extends HttpApiGroup.make("projects").add(HttpApiEndpo
1432
1540
  description: "Project management endpoints"
1433
1541
  })) {};
1434
1542
 
1435
- //#endregion
1436
- //#region ../../packages/api/src/domain/update.ts
1437
- var Update = class extends Schema.Class("Update")({
1438
- id: Id,
1439
- branchId: Id,
1440
- runtimeVersion: Schema.String,
1441
- platform: Platform,
1442
- message: Schema.String,
1443
- metadataJson: Schema.String,
1444
- extraJson: Schema.NullOr(Schema.String),
1445
- groupId: Schema.String,
1446
- rolloutPercentage: Schema.Number,
1447
- isRollback: Schema.Boolean,
1448
- signature: Schema.NullOr(Schema.String),
1449
- certificateChain: Schema.NullOr(Schema.String),
1450
- manifestBody: Schema.NullOr(Schema.String),
1451
- directiveBody: Schema.NullOr(Schema.String),
1452
- createdAt: DateTimeString
1453
- }) {};
1454
- const UpdateSortColumn = Schema.Literal("createdAt", "runtimeVersion", "platform", "rolloutPercentage");
1455
- /**
1456
- * Sort param: column name optionally prefixed with `-` for descending.
1457
- * Example: `runtimeVersion` (asc), `-createdAt` (desc).
1458
- */
1459
- const UpdateSort = Schema.Union(UpdateSortColumn, Schema.TemplateLiteral("-", UpdateSortColumn));
1460
- const ListUpdatesParams = Schema.Struct({
1461
- projectId: Id,
1462
- branchId: Schema.optional(Id),
1463
- platform: Schema.optional(Platform),
1464
- ...PaginationParams.fields,
1465
- sort: Schema.optional(UpdateSort)
1466
- });
1467
- const AssetRef = Schema.Struct({
1468
- hash: Schema.String,
1469
- key: Schema.String,
1470
- isLaunch: Schema.Boolean,
1471
- contentChecksum: Schema.optional(Schema.String)
1472
- });
1473
- const CreateUpdateBody = Schema.Struct({
1474
- branch: Schema.String.pipe(Schema.minLength(1)),
1475
- slug: Schema.String.pipe(Schema.minLength(1)),
1476
- runtimeVersion: Schema.String.pipe(Schema.minLength(1)),
1477
- platform: Platform,
1478
- message: Schema.String,
1479
- groupId: Schema.String.pipe(Schema.minLength(1)),
1480
- metadata: Schema.Record({
1481
- key: Schema.String,
1482
- value: Schema.Unknown
1483
- }),
1484
- extra: Schema.optional(Schema.Record({
1485
- key: Schema.String,
1486
- value: Schema.Unknown
1487
- })),
1488
- assets: Schema.Array(AssetRef),
1489
- manifestBody: Schema.optional(Schema.String),
1490
- directiveBody: Schema.optional(Schema.String),
1491
- isRollback: Schema.optional(Schema.Boolean),
1492
- signature: Schema.optional(Schema.String),
1493
- certificateChain: Schema.optional(Schema.String),
1494
- rolloutPercentage: Schema.optional(Schema.Number.pipe(Schema.int(), Schema.between(1, 100)))
1495
- });
1496
- const RepublishBody = Schema.Struct({
1497
- sourceUpdateId: Schema.optional(Id),
1498
- sourceGroupId: Schema.optional(Schema.String.pipe(Schema.minLength(1))),
1499
- destinationBranchId: Schema.optional(Id),
1500
- destinationChannel: Schema.optional(Schema.String.pipe(Schema.minLength(1))),
1501
- message: Schema.optional(Schema.String),
1502
- signedUpdates: Schema.optional(Schema.Array(Schema.Struct({
1503
- sourceUpdateId: Id,
1504
- manifestBody: Schema.String.pipe(Schema.minLength(1)),
1505
- signature: Schema.String.pipe(Schema.minLength(1)),
1506
- certificateChain: Schema.String.pipe(Schema.minLength(1))
1507
- })))
1508
- });
1509
- const RepublishResult = Schema.Struct({ updates: Schema.Array(Update) });
1510
- const DeleteUpdateResult = Schema.Struct({ deleted: Schema.Number });
1511
-
1512
1543
  //#endregion
1513
1544
  //#region ../../packages/api/src/groups/updates.ts
1514
1545
  const idParam$1 = HttpApiSchema.param("id", Schema.String);
@@ -1527,6 +1558,12 @@ var UpdatesGroup = class extends HttpApiGroup.make("updates").add(HttpApiEndpoin
1527
1558
  }))).add(HttpApiEndpoint.get("get")`/api/updates/${idParam$1}`.addSuccess(Update).annotateContext(OpenApi.annotations({
1528
1559
  title: "Get update",
1529
1560
  description: "Fetch a single update by ID"
1561
+ }))).add(HttpApiEndpoint.get("getGroup")`/api/update-groups/${groupIdParam}`.addSuccess(Schema.Struct({ items: Schema.Array(Update) })).annotateContext(OpenApi.annotations({
1562
+ title: "Get update group",
1563
+ description: "Fetch all updates in a group (paired iOS + Android variants)"
1564
+ }))).add(HttpApiEndpoint.get("listAssets")`/api/updates/${idParam$1}/assets`.addSuccess(Schema.Array(UpdateAssetEntry)).annotateContext(OpenApi.annotations({
1565
+ title: "List update assets",
1566
+ description: "Fetch the asset references (key + hash + launch flag) for an update"
1530
1567
  }))).add(HttpApiEndpoint.del("deleteGroup")`/api/updates/${groupIdParam}`.addSuccess(DeleteUpdateResult).annotateContext(OpenApi.annotations({
1531
1568
  title: "Delete update group",
1532
1569
  description: "Delete all updates in a group (paired iOS + Android updates)"
@@ -1604,7 +1641,7 @@ var WebhooksGroup = class extends HttpApiGroup.make("webhooks").add(HttpApiEndpo
1604
1641
 
1605
1642
  //#endregion
1606
1643
  //#region ../../packages/api/src/api.ts
1607
- var ManagementApi = class extends HttpApi.make("management-api").add(ProjectsGroup).add(BranchesGroup).add(ChannelsGroup).add(UpdatesGroup).add(AssetsGroup).add(AnalyticsGroup).add(BuildsGroup).add(EnvVarsGroup).add(AuditLogsGroup).add(DevicesGroup).add(AppleTeamsGroup).add(AppleDistributionCertificatesGroup).add(ApplePushKeysGroup).add(AscApiKeysGroup).add(AppleProvisioningProfilesGroup).add(GoogleServiceAccountKeysGroup).add(IosBundleConfigurationsGroup).add(AndroidApplicationIdentifiersGroup).add(AndroidUploadKeystoresGroup).add(AndroidBuildCredentialsGroup).add(BuildCredentialsGroup).add(MeGroup).add(WebhooksGroup).middleware(Authentication).annotateContext(OpenApi.annotations({
1644
+ var ManagementApi = class extends HttpApi.make("management-api").add(ProjectsGroup).add(BranchesGroup).add(ChannelsGroup).add(UpdatesGroup).add(AssetsGroup).add(AnalyticsGroup).add(BuildsGroup).add(EnvVarsGroup).add(FingerprintsGroup).add(AuditLogsGroup).add(DevicesGroup).add(AppleTeamsGroup).add(AppleDistributionCertificatesGroup).add(ApplePushKeysGroup).add(AscApiKeysGroup).add(AppleProvisioningProfilesGroup).add(GoogleServiceAccountKeysGroup).add(IosBundleConfigurationsGroup).add(AndroidApplicationIdentifiersGroup).add(AndroidUploadKeystoresGroup).add(AndroidBuildCredentialsGroup).add(BuildCredentialsGroup).add(MeGroup).add(WebhooksGroup).middleware(Authentication).annotateContext(OpenApi.annotations({
1608
1645
  title: "Better Update Management API",
1609
1646
  version: "1.0.0",
1610
1647
  description: "Management API for OTA update publishing, deployment, and analytics"
@@ -1665,6 +1702,7 @@ var InvalidArgumentError = class extends Data.TaggedError("InvalidArgumentError"
1665
1702
  var InteractiveProhibitedError = class extends Data.TaggedError("InteractiveProhibitedError") {};
1666
1703
  var CredentialsJsonError = class extends Data.TaggedError("CredentialsJsonError") {};
1667
1704
  var DirtyRepoError = class extends Data.TaggedError("DirtyRepoError") {};
1705
+ var StagingError = class extends Data.TaggedError("StagingError") {};
1668
1706
 
1669
1707
  //#endregion
1670
1708
  //#region src/lib/format-error.ts
@@ -4148,7 +4186,8 @@ const downloadAndroidCredentials = (api, options) => Effect.gen(function* () {
4148
4186
  path: { projectId: options.projectId },
4149
4187
  payload: {
4150
4188
  platform: "android",
4151
- applicationIdentifier: options.applicationIdentifier
4189
+ applicationIdentifier: options.applicationIdentifier,
4190
+ ...options.buildProfile === void 0 ? {} : { buildProfile: options.buildProfile }
4152
4191
  }
4153
4192
  }).pipe(Effect.mapError((cause) => resolveErrorToMissingCredentials(cause, "android")));
4154
4193
  if (resolved.platform !== "android") return yield* Effect.fail(new MissingCredentialsError({
@@ -4592,7 +4631,8 @@ const runAndroidBuild = (input) => Effect.gen(function* () {
4592
4631
  const credentials = input.credentialsSource === "local" ? yield* loadLocalAndroidCredentials({ projectRoot }) : yield* downloadAndroidCredentials(api, {
4593
4632
  projectId,
4594
4633
  applicationIdentifier,
4595
- tempDir
4634
+ tempDir,
4635
+ buildProfile: input.profileName
4596
4636
  });
4597
4637
  yield* runStep({
4598
4638
  command: "bunx",
@@ -5911,7 +5951,8 @@ const buildReserveCommon = (input) => ({
5911
5951
  ...input.buildNumber === void 0 ? {} : { buildNumber: input.buildNumber },
5912
5952
  ...input.gitContext.ref === void 0 ? {} : { gitRef: input.gitContext.ref },
5913
5953
  ...input.gitContext.commit === void 0 ? {} : { gitCommit: input.gitContext.commit },
5914
- ...input.message === void 0 ? {} : { message: input.message }
5954
+ ...input.message === void 0 ? {} : { message: input.message },
5955
+ ...input.fingerprintHash === void 0 ? {} : { fingerprintHash: input.fingerprintHash }
5915
5956
  });
5916
5957
  const callReserve = (api, input) => {
5917
5958
  const common = buildReserveCommon(input);
@@ -6405,6 +6446,26 @@ const pullEnvVars = (api, { projectId, environment }) => {
6405
6446
  } }).pipe(Effect.map((result) => Object.fromEntries(result.items.map((item) => [item.key, item.value]))), Effect.mapError((cause) => new EnvExportError({ message: `Failed to export environment variables for "${environment}": ${String(cause)}` })));
6406
6447
  };
6407
6448
 
6449
+ //#endregion
6450
+ //#region src/lib/fingerprint.ts
6451
+ var FingerprintError = class extends Data.TaggedError("FingerprintError") {};
6452
+ const runFingerprintFull = (projectRoot) => Effect.gen(function* () {
6453
+ const cmd = Command.make("bunx", "@expo/fingerprint", projectRoot).pipe(Command.workingDirectory(projectRoot));
6454
+ const stdout = yield* Command.string(cmd).pipe(Effect.mapError((cause) => new FingerprintError({ message: `Failed to run "@expo/fingerprint": ${cause.message}` })));
6455
+ const parsed = yield* Effect.try({
6456
+ try: () => JSON.parse(stdout),
6457
+ catch: () => new FingerprintError({ message: "Failed to parse @expo/fingerprint output as JSON." })
6458
+ });
6459
+ if (!isRecord(parsed)) return yield* new FingerprintError({ message: "@expo/fingerprint output was not a JSON object." });
6460
+ const { hash } = parsed;
6461
+ if (typeof hash !== "string" || hash.length === 0) return yield* new FingerprintError({ message: "@expo/fingerprint output did not contain a \"hash\" string field." });
6462
+ const sourcesRaw = parsed["sources"];
6463
+ return {
6464
+ hash,
6465
+ sources: Array.isArray(sourcesRaw) ? sourcesRaw : []
6466
+ };
6467
+ });
6468
+
6408
6469
  //#endregion
6409
6470
  //#region src/lib/git-context.ts
6410
6471
  const runString = (cmd, cwd) => Command.string(Command.workingDirectory(cmd, cwd));
@@ -6520,6 +6581,131 @@ const detectPlatform = (explicit, config) => Effect.gen(function* () {
6520
6581
  })));
6521
6582
  });
6522
6583
 
6584
+ //#endregion
6585
+ //#region src/lib/project-staging.ts
6586
+ const LOCKFILES = [
6587
+ ["bun.lock", "bun"],
6588
+ ["bun.lockb", "bun"],
6589
+ ["pnpm-lock.yaml", "pnpm"],
6590
+ ["yarn.lock", "yarn"],
6591
+ ["package-lock.json", "npm"]
6592
+ ];
6593
+ /**
6594
+ * Paths never copied into staging — covers generated native build outputs and
6595
+ * dependency dirs that must be reinstalled fresh in staging.
6596
+ */
6597
+ const ALWAYS_IGNORE = [
6598
+ "node_modules",
6599
+ ".git",
6600
+ "ios/build",
6601
+ "ios/Pods",
6602
+ "ios/DerivedData",
6603
+ "android/build",
6604
+ "android/app/build",
6605
+ "android/.gradle",
6606
+ "android/.kotlin",
6607
+ ".expo",
6608
+ ".gradle",
6609
+ ".turbo",
6610
+ "dist"
6611
+ ];
6612
+ const findLockfile = (fs, dir) => Effect.gen(function* () {
6613
+ for (const [name, pm] of LOCKFILES) if (yield* fs.exists(path.join(dir, name)).pipe(Effect.catchAll(() => Effect.succeed(false)))) return pm;
6614
+ });
6615
+ const walkUpForLockfile = (startCwd, dir) => Effect.gen(function* () {
6616
+ const pm = yield* findLockfile(yield* FileSystem.FileSystem, dir);
6617
+ if (pm !== void 0) return {
6618
+ workspaceRoot: dir,
6619
+ packageManager: pm
6620
+ };
6621
+ const parent = path.dirname(dir);
6622
+ if (parent === dir) return {
6623
+ workspaceRoot: startCwd,
6624
+ packageManager: "bun"
6625
+ };
6626
+ return yield* walkUpForLockfile(startCwd, parent);
6627
+ });
6628
+ /**
6629
+ * Walk up from `cwd` to the first ancestor directory containing a lockfile.
6630
+ * That directory is the install root (monorepo workspace root or the app dir
6631
+ * itself in single-app layouts). Defaults to `cwd` + bun when no lockfile is
6632
+ * found anywhere up to the volume root.
6633
+ */
6634
+ const detectWorkspaceRoot = (cwd) => walkUpForLockfile(cwd, cwd);
6635
+ /**
6636
+ * Build an `Ignore` matcher for the workspace root. `.easignore` REPLACES
6637
+ * `.gitignore` when present (matches EAS semantics); otherwise `.gitignore`
6638
+ * is layered on top of the always-ignore baseline.
6639
+ */
6640
+ const buildIgnoreInstance = (workspaceRoot) => Effect.gen(function* () {
6641
+ const fs = yield* FileSystem.FileSystem;
6642
+ const ig = ignore();
6643
+ ig.add([...ALWAYS_IGNORE]);
6644
+ const easignorePath = path.join(workspaceRoot, ".easignore");
6645
+ if (yield* fs.exists(easignorePath).pipe(Effect.catchAll(() => Effect.succeed(false)))) {
6646
+ const content = yield* fs.readFileString(easignorePath).pipe(Effect.catchAll(() => Effect.succeed("")));
6647
+ ig.add(content);
6648
+ return ig;
6649
+ }
6650
+ const gitignorePath = path.join(workspaceRoot, ".gitignore");
6651
+ if (yield* fs.exists(gitignorePath).pipe(Effect.catchAll(() => Effect.succeed(false)))) {
6652
+ const content = yield* fs.readFileString(gitignorePath).pipe(Effect.catchAll(() => Effect.succeed("")));
6653
+ ig.add(content);
6654
+ }
6655
+ return ig;
6656
+ });
6657
+ const copyProjectTree = (params) => Effect.tryPromise({
6658
+ try: async () => {
6659
+ await promises.cp(params.source, params.dest, {
6660
+ recursive: true,
6661
+ dereference: false,
6662
+ filter: (src) => {
6663
+ const rel = path.relative(params.source, src);
6664
+ if (rel === "") return true;
6665
+ const posixRel = rel.split(path.sep).join("/");
6666
+ return !params.ig.ignores(posixRel);
6667
+ }
6668
+ });
6669
+ },
6670
+ catch: (cause) => new StagingError({ message: `Failed to copy project to staging dir: ${formatCause(cause)}` })
6671
+ });
6672
+ const runInstall = (params) => runStep({
6673
+ command: params.packageManager,
6674
+ args: ["install"],
6675
+ cwd: params.stagingRoot,
6676
+ env: params.env
6677
+ }, `${params.packageManager} install`);
6678
+ /**
6679
+ * Copy the user's project (or workspace root, for monorepos) into a fresh
6680
+ * directory inside `tempDir`, then run `<pm> install` there. The build then
6681
+ * runs entirely against the staged copy — the user's working tree stays clean
6682
+ * regardless of what `expo prebuild`, `pod install`, or `gradlew` write.
6683
+ */
6684
+ const prepareStagingProject = (input) => Effect.gen(function* () {
6685
+ const runtime = yield* CliRuntime;
6686
+ const { workspaceRoot, packageManager } = yield* detectWorkspaceRoot(input.userCwd);
6687
+ const relAppPath = path.relative(workspaceRoot, input.userCwd);
6688
+ const stagingRoot = path.join(input.tempDir, "project");
6689
+ const projectRoot = relAppPath === "" ? stagingRoot : path.join(stagingRoot, relAppPath);
6690
+ yield* Console.log(`Staging build into ${stagingRoot}${relAppPath === "" ? "" : ` (app: ${relAppPath})`}`);
6691
+ yield* copyProjectTree({
6692
+ source: workspaceRoot,
6693
+ dest: stagingRoot,
6694
+ ig: yield* buildIgnoreInstance(workspaceRoot)
6695
+ });
6696
+ yield* runInstall({
6697
+ stagingRoot,
6698
+ packageManager,
6699
+ env: yield* runtime.commandEnvironment(input.envVars)
6700
+ });
6701
+ return {
6702
+ stagingRoot,
6703
+ projectRoot,
6704
+ packageManager,
6705
+ relAppPath
6706
+ };
6707
+ });
6708
+
6523
6709
  //#endregion
6524
6710
  //#region src/lib/repo-clean.ts
6525
6711
  const MAX_FILES_SHOWN = 10;
@@ -6543,26 +6729,6 @@ const ensureRepoClean = ({ projectRoot, allowDirty, label }) => Effect.gen(funct
6543
6729
  if (!(yield* promptConfirm(`Continue ${label} with uncommitted changes?`, { initialValue: false }))) yield* new DirtyRepoError({ message: `${label} cancelled by user.` });
6544
6730
  });
6545
6731
 
6546
- //#endregion
6547
- //#region src/lib/fingerprint.ts
6548
- var FingerprintError = class extends Data.TaggedError("FingerprintError") {};
6549
- const runFingerprintFull = (projectRoot) => Effect.gen(function* () {
6550
- const cmd = Command.make("bunx", "@expo/fingerprint", projectRoot).pipe(Command.workingDirectory(projectRoot));
6551
- const stdout = yield* Command.string(cmd).pipe(Effect.mapError((cause) => new FingerprintError({ message: `Failed to run "@expo/fingerprint": ${cause.message}` })));
6552
- const parsed = yield* Effect.try({
6553
- try: () => JSON.parse(stdout),
6554
- catch: () => new FingerprintError({ message: "Failed to parse @expo/fingerprint output as JSON." })
6555
- });
6556
- if (!isRecord(parsed)) return yield* new FingerprintError({ message: "@expo/fingerprint output was not a JSON object." });
6557
- const { hash } = parsed;
6558
- if (typeof hash !== "string" || hash.length === 0) return yield* new FingerprintError({ message: "@expo/fingerprint output did not contain a \"hash\" string field." });
6559
- const sourcesRaw = parsed["sources"];
6560
- return {
6561
- hash,
6562
- sources: Array.isArray(sourcesRaw) ? sourcesRaw : []
6563
- };
6564
- });
6565
-
6566
6732
  //#endregion
6567
6733
  //#region src/lib/runtime-version.ts
6568
6734
  const resolveRuntimeVersion = ({ raw, appVersion, projectRoot }) => Effect.gen(function* () {
@@ -6641,7 +6807,8 @@ const runAndroidPlatformBuild = (input) => Effect.gen(function* () {
6641
6807
  applicationIdentifier,
6642
6808
  envVars,
6643
6809
  projectId,
6644
- credentialsSource
6810
+ credentialsSource,
6811
+ profileName: profile.name
6645
6812
  }),
6646
6813
  target: androidProfile.format === "aab" ? {
6647
6814
  platform: "android",
@@ -6669,35 +6836,40 @@ const resolveProfileName = (projectRoot, requested) => Effect.gen(function* () {
6669
6836
  });
6670
6837
  const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
6671
6838
  const api = yield* apiClient;
6672
- const projectRoot = yield* (yield* CliRuntime).cwd;
6839
+ const userCwd = yield* (yield* CliRuntime).cwd;
6673
6840
  yield* ensureRepoClean({
6674
- projectRoot,
6841
+ projectRoot: userCwd,
6675
6842
  allowDirty: options.allowDirty ?? false,
6676
6843
  label: "build"
6677
6844
  });
6678
- const baseConfig = yield* readExpoConfig(projectRoot);
6845
+ const baseConfig = yield* readExpoConfig(userCwd);
6679
6846
  const projectId = yield* extractProjectId(baseConfig);
6680
6847
  const platform = yield* detectPlatform(options.platform, baseConfig);
6681
- const profile = yield* readBuildProfile(projectRoot, yield* resolveProfileName(projectRoot, options.profileName));
6848
+ const profile = yield* readBuildProfile(userCwd, yield* resolveProfileName(userCwd, options.profileName));
6682
6849
  const envVars = yield* pullEnvVars(api, {
6683
6850
  projectId,
6684
6851
  environment: profile.environment
6685
6852
  });
6686
6853
  yield* applyAutoIncrement({
6687
- projectRoot,
6854
+ projectRoot: userCwd,
6688
6855
  platform,
6689
- config: yield* readExpoConfig(projectRoot, envVars),
6856
+ config: yield* readExpoConfig(userCwd, envVars),
6690
6857
  ...platform === "ios" && profile.ios?.autoIncrement !== void 0 ? { iosMode: profile.ios.autoIncrement } : {},
6691
6858
  ...platform === "android" && profile.android?.autoIncrement !== void 0 ? { androidMode: profile.android.autoIncrement } : {}
6692
6859
  });
6693
- const appMeta = yield* readAppMeta(yield* readExpoConfig(projectRoot, envVars), platform);
6860
+ const appMeta = yield* readAppMeta(yield* readExpoConfig(userCwd, envVars), platform);
6694
6861
  const runtimeVersion = yield* resolveRuntimeVersion({
6695
6862
  raw: appMeta.rawRuntimeVersion,
6696
6863
  appVersion: appMeta.appVersion,
6697
- projectRoot
6864
+ projectRoot: userCwd
6698
6865
  });
6699
- if (options.clearCache) yield* clearBuildCaches(projectRoot);
6866
+ if (options.clearCache) yield* clearBuildCaches(userCwd);
6700
6867
  const tempDir = yield* acquireBuildTempDir;
6868
+ const staging = yield* prepareStagingProject({
6869
+ userCwd,
6870
+ tempDir,
6871
+ envVars
6872
+ });
6701
6873
  yield* Console.log(`Building ${platform} artifact for profile "${profile.name}" (runtimeVersion=${runtimeVersion})`);
6702
6874
  const { build, target, bundleId } = yield* runPlatformBuild({
6703
6875
  api,
@@ -6707,14 +6879,14 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
6707
6879
  appMeta,
6708
6880
  envVars,
6709
6881
  projectId,
6710
- projectRoot,
6882
+ projectRoot: staging.projectRoot,
6711
6883
  tempDir
6712
6884
  });
6713
6885
  yield* Console.log(`Artifact produced: ${build.artifactPath}`);
6714
6886
  let exportedArtifactPath = void 0;
6715
6887
  if (options.output !== void 0) {
6716
6888
  const fs = yield* FileSystem.FileSystem;
6717
- const outputPath = path.resolve(projectRoot, options.output);
6889
+ const outputPath = path.resolve(userCwd, options.output);
6718
6890
  const outputDir = path.dirname(outputPath);
6719
6891
  yield* fs.makeDirectory(outputDir, { recursive: true }).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to create output directory: ${formatCause(cause)}` })));
6720
6892
  yield* fs.copyFile(build.artifactPath, outputPath).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to copy artifact to ${outputPath}: ${formatCause(cause)}` })));
@@ -6731,12 +6903,13 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
6731
6903
  ]);
6732
6904
  return;
6733
6905
  }
6734
- const rawGitContext = yield* readGitContext(projectRoot);
6906
+ const rawGitContext = yield* readGitContext(userCwd);
6735
6907
  const gitContext = {
6736
6908
  ...rawGitContext.ref === void 0 ? {} : { ref: rawGitContext.ref },
6737
6909
  ...rawGitContext.commit === void 0 ? {} : { commit: rawGitContext.commit },
6738
6910
  dirty: rawGitContext.dirty
6739
6911
  };
6912
+ const fingerprintHash = yield* runFingerprintFull(userCwd).pipe(Effect.map((entry) => entry.hash), Effect.catchAll(() => Effect.succeed(void 0)));
6740
6913
  const result = yield* reserveAndUpload(api, {
6741
6914
  target,
6742
6915
  projectId,
@@ -6747,6 +6920,7 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
6747
6920
  bundleId,
6748
6921
  gitContext,
6749
6922
  ...options.message === void 0 ? {} : { message: options.message },
6923
+ ...fingerprintHash === void 0 ? {} : { fingerprintHash },
6750
6924
  artifactPath: build.artifactPath,
6751
6925
  sha256: build.sha256,
6752
6926
  byteSize: build.byteSize
@@ -6875,7 +7049,8 @@ const BUILD_EXIT_EXTRAS = {
6875
7049
  PresignedUrlExpiredError: 7,
6876
7050
  CompleteError: 7,
6877
7051
  EnvExportError: 7,
6878
- DirtyRepoError: 3
7052
+ DirtyRepoError: 3,
7053
+ StagingError: 6
6879
7054
  };
6880
7055
  const buildCommand = defineCommand({
6881
7056
  meta: {
@@ -7633,6 +7808,7 @@ const runUploadWorkflow = (options) => Effect.gen(function* () {
7633
7808
  ...rawGitContext.commit === void 0 ? {} : { commit: rawGitContext.commit },
7634
7809
  dirty: rawGitContext.dirty
7635
7810
  };
7811
+ const fingerprintHash = yield* runFingerprintFull(projectRoot).pipe(Effect.map((entry) => entry.hash), Effect.catchAll(() => Effect.succeed(void 0)));
7636
7812
  const result = yield* reserveAndUpload(api, {
7637
7813
  target,
7638
7814
  projectId,
@@ -7643,6 +7819,7 @@ const runUploadWorkflow = (options) => Effect.gen(function* () {
7643
7819
  bundleId,
7644
7820
  gitContext,
7645
7821
  ...options.message === void 0 ? {} : { message: options.message },
7822
+ ...fingerprintHash === void 0 ? {} : { fingerprintHash },
7646
7823
  artifactPath: options.artifactPath,
7647
7824
  sha256,
7648
7825
  byteSize
@@ -13482,7 +13659,8 @@ const publishPlatform = (params) => Effect.gen(function* () {
13482
13659
  signature: params.signedPayload.signature,
13483
13660
  certificateChain: params.signedPayload.certificateChain
13484
13661
  } : {},
13485
- ...params.rolloutPercentage === void 0 ? {} : { rolloutPercentage: params.rolloutPercentage }
13662
+ ...params.rolloutPercentage === void 0 ? {} : { rolloutPercentage: params.rolloutPercentage },
13663
+ ...params.fingerprintHash === void 0 ? {} : { fingerprintHash: params.fingerprintHash }
13486
13664
  } }).pipe(Effect.mapError((cause) => new UpdatePublishError({ message: `Failed to publish ${params.platform} update: ${formatCause(cause)}` })));
13487
13665
  return {
13488
13666
  platform: params.platform,
@@ -13531,6 +13709,7 @@ const runUpdatePublish = (options) => Effect.scoped(Effect.gen(function* () {
13531
13709
  const sharedExportDir = options.inputDir === void 0 ? void 0 : path.resolve(projectRoot, options.inputDir);
13532
13710
  const groupId = randomUUID();
13533
13711
  const message = resolvedMessage ?? "Publish via better-update CLI";
13712
+ const fingerprintHash = yield* runFingerprintFull(projectRoot).pipe(Effect.map((result) => result.hash), Effect.catchAll(() => Effect.succeed(void 0)));
13534
13713
  if ((yield* InteractiveMode).allow && !options.auto) {
13535
13714
  if (!(yield* confirmPublishPreview({
13536
13715
  branch,
@@ -13576,6 +13755,7 @@ const runUpdatePublish = (options) => Effect.scoped(Effect.gen(function* () {
13576
13755
  platform,
13577
13756
  signedPayload: signedPayloads[platform] ?? null,
13578
13757
  rolloutPercentage: options.rolloutPercentage,
13758
+ fingerprintHash,
13579
13759
  skipBundler: options.skipBundler,
13580
13760
  noBytecode: options.noBytecode,
13581
13761
  sourceMaps: options.sourceMaps