@better-update/cli 0.25.0 → 0.27.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 +1732 -411
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import AppleUtils from "@expo/apple-utils";
|
|
|
11
11
|
import { autocomplete, cancel, confirm, isCancel, multiselect, password, select, text } from "@clack/prompts";
|
|
12
12
|
import { open, readFile, writeFile } from "node:fs/promises";
|
|
13
13
|
import { X509Certificate, createHash, createSign, createVerify, randomBytes, randomUUID } from "node:crypto";
|
|
14
|
-
import { accessSync, chmodSync, constants, createReadStream,
|
|
14
|
+
import { accessSync, chmodSync, constants, createReadStream, promises } from "node:fs";
|
|
15
15
|
import { Entry } from "@napi-rs/keyring";
|
|
16
16
|
import { once } from "node:events";
|
|
17
17
|
import { createServer } from "node:http";
|
|
@@ -34,7 +34,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
34
34
|
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region package.json
|
|
37
|
-
var version = "0.
|
|
37
|
+
var version = "0.27.0";
|
|
38
38
|
|
|
39
39
|
//#endregion
|
|
40
40
|
//#region src/lib/interactive-mode.ts
|
|
@@ -67,6 +67,7 @@ const cookieSecurity = HttpApiSecurity.apiKey({
|
|
|
67
67
|
key: "__Secure-better-auth.session_token",
|
|
68
68
|
in: "cookie"
|
|
69
69
|
});
|
|
70
|
+
/** @effect-expect-leaking HttpServerRequest | ParsedSearchParams | RouteContext */
|
|
70
71
|
var Authentication = class extends HttpApiMiddleware.Tag()("api/Authentication", {
|
|
71
72
|
failure: Schema.Union(Unauthorized, Forbidden),
|
|
72
73
|
provides: AuthContext,
|
|
@@ -256,7 +257,7 @@ var AnalyticsGroup = class extends HttpApiGroup.make("analytics").add(HttpApiEnd
|
|
|
256
257
|
|
|
257
258
|
//#endregion
|
|
258
259
|
//#region ../../packages/api/src/domain/android-application-identifier.ts
|
|
259
|
-
const AndroidPackageName = Schema.String.pipe(Schema.pattern(/^[A-Za-z][A-Za-z0-9_]*(
|
|
260
|
+
const AndroidPackageName = Schema.String.pipe(Schema.pattern(/^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+$/u, { message: () => "Package name must be reverse-domain style (e.g., com.acme.app)" }));
|
|
260
261
|
var AndroidApplicationIdentifier = class extends Schema.Class("AndroidApplicationIdentifier")({
|
|
261
262
|
id: Id,
|
|
262
263
|
organizationId: Id,
|
|
@@ -1177,6 +1178,44 @@ var BuildsGroup = class extends HttpApiGroup.make("builds").add(HttpApiEndpoint.
|
|
|
1177
1178
|
description: "Build artifact upload, tracking, and download endpoints"
|
|
1178
1179
|
})) {};
|
|
1179
1180
|
|
|
1181
|
+
//#endregion
|
|
1182
|
+
//#region ../../packages/api/src/domain/channel-grant.ts
|
|
1183
|
+
const GrantEffectSchema = Schema.Literal("allow", "deny");
|
|
1184
|
+
/** One member's allow/deny set on a channel; `actions` are "resource:action". */
|
|
1185
|
+
var ChannelGrant = class extends Schema.Class("ChannelGrant")({
|
|
1186
|
+
id: Id,
|
|
1187
|
+
memberId: Id,
|
|
1188
|
+
scopeKind: Schema.Literal("channel"),
|
|
1189
|
+
scopeId: Id,
|
|
1190
|
+
effect: GrantEffectSchema,
|
|
1191
|
+
actions: Schema.Array(Schema.String),
|
|
1192
|
+
createdAt: DateTimeString
|
|
1193
|
+
}) {};
|
|
1194
|
+
/** Upsert one (member, channel, effect) grant. effect defaults to "allow". */
|
|
1195
|
+
const UpsertChannelGrantBody = Schema.Struct({
|
|
1196
|
+
effect: Schema.optionalWith(GrantEffectSchema, { default: () => "allow" }),
|
|
1197
|
+
actions: Schema.Array(Schema.String).pipe(Schema.minItems(1))
|
|
1198
|
+
});
|
|
1199
|
+
const ListChannelGrantsParams = Schema.Struct({});
|
|
1200
|
+
const DeleteChannelGrantResult = Schema.Struct({ deleted: Schema.Number });
|
|
1201
|
+
|
|
1202
|
+
//#endregion
|
|
1203
|
+
//#region ../../packages/api/src/groups/channel-grants.ts
|
|
1204
|
+
const memberIdParam = HttpApiSchema.param("memberId", Schema.String);
|
|
1205
|
+
var ChannelGrantsGroup = class extends HttpApiGroup.make("channelGrants").add(HttpApiEndpoint.get("list")`/api/channels/${idParam}/grants`.setUrlParams(ListChannelGrantsParams).addSuccess(Schema.Array(ChannelGrant)).annotateContext(OpenApi.annotations({
|
|
1206
|
+
title: "List channel grants",
|
|
1207
|
+
description: "List all per-member allow/deny grants on a channel"
|
|
1208
|
+
}))).add(HttpApiEndpoint.put("upsert")`/api/channels/${idParam}/grants/${memberIdParam}`.setPayload(UpsertChannelGrantBody).addSuccess(ChannelGrant).annotateContext(OpenApi.annotations({
|
|
1209
|
+
title: "Upsert channel grant",
|
|
1210
|
+
description: "Create or replace a member's allow/deny grant on a channel"
|
|
1211
|
+
}))).add(HttpApiEndpoint.del("delete")`/api/channels/${idParam}/grants/${memberIdParam}`.addSuccess(DeleteChannelGrantResult).annotateContext(OpenApi.annotations({
|
|
1212
|
+
title: "Delete channel grant",
|
|
1213
|
+
description: "Revoke a member's grants on a channel"
|
|
1214
|
+
}))).addError(NotFound).addError(Forbidden).annotateContext(OpenApi.annotations({
|
|
1215
|
+
title: "Channel grants",
|
|
1216
|
+
description: "Per-channel ABAC permission grants (allow/deny by member)"
|
|
1217
|
+
})) {};
|
|
1218
|
+
|
|
1180
1219
|
//#endregion
|
|
1181
1220
|
//#region ../../packages/api/src/domain/channel.ts
|
|
1182
1221
|
var Channel = class extends Schema.Class("Channel")({
|
|
@@ -1432,6 +1471,71 @@ const EnvVarRevision = Schema.Struct({
|
|
|
1432
1471
|
const EnvVarRevisionsResult = Schema.Struct({ items: Schema.Array(EnvVarRevision) });
|
|
1433
1472
|
const RollbackEnvVarBody = Schema.Struct({ toRevisionId: Id });
|
|
1434
1473
|
|
|
1474
|
+
//#endregion
|
|
1475
|
+
//#region ../../packages/api/src/domain/env-grant.ts
|
|
1476
|
+
/**
|
|
1477
|
+
* One member's allow/deny set on a (project × environment) env-var scope.
|
|
1478
|
+
* `scopeKind` is fixed to "env_var_environment". `scopeId` is the encoded
|
|
1479
|
+
* `<projectId|global>:<environment>` token (server-built). `actions` are
|
|
1480
|
+
* "resource:action" tokens (here always envVar:*).
|
|
1481
|
+
*/
|
|
1482
|
+
var EnvGrant = class extends Schema.Class("EnvGrant")({
|
|
1483
|
+
id: Id,
|
|
1484
|
+
memberId: Id,
|
|
1485
|
+
scopeKind: Schema.Literal("env_var_environment"),
|
|
1486
|
+
scopeId: Schema.String,
|
|
1487
|
+
effect: GrantEffectSchema,
|
|
1488
|
+
actions: Schema.Array(Schema.String),
|
|
1489
|
+
createdAt: DateTimeString
|
|
1490
|
+
}) {};
|
|
1491
|
+
/** A flattened row for the list UI: one member × environment cell. */
|
|
1492
|
+
var EnvGrantRow = class extends Schema.Class("EnvGrantRow")({
|
|
1493
|
+
memberId: Id,
|
|
1494
|
+
environment: EnvVarEnvironment,
|
|
1495
|
+
effect: GrantEffectSchema,
|
|
1496
|
+
actions: Schema.Array(Schema.String)
|
|
1497
|
+
}) {};
|
|
1498
|
+
/**
|
|
1499
|
+
* URL params for listing grants on a project-or-global scope. `projectId` is the
|
|
1500
|
+
* sentinel "global" or a real project id (the server resolves null vs the
|
|
1501
|
+
* sentinel). Carried as a query param.
|
|
1502
|
+
*/
|
|
1503
|
+
const ListEnvGrantsParams = Schema.Struct({ projectId: Schema.String });
|
|
1504
|
+
/**
|
|
1505
|
+
* Upsert one (member, project-or-global, environment) grant. `projectId` null =
|
|
1506
|
+
* org-global vault. effect defaults to "allow". actions are envVar:* tokens.
|
|
1507
|
+
*/
|
|
1508
|
+
const UpsertEnvGrantBody = Schema.Struct({
|
|
1509
|
+
memberId: Id,
|
|
1510
|
+
projectId: Schema.NullOr(Id),
|
|
1511
|
+
environment: EnvVarEnvironment,
|
|
1512
|
+
effect: Schema.optionalWith(GrantEffectSchema, { default: () => "allow" }),
|
|
1513
|
+
actions: Schema.Array(Schema.String).pipe(Schema.minItems(1))
|
|
1514
|
+
});
|
|
1515
|
+
/** Delete both effects for (member, project-or-global, environment). */
|
|
1516
|
+
const DeleteEnvGrantBody = Schema.Struct({
|
|
1517
|
+
memberId: Id,
|
|
1518
|
+
projectId: Schema.NullOr(Id),
|
|
1519
|
+
environment: EnvVarEnvironment
|
|
1520
|
+
});
|
|
1521
|
+
const DeleteEnvGrantResult = Schema.Struct({ deleted: Schema.Number });
|
|
1522
|
+
|
|
1523
|
+
//#endregion
|
|
1524
|
+
//#region ../../packages/api/src/groups/env-grants.ts
|
|
1525
|
+
var EnvGrantsGroup = class extends HttpApiGroup.make("envGrants").add(HttpApiEndpoint.get("list", "/api/env-grants").setUrlParams(ListEnvGrantsParams).addSuccess(Schema.Array(EnvGrantRow)).annotateContext(OpenApi.annotations({
|
|
1526
|
+
title: "List env-var environment grants",
|
|
1527
|
+
description: "List per-member allow/deny env-var grants on a project-or-global scope across all environments. projectId is a real id or the sentinel 'global'."
|
|
1528
|
+
}))).add(HttpApiEndpoint.put("upsert", "/api/env-grants").setPayload(UpsertEnvGrantBody).addSuccess(EnvGrant).annotateContext(OpenApi.annotations({
|
|
1529
|
+
title: "Upsert env-var environment grant",
|
|
1530
|
+
description: "Create or replace a member's allow/deny env-var grant on one (project-or-global × environment) scope. projectId null = org-global."
|
|
1531
|
+
}))).add(HttpApiEndpoint.del("delete", "/api/env-grants").setPayload(DeleteEnvGrantBody).addSuccess(DeleteEnvGrantResult).annotateContext(OpenApi.annotations({
|
|
1532
|
+
title: "Delete env-var environment grants",
|
|
1533
|
+
description: "Revoke both allow and deny grants for a member on one (project-or-global × environment) scope."
|
|
1534
|
+
}))).addError(NotFound).addError(Forbidden).addError(BadRequest).annotateContext(OpenApi.annotations({
|
|
1535
|
+
title: "Env-var environment grants",
|
|
1536
|
+
description: "Per (project × environment) ABAC permission grants for env vars (allow/deny by member)"
|
|
1537
|
+
})) {};
|
|
1538
|
+
|
|
1435
1539
|
//#endregion
|
|
1436
1540
|
//#region ../../packages/api/src/groups/env-vars.ts
|
|
1437
1541
|
var EnvVarsGroup = class extends HttpApiGroup.make("env-vars").add(HttpApiEndpoint.post("create", "/api/env-vars").setPayload(CreateEnvVarBody).addSuccess(EnvVar, { status: 201 }).annotateContext(OpenApi.annotations({
|
|
@@ -1805,6 +1909,54 @@ var MeGroup = class extends HttpApiGroup.make("me").add(HttpApiEndpoint.get("get
|
|
|
1805
1909
|
description: "Current authenticated actor information"
|
|
1806
1910
|
})) {};
|
|
1807
1911
|
|
|
1912
|
+
//#endregion
|
|
1913
|
+
//#region ../../packages/api/src/domain/org-role.ts
|
|
1914
|
+
/** One resource→actions grant inside a role's permission set. */
|
|
1915
|
+
const PermissionGrantSchema = Schema.Struct({
|
|
1916
|
+
resource: Schema.String,
|
|
1917
|
+
actions: Schema.Array(Schema.String)
|
|
1918
|
+
});
|
|
1919
|
+
var OrgRole = class extends Schema.Class("OrgRole")({
|
|
1920
|
+
id: Id,
|
|
1921
|
+
organizationId: Id,
|
|
1922
|
+
role: Schema.String,
|
|
1923
|
+
permissions: Schema.Array(PermissionGrantSchema),
|
|
1924
|
+
createdAt: DateTimeString,
|
|
1925
|
+
updatedAt: Schema.NullOr(DateTimeString)
|
|
1926
|
+
}) {};
|
|
1927
|
+
const CreateOrgRoleBody = Schema.Struct({
|
|
1928
|
+
name: Schema.String.pipe(Schema.minLength(1)),
|
|
1929
|
+
permissions: Schema.Array(PermissionGrantSchema)
|
|
1930
|
+
});
|
|
1931
|
+
const UpdateOrgRoleBody = Schema.Struct({
|
|
1932
|
+
permissions: Schema.optional(Schema.Array(PermissionGrantSchema)),
|
|
1933
|
+
name: Schema.optional(Schema.String.pipe(Schema.minLength(1)))
|
|
1934
|
+
});
|
|
1935
|
+
const ListOrgRolesParams = Schema.Struct({ organizationId: Id });
|
|
1936
|
+
const DeleteOrgRoleResult = Schema.Struct({ deleted: Schema.Number });
|
|
1937
|
+
|
|
1938
|
+
//#endregion
|
|
1939
|
+
//#region ../../packages/api/src/groups/org-roles.ts
|
|
1940
|
+
var OrgRolesGroup = class extends HttpApiGroup.make("roles").add(HttpApiEndpoint.get("list", "/api/roles").setUrlParams(ListOrgRolesParams).addSuccess(Schema.Array(OrgRole)).annotateContext(OpenApi.annotations({
|
|
1941
|
+
title: "List custom roles",
|
|
1942
|
+
description: "List all custom roles defined for an organization"
|
|
1943
|
+
}))).add(HttpApiEndpoint.post("create", "/api/roles").setPayload(CreateOrgRoleBody).addSuccess(OrgRole, { status: 201 }).annotateContext(OpenApi.annotations({
|
|
1944
|
+
title: "Create custom role",
|
|
1945
|
+
description: "Create a new custom role with a permission set"
|
|
1946
|
+
}))).add(HttpApiEndpoint.get("get")`/api/roles/${idParam}`.addSuccess(OrgRole).annotateContext(OpenApi.annotations({
|
|
1947
|
+
title: "Get custom role",
|
|
1948
|
+
description: "Fetch a single custom role by id"
|
|
1949
|
+
}))).add(HttpApiEndpoint.patch("update")`/api/roles/${idParam}`.setPayload(UpdateOrgRoleBody).addSuccess(OrgRole).annotateContext(OpenApi.annotations({
|
|
1950
|
+
title: "Update custom role",
|
|
1951
|
+
description: "Rename a custom role or replace its permission set"
|
|
1952
|
+
}))).add(HttpApiEndpoint.del("delete")`/api/roles/${idParam}`.addSuccess(DeleteOrgRoleResult).annotateContext(OpenApi.annotations({
|
|
1953
|
+
title: "Delete custom role",
|
|
1954
|
+
description: "Delete a custom role"
|
|
1955
|
+
}))).addError(NotFound).addError(Conflict).addError(Forbidden).annotateContext(OpenApi.annotations({
|
|
1956
|
+
title: "Roles",
|
|
1957
|
+
description: "Custom organization role management (dynamic access control)"
|
|
1958
|
+
})) {};
|
|
1959
|
+
|
|
1808
1960
|
//#endregion
|
|
1809
1961
|
//#region ../../packages/api/src/groups/org-vault.ts
|
|
1810
1962
|
/** `:keyId` path parameter — a registered recipient's `user_encryption_keys.id`. */
|
|
@@ -2155,7 +2307,7 @@ var WebhooksGroup = class extends HttpApiGroup.make("webhooks").add(HttpApiEndpo
|
|
|
2155
2307
|
|
|
2156
2308
|
//#endregion
|
|
2157
2309
|
//#region ../../packages/api/src/api.ts
|
|
2158
|
-
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(IosAppMetadataGroup).add(SubmissionsGroup).add(AndroidApplicationIdentifiersGroup).add(AndroidUploadKeystoresGroup).add(AndroidBuildCredentialsGroup).add(BuildCredentialsGroup).add(UserEncryptionKeysGroup).add(OrgVaultGroup).add(MeGroup).add(WebhooksGroup).add(AdminGroup).middleware(Authentication).annotateContext(OpenApi.annotations({
|
|
2310
|
+
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(IosAppMetadataGroup).add(SubmissionsGroup).add(AndroidApplicationIdentifiersGroup).add(AndroidUploadKeystoresGroup).add(AndroidBuildCredentialsGroup).add(BuildCredentialsGroup).add(UserEncryptionKeysGroup).add(OrgVaultGroup).add(MeGroup).add(WebhooksGroup).add(AdminGroup).add(OrgRolesGroup).add(ChannelGrantsGroup).add(EnvGrantsGroup).middleware(Authentication).annotateContext(OpenApi.annotations({
|
|
2159
2311
|
title: "Better Update Management API",
|
|
2160
2312
|
version: "1.0.0",
|
|
2161
2313
|
description: "Management API for OTA update publishing, deployment, and analytics"
|
|
@@ -2308,13 +2460,13 @@ const ConfigStoreLive = Layer.effect(ConfigStore, Effect.gen(function* () {
|
|
|
2308
2460
|
const runtime = yield* CliRuntime;
|
|
2309
2461
|
const homeDirectory = yield* runtime.homeDirectory;
|
|
2310
2462
|
const configFile = path.join(homeDirectory, ".better-update", "config.json");
|
|
2311
|
-
const readConfig = fs.readFileString(configFile).pipe(Effect.
|
|
2463
|
+
const readConfig = fs.readFileString(configFile).pipe(Effect.orElseSucceed(() => ""), Effect.flatMap((content) => content.length === 0 ? Effect.void : Effect.try({
|
|
2312
2464
|
try: () => JSON.parse(content),
|
|
2313
2465
|
catch: (cause) => new ConfigStoreParseError({
|
|
2314
2466
|
message: "Config file contains invalid JSON",
|
|
2315
2467
|
cause
|
|
2316
2468
|
})
|
|
2317
|
-
}).pipe(Effect.map((parsed) => isRecord(parsed) ? parsed : void 0), Effect.
|
|
2469
|
+
}).pipe(Effect.map((parsed) => isRecord(parsed) ? parsed : void 0), Effect.orElseSucceed(() => void 0))));
|
|
2318
2470
|
return {
|
|
2319
2471
|
getBaseUrl: Effect.gen(function* () {
|
|
2320
2472
|
const envUrl = yield* runtime.getEnv("BETTER_UPDATE_URL");
|
|
@@ -2537,7 +2689,7 @@ const AppleSessionStoreLive = Layer.effect(AppleSessionStore, Effect.gen(functio
|
|
|
2537
2689
|
const usernameFile = path.join(sessionDir, "apple-username.json");
|
|
2538
2690
|
return {
|
|
2539
2691
|
loadSession: Effect.gen(function* () {
|
|
2540
|
-
const content = yield* fs.readFileString(sessionFile).pipe(Effect.
|
|
2692
|
+
const content = yield* fs.readFileString(sessionFile).pipe(Effect.orElseSucceed(() => null));
|
|
2541
2693
|
if (!content) return null;
|
|
2542
2694
|
const parsed = safeJsonParse(content);
|
|
2543
2695
|
if (!isRecord(parsed)) return null;
|
|
@@ -2555,7 +2707,7 @@ const AppleSessionStoreLive = Layer.effect(AppleSessionStore, Effect.gen(functio
|
|
|
2555
2707
|
}).pipe(Effect.mapError((cause) => new AppleAuthError$1({ message: `Failed to save Apple session: ${formatCause(cause)}` }))),
|
|
2556
2708
|
clearSession: fs.remove(sessionFile).pipe(Effect.catchAll(() => Effect.void)),
|
|
2557
2709
|
loadLastUsername: Effect.gen(function* () {
|
|
2558
|
-
const content = yield* fs.readFileString(usernameFile).pipe(Effect.
|
|
2710
|
+
const content = yield* fs.readFileString(usernameFile).pipe(Effect.orElseSucceed(() => null));
|
|
2559
2711
|
if (!content) return null;
|
|
2560
2712
|
const parsed = safeJsonParse(content);
|
|
2561
2713
|
if (!isRecord(parsed) || typeof parsed["username"] !== "string") return null;
|
|
@@ -2651,7 +2803,7 @@ const interactiveLogin = (appleUtils, options, cachedUsername) => Effect.gen(fun
|
|
|
2651
2803
|
const tryRestore = (appleUtils, store) => Effect.gen(function* () {
|
|
2652
2804
|
const stored = yield* store.loadSession;
|
|
2653
2805
|
if (stored === null) return null;
|
|
2654
|
-
const restored = yield* restoreFromCookies(appleUtils, stored.cookies).pipe(Effect.
|
|
2806
|
+
const restored = yield* restoreFromCookies(appleUtils, stored.cookies).pipe(Effect.orElseSucceed(() => null));
|
|
2655
2807
|
if (restored === null) return null;
|
|
2656
2808
|
return yield* resolveSessionTeam(appleUtils, restored);
|
|
2657
2809
|
});
|
|
@@ -2670,7 +2822,7 @@ const makeAppleAuthLive = (appleUtils = defaultAppleUtils) => Layer.effect(Apple
|
|
|
2670
2822
|
whoami: Effect.gen(function* () {
|
|
2671
2823
|
const stored = yield* store.loadSession;
|
|
2672
2824
|
if (stored === null) return null;
|
|
2673
|
-
const restored = yield* restoreFromCookies(appleUtils, stored.cookies).pipe(Effect.
|
|
2825
|
+
const restored = yield* restoreFromCookies(appleUtils, stored.cookies).pipe(Effect.orElseSucceed(() => null));
|
|
2674
2826
|
if (restored !== null) return sessionFromAuthState(restored);
|
|
2675
2827
|
const info = appleUtils.Session.getAnySessionInfo();
|
|
2676
2828
|
return info === null ? null : sessionFromInfo(stored.username, info);
|
|
@@ -2756,7 +2908,7 @@ const IdentityStoreLive = Layer.effect(IdentityStore, Effect.gen(function* () {
|
|
|
2756
2908
|
const identityFile = path.join(identityDir, "identity.json");
|
|
2757
2909
|
return {
|
|
2758
2910
|
load: Effect.gen(function* () {
|
|
2759
|
-
const content = yield* fs.readFileString(identityFile).pipe(Effect.
|
|
2911
|
+
const content = yield* fs.readFileString(identityFile).pipe(Effect.orElseSucceed(() => ""));
|
|
2760
2912
|
if (content.length === 0) return null;
|
|
2761
2913
|
const parsed = safeJsonParse(content);
|
|
2762
2914
|
return isIdentityFile(parsed) ? parsed : null;
|
|
@@ -2980,7 +3132,7 @@ const VaultCacheLive = Layer.effect(VaultCache, Effect.gen(function* () {
|
|
|
2980
3132
|
const flag = yield* runtime.getEnv("BETTER_UPDATE_NO_CACHE");
|
|
2981
3133
|
return flag !== void 0 && flag.length > 0 && flag !== "0" && flag !== "false";
|
|
2982
3134
|
});
|
|
2983
|
-
const readRaw = (publicKey) => Effect.try(() => new Entry(KEYCHAIN_SERVICE, publicKey).getPassword()).pipe(Effect.
|
|
3135
|
+
const readRaw = (publicKey) => Effect.try(() => new Entry(KEYCHAIN_SERVICE, publicKey).getPassword()).pipe(Effect.orElseSucceed(() => null));
|
|
2984
3136
|
const writeRaw = (publicKey, blob) => Effect.try(() => {
|
|
2985
3137
|
new Entry(KEYCHAIN_SERVICE, publicKey).setPassword(blob);
|
|
2986
3138
|
}).pipe(Effect.ignore);
|
|
@@ -3018,12 +3170,12 @@ const VersionCheckLive = Layer.effect(VersionCheck, Effect.gen(function* () {
|
|
|
3018
3170
|
const cacheDir = path.join(homeDirectory, ".better-update");
|
|
3019
3171
|
const cacheFile = path.join(cacheDir, "version-check.json");
|
|
3020
3172
|
const readCache = Effect.gen(function* () {
|
|
3021
|
-
const content = yield* fs.readFileString(cacheFile).pipe(Effect.
|
|
3173
|
+
const content = yield* fs.readFileString(cacheFile).pipe(Effect.orElseSucceed(() => ""));
|
|
3022
3174
|
if (content.length === 0) return;
|
|
3023
3175
|
const parsed = yield* Effect.try({
|
|
3024
3176
|
try: () => JSON.parse(content),
|
|
3025
3177
|
catch: () => "parse-error"
|
|
3026
|
-
}).pipe(Effect.
|
|
3178
|
+
}).pipe(Effect.orElseSucceed(() => void 0));
|
|
3027
3179
|
if (isRecord(parsed) && typeof parsed["latest"] === "string" && typeof parsed["checkedAt"] === "number") return {
|
|
3028
3180
|
latest: parsed["latest"],
|
|
3029
3181
|
checkedAt: parsed["checkedAt"]
|
|
@@ -3224,7 +3376,7 @@ const buildOpenBrowserCommand = (platform, url) => {
|
|
|
3224
3376
|
};
|
|
3225
3377
|
const openBrowser = (url) => Effect.gen(function* () {
|
|
3226
3378
|
const command = buildOpenBrowserCommand((yield* CliRuntime).platform, url);
|
|
3227
|
-
if (!(yield* Command.exitCode(command).pipe(Effect.map((code) => code === 0), Effect.
|
|
3379
|
+
if (!(yield* Command.exitCode(command).pipe(Effect.map((code) => code === 0), Effect.orElseSucceed(() => false)))) yield* Console.log(`Open this URL manually:\n${url}`);
|
|
3228
3380
|
});
|
|
3229
3381
|
const browserLogin = Effect.scoped(Effect.gen(function* () {
|
|
3230
3382
|
const configStore = yield* ConfigStore;
|
|
@@ -3623,9 +3775,9 @@ const configPath = (projectRoot) => path.join(projectRoot, BETTER_UPDATE_CONFIG_
|
|
|
3623
3775
|
* graceful `readConfig`.
|
|
3624
3776
|
*/
|
|
3625
3777
|
const readBetterUpdateConfig = (projectRoot) => Effect.gen(function* () {
|
|
3626
|
-
const content = yield* (yield* FileSystem.FileSystem).readFileString(configPath(projectRoot)).pipe(Effect.
|
|
3778
|
+
const content = yield* (yield* FileSystem.FileSystem).readFileString(configPath(projectRoot)).pipe(Effect.orElseSucceed(() => ""));
|
|
3627
3779
|
if (content.length === 0) return;
|
|
3628
|
-
return yield* Effect.try(() => JSON.parse(content)).pipe(Effect.map((parsed) => isRecord(parsed) ? parsed : void 0), Effect.
|
|
3780
|
+
return yield* Effect.try(() => JSON.parse(content)).pipe(Effect.map((parsed) => isRecord(parsed) ? parsed : void 0), Effect.orElseSucceed(() => void 0));
|
|
3629
3781
|
});
|
|
3630
3782
|
/**
|
|
3631
3783
|
* Resolve the linked project id from `better-update.json`, or `undefined` when
|
|
@@ -4354,14 +4506,8 @@ const KeyValueFromString = Schema.transformOrFail(Schema.String, KeyValuePair, {
|
|
|
4354
4506
|
},
|
|
4355
4507
|
encode: ({ key, value }) => ParseResult.succeed(`${key}=${value}`)
|
|
4356
4508
|
});
|
|
4357
|
-
const parseRolloutPercentage = (raw, flag) => Effect.
|
|
4358
|
-
|
|
4359
|
-
catch: () => new InvalidArgumentError({ message: `--${flag} must be an integer between 1 and 100, got "${raw}".` })
|
|
4360
|
-
});
|
|
4361
|
-
const parseKeyValue = (raw) => Effect.try({
|
|
4362
|
-
try: () => Schema.decodeUnknownSync(KeyValueFromString)(raw),
|
|
4363
|
-
catch: () => new InvalidArgumentError({ message: "Invalid format. Use KEY=VALUE (e.g. API_KEY=abc123)" })
|
|
4364
|
-
});
|
|
4509
|
+
const parseRolloutPercentage = (raw, flag) => Schema.decodeUnknown(RolloutPercentage)(Number(raw)).pipe(Effect.mapError(() => new InvalidArgumentError({ message: `--${flag} must be an integer between 1 and 100, got "${raw}".` })));
|
|
4510
|
+
const parseKeyValue = (raw) => Schema.decodeUnknown(KeyValueFromString)(raw).pipe(Effect.mapError(() => new InvalidArgumentError({ message: "Invalid format. Use KEY=VALUE (e.g. API_KEY=abc123)" })));
|
|
4365
4511
|
const parseLimit = (raw, defaultValue) => {
|
|
4366
4512
|
if (raw === void 0) return Effect.succeed(defaultValue);
|
|
4367
4513
|
const parsed = Number(raw);
|
|
@@ -4371,7 +4517,7 @@ const parseLimit = (raw, defaultValue) => {
|
|
|
4371
4517
|
|
|
4372
4518
|
//#endregion
|
|
4373
4519
|
//#region src/commands/audit-logs/list.ts
|
|
4374
|
-
const listCommand$
|
|
4520
|
+
const listCommand$12 = defineCommand({
|
|
4375
4521
|
meta: {
|
|
4376
4522
|
name: "list",
|
|
4377
4523
|
description: "List audit log entries"
|
|
@@ -4433,7 +4579,7 @@ const auditLogsCommand = defineCommand({
|
|
|
4433
4579
|
name: "audit-logs",
|
|
4434
4580
|
description: "View audit logs"
|
|
4435
4581
|
},
|
|
4436
|
-
subCommands: { list: listCommand$
|
|
4582
|
+
subCommands: { list: listCommand$12 }
|
|
4437
4583
|
});
|
|
4438
4584
|
|
|
4439
4585
|
//#endregion
|
|
@@ -4537,7 +4683,7 @@ const drainPages = (fetchPage) => {
|
|
|
4537
4683
|
|
|
4538
4684
|
//#endregion
|
|
4539
4685
|
//#region src/commands/branches.ts
|
|
4540
|
-
const listCommand$
|
|
4686
|
+
const listCommand$11 = defineCommand({
|
|
4541
4687
|
meta: {
|
|
4542
4688
|
name: "list",
|
|
4543
4689
|
description: "List branches for the linked project"
|
|
@@ -4560,7 +4706,7 @@ const listCommand$8 = defineCommand({
|
|
|
4560
4706
|
]), "No branches found.");
|
|
4561
4707
|
}))
|
|
4562
4708
|
});
|
|
4563
|
-
const createCommand$
|
|
4709
|
+
const createCommand$5 = defineCommand({
|
|
4564
4710
|
meta: {
|
|
4565
4711
|
name: "create",
|
|
4566
4712
|
description: "Create a branch"
|
|
@@ -4583,7 +4729,7 @@ const createCommand$4 = defineCommand({
|
|
|
4583
4729
|
]);
|
|
4584
4730
|
}))
|
|
4585
4731
|
});
|
|
4586
|
-
const viewCommand$
|
|
4732
|
+
const viewCommand$4 = defineCommand({
|
|
4587
4733
|
meta: {
|
|
4588
4734
|
name: "view",
|
|
4589
4735
|
description: "Show a branch by ID or name"
|
|
@@ -4641,7 +4787,7 @@ const renameCommand$1 = defineCommand({
|
|
|
4641
4787
|
return branch;
|
|
4642
4788
|
}), { json: "value" })
|
|
4643
4789
|
});
|
|
4644
|
-
const deleteCommand$
|
|
4790
|
+
const deleteCommand$7 = defineCommand({
|
|
4645
4791
|
meta: {
|
|
4646
4792
|
name: "delete",
|
|
4647
4793
|
description: "Delete a branch"
|
|
@@ -4666,11 +4812,11 @@ const branchesCommand = defineCommand({
|
|
|
4666
4812
|
description: "Manage branches"
|
|
4667
4813
|
},
|
|
4668
4814
|
subCommands: {
|
|
4669
|
-
list: listCommand$
|
|
4670
|
-
view: viewCommand$
|
|
4671
|
-
create: createCommand$
|
|
4815
|
+
list: listCommand$11,
|
|
4816
|
+
view: viewCommand$4,
|
|
4817
|
+
create: createCommand$5,
|
|
4672
4818
|
rename: renameCommand$1,
|
|
4673
|
-
delete: deleteCommand$
|
|
4819
|
+
delete: deleteCommand$7
|
|
4674
4820
|
}
|
|
4675
4821
|
});
|
|
4676
4822
|
|
|
@@ -4751,8 +4897,31 @@ const findIosArtifact = ({ exportPath }) => Effect.gen(function* () {
|
|
|
4751
4897
|
if (!picked) return yield* new ArtifactNotFoundError({ message: `No .ipa file found under "${exportPath}".` });
|
|
4752
4898
|
return picked.path;
|
|
4753
4899
|
});
|
|
4754
|
-
|
|
4755
|
-
|
|
4900
|
+
/**
|
|
4901
|
+
* Resolve a custom-command build artifact from a user-supplied path. A pattern
|
|
4902
|
+
* without wildcards is treated as a literal path (relative to `baseDir`);
|
|
4903
|
+
* otherwise the fixed leading directory + file extension are extracted and the
|
|
4904
|
+
* newest matching file under that directory is returned.
|
|
4905
|
+
*/
|
|
4906
|
+
const findArtifactByGlob = ({ baseDir, pattern, minMtimeMs }) => Effect.gen(function* () {
|
|
4907
|
+
const fs = yield* FileSystem.FileSystem;
|
|
4908
|
+
if (!/[*?[]/u.test(pattern)) {
|
|
4909
|
+
const full = path.isAbsolute(pattern) ? pattern : path.join(baseDir, pattern);
|
|
4910
|
+
if (yield* fs.exists(full).pipe(Effect.orElseSucceed(() => false))) return full;
|
|
4911
|
+
return yield* new ArtifactNotFoundError({ message: `No artifact found at "${full}".` });
|
|
4912
|
+
}
|
|
4913
|
+
const extension = path.extname(pattern).toLowerCase();
|
|
4914
|
+
if (extension === "") return yield* new ArtifactNotFoundError({ message: `artifactPath "${pattern}" must end in a file extension (e.g. **/*.aab).` });
|
|
4915
|
+
const wildcardIndex = pattern.search(/[*?[]/u);
|
|
4916
|
+
const fixedPrefix = pattern.slice(0, wildcardIndex);
|
|
4917
|
+
const prefixDir = fixedPrefix.includes("/") ? fixedPrefix.slice(0, fixedPrefix.lastIndexOf("/")) : "";
|
|
4918
|
+
const searchRoot = prefixDir === "" ? baseDir : path.join(baseDir, prefixDir);
|
|
4919
|
+
const picked = newest(yield* walkAndFind(searchRoot, extension), minMtimeMs);
|
|
4920
|
+
if (!picked) return yield* new ArtifactNotFoundError({ message: `No file matching "${pattern}" found under "${searchRoot}".` });
|
|
4921
|
+
return picked.path;
|
|
4922
|
+
});
|
|
4923
|
+
const findAndroidArtifact = ({ projectRoot, format, flavor, buildType, minMtimeMs, module: gradleModule = "app" }) => Effect.gen(function* () {
|
|
4924
|
+
const outputsRoot = path.join(projectRoot, "android", gradleModule, "build", "outputs");
|
|
4756
4925
|
const subdir = format === "aab" ? "bundle" : "apk";
|
|
4757
4926
|
const variantDir = flavor ? `${flavor}${capitalize(buildType)}` : buildType;
|
|
4758
4927
|
const pickedDirect = newest(yield* walkAndFind(path.join(outputsRoot, subdir, variantDir), `.${format}`), minMtimeMs);
|
|
@@ -18686,7 +18855,7 @@ const asArrayBuffer$1 = (bytes) => {
|
|
|
18686
18855
|
};
|
|
18687
18856
|
const signAscJwt = (credentials) => Effect.gen(function* () {
|
|
18688
18857
|
const der = pemToPkcs8Der(credentials.p8Pem);
|
|
18689
|
-
if (der === null) return yield*
|
|
18858
|
+
if (der === null) return yield* new AppleAuthError({ cause: /* @__PURE__ */ new Error("Invalid .p8 PEM") });
|
|
18690
18859
|
const header = {
|
|
18691
18860
|
alg: "ES256",
|
|
18692
18861
|
kid: credentials.keyId,
|
|
@@ -18770,7 +18939,7 @@ const fetchRaw = (jwt, path, init) => Effect.gen(function* () {
|
|
|
18770
18939
|
try: () => text.length === 0 ? {} : JSON.parse(text),
|
|
18771
18940
|
catch: (cause) => new AscNetworkError({ cause })
|
|
18772
18941
|
});
|
|
18773
|
-
if (!response.ok) return yield*
|
|
18942
|
+
if (!response.ok) return yield* parseApiError(response, body, text);
|
|
18774
18943
|
return body;
|
|
18775
18944
|
});
|
|
18776
18945
|
const toAscCertificate = (value) => {
|
|
@@ -18870,12 +19039,10 @@ const createCertificate = (credentials, params) => withJwt(credentials, (jwt) =>
|
|
|
18870
19039
|
}
|
|
18871
19040
|
} })
|
|
18872
19041
|
}), toAscCertificate);
|
|
18873
|
-
if (resource === null) return yield*
|
|
19042
|
+
if (resource === null) return yield* malformed("certificate");
|
|
18874
19043
|
return resource;
|
|
18875
19044
|
}));
|
|
18876
|
-
const deleteCertificate = (credentials, id) => withJwt(credentials, (jwt) => Effect.
|
|
18877
|
-
yield* fetchRaw(jwt, `/v1/certificates/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
18878
|
-
}));
|
|
19045
|
+
const deleteCertificate = (credentials, id) => withJwt(credentials, (jwt) => Effect.asVoid(fetchRaw(jwt, `/v1/certificates/${encodeURIComponent(id)}`, { method: "DELETE" })));
|
|
18879
19046
|
const listBundleIds = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
18880
19047
|
return extractList(yield* fetchRaw(jwt, "/v1/bundleIds?limit=200"), toAscBundleId);
|
|
18881
19048
|
}));
|
|
@@ -18891,7 +19058,7 @@ const createBundleId = (credentials, params) => withJwt(credentials, (jwt) => Ef
|
|
|
18891
19058
|
}
|
|
18892
19059
|
} })
|
|
18893
19060
|
}), toAscBundleId);
|
|
18894
|
-
if (resource === null) return yield*
|
|
19061
|
+
if (resource === null) return yield* malformed("bundleId");
|
|
18895
19062
|
return resource;
|
|
18896
19063
|
}));
|
|
18897
19064
|
const listDevices = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
@@ -18923,7 +19090,7 @@ const createProvisioningProfile = (credentials, params) => withJwt(credentials,
|
|
|
18923
19090
|
relationships
|
|
18924
19091
|
} })
|
|
18925
19092
|
}), toAscProfile);
|
|
18926
|
-
if (resource === null) return yield*
|
|
19093
|
+
if (resource === null) return yield* malformed("profile");
|
|
18927
19094
|
return resource;
|
|
18928
19095
|
}));
|
|
18929
19096
|
const isCertificateLimitError = (error) => {
|
|
@@ -18940,7 +19107,7 @@ const stringField = (cert, name) => {
|
|
|
18940
19107
|
return typeof value === "string" ? value : null;
|
|
18941
19108
|
};
|
|
18942
19109
|
const matchTeamFromCommonName = (cn) => {
|
|
18943
|
-
const match = /\(([A-Z0-9]{10})\)/u.exec(cn);
|
|
19110
|
+
const match = /\((?<team>[A-Z0-9]{10})\)/u.exec(cn);
|
|
18944
19111
|
if (match === null) return null;
|
|
18945
19112
|
const [, captured] = match;
|
|
18946
19113
|
return captured === void 0 ? null : captured;
|
|
@@ -18959,7 +19126,7 @@ const parseCert = (certDerBytes) => {
|
|
|
18959
19126
|
const generatePassword = () => forge.util.encode64(forge.random.getBytesSync(16));
|
|
18960
19127
|
const extractCertMetadata = (cert) => Effect.gen(function* () {
|
|
18961
19128
|
const appleTeamId = extractTeamId$1(cert);
|
|
18962
|
-
if (appleTeamId === null) return yield*
|
|
19129
|
+
if (appleTeamId === null) return yield* new CertParseError({ message: "Could not extract Apple team identifier from certificate subject" });
|
|
18963
19130
|
return {
|
|
18964
19131
|
serialNumber: cert.serialNumber.toUpperCase(),
|
|
18965
19132
|
validFrom: cert.validity.notBefore.toISOString(),
|
|
@@ -18977,7 +19144,7 @@ const extractCertMetadata = (cert) => Effect.gen(function* () {
|
|
|
18977
19144
|
*/
|
|
18978
19145
|
const extractMetadataFromP12 = (params) => Effect.gen(function* () {
|
|
18979
19146
|
const certBagOid = forge.pki.oids["certBag"];
|
|
18980
|
-
if (certBagOid === void 0) return yield*
|
|
19147
|
+
if (certBagOid === void 0) return yield* new CertParseError({ message: "PKCS#12 OID lookup for certBag failed" });
|
|
18981
19148
|
const [first] = yield* Effect.try({
|
|
18982
19149
|
try: () => {
|
|
18983
19150
|
const p12Der = forge.util.decode64(params.p12Base64);
|
|
@@ -18986,7 +19153,7 @@ const extractMetadataFromP12 = (params) => Effect.gen(function* () {
|
|
|
18986
19153
|
},
|
|
18987
19154
|
catch: (error) => new CertParseError({ message: `Failed to parse PKCS#12 bundle: ${error instanceof Error ? error.message : String(error)}` })
|
|
18988
19155
|
});
|
|
18989
|
-
if (first?.cert === void 0) return yield*
|
|
19156
|
+
if (first?.cert === void 0) return yield* new CertParseError({ message: "PKCS#12 bundle does not contain a certificate" });
|
|
18990
19157
|
return yield* extractCertMetadata(first.cert);
|
|
18991
19158
|
});
|
|
18992
19159
|
const buildDistributionCertP12 = (params) => Effect.gen(function* () {
|
|
@@ -19167,10 +19334,10 @@ const generateAndUploadDistributionCertificate = (api, input) => Effect.gen(func
|
|
|
19167
19334
|
csrPem: csrResult.csrPem,
|
|
19168
19335
|
certificateType
|
|
19169
19336
|
}).pipe(Effect.mapError(wrapAscError("apple-create-certificate")));
|
|
19170
|
-
if (apple.certificateContent === null) return yield*
|
|
19337
|
+
if (apple.certificateContent === null) return yield* new GenerateFailedError({
|
|
19171
19338
|
step: "apple-create-certificate",
|
|
19172
19339
|
message: "Apple response missing certificateContent"
|
|
19173
|
-
})
|
|
19340
|
+
});
|
|
19174
19341
|
const bundle = yield* buildDistributionCertP12({
|
|
19175
19342
|
certificateContentBase64: apple.certificateContent,
|
|
19176
19343
|
privateKey: csrResult.privateKey
|
|
@@ -19220,10 +19387,10 @@ const revokeAppleCertificate = (api, input) => Effect.gen(function* () {
|
|
|
19220
19387
|
});
|
|
19221
19388
|
const revokeLocalDistributionCertificate = (api, input) => Effect.gen(function* () {
|
|
19222
19389
|
const local = (yield* api.appleDistributionCertificates.list()).items.find((entry) => entry.id === input.distributionCertificateId);
|
|
19223
|
-
if (local === void 0) return yield*
|
|
19390
|
+
if (local === void 0) return yield* new GenerateFailedError({
|
|
19224
19391
|
step: "load-distribution-certificate",
|
|
19225
19392
|
message: `Distribution certificate ${input.distributionCertificateId} not found on this account`
|
|
19226
|
-
})
|
|
19393
|
+
});
|
|
19227
19394
|
const creds = yield* fetchAscCredentials(api, input.ascApiKeyId);
|
|
19228
19395
|
const ascCreds = {
|
|
19229
19396
|
keyId: creds.keyId,
|
|
@@ -19260,10 +19427,10 @@ const listAppleCertificates = (api, input) => Effect.gen(function* () {
|
|
|
19260
19427
|
});
|
|
19261
19428
|
const resolveCertAscId = (creds, serialNumber, certificateType) => Effect.gen(function* () {
|
|
19262
19429
|
const match = (yield* listCertificates(creds, { certificateType }).pipe(Effect.mapError(wrapAscError("apple-list-certificates")))).find((entry) => entry.serialNumber.toUpperCase() === serialNumber);
|
|
19263
|
-
if (match === void 0) return yield*
|
|
19430
|
+
if (match === void 0) return yield* new GenerateFailedError({
|
|
19264
19431
|
step: "match-apple-certificate",
|
|
19265
19432
|
message: `Distribution certificate ${serialNumber} not present on Apple Developer Portal; upload or re-generate it`
|
|
19266
|
-
})
|
|
19433
|
+
});
|
|
19267
19434
|
return match.id;
|
|
19268
19435
|
});
|
|
19269
19436
|
const ensureBundleId = (creds, bundleIdentifier) => Effect.gen(function* () {
|
|
@@ -19296,10 +19463,10 @@ const generateAndUploadProvisioningProfile = (api, input) => Effect.gen(function
|
|
|
19296
19463
|
const [certAscId, bundleIdAscId] = yield* Effect.all([resolveCertAscId(ascCreds, cert.serialNumber.toUpperCase(), certificateType), ensureBundleId(ascCreds, input.bundleIdentifier)], { concurrency: 2 });
|
|
19297
19464
|
const useDevices = input.distributionType === "AD_HOC" || input.distributionType === "DEVELOPMENT";
|
|
19298
19465
|
const { ids: deviceAscIds } = useDevices ? yield* collectDeviceAscIds(ascCreds, cert.appleTeamId, input.deviceIds) : { ids: [] };
|
|
19299
|
-
if (useDevices && deviceAscIds.length === 0) return yield*
|
|
19466
|
+
if (useDevices && deviceAscIds.length === 0) return yield* new GenerateFailedError({
|
|
19300
19467
|
step: "collect-devices",
|
|
19301
19468
|
message: "No registered devices to attach to the provisioning profile"
|
|
19302
|
-
})
|
|
19469
|
+
});
|
|
19303
19470
|
const profileBytes = fromBase64((yield* createProvisioningProfile(ascCreds, {
|
|
19304
19471
|
profileName: `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`,
|
|
19305
19472
|
profileType: DISTRIBUTION_TO_PROFILE_TYPE$1[input.distributionType],
|
|
@@ -19610,10 +19777,10 @@ const downloadAndroidCredentials = (api, options) => Effect.gen(function* () {
|
|
|
19610
19777
|
...compact({ buildProfile: options.buildProfile })
|
|
19611
19778
|
}
|
|
19612
19779
|
}).pipe(Effect.mapError((cause) => resolveErrorToMissingCredentials(cause, "android")));
|
|
19613
|
-
if (resolved.platform !== "android") return yield*
|
|
19780
|
+
if (resolved.platform !== "android") return yield* new MissingCredentialsError({
|
|
19614
19781
|
message: "Server returned non-Android credentials for an Android build request",
|
|
19615
19782
|
hint: androidBindHint
|
|
19616
|
-
})
|
|
19783
|
+
});
|
|
19617
19784
|
const secret = yield* decryptResolveSecret({
|
|
19618
19785
|
session: yield* openVaultSessionForBuild(api, androidBindHint),
|
|
19619
19786
|
credentialType: "keystore",
|
|
@@ -19734,7 +19901,7 @@ const credentialsJsonPath = (projectRoot) => path.join(projectRoot, CREDENTIALS_
|
|
|
19734
19901
|
const readCredentialsJson = (projectRoot) => Effect.gen(function* () {
|
|
19735
19902
|
const fs = yield* FileSystem.FileSystem;
|
|
19736
19903
|
const filePath = credentialsJsonPath(projectRoot);
|
|
19737
|
-
if (!(yield* fs.exists(filePath).pipe(Effect.
|
|
19904
|
+
if (!(yield* fs.exists(filePath).pipe(Effect.orElseSucceed(() => false)))) return yield* new CredentialsJsonError({ message: `credentials.json not found at ${filePath}.` });
|
|
19738
19905
|
return yield* parseCredentialsJson(yield* fs.readFileString(filePath).pipe(Effect.mapError((cause) => new CredentialsJsonError({ message: `Failed to read credentials.json: ${String(cause)}` }))));
|
|
19739
19906
|
});
|
|
19740
19907
|
const writeCredentialsJson = (projectRoot, data) => Effect.gen(function* () {
|
|
@@ -19751,7 +19918,7 @@ const resolveCredentialPath = (projectRoot, candidate) => path.isAbsolute(candid
|
|
|
19751
19918
|
|
|
19752
19919
|
//#endregion
|
|
19753
19920
|
//#region src/lib/local-credentials.ts
|
|
19754
|
-
const requirePath = (fs, absolutePath, label) => fs.exists(absolutePath).pipe(Effect.
|
|
19921
|
+
const requirePath = (fs, absolutePath, label) => fs.exists(absolutePath).pipe(Effect.orElseSucceed(() => false), Effect.flatMap((exists) => exists ? Effect.void : Effect.fail(new MissingCredentialsError({
|
|
19755
19922
|
message: `Local credentials.json: ${label} not found at ${absolutePath}.`,
|
|
19756
19923
|
hint: "Run `better-update credentials sync pull` to materialize files, or fix the path in credentials.json."
|
|
19757
19924
|
}))));
|
|
@@ -20006,7 +20173,7 @@ const runStepFormatted = (cmd, step, formatter) => Effect.gen(function* () {
|
|
|
20006
20173
|
if (code !== 0) {
|
|
20007
20174
|
const summary = formatter.getBuildSummary();
|
|
20008
20175
|
if (summary.length > 0) process$1.stderr.write(`${summary}\n`);
|
|
20009
|
-
return yield*
|
|
20176
|
+
return yield* buildFailed(step, code, `${step} exited with code ${code}`);
|
|
20010
20177
|
}
|
|
20011
20178
|
});
|
|
20012
20179
|
|
|
@@ -20025,56 +20192,44 @@ const gradleTaskName = (format, flavor, buildType) => {
|
|
|
20025
20192
|
const verb = format === "aab" ? "bundle" : "assemble";
|
|
20026
20193
|
return flavor ? `${verb}${capitalize(flavor)}${capitalize(buildType)}` : `${verb}${capitalize(buildType)}`;
|
|
20027
20194
|
};
|
|
20028
|
-
|
|
20029
|
-
|
|
20030
|
-
|
|
20195
|
+
/** Resolve the signing keystore (remote or local), or `undefined` when skipped. */
|
|
20196
|
+
const resolveAndroidCredentials = (input) => {
|
|
20197
|
+
if (input.skipCredentials) return Effect.succeed(void 0);
|
|
20198
|
+
return input.credentialsSource === "local" ? loadLocalAndroidCredentials({ projectRoot: input.projectRoot }) : downloadAndroidCredentials(input.api, {
|
|
20199
|
+
projectId: input.projectId,
|
|
20200
|
+
applicationIdentifier: input.applicationIdentifier,
|
|
20201
|
+
tempDir: input.tempDir,
|
|
20202
|
+
buildProfile: input.profileName
|
|
20203
|
+
});
|
|
20204
|
+
};
|
|
20205
|
+
/** Gradle build against the (already-prepared) `android/` dir. */
|
|
20206
|
+
const runGradleBuild = (input, commandEnv) => Effect.gen(function* () {
|
|
20031
20207
|
const buildStartMs = Date.now();
|
|
20032
|
-
const { format } = androidProfile;
|
|
20033
|
-
const
|
|
20034
|
-
const
|
|
20035
|
-
const androidDir = path.join(projectRoot, "android");
|
|
20036
|
-
const
|
|
20037
|
-
yield*
|
|
20038
|
-
command: "bunx",
|
|
20039
|
-
args: [
|
|
20040
|
-
"expo",
|
|
20041
|
-
"prebuild",
|
|
20042
|
-
"--platform",
|
|
20043
|
-
"android",
|
|
20044
|
-
"--clean"
|
|
20045
|
-
],
|
|
20046
|
-
cwd: projectRoot,
|
|
20047
|
-
env: commandEnv
|
|
20048
|
-
}, "expo prebuild android");
|
|
20049
|
-
const gradleArgs = yield* input.skipCredentials ? Effect.succeed([]) : Effect.gen(function* () {
|
|
20050
|
-
const credentials = input.credentialsSource === "local" ? yield* loadLocalAndroidCredentials({ projectRoot }) : yield* downloadAndroidCredentials(api, {
|
|
20051
|
-
projectId,
|
|
20052
|
-
applicationIdentifier,
|
|
20053
|
-
tempDir,
|
|
20054
|
-
buildProfile: input.profileName
|
|
20055
|
-
});
|
|
20208
|
+
const { format, flavor } = input.androidProfile;
|
|
20209
|
+
const buildType = input.androidProfile.buildType ?? "release";
|
|
20210
|
+
const moduleName = input.androidProfile.module ?? "app";
|
|
20211
|
+
const androidDir = path.join(input.projectRoot, "android");
|
|
20212
|
+
const credentials = yield* resolveAndroidCredentials(input);
|
|
20213
|
+
const gradleArgs = credentials === void 0 ? [] : yield* Effect.gen(function* () {
|
|
20056
20214
|
const fs = yield* FileSystem.FileSystem;
|
|
20057
|
-
const signingGradlePath = path.join(tempDir, "signing.gradle");
|
|
20058
|
-
yield* fs.writeFileString(signingGradlePath, renderSigningGradle(
|
|
20059
|
-
keystorePath: credentials.keystorePath,
|
|
20060
|
-
storePassword: credentials.storePassword,
|
|
20061
|
-
keyAlias: credentials.keyAlias,
|
|
20062
|
-
keyPassword: credentials.keyPassword
|
|
20063
|
-
}));
|
|
20215
|
+
const signingGradlePath = path.join(input.tempDir, "signing.gradle");
|
|
20216
|
+
yield* fs.writeFileString(signingGradlePath, renderSigningGradle(credentials));
|
|
20064
20217
|
return ["--init-script", signingGradlePath];
|
|
20065
20218
|
});
|
|
20066
|
-
const taskName = gradleTaskName(format, flavor, buildType);
|
|
20219
|
+
const taskName = input.androidProfile.gradleTask ?? gradleTaskName(format, flavor, buildType);
|
|
20220
|
+
const taskArg = taskName.startsWith(":") ? taskName : `:${moduleName}:${taskName}`;
|
|
20067
20221
|
yield* runStep({
|
|
20068
20222
|
command: "./gradlew",
|
|
20069
|
-
args: [...gradleArgs,
|
|
20223
|
+
args: [...gradleArgs, taskArg],
|
|
20070
20224
|
cwd: androidDir,
|
|
20071
20225
|
env: commandEnv
|
|
20072
20226
|
}, "gradlew");
|
|
20073
20227
|
const artifactPath = yield* findAndroidArtifact({
|
|
20074
|
-
projectRoot,
|
|
20228
|
+
projectRoot: input.projectRoot,
|
|
20075
20229
|
format,
|
|
20076
20230
|
buildType,
|
|
20077
20231
|
minMtimeMs: buildStartMs,
|
|
20232
|
+
module: moduleName,
|
|
20078
20233
|
...compact({ flavor })
|
|
20079
20234
|
});
|
|
20080
20235
|
const { sha256, byteSize } = yield* sha256File(artifactPath);
|
|
@@ -20084,6 +20239,71 @@ const runAndroidBuild = (input) => Effect.gen(function* () {
|
|
|
20084
20239
|
sha256
|
|
20085
20240
|
};
|
|
20086
20241
|
});
|
|
20242
|
+
/**
|
|
20243
|
+
* Custom-command build. We can't inject signing into an arbitrary build, so the
|
|
20244
|
+
* resolved keystore + passwords are exposed to the command as `BETTER_UPDATE_*`
|
|
20245
|
+
* env vars; the user's script consumes them. The artifact is located via the
|
|
20246
|
+
* profile's `artifactPath` glob.
|
|
20247
|
+
*/
|
|
20248
|
+
const runAndroidCustom = (input, commandEnv) => Effect.gen(function* () {
|
|
20249
|
+
const buildStartMs = Date.now();
|
|
20250
|
+
const custom = input.customCommand;
|
|
20251
|
+
if (custom === void 0) return yield* new BuildFailedError({
|
|
20252
|
+
step: "custom android build",
|
|
20253
|
+
exitCode: 1,
|
|
20254
|
+
message: "Internal: custom Android strategy selected without a custom command."
|
|
20255
|
+
});
|
|
20256
|
+
if (custom.artifactPath === void 0) return yield* new BuildFailedError({
|
|
20257
|
+
step: "custom android build",
|
|
20258
|
+
exitCode: 1,
|
|
20259
|
+
message: "Custom Android build requires \"artifactPath\" (e.g. \"**/*.aab\") in better-update.json."
|
|
20260
|
+
});
|
|
20261
|
+
const credentials = yield* resolveAndroidCredentials(input);
|
|
20262
|
+
const credEnv = credentials === void 0 ? {} : {
|
|
20263
|
+
BETTER_UPDATE_ANDROID_KEYSTORE_PATH: credentials.keystorePath,
|
|
20264
|
+
BETTER_UPDATE_ANDROID_KEYSTORE_PASSWORD: credentials.storePassword,
|
|
20265
|
+
BETTER_UPDATE_ANDROID_KEY_ALIAS: credentials.keyAlias,
|
|
20266
|
+
BETTER_UPDATE_ANDROID_KEY_PASSWORD: credentials.keyPassword
|
|
20267
|
+
};
|
|
20268
|
+
const cwd = custom.cwd === void 0 ? input.projectRoot : path.join(input.projectRoot, custom.cwd);
|
|
20269
|
+
yield* runStep({
|
|
20270
|
+
command: "sh",
|
|
20271
|
+
args: ["-c", custom.command],
|
|
20272
|
+
cwd,
|
|
20273
|
+
env: {
|
|
20274
|
+
...commandEnv,
|
|
20275
|
+
...credEnv,
|
|
20276
|
+
...custom.env
|
|
20277
|
+
}
|
|
20278
|
+
}, "custom android build");
|
|
20279
|
+
const artifactPath = yield* findArtifactByGlob({
|
|
20280
|
+
baseDir: cwd,
|
|
20281
|
+
pattern: custom.artifactPath,
|
|
20282
|
+
minMtimeMs: buildStartMs
|
|
20283
|
+
});
|
|
20284
|
+
const { sha256, byteSize } = yield* sha256File(artifactPath);
|
|
20285
|
+
return {
|
|
20286
|
+
artifactPath,
|
|
20287
|
+
byteSize,
|
|
20288
|
+
sha256
|
|
20289
|
+
};
|
|
20290
|
+
});
|
|
20291
|
+
const runAndroidBuild = (input) => Effect.gen(function* () {
|
|
20292
|
+
const commandEnv = yield* (yield* CliRuntime).commandEnvironment(input.envVars);
|
|
20293
|
+
if (input.strategy === "expo") yield* runStep({
|
|
20294
|
+
command: "bunx",
|
|
20295
|
+
args: [
|
|
20296
|
+
"expo",
|
|
20297
|
+
"prebuild",
|
|
20298
|
+
"--platform",
|
|
20299
|
+
"android",
|
|
20300
|
+
"--clean"
|
|
20301
|
+
],
|
|
20302
|
+
cwd: input.projectRoot,
|
|
20303
|
+
env: commandEnv
|
|
20304
|
+
}, "expo prebuild android");
|
|
20305
|
+
return input.strategy === "custom" ? yield* runAndroidCustom(input, commandEnv) : yield* runGradleBuild(input, commandEnv);
|
|
20306
|
+
});
|
|
20087
20307
|
|
|
20088
20308
|
//#endregion
|
|
20089
20309
|
//#region src/lib/credentials-generator-apple-id.ts
|
|
@@ -20189,10 +20409,10 @@ const findAscCertificateId = (ctx, serialNumber, certificateType) => Effect.gen(
|
|
|
20189
20409
|
const certs = yield* wrap("apple-list-certificates", async () => AppleUtils.Certificate.getAsync(ctx, { query: { filter: { certificateType } } }));
|
|
20190
20410
|
const upper = serialNumber.toUpperCase();
|
|
20191
20411
|
const match = certs.find((entry) => entry.attributes.serialNumber.toUpperCase() === upper);
|
|
20192
|
-
if (match === void 0) return yield*
|
|
20412
|
+
if (match === void 0) return yield* new AppleIdGenerateFailedError({
|
|
20193
20413
|
step: "match-apple-certificate",
|
|
20194
20414
|
message: `Distribution certificate ${serialNumber} not present on Apple Developer Portal; upload or re-generate it`
|
|
20195
|
-
})
|
|
20415
|
+
});
|
|
20196
20416
|
return match.id;
|
|
20197
20417
|
});
|
|
20198
20418
|
const collectIosDeviceIds = (ctx, deviceIds) => Effect.gen(function* () {
|
|
@@ -20211,10 +20431,10 @@ const generateAndUploadProvisioningProfileViaAppleId = (api, input) => Effect.ge
|
|
|
20211
20431
|
const [certAscId, bundleIdAscId] = yield* Effect.all([findAscCertificateId(ctx, cert.serialNumber, certificateType), findOrCreateBundleId(ctx, input.bundleIdentifier)], { concurrency: 2 });
|
|
20212
20432
|
const useDevices = input.distributionType === "AD_HOC" || input.distributionType === "DEVELOPMENT";
|
|
20213
20433
|
const deviceIds = useDevices ? yield* collectIosDeviceIds(ctx, input.deviceIds) : [];
|
|
20214
|
-
if (useDevices && deviceIds.length === 0) return yield*
|
|
20434
|
+
if (useDevices && deviceIds.length === 0) return yield* new AppleIdGenerateFailedError({
|
|
20215
20435
|
step: "collect-devices",
|
|
20216
20436
|
message: "No registered devices to attach to the provisioning profile"
|
|
20217
|
-
})
|
|
20437
|
+
});
|
|
20218
20438
|
const profileName = `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`;
|
|
20219
20439
|
const { profileContent } = (yield* wrap("apple-create-profile", async () => AppleUtils.Profile.createAsync(ctx, {
|
|
20220
20440
|
bundleId: bundleIdAscId,
|
|
@@ -20223,10 +20443,10 @@ const generateAndUploadProvisioningProfileViaAppleId = (api, input) => Effect.ge
|
|
|
20223
20443
|
name: profileName,
|
|
20224
20444
|
profileType: DISTRIBUTION_TO_PROFILE_TYPE[input.distributionType]
|
|
20225
20445
|
}))).attributes;
|
|
20226
|
-
if (profileContent === null) return yield*
|
|
20446
|
+
if (profileContent === null) return yield* new AppleIdGenerateFailedError({
|
|
20227
20447
|
step: "extract-profile-content",
|
|
20228
20448
|
message: "Apple returned a profile with no content (likely expired/invalid)"
|
|
20229
|
-
})
|
|
20449
|
+
});
|
|
20230
20450
|
const profileBytes = fromBase64(profileContent);
|
|
20231
20451
|
const rosterHash = useDevices ? computeDeviceRosterHashHex(deviceIds) : void 0;
|
|
20232
20452
|
const created = yield* api.appleProvisioningProfiles.upload({ payload: {
|
|
@@ -20406,10 +20626,10 @@ const interactiveCertLimitRecover = (api, ascApiKeyId) => Effect.gen(function* (
|
|
|
20406
20626
|
ascApiKeyId,
|
|
20407
20627
|
certificateType: "IOS_DISTRIBUTION"
|
|
20408
20628
|
});
|
|
20409
|
-
if (certs.length === 0) return yield*
|
|
20629
|
+
if (certs.length === 0) return yield* new MissingCredentialsError({
|
|
20410
20630
|
message: "Apple says the certificate limit is hit but no existing certificates were returned.",
|
|
20411
20631
|
hint: "Try again later or check the Apple Developer portal."
|
|
20412
|
-
})
|
|
20632
|
+
});
|
|
20413
20633
|
const toRevoke = yield* promptMultiSelect("Select one or more certificates to revoke before retrying", certs.map((entry) => ({
|
|
20414
20634
|
value: entry.id,
|
|
20415
20635
|
label: `${entry.serialNumber.slice(0, 12)}… (${entry.displayName ?? entry.certificateType}, exp ${entry.expirationDate.slice(0, 10)})`
|
|
@@ -20422,10 +20642,10 @@ const interactiveCertLimitRecover = (api, ascApiKeyId) => Effect.gen(function* (
|
|
|
20422
20642
|
});
|
|
20423
20643
|
const generateDistributionCertInteractive = (api) => Effect.gen(function* () {
|
|
20424
20644
|
const teamAscKeys = (yield* api.ascApiKeys.list()).items.filter((key) => key.appleTeamId !== null);
|
|
20425
|
-
if (teamAscKeys.length === 0) return yield*
|
|
20645
|
+
if (teamAscKeys.length === 0) return yield* new MissingCredentialsError({
|
|
20426
20646
|
message: "No ASC API key linked to an Apple team in this organization.",
|
|
20427
20647
|
hint: "Upload an ASC API key with a team assignment via the dashboard, then retry."
|
|
20428
|
-
})
|
|
20648
|
+
});
|
|
20429
20649
|
const ascKeyId = yield* promptSelect("Select an ASC API key to issue the certificate against", teamAscKeys.map((key) => ({
|
|
20430
20650
|
value: key.id,
|
|
20431
20651
|
label: `${key.name} (${key.keyId})`
|
|
@@ -20444,10 +20664,10 @@ const chooseIosCertificateId = (api) => Effect.gen(function* () {
|
|
|
20444
20664
|
}, {
|
|
20445
20665
|
value: "abort",
|
|
20446
20666
|
label: "Abort — I'll upload one manually"
|
|
20447
|
-
}])) === "abort") return yield*
|
|
20667
|
+
}])) === "abort") return yield* new MissingCredentialsError({
|
|
20448
20668
|
message: "Build aborted — no distribution certificate available.",
|
|
20449
20669
|
hint: "Run `better-update credentials generate distribution-certificate --asc-key-id <id>` or upload via the dashboard."
|
|
20450
|
-
})
|
|
20670
|
+
});
|
|
20451
20671
|
return (yield* generateDistributionCertInteractive(api)).id;
|
|
20452
20672
|
}
|
|
20453
20673
|
const choice = yield* promptSelect("Select a distribution certificate (or 'generate' for a fresh one)", [{
|
|
@@ -20463,10 +20683,10 @@ const chooseIosCertificateId = (api) => Effect.gen(function* () {
|
|
|
20463
20683
|
const pickIosCertificate = (api) => Effect.gen(function* () {
|
|
20464
20684
|
const chosenId = yield* chooseIosCertificateId(api);
|
|
20465
20685
|
const cert = (yield* api.appleDistributionCertificates.list()).items.find((entry) => entry.id === chosenId);
|
|
20466
|
-
if (cert === void 0) return yield*
|
|
20686
|
+
if (cert === void 0) return yield* new MissingCredentialsError({
|
|
20467
20687
|
message: "Selected certificate not found after generation.",
|
|
20468
20688
|
hint: "Retry."
|
|
20469
|
-
})
|
|
20689
|
+
});
|
|
20470
20690
|
return {
|
|
20471
20691
|
certId: chosenId,
|
|
20472
20692
|
cert
|
|
@@ -20474,10 +20694,10 @@ const pickIosCertificate = (api) => Effect.gen(function* () {
|
|
|
20474
20694
|
});
|
|
20475
20695
|
const pickIosAscKey = (api, appleTeamId) => Effect.gen(function* () {
|
|
20476
20696
|
const teamAscKeys = (yield* api.ascApiKeys.list()).items.filter((key) => key.appleTeamId !== null && key.appleTeamId === appleTeamId);
|
|
20477
|
-
if (teamAscKeys.length === 0) return yield*
|
|
20697
|
+
if (teamAscKeys.length === 0) return yield* new MissingCredentialsError({
|
|
20478
20698
|
message: `No ASC API key linked to Apple team ${appleTeamId}.`,
|
|
20479
20699
|
hint: "Upload an ASC API key for that team via the dashboard, then retry."
|
|
20480
|
-
})
|
|
20700
|
+
});
|
|
20481
20701
|
return yield* promptSelect("Select an ASC API key", teamAscKeys.map((key) => ({
|
|
20482
20702
|
value: key.id,
|
|
20483
20703
|
label: `${key.name} (${key.keyId})`
|
|
@@ -20558,10 +20778,10 @@ const generateKeystoreInteractive = (api) => Effect.gen(function* () {
|
|
|
20558
20778
|
});
|
|
20559
20779
|
const pickExistingKeystore = (api) => Effect.gen(function* () {
|
|
20560
20780
|
const keystores = yield* api.androidUploadKeystores.list();
|
|
20561
|
-
if (keystores.items.length === 0) return yield*
|
|
20781
|
+
if (keystores.items.length === 0) return yield* new MissingCredentialsError({
|
|
20562
20782
|
message: "No existing keystores in this organization.",
|
|
20563
20783
|
hint: "Re-run and choose 'Generate new keystore'."
|
|
20564
|
-
})
|
|
20784
|
+
});
|
|
20565
20785
|
return yield* promptSelect("Select a keystore", keystores.items.map((item) => ({
|
|
20566
20786
|
value: item.id,
|
|
20567
20787
|
label: item.keyAlias
|
|
@@ -20594,10 +20814,10 @@ const setupAndroidInteractive = (api, input) => Effect.gen(function* () {
|
|
|
20594
20814
|
label: "Abort — I'll configure it in the dashboard"
|
|
20595
20815
|
}
|
|
20596
20816
|
]);
|
|
20597
|
-
if (choice === "abort") return yield*
|
|
20817
|
+
if (choice === "abort") return yield* new MissingCredentialsError({
|
|
20598
20818
|
message: `Build aborted — no keystore bound to ${input.applicationIdentifier}.`,
|
|
20599
20819
|
hint: "Run `better-update credentials generate keystore` or upload via the dashboard."
|
|
20600
|
-
})
|
|
20820
|
+
});
|
|
20601
20821
|
const keystoreId = yield* choice === "generate" ? generateKeystoreAuto(api, input.applicationIdentifier) : pickExistingKeystore(api);
|
|
20602
20822
|
yield* api.androidBuildCredentials.create({
|
|
20603
20823
|
path: { applicationIdentifierId: appId },
|
|
@@ -20618,10 +20838,10 @@ const ensureAndroidCredentialsAvailable = (api, input) => api.buildCredentials.r
|
|
|
20618
20838
|
}).pipe(Effect.asVoid);
|
|
20619
20839
|
const ensureAndroidCredentials = (api, input, options) => ensureAndroidCredentialsAvailable(api, input).pipe(Effect.catchIf(isMissingResolveError, () => Effect.gen(function* () {
|
|
20620
20840
|
const mode = yield* InteractiveMode;
|
|
20621
|
-
if (options.freezeCredentials || !mode.allow) return yield*
|
|
20841
|
+
if (options.freezeCredentials || !mode.allow) return yield* new MissingCredentialsError({
|
|
20622
20842
|
message: `No Android build credentials for ${input.applicationIdentifier}.`,
|
|
20623
20843
|
hint: options.freezeCredentials ? "Run `better-update credentials generate` first, or remove --freeze-credentials." : "Run `better-update credentials generate` first, or rerun with --interactive to configure now."
|
|
20624
|
-
})
|
|
20844
|
+
});
|
|
20625
20845
|
yield* setupAndroidInteractive(api, input);
|
|
20626
20846
|
return yield* ensureAndroidCredentialsAvailable(api, input);
|
|
20627
20847
|
})));
|
|
@@ -20642,10 +20862,10 @@ const resolveIosBuildCredentials = (api, input) => api.buildCredentials.resolve(
|
|
|
20642
20862
|
const findBoundIosConfig = (api, input) => Effect.gen(function* () {
|
|
20643
20863
|
const distributionType = IOS_DISTRIBUTION_TO_TYPE[input.distribution];
|
|
20644
20864
|
const match = (yield* api.iosBundleConfigurations.list({ path: { projectId: input.projectId } })).items.find((config) => config.bundleIdentifier === input.bundleIdentifier && config.distributionType === distributionType);
|
|
20645
|
-
if (match === void 0) return yield*
|
|
20865
|
+
if (match === void 0) return yield* new MissingCredentialsError({
|
|
20646
20866
|
message: `iOS bundle configuration vanished while regenerating stale profile for ${input.bundleIdentifier}`,
|
|
20647
20867
|
hint: "Retry; the configuration must exist before regeneration"
|
|
20648
|
-
})
|
|
20868
|
+
});
|
|
20649
20869
|
return match;
|
|
20650
20870
|
});
|
|
20651
20871
|
const regenerateProvisioningProfile = (api, input) => Effect.gen(function* () {
|
|
@@ -20676,19 +20896,19 @@ const regenerateProvisioningProfile = (api, input) => Effect.gen(function* () {
|
|
|
20676
20896
|
});
|
|
20677
20897
|
const ensureIosCredentials = (api, input, options) => resolveIosBuildCredentials(api, input).pipe(Effect.catchIf(isMissingResolveError, () => Effect.gen(function* () {
|
|
20678
20898
|
const mode = yield* InteractiveMode;
|
|
20679
|
-
if (options.freezeCredentials || !mode.allow) return yield*
|
|
20899
|
+
if (options.freezeCredentials || !mode.allow) return yield* new MissingCredentialsError({
|
|
20680
20900
|
message: `No iOS build credentials for ${input.bundleIdentifier} (${input.distribution}).`,
|
|
20681
20901
|
hint: options.freezeCredentials ? "Run `better-update credentials generate` first, or remove --freeze-credentials." : "Run `better-update credentials generate` first, or rerun with --interactive to configure now."
|
|
20682
|
-
})
|
|
20902
|
+
});
|
|
20683
20903
|
yield* setupIosInteractive(api, input);
|
|
20684
20904
|
return yield* resolveIosBuildCredentials(api, input);
|
|
20685
20905
|
})), Effect.flatMap((resolved) => Effect.gen(function* () {
|
|
20686
20906
|
if (resolved.platform !== "ios" || !resolved.profileStale) return;
|
|
20687
20907
|
const mode = yield* InteractiveMode;
|
|
20688
|
-
if (options.freezeCredentials || !mode.allow) return yield*
|
|
20908
|
+
if (options.freezeCredentials || !mode.allow) return yield* new MissingCredentialsError({
|
|
20689
20909
|
message: `Stale provisioning profile for ${input.bundleIdentifier}; cannot regenerate without an interactive session.`,
|
|
20690
20910
|
hint: options.freezeCredentials ? "Run a build without --freeze-credentials once to refresh the profile, or run `better-update credentials regenerate-profile`." : "Run `better-update credentials regenerate-profile --bundle <id> --distribution <type>` from an interactive terminal."
|
|
20691
|
-
})
|
|
20911
|
+
});
|
|
20692
20912
|
yield* Console.log(`Stale provisioning profile for ${input.bundleIdentifier} (device roster changed). Regenerating...`);
|
|
20693
20913
|
yield* regenerateProvisioningProfile(api, input);
|
|
20694
20914
|
})));
|
|
@@ -20738,7 +20958,7 @@ const applyTargetSigning = (options) => Effect.gen(function* () {
|
|
|
20738
20958
|
const projectDir = yield* findXcodeProjectDir$1(options.iosDir);
|
|
20739
20959
|
const pbxprojPath = path.join(projectDir, "project.pbxproj");
|
|
20740
20960
|
const project = yield* parseProject$1(pbxprojPath);
|
|
20741
|
-
for (const entry of options.entries) for (const configUuid of entry.buildConfigurationUuids) if (!mutateConfig(project, configUuid, entry.settings)) yield* new XcodeProjectError({ message: `Build configuration ${configUuid} not found for target "${entry.targetName}" in ${pbxprojPath}.` });
|
|
20961
|
+
for (const entry of options.entries) for (const configUuid of entry.buildConfigurationUuids) if (!mutateConfig(project, configUuid, entry.settings)) return yield* new XcodeProjectError({ message: `Build configuration ${configUuid} not found for target "${entry.targetName}" in ${pbxprojPath}.` });
|
|
20742
20962
|
const serialized = yield* Effect.try({
|
|
20743
20963
|
try: () => project.writeSync(),
|
|
20744
20964
|
catch: (cause) => new XcodeProjectError({ message: `Failed to serialize ${pbxprojPath}: ${cause instanceof Error ? cause.message : String(cause)}` })
|
|
@@ -20798,7 +21018,7 @@ const listCurrentKeychains = Effect.gen(function* () {
|
|
|
20798
21018
|
const parseSigningIdentity = (output) => {
|
|
20799
21019
|
const lines = output.split("\n");
|
|
20800
21020
|
for (const line of lines) {
|
|
20801
|
-
const match = /"([^"]+)"/u.exec(line);
|
|
21021
|
+
const match = /"(?<identity>[^"]+)"/u.exec(line);
|
|
20802
21022
|
if (match?.[1]) return match[1];
|
|
20803
21023
|
}
|
|
20804
21024
|
};
|
|
@@ -20926,7 +21146,7 @@ const installProvisioningProfile = ({ profilePath }) => Effect.acquireRelease(Ef
|
|
|
20926
21146
|
//#endregion
|
|
20927
21147
|
//#region src/lib/post-build-validation.ts
|
|
20928
21148
|
const validateOneBundle = (bundleDir, expectedByBundleId, expectedTeamId) => Effect.gen(function* () {
|
|
20929
|
-
const bundleId = yield* readBundleId(bundleDir).pipe(Effect.
|
|
21149
|
+
const bundleId = yield* readBundleId(bundleDir).pipe(Effect.orElseSucceed(() => void 0));
|
|
20930
21150
|
if (!bundleId) return {
|
|
20931
21151
|
bundleId: void 0,
|
|
20932
21152
|
warnings: [`Missing CFBundleIdentifier in Info.plist at ${bundleDir}`]
|
|
@@ -20938,16 +21158,16 @@ const validateOneBundle = (bundleDir, expectedByBundleId, expectedTeamId) => Eff
|
|
|
20938
21158
|
};
|
|
20939
21159
|
return {
|
|
20940
21160
|
bundleId,
|
|
20941
|
-
warnings: yield* validateEmbeddedProfile(bundleDir, expected.profileUuid, expectedTeamId, bundleId).pipe(Effect.
|
|
21161
|
+
warnings: yield* validateEmbeddedProfile(bundleDir, expected.profileUuid, expectedTeamId, bundleId).pipe(Effect.orElseSucceed(() => []))
|
|
20942
21162
|
};
|
|
20943
21163
|
});
|
|
20944
21164
|
const validateIosBuild = (params) => Effect.gen(function* () {
|
|
20945
|
-
const appDir = yield* findAppDirectory$1(params.archivePath).pipe(Effect.
|
|
21165
|
+
const appDir = yield* findAppDirectory$1(params.archivePath).pipe(Effect.orElseSucceed(() => void 0));
|
|
20946
21166
|
if (!appDir) return {
|
|
20947
21167
|
passed: false,
|
|
20948
21168
|
warnings: ["Could not locate .app bundle in archive — skipping post-build validation"]
|
|
20949
21169
|
};
|
|
20950
|
-
const bundleDirs = yield* listSignedBundleDirs(appDir).pipe(Effect.
|
|
21170
|
+
const bundleDirs = yield* listSignedBundleDirs(appDir).pipe(Effect.orElseSucceed(() => [appDir]));
|
|
20951
21171
|
const expectedByBundleId = new Map(params.expectedTargets.map((target) => [target.bundleId, target]));
|
|
20952
21172
|
const perBundle = yield* Effect.forEach(bundleDirs, (bundleDir) => validateOneBundle(bundleDir, expectedByBundleId, params.expectedTeamId));
|
|
20953
21173
|
const warnings = perBundle.flatMap((entry) => [...entry.warnings]);
|
|
@@ -20977,7 +21197,7 @@ const findAppDirectory$1 = (archivePath) => Effect.gen(function* () {
|
|
|
20977
21197
|
const listSignedBundleDirs = (appDir) => Effect.gen(function* () {
|
|
20978
21198
|
const fs = yield* FileSystem.FileSystem;
|
|
20979
21199
|
const plugInsDir = path.join(appDir, "PlugIns");
|
|
20980
|
-
if (!(yield* fs.exists(plugInsDir).pipe(Effect.
|
|
21200
|
+
if (!(yield* fs.exists(plugInsDir).pipe(Effect.orElseSucceed(() => false)))) return [appDir];
|
|
20981
21201
|
return [appDir, ...(yield* fs.readDirectory(plugInsDir)).filter((entry) => entry.endsWith(".appex")).map((entry) => path.join(plugInsDir, entry))];
|
|
20982
21202
|
});
|
|
20983
21203
|
const readBundleId = (bundleDir) => Effect.gen(function* () {
|
|
@@ -21101,36 +21321,85 @@ const createXcodebuildFormatter = (projectRoot) => {
|
|
|
21101
21321
|
};
|
|
21102
21322
|
|
|
21103
21323
|
//#endregion
|
|
21104
|
-
//#region src/commands/build/ios.ts
|
|
21105
|
-
const
|
|
21106
|
-
|
|
21107
|
-
|
|
21108
|
-
|
|
21324
|
+
//#region src/commands/build/ios-prepare.ts
|
|
21325
|
+
const baseName = (entry) => entry.replace(/\.(?<ext>xcworkspace|xcodeproj)$/u, "");
|
|
21326
|
+
/**
|
|
21327
|
+
* Resolve the Xcode container to build: an explicit `workspace`/`project` from
|
|
21328
|
+
* the profile, else an auto-discovered `.xcworkspace` (CocoaPods), else the
|
|
21329
|
+
* `.xcodeproj` (pure-native apps without Pods).
|
|
21330
|
+
*/
|
|
21331
|
+
const resolveXcodeContainer = (projectRoot, iosDir, iosProfile) => Effect.gen(function* () {
|
|
21332
|
+
if (iosProfile.workspace !== void 0) {
|
|
21333
|
+
const containerPath = path.resolve(projectRoot, iosProfile.workspace);
|
|
21334
|
+
return {
|
|
21335
|
+
flag: "-workspace",
|
|
21336
|
+
containerPath,
|
|
21337
|
+
schemeBase: baseName(path.basename(containerPath))
|
|
21338
|
+
};
|
|
21339
|
+
}
|
|
21340
|
+
if (iosProfile.project !== void 0) {
|
|
21341
|
+
const containerPath = path.resolve(projectRoot, iosProfile.project);
|
|
21342
|
+
return {
|
|
21343
|
+
flag: "-project",
|
|
21344
|
+
containerPath,
|
|
21345
|
+
schemeBase: baseName(path.basename(containerPath))
|
|
21346
|
+
};
|
|
21347
|
+
}
|
|
21348
|
+
const entries = yield* (yield* FileSystem.FileSystem).readDirectory(iosDir).pipe(Effect.orElseSucceed(() => []));
|
|
21349
|
+
const workspace = entries.find((entry) => entry.endsWith(".xcworkspace"));
|
|
21350
|
+
if (workspace !== void 0) return {
|
|
21351
|
+
flag: "-workspace",
|
|
21352
|
+
containerPath: path.join(iosDir, workspace),
|
|
21353
|
+
schemeBase: baseName(workspace)
|
|
21354
|
+
};
|
|
21355
|
+
const project = entries.find((entry) => entry.endsWith(".xcodeproj"));
|
|
21356
|
+
if (project !== void 0) return {
|
|
21357
|
+
flag: "-project",
|
|
21358
|
+
containerPath: path.join(iosDir, project),
|
|
21359
|
+
schemeBase: baseName(project)
|
|
21360
|
+
};
|
|
21361
|
+
return yield* new BuildFailedError({
|
|
21362
|
+
step: "resolve Xcode container",
|
|
21109
21363
|
exitCode: 1,
|
|
21110
|
-
message: `No .xcworkspace found under ${iosDir}.
|
|
21364
|
+
message: `No .xcworkspace or .xcodeproj found under ${iosDir}. Set ios.workspace / ios.project in better-update.json.`
|
|
21111
21365
|
});
|
|
21112
|
-
return workspace;
|
|
21113
21366
|
});
|
|
21114
|
-
|
|
21115
|
-
|
|
21116
|
-
|
|
21117
|
-
|
|
21118
|
-
|
|
21119
|
-
|
|
21120
|
-
|
|
21121
|
-
|
|
21122
|
-
"
|
|
21123
|
-
|
|
21124
|
-
|
|
21125
|
-
|
|
21126
|
-
|
|
21127
|
-
|
|
21367
|
+
/**
|
|
21368
|
+
* Prepare the `ios/` dir for an xcodebuild. Expo regenerates it from app.json
|
|
21369
|
+
* via prebuild then runs `pod install`; bare/KMP/native build the committed dir
|
|
21370
|
+
* and only run `pod install` when a Podfile is present (unless disabled).
|
|
21371
|
+
*/
|
|
21372
|
+
const prepareIosNative = (params) => Effect.gen(function* () {
|
|
21373
|
+
if (params.strategy === "expo") {
|
|
21374
|
+
yield* runStep({
|
|
21375
|
+
command: "bunx",
|
|
21376
|
+
args: [
|
|
21377
|
+
"expo",
|
|
21378
|
+
"prebuild",
|
|
21379
|
+
"--platform",
|
|
21380
|
+
"ios",
|
|
21381
|
+
"--clean"
|
|
21382
|
+
],
|
|
21383
|
+
cwd: params.projectRoot,
|
|
21384
|
+
env: params.commandEnv
|
|
21385
|
+
}, "expo prebuild ios");
|
|
21386
|
+
yield* runStep({
|
|
21387
|
+
command: "pod",
|
|
21388
|
+
args: ["install"],
|
|
21389
|
+
cwd: params.iosDir,
|
|
21390
|
+
env: params.commandEnv
|
|
21391
|
+
}, "pod install");
|
|
21392
|
+
return;
|
|
21393
|
+
}
|
|
21394
|
+
if (params.iosProfile.podInstall === false) return;
|
|
21395
|
+
if (yield* (yield* FileSystem.FileSystem).exists(path.join(params.iosDir, "Podfile")).pipe(Effect.orElseSucceed(() => false))) yield* runStep({
|
|
21128
21396
|
command: "pod",
|
|
21129
21397
|
args: ["install"],
|
|
21130
21398
|
cwd: params.iosDir,
|
|
21131
21399
|
env: params.commandEnv
|
|
21132
21400
|
}, "pod install");
|
|
21133
21401
|
});
|
|
21402
|
+
/** Recursively locate the first `.app` bundle under `root` (simulator output). */
|
|
21134
21403
|
const findAppDirectory = (root) => Effect.gen(function* () {
|
|
21135
21404
|
const fs = yield* FileSystem.FileSystem;
|
|
21136
21405
|
const stack = [root];
|
|
@@ -21150,25 +21419,30 @@ const findAppDirectory = (root) => Effect.gen(function* () {
|
|
|
21150
21419
|
}
|
|
21151
21420
|
return yield* new ArtifactNotFoundError({ message: `No .app bundle found under "${root}".` });
|
|
21152
21421
|
});
|
|
21422
|
+
|
|
21423
|
+
//#endregion
|
|
21424
|
+
//#region src/commands/build/ios.ts
|
|
21153
21425
|
const runIosSimulatorBuild = (input) => Effect.gen(function* () {
|
|
21154
21426
|
const { projectRoot, iosProfile, envVars, tempDir } = input;
|
|
21155
21427
|
const runtime = yield* CliRuntime;
|
|
21156
21428
|
const iosDir = path.join(projectRoot, "ios");
|
|
21157
21429
|
const commandEnv = yield* runtime.commandEnvironment(envVars);
|
|
21158
|
-
yield*
|
|
21430
|
+
yield* prepareIosNative({
|
|
21431
|
+
strategy: input.strategy,
|
|
21159
21432
|
projectRoot,
|
|
21160
21433
|
iosDir,
|
|
21434
|
+
iosProfile,
|
|
21161
21435
|
commandEnv
|
|
21162
21436
|
});
|
|
21163
|
-
const
|
|
21164
|
-
const scheme = iosProfile.scheme ??
|
|
21437
|
+
const container = yield* resolveXcodeContainer(projectRoot, iosDir, iosProfile);
|
|
21438
|
+
const scheme = iosProfile.scheme ?? container.schemeBase;
|
|
21165
21439
|
const configuration = iosProfile.buildConfiguration ?? "Release";
|
|
21166
21440
|
const derivedDataPath = path.join(tempDir, "derived-data");
|
|
21167
21441
|
const buildCmd = {
|
|
21168
21442
|
command: "xcodebuild",
|
|
21169
21443
|
args: [
|
|
21170
|
-
|
|
21171
|
-
|
|
21444
|
+
container.flag,
|
|
21445
|
+
container.containerPath,
|
|
21172
21446
|
"-scheme",
|
|
21173
21447
|
scheme,
|
|
21174
21448
|
"-configuration",
|
|
@@ -21256,13 +21530,15 @@ const runIosDeviceBuild = (input) => Effect.gen(function* () {
|
|
|
21256
21530
|
const iosDir = path.join(projectRoot, "ios");
|
|
21257
21531
|
const { distribution } = iosProfile;
|
|
21258
21532
|
const commandEnv = yield* runtime.commandEnvironment(envVars);
|
|
21259
|
-
yield*
|
|
21533
|
+
yield* prepareIosNative({
|
|
21534
|
+
strategy: input.strategy,
|
|
21260
21535
|
projectRoot,
|
|
21261
21536
|
iosDir,
|
|
21537
|
+
iosProfile,
|
|
21262
21538
|
commandEnv
|
|
21263
21539
|
});
|
|
21264
|
-
const
|
|
21265
|
-
const scheme = iosProfile.scheme ??
|
|
21540
|
+
const container = yield* resolveXcodeContainer(projectRoot, iosDir, iosProfile);
|
|
21541
|
+
const scheme = iosProfile.scheme ?? container.schemeBase;
|
|
21266
21542
|
const configuration = iosProfile.buildConfiguration ?? "Release";
|
|
21267
21543
|
const signedTargets = yield* discoverSignedTargets({
|
|
21268
21544
|
iosDir,
|
|
@@ -21309,8 +21585,8 @@ const runIosDeviceBuild = (input) => Effect.gen(function* () {
|
|
|
21309
21585
|
const archiveCmd = {
|
|
21310
21586
|
command: "xcodebuild",
|
|
21311
21587
|
args: [
|
|
21312
|
-
|
|
21313
|
-
|
|
21588
|
+
container.flag,
|
|
21589
|
+
container.containerPath,
|
|
21314
21590
|
"-scheme",
|
|
21315
21591
|
scheme,
|
|
21316
21592
|
"-configuration",
|
|
@@ -21374,19 +21650,76 @@ const runIosDeviceBuild = (input) => Effect.gen(function* () {
|
|
|
21374
21650
|
sha256
|
|
21375
21651
|
};
|
|
21376
21652
|
});
|
|
21377
|
-
|
|
21653
|
+
/**
|
|
21654
|
+
* Custom-command iOS build. The resolved p12 + provisioning profiles are written
|
|
21655
|
+
* to `tempDir` and their paths exposed via `BETTER_UPDATE_IOS_*` env vars; the
|
|
21656
|
+
* user's command performs the actual signing/archive. The artifact is located
|
|
21657
|
+
* via the profile's `artifactPath` glob.
|
|
21658
|
+
*/
|
|
21659
|
+
const runIosCustom = (input) => Effect.gen(function* () {
|
|
21660
|
+
const commandEnv = yield* (yield* CliRuntime).commandEnvironment(input.envVars);
|
|
21661
|
+
const custom = input.customCommand;
|
|
21662
|
+
if (custom === void 0) return yield* new BuildFailedError({
|
|
21663
|
+
step: "custom ios build",
|
|
21664
|
+
exitCode: 1,
|
|
21665
|
+
message: "Internal: custom iOS strategy selected without a custom command."
|
|
21666
|
+
});
|
|
21667
|
+
if (custom.artifactPath === void 0) return yield* new BuildFailedError({
|
|
21668
|
+
step: "custom ios build",
|
|
21669
|
+
exitCode: 1,
|
|
21670
|
+
message: "Custom iOS build requires \"artifactPath\" (e.g. \"build/*.ipa\") in better-update.json."
|
|
21671
|
+
});
|
|
21672
|
+
const credentials = yield* fetchAllCredentials({
|
|
21673
|
+
api: input.api,
|
|
21674
|
+
input,
|
|
21675
|
+
mainBundleIdentifier: input.bundleId,
|
|
21676
|
+
allBundleIdentifiers: [input.bundleId]
|
|
21677
|
+
});
|
|
21678
|
+
const credEnv = {
|
|
21679
|
+
BETTER_UPDATE_IOS_P12_PATH: credentials.p12Path,
|
|
21680
|
+
BETTER_UPDATE_IOS_P12_PASSWORD: credentials.p12Password,
|
|
21681
|
+
BETTER_UPDATE_IOS_PROVISIONING_PROFILES: credentials.profiles.map((profile) => profile.profilePath).join(":")
|
|
21682
|
+
};
|
|
21683
|
+
const cwd = custom.cwd === void 0 ? input.projectRoot : path.join(input.projectRoot, custom.cwd);
|
|
21684
|
+
const buildStartMs = Date.now();
|
|
21685
|
+
yield* runStep({
|
|
21686
|
+
command: "sh",
|
|
21687
|
+
args: ["-c", custom.command],
|
|
21688
|
+
cwd,
|
|
21689
|
+
env: {
|
|
21690
|
+
...commandEnv,
|
|
21691
|
+
...credEnv,
|
|
21692
|
+
...custom.env
|
|
21693
|
+
}
|
|
21694
|
+
}, "custom ios build");
|
|
21695
|
+
const artifactPath = yield* findArtifactByGlob({
|
|
21696
|
+
baseDir: cwd,
|
|
21697
|
+
pattern: custom.artifactPath,
|
|
21698
|
+
minMtimeMs: buildStartMs
|
|
21699
|
+
});
|
|
21700
|
+
const { sha256, byteSize } = yield* sha256File(artifactPath);
|
|
21701
|
+
return {
|
|
21702
|
+
artifactPath,
|
|
21703
|
+
byteSize,
|
|
21704
|
+
sha256
|
|
21705
|
+
};
|
|
21706
|
+
});
|
|
21707
|
+
const runIosBuild = (input) => {
|
|
21708
|
+
if (input.strategy === "custom") return runIosCustom(input);
|
|
21709
|
+
return input.iosProfile.simulator === true ? runIosSimulatorBuild(input) : runIosDeviceBuild(input);
|
|
21710
|
+
};
|
|
21378
21711
|
|
|
21379
21712
|
//#endregion
|
|
21380
21713
|
//#region src/commands/build/reserve-and-upload.ts
|
|
21381
21714
|
const buildReserveCommon = (input) => ({
|
|
21382
21715
|
projectId: input.projectId,
|
|
21383
21716
|
profile: input.profileName,
|
|
21384
|
-
runtimeVersion: input.runtimeVersion,
|
|
21385
21717
|
bundleId: input.bundleId,
|
|
21386
21718
|
sha256: input.sha256,
|
|
21387
21719
|
byteSize: input.byteSize,
|
|
21388
21720
|
gitDirty: input.gitContext.dirty,
|
|
21389
21721
|
...compact({
|
|
21722
|
+
runtimeVersion: input.runtimeVersion,
|
|
21390
21723
|
appVersion: input.appVersion,
|
|
21391
21724
|
buildNumber: input.buildNumber,
|
|
21392
21725
|
gitRef: input.gitContext.ref,
|
|
@@ -21462,7 +21795,7 @@ const bumpVersionCode = (current) => Effect.gen(function* () {
|
|
|
21462
21795
|
if (!Number.isInteger(value) || value < 0) return yield* new BuildProfileError({ message: `Cannot autoIncrement android.versionCode: current value ${String(value)} is not a non-negative integer.` });
|
|
21463
21796
|
return value + 1;
|
|
21464
21797
|
});
|
|
21465
|
-
const SEMVER_PATCH = /^(
|
|
21798
|
+
const SEMVER_PATCH = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?<suffix>.*)$/u;
|
|
21466
21799
|
const bumpVersion = (current) => Effect.gen(function* () {
|
|
21467
21800
|
if (current === void 0) return yield* new BuildProfileError({ message: "Cannot autoIncrement version: no `version` field set in Expo config." });
|
|
21468
21801
|
const match = SEMVER_PATCH.exec(current);
|
|
@@ -21543,20 +21876,21 @@ const stripExtends = (profile) => {
|
|
|
21543
21876
|
};
|
|
21544
21877
|
const resolveExtendsChain = (params) => Effect.gen(function* () {
|
|
21545
21878
|
const { profiles, profileName, label, maxDepth, makeError } = params;
|
|
21879
|
+
const sourceLabel = params.sourceLabel ?? "eas.json";
|
|
21546
21880
|
const noun = label === "build" ? "Build" : "Submit";
|
|
21547
21881
|
const chain = [];
|
|
21548
21882
|
const visited = /* @__PURE__ */ new Set();
|
|
21549
21883
|
let current = profileName;
|
|
21550
21884
|
let depth = 0;
|
|
21551
21885
|
while (current !== void 0) {
|
|
21552
|
-
if (visited.has(current)) return yield* Effect.fail(makeError(`Cycle detected in
|
|
21886
|
+
if (visited.has(current)) return yield* Effect.fail(makeError(`Cycle detected in ${sourceLabel} ${label}.${profileName} extends chain at "${current}".`));
|
|
21553
21887
|
visited.add(current);
|
|
21554
21888
|
const profile = profiles[current];
|
|
21555
|
-
if (!profile) return yield* Effect.fail(makeError(current === profileName ? `${noun} profile "${profileName}" not found in
|
|
21889
|
+
if (!profile) return yield* Effect.fail(makeError(current === profileName ? `${noun} profile "${profileName}" not found in ${sourceLabel}.` : `${noun} profile "${profileName}" extends missing profile "${current}".`));
|
|
21556
21890
|
chain.unshift(profile);
|
|
21557
21891
|
current = profile.extends;
|
|
21558
21892
|
depth += 1;
|
|
21559
|
-
if (depth > maxDepth) return yield* Effect.fail(makeError(`Too many "extends" levels (max ${String(maxDepth)}) in
|
|
21893
|
+
if (depth > maxDepth) return yield* Effect.fail(makeError(`Too many "extends" levels (max ${String(maxDepth)}) in ${sourceLabel} ${label}.${profileName}.`));
|
|
21560
21894
|
}
|
|
21561
21895
|
return chain;
|
|
21562
21896
|
});
|
|
@@ -21623,13 +21957,14 @@ const mergeSubmitProfile = (base, overlay) => {
|
|
|
21623
21957
|
android
|
|
21624
21958
|
});
|
|
21625
21959
|
};
|
|
21626
|
-
const resolveEasSubmitProfile = (profiles, profileName) => Effect.gen(function* () {
|
|
21627
|
-
if (!profiles) return yield* new BuildProfileError({ message:
|
|
21960
|
+
const resolveEasSubmitProfile = (profiles, profileName, sourceLabel = "eas.json") => Effect.gen(function* () {
|
|
21961
|
+
if (!profiles) return yield* new BuildProfileError({ message: `${sourceLabel} has no "submit" section. Add at least one submit profile.` });
|
|
21628
21962
|
return stripExtends((yield* resolveExtendsChain({
|
|
21629
21963
|
profiles,
|
|
21630
21964
|
profileName,
|
|
21631
21965
|
label: "submit",
|
|
21632
21966
|
maxDepth: MAX_SUBMIT_EXTENDS_DEPTH,
|
|
21967
|
+
sourceLabel,
|
|
21633
21968
|
makeError: (message) => new BuildProfileError({ message })
|
|
21634
21969
|
})).reduce((acc, next, index) => index === 0 ? next : mergeSubmitProfile(acc, next), {}));
|
|
21635
21970
|
});
|
|
@@ -21696,7 +22031,13 @@ const parseIosProfile = (raw) => {
|
|
|
21696
22031
|
scheme: asStringValue(record["scheme"]),
|
|
21697
22032
|
simulator: asBooleanValue(record["simulator"]),
|
|
21698
22033
|
enterpriseProvisioning: asEnterpriseProvisioning(record["enterpriseProvisioning"]),
|
|
21699
|
-
autoIncrement: asIosAutoIncrement(record["autoIncrement"])
|
|
22034
|
+
autoIncrement: asIosAutoIncrement(record["autoIncrement"]),
|
|
22035
|
+
workspace: asStringValue(record["workspace"]),
|
|
22036
|
+
project: asStringValue(record["project"]),
|
|
22037
|
+
podInstall: asBooleanValue(record["podInstall"]),
|
|
22038
|
+
bundleIdentifier: asStringValue(record["bundleIdentifier"]),
|
|
22039
|
+
version: asStringValue(record["version"]),
|
|
22040
|
+
buildNumber: asStringValue(record["buildNumber"])
|
|
21700
22041
|
});
|
|
21701
22042
|
};
|
|
21702
22043
|
const parseAndroidProfile = (raw) => {
|
|
@@ -21708,9 +22049,35 @@ const parseAndroidProfile = (raw) => {
|
|
|
21708
22049
|
gradleCommand: asStringValue(record["gradleCommand"]),
|
|
21709
22050
|
format: asAndroidFormat(record["format"]),
|
|
21710
22051
|
distribution: asAndroidDistribution(record["distribution"]),
|
|
21711
|
-
autoIncrement: asAndroidAutoIncrement(record["autoIncrement"])
|
|
22052
|
+
autoIncrement: asAndroidAutoIncrement(record["autoIncrement"]),
|
|
22053
|
+
module: asStringValue(record["module"]),
|
|
22054
|
+
gradleTask: asStringValue(record["gradleTask"]),
|
|
22055
|
+
applicationId: asStringValue(record["applicationId"]),
|
|
22056
|
+
version: asStringValue(record["version"]),
|
|
22057
|
+
versionCode: asStringValue(record["versionCode"])
|
|
21712
22058
|
});
|
|
21713
22059
|
};
|
|
22060
|
+
const parseCustomCommandSpec = (raw) => {
|
|
22061
|
+
const record = asRecord(raw);
|
|
22062
|
+
if (!record) return;
|
|
22063
|
+
const command = asStringValue(record["command"]);
|
|
22064
|
+
if (command === void 0) return;
|
|
22065
|
+
return compact({
|
|
22066
|
+
command,
|
|
22067
|
+
cwd: asStringValue(record["cwd"]),
|
|
22068
|
+
env: asEnv(record["env"]),
|
|
22069
|
+
artifactPath: asStringValue(record["artifactPath"])
|
|
22070
|
+
});
|
|
22071
|
+
};
|
|
22072
|
+
const parseCustomCommandProfile = (raw) => {
|
|
22073
|
+
const record = asRecord(raw);
|
|
22074
|
+
if (!record) return;
|
|
22075
|
+
const result = compact({
|
|
22076
|
+
ios: parseCustomCommandSpec(record["ios"]),
|
|
22077
|
+
android: parseCustomCommandSpec(record["android"])
|
|
22078
|
+
});
|
|
22079
|
+
return Object.keys(result).length === 0 ? void 0 : result;
|
|
22080
|
+
};
|
|
21714
22081
|
const parseBuildProfile = (raw) => {
|
|
21715
22082
|
const record = asRecord(raw);
|
|
21716
22083
|
if (!record) return;
|
|
@@ -21725,15 +22092,16 @@ const parseBuildProfile = (raw) => {
|
|
|
21725
22092
|
android: parseAndroidProfile(record["android"]),
|
|
21726
22093
|
credentialsSource: asCredentialsSource(record["credentialsSource"]),
|
|
21727
22094
|
autoIncrement: asAutoIncrement(record["autoIncrement"]),
|
|
21728
|
-
withoutCredentials: asBooleanValue(record["withoutCredentials"])
|
|
22095
|
+
withoutCredentials: asBooleanValue(record["withoutCredentials"]),
|
|
22096
|
+
custom: parseCustomCommandProfile(record["custom"])
|
|
21729
22097
|
});
|
|
21730
22098
|
};
|
|
21731
|
-
|
|
21732
|
-
|
|
21733
|
-
|
|
21734
|
-
|
|
21735
|
-
|
|
21736
|
-
|
|
22099
|
+
/**
|
|
22100
|
+
* Parse an already-decoded JSON object into an {@link EasConfig}. Shared by the
|
|
22101
|
+
* `eas.json` reader and the `better-update.json` build-config reader — both hold
|
|
22102
|
+
* the same `build`/`submit`/`cli` shape, only the source file differs.
|
|
22103
|
+
*/
|
|
22104
|
+
const parseConfigFromRecord = (root) => {
|
|
21737
22105
|
const buildRecord = asRecord(root["build"]);
|
|
21738
22106
|
if (!buildRecord) return asRecord(root["cli"]) ? { cli: parseCli(root["cli"]) } : {};
|
|
21739
22107
|
const profiles = {};
|
|
@@ -21752,6 +22120,14 @@ const parseEasConfig = (text) => Effect.gen(function* () {
|
|
|
21752
22120
|
build: profiles,
|
|
21753
22121
|
...Object.keys(submit).length === 0 ? {} : { submit }
|
|
21754
22122
|
};
|
|
22123
|
+
};
|
|
22124
|
+
const parseEasConfig = (text) => Effect.gen(function* () {
|
|
22125
|
+
const root = asRecord(yield* Effect.try({
|
|
22126
|
+
try: () => JSON.parse(text),
|
|
22127
|
+
catch: (cause) => new BuildProfileError({ message: `eas.json is not valid JSON: ${formatCause(cause)}` })
|
|
22128
|
+
}));
|
|
22129
|
+
if (!root) return yield* new BuildProfileError({ message: "eas.json must be a JSON object at the top level." });
|
|
22130
|
+
return parseConfigFromRecord(root);
|
|
21755
22131
|
});
|
|
21756
22132
|
const parseCli = (raw) => {
|
|
21757
22133
|
const record = asRecord(raw);
|
|
@@ -21764,12 +22140,17 @@ const easJsonPath = (projectRoot) => Effect.gen(function* () {
|
|
|
21764
22140
|
const readEasJson = (projectRoot) => Effect.gen(function* () {
|
|
21765
22141
|
const fs = yield* FileSystem.FileSystem;
|
|
21766
22142
|
const filePath = yield* easJsonPath(projectRoot);
|
|
21767
|
-
return yield* parseEasConfig(yield* fs.readFileString(filePath).pipe(Effect.
|
|
22143
|
+
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}` }))));
|
|
21768
22144
|
});
|
|
22145
|
+
const mergeCustom = (base, overlay) => {
|
|
22146
|
+
const merged = shallowMerge(base, overlay);
|
|
22147
|
+
return merged === void 0 || Object.keys(merged).length === 0 ? void 0 : merged;
|
|
22148
|
+
};
|
|
21769
22149
|
const mergeProfile = (base, overlay) => {
|
|
21770
22150
|
const ios = shallowMerge(base.ios, overlay.ios);
|
|
21771
22151
|
const android = shallowMerge(base.android, overlay.android);
|
|
21772
22152
|
const env = shallowMerge(base.env, overlay.env);
|
|
22153
|
+
const custom = mergeCustom(base.custom, overlay.custom);
|
|
21773
22154
|
const developmentClient = overlay.developmentClient ?? base.developmentClient;
|
|
21774
22155
|
const distribution = overlay.distribution ?? base.distribution;
|
|
21775
22156
|
const channel = overlay.channel ?? base.channel;
|
|
@@ -21788,21 +22169,41 @@ const mergeProfile = (base, overlay) => {
|
|
|
21788
22169
|
android,
|
|
21789
22170
|
credentialsSource,
|
|
21790
22171
|
autoIncrement,
|
|
21791
|
-
withoutCredentials
|
|
22172
|
+
withoutCredentials,
|
|
22173
|
+
custom
|
|
21792
22174
|
});
|
|
21793
22175
|
};
|
|
21794
|
-
const resolveEasBuildProfile = (config, profileName) => Effect.gen(function* () {
|
|
22176
|
+
const resolveEasBuildProfile = (config, profileName, sourceLabel = "eas.json") => Effect.gen(function* () {
|
|
21795
22177
|
const profiles = config.build;
|
|
21796
|
-
if (!profiles) return yield* new BuildProfileError({ message:
|
|
22178
|
+
if (!profiles) return yield* new BuildProfileError({ message: `${sourceLabel} has no "build" section. Add at least one profile.` });
|
|
21797
22179
|
return stripExtends((yield* resolveExtendsChain({
|
|
21798
22180
|
profiles,
|
|
21799
22181
|
profileName,
|
|
21800
22182
|
label: "build",
|
|
21801
22183
|
maxDepth: MAX_EXTENDS_DEPTH,
|
|
22184
|
+
sourceLabel,
|
|
21802
22185
|
makeError: (message) => new BuildProfileError({ message })
|
|
21803
22186
|
})).reduce((acc, next, index) => index === 0 ? next : mergeProfile(acc, next), {}));
|
|
21804
22187
|
});
|
|
21805
22188
|
|
|
22189
|
+
//#endregion
|
|
22190
|
+
//#region src/lib/better-update-build-config.ts
|
|
22191
|
+
/** Label used in profile-resolution error copy when config comes from this file. */
|
|
22192
|
+
const BETTER_UPDATE_SOURCE_LABEL = "better-update.json";
|
|
22193
|
+
/**
|
|
22194
|
+
* Read the `build`/`submit`/`cli` config from `better-update.json`. Returns an
|
|
22195
|
+
* empty config (no `build` key) when the file is absent or carries no build
|
|
22196
|
+
* section. Shares the parser with {@link file://./eas-config.ts}; only the
|
|
22197
|
+
* source file and the error `sourceLabel` differ.
|
|
22198
|
+
*/
|
|
22199
|
+
const readBuildConfig = (projectRoot) => readBetterUpdateConfig(projectRoot).pipe(Effect.map((config) => config === void 0 ? {} : parseConfigFromRecord(config)));
|
|
22200
|
+
/** List available build-profile names declared in `better-update.json`. */
|
|
22201
|
+
const listBuildProfileNames = (projectRoot) => readBuildConfig(projectRoot).pipe(Effect.map((config) => Object.keys(config.build ?? {})));
|
|
22202
|
+
/** Resolve a submit profile from `better-update.json`'s `submit` section. */
|
|
22203
|
+
const readSubmitProfile = (projectRoot, profileName) => Effect.gen(function* () {
|
|
22204
|
+
return yield* resolveEasSubmitProfile((yield* readBuildConfig(projectRoot)).submit, profileName, BETTER_UPDATE_SOURCE_LABEL);
|
|
22205
|
+
});
|
|
22206
|
+
|
|
21806
22207
|
//#endregion
|
|
21807
22208
|
//#region src/lib/build-profile.ts
|
|
21808
22209
|
const deriveIosDistribution = (eas) => {
|
|
@@ -21849,12 +22250,22 @@ const toIosProfile = (eas) => {
|
|
|
21849
22250
|
if (!distribution) return;
|
|
21850
22251
|
const ios = eas.ios ?? {};
|
|
21851
22252
|
const autoIncrement = resolveIosAutoIncrement(eas);
|
|
22253
|
+
const buildConfiguration = ios.buildConfiguration ?? (eas.developmentClient === true ? "Debug" : void 0);
|
|
22254
|
+
const metaOverride = compact({
|
|
22255
|
+
bundleIdentifier: ios.bundleIdentifier,
|
|
22256
|
+
version: ios.version,
|
|
22257
|
+
buildNumber: ios.buildNumber
|
|
22258
|
+
});
|
|
21852
22259
|
return compact({
|
|
21853
22260
|
distribution,
|
|
21854
|
-
buildConfiguration
|
|
22261
|
+
buildConfiguration,
|
|
21855
22262
|
scheme: ios.scheme,
|
|
21856
22263
|
simulator: ios.simulator,
|
|
21857
|
-
autoIncrement
|
|
22264
|
+
autoIncrement,
|
|
22265
|
+
workspace: ios.workspace,
|
|
22266
|
+
project: ios.project,
|
|
22267
|
+
podInstall: ios.podInstall,
|
|
22268
|
+
metaOverride: Object.keys(metaOverride).length === 0 ? void 0 : metaOverride
|
|
21858
22269
|
});
|
|
21859
22270
|
};
|
|
21860
22271
|
const toAndroidProfile = (eas) => {
|
|
@@ -21864,16 +22275,25 @@ const toAndroidProfile = (eas) => {
|
|
|
21864
22275
|
const android = eas.android ?? {};
|
|
21865
22276
|
const distribution = deriveAndroidDistribution(eas, format);
|
|
21866
22277
|
const autoIncrement = resolveAndroidAutoIncrement(eas);
|
|
22278
|
+
const buildType = android.buildType ?? (eas.developmentClient === true ? "debug" : void 0);
|
|
22279
|
+
const metaOverride = compact({
|
|
22280
|
+
applicationId: android.applicationId,
|
|
22281
|
+
version: android.version,
|
|
22282
|
+
versionCode: android.versionCode
|
|
22283
|
+
});
|
|
21867
22284
|
return compact({
|
|
21868
22285
|
format,
|
|
21869
22286
|
distribution,
|
|
21870
|
-
buildType
|
|
22287
|
+
buildType,
|
|
21871
22288
|
flavor: android.flavor,
|
|
21872
22289
|
gradleCommand: android.gradleCommand,
|
|
21873
|
-
autoIncrement
|
|
22290
|
+
autoIncrement,
|
|
22291
|
+
module: android.module,
|
|
22292
|
+
gradleTask: android.gradleTask,
|
|
22293
|
+
metaOverride: Object.keys(metaOverride).length === 0 ? void 0 : metaOverride
|
|
21874
22294
|
});
|
|
21875
22295
|
};
|
|
21876
|
-
const
|
|
22296
|
+
const fromGenericProfile = (eas, profileName) => {
|
|
21877
22297
|
const ios = toIosProfile(eas);
|
|
21878
22298
|
const android = toAndroidProfile(eas);
|
|
21879
22299
|
return compact({
|
|
@@ -21885,11 +22305,13 @@ const fromEasProfile = (eas, profileName) => {
|
|
|
21885
22305
|
android,
|
|
21886
22306
|
credentialsSource: eas.credentialsSource,
|
|
21887
22307
|
developmentClient: eas.developmentClient,
|
|
21888
|
-
withoutCredentials: eas.withoutCredentials
|
|
22308
|
+
withoutCredentials: eas.withoutCredentials,
|
|
22309
|
+
customCommand: eas.custom
|
|
21889
22310
|
});
|
|
21890
22311
|
};
|
|
22312
|
+
/** Resolve a build profile from `better-update.json`'s `build` section. */
|
|
21891
22313
|
const readBuildProfile = (projectRoot, profileName) => Effect.gen(function* () {
|
|
21892
|
-
return
|
|
22314
|
+
return fromGenericProfile(yield* resolveEasBuildProfile(yield* readBuildConfig(projectRoot), profileName, "better-update.json"), profileName);
|
|
21893
22315
|
});
|
|
21894
22316
|
const readRuntimeVersionMeta = (config, platform) => ({
|
|
21895
22317
|
platform,
|
|
@@ -21899,6 +22321,17 @@ const readRuntimeVersionMeta = (config, platform) => ({
|
|
|
21899
22321
|
rawRuntimeVersion: extractRawRuntimeVersion(config, platform)
|
|
21900
22322
|
});
|
|
21901
22323
|
|
|
22324
|
+
//#endregion
|
|
22325
|
+
//#region src/lib/build-strategy.ts
|
|
22326
|
+
const resolveAndroidStrategy = (profile, projectType) => {
|
|
22327
|
+
if (profile.customCommand?.android !== void 0) return "custom";
|
|
22328
|
+
return projectType === "expo" ? "expo" : "gradle";
|
|
22329
|
+
};
|
|
22330
|
+
const resolveIosStrategy = (profile, projectType) => {
|
|
22331
|
+
if (profile.customCommand?.ios !== void 0) return "custom";
|
|
22332
|
+
return projectType === "expo" ? "expo" : "xcode";
|
|
22333
|
+
};
|
|
22334
|
+
|
|
21902
22335
|
//#endregion
|
|
21903
22336
|
//#region src/lib/clear-cache.ts
|
|
21904
22337
|
/**
|
|
@@ -21919,13 +22352,71 @@ const clearBuildCaches = (projectRoot) => Effect.gen(function* () {
|
|
|
21919
22352
|
const removed = [];
|
|
21920
22353
|
yield* Effect.forEach(CACHE_DIRS, (rel) => Effect.gen(function* () {
|
|
21921
22354
|
const target = path.join(projectRoot, rel);
|
|
21922
|
-
if (!(yield* fs.exists(target).pipe(Effect.
|
|
21923
|
-
yield* fs.remove(target, { recursive: true }).pipe(Effect.
|
|
22355
|
+
if (!(yield* fs.exists(target).pipe(Effect.orElseSucceed(() => false)))) return;
|
|
22356
|
+
yield* fs.remove(target, { recursive: true }).pipe(Effect.orElseSucceed(() => void 0));
|
|
21924
22357
|
removed.push(rel);
|
|
21925
22358
|
}), { concurrency: 4 });
|
|
21926
22359
|
if (removed.length > 0) yield* Console.error(`Cleared caches: ${removed.join(", ")}`);
|
|
21927
22360
|
});
|
|
21928
22361
|
|
|
22362
|
+
//#endregion
|
|
22363
|
+
//#region src/lib/detect-project-type.ts
|
|
22364
|
+
const PROJECT_TYPES = [
|
|
22365
|
+
"expo",
|
|
22366
|
+
"bare",
|
|
22367
|
+
"kmp",
|
|
22368
|
+
"native",
|
|
22369
|
+
"custom"
|
|
22370
|
+
];
|
|
22371
|
+
/** Narrow an arbitrary `projectType` override (e.g. from better-update.json) to a valid value. */
|
|
22372
|
+
const asProjectType = (raw) => PROJECT_TYPES.find((type) => type === raw);
|
|
22373
|
+
const exists = (filePath) => Effect.gen(function* () {
|
|
22374
|
+
return yield* (yield* FileSystem.FileSystem).exists(filePath).pipe(Effect.orElseSucceed(() => false));
|
|
22375
|
+
});
|
|
22376
|
+
const readText = (filePath) => Effect.gen(function* () {
|
|
22377
|
+
return yield* (yield* FileSystem.FileSystem).readFileString(filePath).pipe(Effect.orElseSucceed(() => ""));
|
|
22378
|
+
});
|
|
22379
|
+
const hasExpoDependency = (projectRoot) => Effect.gen(function* () {
|
|
22380
|
+
const text = yield* readText(path.join(projectRoot, "package.json"));
|
|
22381
|
+
if (text.length === 0) return false;
|
|
22382
|
+
const pkg = asRecord(yield* Effect.try(() => JSON.parse(text)).pipe(Effect.orElseSucceed(() => void 0)));
|
|
22383
|
+
const deps = asRecord(pkg?.["dependencies"]);
|
|
22384
|
+
const devDeps = asRecord(pkg?.["devDependencies"]);
|
|
22385
|
+
return deps?.["expo"] !== void 0 || devDeps?.["expo"] !== void 0;
|
|
22386
|
+
});
|
|
22387
|
+
const hasAnyExpoConfigFile = (projectRoot) => Effect.gen(function* () {
|
|
22388
|
+
for (const name of [
|
|
22389
|
+
"app.json",
|
|
22390
|
+
"app.config.js",
|
|
22391
|
+
"app.config.ts"
|
|
22392
|
+
]) if (yield* exists(path.join(projectRoot, name))) return true;
|
|
22393
|
+
return false;
|
|
22394
|
+
});
|
|
22395
|
+
const looksKmp = (projectRoot) => Effect.gen(function* () {
|
|
22396
|
+
if (yield* exists(path.join(projectRoot, "composeApp"))) return true;
|
|
22397
|
+
for (const name of ["settings.gradle.kts", "settings.gradle"]) {
|
|
22398
|
+
const text = yield* readText(path.join(projectRoot, name));
|
|
22399
|
+
if (text.includes("composeApp") || text.includes(":shared")) return true;
|
|
22400
|
+
}
|
|
22401
|
+
return yield* exists(path.join(projectRoot, "android", "app", "build.gradle.kts"));
|
|
22402
|
+
});
|
|
22403
|
+
/**
|
|
22404
|
+
* Resolve a project's build-system family. An explicit override always wins;
|
|
22405
|
+
* otherwise the filesystem shape is inspected. `custom` is never auto-detected —
|
|
22406
|
+
* it is intent expressed via override or a profile `custom` block.
|
|
22407
|
+
*/
|
|
22408
|
+
const detectProjectType = (params) => Effect.gen(function* () {
|
|
22409
|
+
if (params.override !== void 0) return params.override;
|
|
22410
|
+
const { projectRoot } = params;
|
|
22411
|
+
if (isExpoConfigInstalled() && ((yield* hasExpoDependency(projectRoot)) || (yield* hasAnyExpoConfigFile(projectRoot)))) return "expo";
|
|
22412
|
+
if (yield* looksKmp(projectRoot)) return "kmp";
|
|
22413
|
+
const hasAndroid = yield* exists(path.join(projectRoot, "android"));
|
|
22414
|
+
const hasIos = yield* exists(path.join(projectRoot, "ios"));
|
|
22415
|
+
const hasPackageJson = yield* exists(path.join(projectRoot, "package.json"));
|
|
22416
|
+
if (hasAndroid && hasIos && hasPackageJson) return "bare";
|
|
22417
|
+
return "native";
|
|
22418
|
+
});
|
|
22419
|
+
|
|
21929
22420
|
//#endregion
|
|
21930
22421
|
//#region src/lib/dev-client-check.ts
|
|
21931
22422
|
const readDeps = (filePath) => Effect.gen(function* () {
|
|
@@ -22191,10 +22682,10 @@ const runString = (cmd, cwd) => Command.string(Command.workingDirectory(cmd, cwd
|
|
|
22191
22682
|
*/
|
|
22192
22683
|
const readGitContext = (projectRoot) => Effect.gen(function* () {
|
|
22193
22684
|
const [commit, ref, commitMessage, status] = yield* Effect.all([
|
|
22194
|
-
runString(Command.make("git", "rev-parse", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.
|
|
22195
|
-
runString(Command.make("git", "symbolic-ref", "--short", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.
|
|
22196
|
-
runString(Command.make("git", "log", "-1", "--format=%s"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.
|
|
22197
|
-
runString(Command.make("git", "status", "--porcelain"), projectRoot).pipe(Effect.
|
|
22685
|
+
runString(Command.make("git", "rev-parse", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.orElseSucceed(() => "")),
|
|
22686
|
+
runString(Command.make("git", "symbolic-ref", "--short", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.orElseSucceed(() => "")),
|
|
22687
|
+
runString(Command.make("git", "log", "-1", "--format=%s"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.orElseSucceed(() => "")),
|
|
22688
|
+
runString(Command.make("git", "status", "--porcelain"), projectRoot).pipe(Effect.orElseSucceed(() => ""))
|
|
22198
22689
|
], { concurrency: "unbounded" });
|
|
22199
22690
|
return {
|
|
22200
22691
|
ref: ref.length > 0 ? ref : void 0,
|
|
@@ -22223,14 +22714,14 @@ const readGradleConfig = (androidDir) => Effect.gen(function* () {
|
|
|
22223
22714
|
const hasKts = yield* fs.exists(ktsPath).pipe(Effect.orElseSucceed(() => false));
|
|
22224
22715
|
if (!hasGroovy && hasKts) return;
|
|
22225
22716
|
if (!hasGroovy) return;
|
|
22226
|
-
const content = yield* fs.readFileString(gradlePath).pipe(Effect.
|
|
22717
|
+
const content = yield* fs.readFileString(gradlePath).pipe(Effect.orElseSucceed(() => void 0));
|
|
22227
22718
|
if (!content) return;
|
|
22228
22719
|
return yield* Effect.tryPromise({
|
|
22229
22720
|
try: async () => {
|
|
22230
22721
|
return __require("gradle-to-js").parseText(stripGroovyComments(content));
|
|
22231
22722
|
},
|
|
22232
22723
|
catch: () => void 0
|
|
22233
|
-
}).pipe(Effect.map(extractGradleConfig), Effect.
|
|
22724
|
+
}).pipe(Effect.map(extractGradleConfig), Effect.orElseSucceed(() => void 0));
|
|
22234
22725
|
});
|
|
22235
22726
|
/**
|
|
22236
22727
|
* Log a warning if Gradle applicationId differs from app.json package name.
|
|
@@ -22291,6 +22782,28 @@ const detectPlatform = (explicit, config) => Effect.gen(function* () {
|
|
|
22291
22782
|
label: entry
|
|
22292
22783
|
})));
|
|
22293
22784
|
});
|
|
22785
|
+
/**
|
|
22786
|
+
* Resolve a build platform for non-Expo projects from an explicit flag, or by
|
|
22787
|
+
* intersecting the profile's declared platform sections with the native dirs
|
|
22788
|
+
* present on disk. Prompts when both remain; fails when ambiguous and prompts
|
|
22789
|
+
* are disallowed.
|
|
22790
|
+
*/
|
|
22791
|
+
const detectPlatformGeneric = (explicit, context) => Effect.gen(function* () {
|
|
22792
|
+
if (explicit !== void 0) return explicit;
|
|
22793
|
+
const candidates = [];
|
|
22794
|
+
const wantsIos = context.profile.ios !== void 0 || context.profile.customCommand?.ios !== void 0;
|
|
22795
|
+
const wantsAndroid = context.profile.android !== void 0 || context.profile.customCommand?.android !== void 0;
|
|
22796
|
+
if (wantsIos && (context.hasIosDir || context.profile.customCommand?.ios !== void 0)) candidates.push("ios");
|
|
22797
|
+
if (wantsAndroid && (context.hasAndroidDir || context.profile.customCommand?.android !== void 0)) candidates.push("android");
|
|
22798
|
+
if (candidates.length === 0) return yield* new BuildProfileError({ message: "Cannot infer build platform. Add an `ios` or `android` section to the build profile in better-update.json, or pass --platform." });
|
|
22799
|
+
const [only] = candidates;
|
|
22800
|
+
if (candidates.length === 1 && only !== void 0) return only;
|
|
22801
|
+
if (!(yield* InteractiveMode).allow) return yield* new BuildProfileError({ message: `Multiple platforms available (${candidates.join(", ")}). Pass --platform explicitly when running non-interactively.` });
|
|
22802
|
+
return yield* promptSelect("Which platform to build?", candidates.map((entry) => ({
|
|
22803
|
+
value: entry,
|
|
22804
|
+
label: entry
|
|
22805
|
+
})));
|
|
22806
|
+
});
|
|
22294
22807
|
|
|
22295
22808
|
//#endregion
|
|
22296
22809
|
//#region src/lib/project-staging.ts
|
|
@@ -22303,26 +22816,32 @@ const LOCKFILES = [
|
|
|
22303
22816
|
["package-lock.json", "npm"]
|
|
22304
22817
|
];
|
|
22305
22818
|
/**
|
|
22306
|
-
*
|
|
22307
|
-
*
|
|
22819
|
+
* Generated native build outputs / dependency dirs that must never be copied —
|
|
22820
|
+
* they are regenerated in staging (Pods via `pod install`, build/ via gradle).
|
|
22308
22821
|
*/
|
|
22309
|
-
const
|
|
22310
|
-
"node_modules",
|
|
22311
|
-
".git",
|
|
22822
|
+
const NATIVE_BUILD_OUTPUTS = [
|
|
22312
22823
|
"ios/build",
|
|
22313
22824
|
"ios/Pods",
|
|
22314
22825
|
"ios/DerivedData",
|
|
22315
22826
|
"android/build",
|
|
22316
22827
|
"android/app/build",
|
|
22317
22828
|
"android/.gradle",
|
|
22318
|
-
"android/.kotlin"
|
|
22829
|
+
"android/.kotlin"
|
|
22830
|
+
];
|
|
22831
|
+
/**
|
|
22832
|
+
* Paths never copied into staging — covers generated native build outputs and
|
|
22833
|
+
* dependency dirs that must be reinstalled fresh in staging.
|
|
22834
|
+
*/
|
|
22835
|
+
const ALWAYS_IGNORE = [...[
|
|
22836
|
+
"node_modules",
|
|
22837
|
+
".git",
|
|
22319
22838
|
".expo",
|
|
22320
22839
|
".gradle",
|
|
22321
22840
|
".turbo",
|
|
22322
22841
|
"dist"
|
|
22323
|
-
];
|
|
22842
|
+
], ...NATIVE_BUILD_OUTPUTS];
|
|
22324
22843
|
const findLockfile = (fs, dir) => Effect.gen(function* () {
|
|
22325
|
-
for (const [name, pm] of LOCKFILES) if (yield* fs.exists(path.join(dir, name)).pipe(Effect.
|
|
22844
|
+
for (const [name, pm] of LOCKFILES) if (yield* fs.exists(path.join(dir, name)).pipe(Effect.orElseSucceed(() => false))) return pm;
|
|
22326
22845
|
});
|
|
22327
22846
|
const walkUpForLockfile = (startCwd, dir) => Effect.gen(function* () {
|
|
22328
22847
|
const pm = yield* findLockfile(yield* FileSystem.FileSystem, dir);
|
|
@@ -22348,21 +22867,30 @@ const detectWorkspaceRoot = (cwd) => walkUpForLockfile(cwd, cwd);
|
|
|
22348
22867
|
* Build an `Ignore` matcher for the workspace root. `.easignore` REPLACES
|
|
22349
22868
|
* `.gitignore` when present (matches EAS semantics); otherwise `.gitignore`
|
|
22350
22869
|
* is layered on top of the always-ignore baseline.
|
|
22870
|
+
*
|
|
22871
|
+
* When `includeNativeSource` is set, the native source dirs are re-included
|
|
22872
|
+
* after the ignore files are applied, then their build outputs re-excluded, so
|
|
22873
|
+
* a committed `ios/`/`android/` reaches staging intact.
|
|
22351
22874
|
*/
|
|
22352
|
-
const buildIgnoreInstance = (workspaceRoot) => Effect.gen(function* () {
|
|
22875
|
+
const buildIgnoreInstance = (workspaceRoot, options = {}) => Effect.gen(function* () {
|
|
22353
22876
|
const fs = yield* FileSystem.FileSystem;
|
|
22354
22877
|
const ig = ignore();
|
|
22355
22878
|
ig.add([...ALWAYS_IGNORE]);
|
|
22356
22879
|
const easignorePath = path.join(workspaceRoot, ".easignore");
|
|
22357
|
-
if (yield* fs.exists(easignorePath).pipe(Effect.
|
|
22358
|
-
const content = yield* fs.readFileString(easignorePath).pipe(Effect.
|
|
22880
|
+
if (yield* fs.exists(easignorePath).pipe(Effect.orElseSucceed(() => false))) {
|
|
22881
|
+
const content = yield* fs.readFileString(easignorePath).pipe(Effect.orElseSucceed(() => ""));
|
|
22359
22882
|
ig.add(content);
|
|
22360
|
-
|
|
22883
|
+
} else {
|
|
22884
|
+
const gitignorePath = path.join(workspaceRoot, ".gitignore");
|
|
22885
|
+
if (yield* fs.exists(gitignorePath).pipe(Effect.orElseSucceed(() => false))) {
|
|
22886
|
+
const content = yield* fs.readFileString(gitignorePath).pipe(Effect.orElseSucceed(() => ""));
|
|
22887
|
+
ig.add(content);
|
|
22888
|
+
}
|
|
22361
22889
|
}
|
|
22362
|
-
|
|
22363
|
-
|
|
22364
|
-
|
|
22365
|
-
ig.add(
|
|
22890
|
+
if (options.includeNativeSource === true) {
|
|
22891
|
+
const base = options.appRelPath === void 0 || options.appRelPath === "" ? "" : `${options.appRelPath}/`;
|
|
22892
|
+
ig.add([`!${base}android`, `!${base}ios`]);
|
|
22893
|
+
ig.add(NATIVE_BUILD_OUTPUTS.map((entry) => `${base}${entry}`));
|
|
22366
22894
|
}
|
|
22367
22895
|
return ig;
|
|
22368
22896
|
});
|
|
@@ -22410,6 +22938,7 @@ const runInstall = (params) => runStep({
|
|
|
22410
22938
|
* regardless of what `expo prebuild`, `pod install`, or `gradlew` write.
|
|
22411
22939
|
*/
|
|
22412
22940
|
const prepareStagingProject = (input) => Effect.gen(function* () {
|
|
22941
|
+
const fs = yield* FileSystem.FileSystem;
|
|
22413
22942
|
const runtime = yield* CliRuntime;
|
|
22414
22943
|
const { workspaceRoot, packageManager } = yield* detectWorkspaceRoot(input.userCwd);
|
|
22415
22944
|
const relAppPath = path.relative(workspaceRoot, input.userCwd);
|
|
@@ -22419,14 +22948,18 @@ const prepareStagingProject = (input) => Effect.gen(function* () {
|
|
|
22419
22948
|
yield* copyProjectTree({
|
|
22420
22949
|
source: workspaceRoot,
|
|
22421
22950
|
dest: stagingRoot,
|
|
22422
|
-
ig: yield* buildIgnoreInstance(workspaceRoot
|
|
22951
|
+
ig: yield* buildIgnoreInstance(workspaceRoot, {
|
|
22952
|
+
includeNativeSource: input.projectType !== void 0 && input.projectType !== "expo",
|
|
22953
|
+
appRelPath: relAppPath
|
|
22954
|
+
})
|
|
22423
22955
|
});
|
|
22424
22956
|
yield* initGitRepo(stagingRoot);
|
|
22425
|
-
yield* runInstall({
|
|
22957
|
+
if (yield* fs.exists(path.join(workspaceRoot, "package.json")).pipe(Effect.orElseSucceed(() => false))) yield* runInstall({
|
|
22426
22958
|
stagingRoot,
|
|
22427
22959
|
packageManager,
|
|
22428
22960
|
env: yield* runtime.commandEnvironment(input.envVars)
|
|
22429
22961
|
});
|
|
22962
|
+
else yield* printHuman("No package.json at the staging root — skipping dependency install.");
|
|
22430
22963
|
return {
|
|
22431
22964
|
stagingRoot,
|
|
22432
22965
|
projectRoot,
|
|
@@ -22438,7 +22971,7 @@ const prepareStagingProject = (input) => Effect.gen(function* () {
|
|
|
22438
22971
|
//#endregion
|
|
22439
22972
|
//#region src/lib/repo-clean.ts
|
|
22440
22973
|
const MAX_FILES_SHOWN = 10;
|
|
22441
|
-
const readPorcelain = (projectRoot) => Command.make("git", "status", "--porcelain").pipe(Command.workingDirectory(projectRoot), Command.string, Effect.map((output) => output.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0)), Effect.
|
|
22974
|
+
const readPorcelain = (projectRoot) => Command.make("git", "status", "--porcelain").pipe(Command.workingDirectory(projectRoot), Command.string, Effect.map((output) => output.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0)), Effect.orElseSucceed(() => []));
|
|
22442
22975
|
/**
|
|
22443
22976
|
* Refuse to proceed when the working tree has uncommitted changes. Skipped when
|
|
22444
22977
|
* `allowDirty` is true. In interactive mode, prompts the user to confirm; in
|
|
@@ -22451,11 +22984,8 @@ const ensureRepoClean = ({ projectRoot, allowDirty, label }) => Effect.gen(funct
|
|
|
22451
22984
|
const preview = dirty.slice(0, MAX_FILES_SHOWN).join("\n ");
|
|
22452
22985
|
const overflow = dirty.length > MAX_FILES_SHOWN ? `\n ... and ${String(dirty.length - MAX_FILES_SHOWN)} more` : "";
|
|
22453
22986
|
yield* Console.error(`Uncommitted changes (${String(dirty.length)} file(s)):\n ${preview}${overflow}`);
|
|
22454
|
-
if (!(yield* InteractiveMode).allow) {
|
|
22455
|
-
|
|
22456
|
-
return;
|
|
22457
|
-
}
|
|
22458
|
-
if (!(yield* promptConfirm(`Continue ${label} with uncommitted changes?`, { initialValue: false }))) yield* new DirtyRepoError({ message: `${label} cancelled by user.` });
|
|
22987
|
+
if (!(yield* InteractiveMode).allow) return yield* new DirtyRepoError({ message: `Refusing to ${label} with a dirty working tree. Commit your changes or pass --allow-dirty.` });
|
|
22988
|
+
if (!(yield* promptConfirm(`Continue ${label} with uncommitted changes?`, { initialValue: false }))) return yield* new DirtyRepoError({ message: `${label} cancelled by user.` });
|
|
22459
22989
|
});
|
|
22460
22990
|
|
|
22461
22991
|
//#endregion
|
|
@@ -22595,7 +23125,7 @@ const postTokenRequest = (tokenUri, jwt) => Effect.tryPromise({
|
|
|
22595
23125
|
});
|
|
22596
23126
|
const exchangeJwtForAccessToken = (tokenUri, jwt) => Effect.gen(function* () {
|
|
22597
23127
|
const result = yield* postTokenRequest(tokenUri, jwt);
|
|
22598
|
-
if (!result.ok) return yield*
|
|
23128
|
+
if (!result.ok) return yield* new GooglePlayAuthError({ message: `OAuth token exchange failed: ${String(result.status)} ${result.text}` });
|
|
22599
23129
|
const json = yield* Effect.try({
|
|
22600
23130
|
try: () => JSON.parse(result.text),
|
|
22601
23131
|
catch: (cause) => new GooglePlayAuthError({
|
|
@@ -22620,7 +23150,7 @@ const acquireGooglePlayAccessToken = (serviceAccountJson) => Effect.gen(function
|
|
|
22620
23150
|
message: "Service account JSON missing required fields (type, client_email, private_key)",
|
|
22621
23151
|
cause
|
|
22622
23152
|
})));
|
|
22623
|
-
if (parsed.type !== "service_account") return yield*
|
|
23153
|
+
if (parsed.type !== "service_account") return yield* new GooglePlayAuthError({ message: `Service account JSON has wrong type: ${parsed.type}` });
|
|
22624
23154
|
const tokenUri = parsed.token_uri ?? GOOGLE_OAUTH_TOKEN_URL;
|
|
22625
23155
|
return {
|
|
22626
23156
|
accessToken: yield* exchangeJwtForAccessToken(tokenUri, yield* signJwt(yield* importPrivateKey(parsed.private_key), buildJwtAssertion({
|
|
@@ -22660,10 +23190,10 @@ const performFetch = (params) => Effect.tryPromise({
|
|
|
22660
23190
|
});
|
|
22661
23191
|
const callJsonRaw = (params) => Effect.gen(function* () {
|
|
22662
23192
|
const result = yield* performFetch(params);
|
|
22663
|
-
if (!result.ok) return yield*
|
|
23193
|
+
if (!result.ok) return yield* new GooglePlayApiError({
|
|
22664
23194
|
message: `${params.label} failed: ${String(result.status)} ${result.text}`,
|
|
22665
23195
|
httpStatus: result.status
|
|
22666
|
-
})
|
|
23196
|
+
});
|
|
22667
23197
|
return yield* Effect.try({
|
|
22668
23198
|
try: () => result.text === "" ? {} : JSON.parse(result.text),
|
|
22669
23199
|
catch: (cause) => new GooglePlayApiError({
|
|
@@ -22718,10 +23248,10 @@ const performBundleUpload = (params) => Effect.tryPromise({
|
|
|
22718
23248
|
});
|
|
22719
23249
|
const uploadBundle = (params) => Effect.gen(function* () {
|
|
22720
23250
|
const result = yield* performBundleUpload(params);
|
|
22721
|
-
if (!result.ok) return yield*
|
|
23251
|
+
if (!result.ok) return yield* new GooglePlayApiError({
|
|
22722
23252
|
message: `edits.bundles.upload failed: ${String(result.status)} ${result.text}`,
|
|
22723
23253
|
httpStatus: result.status
|
|
22724
|
-
})
|
|
23254
|
+
});
|
|
22725
23255
|
const raw = yield* Effect.try({
|
|
22726
23256
|
try: () => JSON.parse(result.text),
|
|
22727
23257
|
catch: (cause) => new GooglePlayApiError({
|
|
@@ -22912,10 +23442,10 @@ const fetchArchiveOverHttp = (url) => Effect.gen(function* () {
|
|
|
22912
23442
|
message: `Failed to download AAB from ${url}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
22913
23443
|
})
|
|
22914
23444
|
});
|
|
22915
|
-
if (!result.ok || result.bytes === null) return yield*
|
|
23445
|
+
if (!result.ok || result.bytes === null) return yield* new CliSubmitError({
|
|
22916
23446
|
code: "SUBMISSION_ARCHIVE_DOWNLOAD_FAILED",
|
|
22917
23447
|
message: `HTTP ${String(result.status)} fetching archive at ${url}`
|
|
22918
|
-
})
|
|
23448
|
+
});
|
|
22919
23449
|
return result.bytes;
|
|
22920
23450
|
});
|
|
22921
23451
|
const readArchiveBytes = (archive) => archive.source === "path" ? Effect.map(readLocalFile(archive.value, "SUBMISSION_ARCHIVE_READ_FAILED", (cause) => `Failed to read AAB at ${archive.value}: ${cause instanceof Error ? cause.message : String(cause)}`), (buf) => new Uint8Array(buf)) : fetchArchiveOverHttp(archive.value);
|
|
@@ -22990,10 +23520,10 @@ const runGooglePlayPipeline = (params) => Effect.gen(function* () {
|
|
|
22990
23520
|
});
|
|
22991
23521
|
const runAndroidGooglePlayUpload = (inputs) => Effect.gen(function* () {
|
|
22992
23522
|
const { applicationId } = inputs.androidProfile;
|
|
22993
|
-
if (applicationId === void 0) return yield*
|
|
23523
|
+
if (applicationId === void 0) return yield* new CliSubmitError({
|
|
22994
23524
|
code: "SUBMISSION_ANDROID_APP_ID_MISSING",
|
|
22995
23525
|
message: "Android submit profile requires applicationId — set submit.<profile>.android.applicationId in eas.json"
|
|
22996
|
-
})
|
|
23526
|
+
});
|
|
22997
23527
|
const serviceAccountJson = yield* resolveServiceAccountJson({
|
|
22998
23528
|
api: inputs.api,
|
|
22999
23529
|
serviceAccountKeyId: inputs.serviceAccountKeyId,
|
|
@@ -23018,7 +23548,7 @@ const runAndroidGooglePlayUpload = (inputs) => Effect.gen(function* () {
|
|
|
23018
23548
|
errorCode: engineError.code,
|
|
23019
23549
|
errorMessage: engineError.message
|
|
23020
23550
|
});
|
|
23021
|
-
return yield*
|
|
23551
|
+
return yield* engineError;
|
|
23022
23552
|
})));
|
|
23023
23553
|
yield* patchSubmissionStatus(inputs.api, inputs.submissionId, { status: "FINISHED" });
|
|
23024
23554
|
yield* printHuman(`Google Play bundle uploaded (versionCode ${String(result.versionCode)})`);
|
|
@@ -23026,7 +23556,7 @@ const runAndroidGooglePlayUpload = (inputs) => Effect.gen(function* () {
|
|
|
23026
23556
|
});
|
|
23027
23557
|
|
|
23028
23558
|
//#endregion
|
|
23029
|
-
//#region src/application/build-
|
|
23559
|
+
//#region src/application/build-auto-submit.ts
|
|
23030
23560
|
const buildAutoSubmitIosConfig = (iosProfile, whatToTest) => {
|
|
23031
23561
|
if (iosProfile?.bundleIdentifier === void 0) return;
|
|
23032
23562
|
return compact({
|
|
@@ -23052,9 +23582,10 @@ const buildAutoSubmitAndroidConfig = (androidProfile) => {
|
|
|
23052
23582
|
rollout: androidProfile.rollout
|
|
23053
23583
|
});
|
|
23054
23584
|
};
|
|
23585
|
+
/** Submit a freshly-built artifact to the store using the profile's submit config. */
|
|
23055
23586
|
const runAutoSubmit = (input) => Effect.gen(function* () {
|
|
23056
23587
|
yield* printHuman(`\nAuto-submitting build ${input.buildId} (profile ${input.profileName})...`);
|
|
23057
|
-
const easProfile = yield*
|
|
23588
|
+
const easProfile = yield* readSubmitProfile(yield* (yield* CliRuntime).cwd, input.profileName);
|
|
23058
23589
|
const archiveUrl = (yield* input.api.builds.getInstallLink({ path: { id: input.buildId } })).artifactUrl;
|
|
23059
23590
|
const iosConfig = input.platform === "ios" ? buildAutoSubmitIosConfig(easProfile.ios, input.whatToTest) : void 0;
|
|
23060
23591
|
const androidConfig = input.platform === "android" ? buildAutoSubmitAndroidConfig(easProfile.android) : void 0;
|
|
@@ -23082,15 +23613,146 @@ const runAutoSubmit = (input) => Effect.gen(function* () {
|
|
|
23082
23613
|
}
|
|
23083
23614
|
yield* printHuman(`Submission final status: ${(yield* pollSubmissionUntilTerminal(input.api, submission.id)).status}`);
|
|
23084
23615
|
});
|
|
23616
|
+
|
|
23617
|
+
//#endregion
|
|
23618
|
+
//#region src/lib/ios-native-meta.ts
|
|
23619
|
+
const APPLICATION_PRODUCT_TYPE = "com.apple.product-type.application";
|
|
23620
|
+
/**
|
|
23621
|
+
* A build setting that is an unresolved reference (`$(MARKETING_VERSION)`) or a
|
|
23622
|
+
* variable interpolation tells us nothing concrete — treat it as absent so the
|
|
23623
|
+
* profile `metaOverride` can supply a real value.
|
|
23624
|
+
*/
|
|
23625
|
+
const concreteSetting = (raw) => {
|
|
23626
|
+
if (typeof raw === "number") return String(raw);
|
|
23627
|
+
if (typeof raw !== "string") return;
|
|
23628
|
+
const value = unquote$1(raw);
|
|
23629
|
+
return value.length === 0 || value.includes("$(") ? void 0 : value;
|
|
23630
|
+
};
|
|
23631
|
+
const findApplicationTarget = (project) => {
|
|
23632
|
+
const nativeTargets = project.pbxNativeTargetSection();
|
|
23633
|
+
for (const [uuid, entry] of Object.entries(nativeTargets)) {
|
|
23634
|
+
if (uuid.endsWith("_comment") || typeof entry === "string") continue;
|
|
23635
|
+
if (unquote$1(entry.productType) === APPLICATION_PRODUCT_TYPE) return entry;
|
|
23636
|
+
}
|
|
23637
|
+
};
|
|
23638
|
+
const configUuidForName = (project, target, configurationName) => {
|
|
23639
|
+
const configList = project.pbxXCConfigurationList()[target.buildConfigurationList];
|
|
23640
|
+
if (!configList || typeof configList === "string") return;
|
|
23641
|
+
const buildConfigSection = project.pbxXCBuildConfigurationSection();
|
|
23642
|
+
return configList.buildConfigurations.map((entry) => entry.value).find((uuid) => {
|
|
23643
|
+
const cfg = buildConfigSection[uuid];
|
|
23644
|
+
return cfg !== void 0 && typeof cfg !== "string" && unquote$1(cfg.name) === configurationName;
|
|
23645
|
+
});
|
|
23646
|
+
};
|
|
23647
|
+
/**
|
|
23648
|
+
* Read app metadata (bundle id, marketing version, build number) for the main
|
|
23649
|
+
* application target of the single `.xcodeproj` under `iosDir`, for a given build
|
|
23650
|
+
* configuration. Used for non-Expo (bare/native) projects where there is no
|
|
23651
|
+
* `app.json`. Missing or unresolved settings come back `undefined` so the caller
|
|
23652
|
+
* can fall back to profile `metaOverride`.
|
|
23653
|
+
*/
|
|
23654
|
+
const readIosNativeMeta = (params) => Effect.gen(function* () {
|
|
23655
|
+
const projectDir = yield* findXcodeProjectDir(params.iosDir);
|
|
23656
|
+
const project = yield* parseProject(path.join(projectDir, "project.pbxproj"));
|
|
23657
|
+
const target = findApplicationTarget(project);
|
|
23658
|
+
if (target === void 0) return {};
|
|
23659
|
+
const configUuid = configUuidForName(project, target, params.configurationName);
|
|
23660
|
+
if (configUuid === void 0) return {};
|
|
23661
|
+
const cfg = project.pbxXCBuildConfigurationSection()[configUuid];
|
|
23662
|
+
if (cfg === void 0 || typeof cfg === "string") return {};
|
|
23663
|
+
const settings = cfg.buildSettings;
|
|
23664
|
+
return compact({
|
|
23665
|
+
bundleId: concreteSetting(settings["PRODUCT_BUNDLE_IDENTIFIER"]),
|
|
23666
|
+
marketingVersion: concreteSetting(settings["MARKETING_VERSION"]),
|
|
23667
|
+
currentProjectVersion: concreteSetting(settings["CURRENT_PROJECT_VERSION"])
|
|
23668
|
+
});
|
|
23669
|
+
});
|
|
23670
|
+
|
|
23671
|
+
//#endregion
|
|
23672
|
+
//#region src/application/resolve-app-meta.ts
|
|
23673
|
+
const EMPTY = {
|
|
23674
|
+
bundleId: void 0,
|
|
23675
|
+
androidPackage: void 0,
|
|
23676
|
+
appVersion: void 0,
|
|
23677
|
+
buildNumber: void 0,
|
|
23678
|
+
rawRuntimeVersion: void 0
|
|
23679
|
+
};
|
|
23680
|
+
const warnIfMismatch = (label, override, native) => override !== void 0 && native !== void 0 && override !== native ? printWarn(`${label} override "${override}" differs from the native value "${native}". The better-update.json value will be used for build metadata.`) : Effect.void;
|
|
23681
|
+
const resolveAndroidMeta = (projectRoot, profile) => Effect.gen(function* () {
|
|
23682
|
+
const gradle = yield* readGradleConfig(path.join(projectRoot, "android"));
|
|
23683
|
+
const override = profile.android?.metaOverride;
|
|
23684
|
+
yield* warnIfMismatch("android.applicationId", override?.applicationId, gradle?.applicationId);
|
|
23685
|
+
const androidPackage = override?.applicationId ?? gradle?.applicationId;
|
|
23686
|
+
if (androidPackage === void 0) return yield* new BuildProfileError({ message: "Could not determine the Android applicationId. Set android.applicationId under this build profile in better-update.json, or ensure android/app/build.gradle defines it." });
|
|
23687
|
+
const versionCode = override?.versionCode ?? (gradle?.versionCode === void 0 ? void 0 : String(gradle.versionCode));
|
|
23688
|
+
return {
|
|
23689
|
+
...EMPTY,
|
|
23690
|
+
androidPackage,
|
|
23691
|
+
appVersion: override?.version ?? gradle?.versionName,
|
|
23692
|
+
buildNumber: versionCode
|
|
23693
|
+
};
|
|
23694
|
+
});
|
|
23695
|
+
const resolveIosMeta = (projectRoot, profile) => Effect.gen(function* () {
|
|
23696
|
+
const configurationName = profile.ios?.buildConfiguration ?? "Release";
|
|
23697
|
+
const native = yield* readIosNativeMeta({
|
|
23698
|
+
iosDir: path.join(projectRoot, "ios"),
|
|
23699
|
+
configurationName
|
|
23700
|
+
}).pipe(Effect.orElseSucceed(() => ({})));
|
|
23701
|
+
const override = profile.ios?.metaOverride;
|
|
23702
|
+
yield* warnIfMismatch("ios.bundleIdentifier", override?.bundleIdentifier, native.bundleId);
|
|
23703
|
+
const bundleId = override?.bundleIdentifier ?? native.bundleId;
|
|
23704
|
+
if (bundleId === void 0) return yield* new BuildProfileError({ message: "Could not determine the iOS bundle identifier. Set ios.bundleIdentifier under this build profile in better-update.json, or ensure the Xcode project defines PRODUCT_BUNDLE_IDENTIFIER for the build configuration." });
|
|
23705
|
+
return {
|
|
23706
|
+
...EMPTY,
|
|
23707
|
+
bundleId,
|
|
23708
|
+
appVersion: override?.version ?? native.marketingVersion,
|
|
23709
|
+
buildNumber: override?.buildNumber ?? native.currentProjectVersion
|
|
23710
|
+
};
|
|
23711
|
+
});
|
|
23712
|
+
const overlayExpoOverride = (meta, platform, profile) => {
|
|
23713
|
+
if (platform === "ios") {
|
|
23714
|
+
const override = profile.ios?.metaOverride;
|
|
23715
|
+
return {
|
|
23716
|
+
...meta,
|
|
23717
|
+
bundleId: override?.bundleIdentifier ?? meta.bundleId,
|
|
23718
|
+
appVersion: override?.version ?? meta.appVersion,
|
|
23719
|
+
buildNumber: override?.buildNumber ?? meta.buildNumber
|
|
23720
|
+
};
|
|
23721
|
+
}
|
|
23722
|
+
const override = profile.android?.metaOverride;
|
|
23723
|
+
return {
|
|
23724
|
+
...meta,
|
|
23725
|
+
androidPackage: override?.applicationId ?? meta.androidPackage,
|
|
23726
|
+
appVersion: override?.version ?? meta.appVersion,
|
|
23727
|
+
buildNumber: override?.versionCode ?? meta.buildNumber
|
|
23728
|
+
};
|
|
23729
|
+
};
|
|
23730
|
+
/**
|
|
23731
|
+
* Resolve app metadata (bundle id / package, version, build number) in a
|
|
23732
|
+
* project-type-aware way. Expo reads from app.json; bare/native read from native
|
|
23733
|
+
* files (build.gradle / pbxproj); KMP/custom rely on profile `metaOverride`,
|
|
23734
|
+
* which also overrides native values when both are present.
|
|
23735
|
+
*/
|
|
23736
|
+
const resolveAppMeta = (params) => {
|
|
23737
|
+
if (params.projectType === "expo") {
|
|
23738
|
+
if (params.expoAppMeta === void 0) return Effect.fail(new BuildProfileError({ message: "Internal: missing Expo app metadata for expo project." }));
|
|
23739
|
+
return Effect.succeed(overlayExpoOverride(params.expoAppMeta, params.platform, params.profile));
|
|
23740
|
+
}
|
|
23741
|
+
return params.platform === "ios" ? resolveIosMeta(params.projectRoot, params.profile) : resolveAndroidMeta(params.projectRoot, params.profile);
|
|
23742
|
+
};
|
|
23743
|
+
|
|
23744
|
+
//#endregion
|
|
23745
|
+
//#region src/application/build-workflow.ts
|
|
23085
23746
|
const runIosPlatformBuild = (input) => Effect.gen(function* () {
|
|
23086
23747
|
const { api, appMeta, envVars, options, profile, projectId, projectRoot, tempDir } = input;
|
|
23087
23748
|
if (!profile.ios) return yield* new BuildProfileError({ message: `Profile "${profile.name}" has no ios section.` });
|
|
23088
23749
|
const iosProfile = profile.ios;
|
|
23089
23750
|
const iosBundleId = appMeta.bundleId;
|
|
23090
|
-
if (!iosBundleId) return yield* new BuildProfileError({ message: "Missing ios.bundleIdentifier
|
|
23751
|
+
if (!iosBundleId) return yield* new BuildProfileError({ message: "Missing iOS bundle identifier (set ios.bundleIdentifier or your Expo config)." });
|
|
23752
|
+
const strategy = resolveIosStrategy(profile, input.projectType);
|
|
23091
23753
|
const isSimulator = iosProfile.simulator === true;
|
|
23092
23754
|
const credentialsSource = profile.credentialsSource ?? "remote";
|
|
23093
|
-
if (!isSimulator && credentialsSource === "remote") yield* ensureIosCredentials(api, {
|
|
23755
|
+
if (strategy !== "custom" && !isSimulator && credentialsSource === "remote") yield* ensureIosCredentials(api, {
|
|
23094
23756
|
projectId,
|
|
23095
23757
|
bundleIdentifier: iosBundleId,
|
|
23096
23758
|
distribution: iosProfile.distribution
|
|
@@ -23105,8 +23767,10 @@ const runIosPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
23105
23767
|
envVars,
|
|
23106
23768
|
projectId,
|
|
23107
23769
|
credentialsSource,
|
|
23770
|
+
strategy,
|
|
23108
23771
|
rawOutput: options.rawOutput,
|
|
23109
|
-
freezeCredentials: options.freezeCredentials ?? false
|
|
23772
|
+
freezeCredentials: options.freezeCredentials ?? false,
|
|
23773
|
+
...compact({ customCommand: profile.customCommand?.ios })
|
|
23110
23774
|
}),
|
|
23111
23775
|
target: isSimulator ? {
|
|
23112
23776
|
platform: "ios",
|
|
@@ -23125,7 +23789,8 @@ const runAndroidPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
23125
23789
|
if (!profile.android) return yield* new BuildProfileError({ message: `Profile "${profile.name}" has no android section.` });
|
|
23126
23790
|
const androidProfile = profile.android;
|
|
23127
23791
|
const androidBundleId = appMeta.androidPackage;
|
|
23128
|
-
if (!androidBundleId) return yield* new BuildProfileError({ message: "Missing android.
|
|
23792
|
+
if (!androidBundleId) return yield* new BuildProfileError({ message: "Missing Android applicationId (set android.applicationId or your Expo config)." });
|
|
23793
|
+
const strategy = resolveAndroidStrategy(profile, input.projectType);
|
|
23129
23794
|
const gradleConfig = yield* readGradleConfig(`${projectRoot}/android`);
|
|
23130
23795
|
yield* warnOnGradleMismatch(gradleConfig, androidBundleId);
|
|
23131
23796
|
const applicationIdentifier = gradleConfig?.applicationId ?? androidBundleId;
|
|
@@ -23146,7 +23811,9 @@ const runAndroidPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
23146
23811
|
projectId,
|
|
23147
23812
|
credentialsSource,
|
|
23148
23813
|
profileName: profile.name,
|
|
23149
|
-
skipCredentials
|
|
23814
|
+
skipCredentials,
|
|
23815
|
+
strategy,
|
|
23816
|
+
...compact({ customCommand: profile.customCommand?.android })
|
|
23150
23817
|
}),
|
|
23151
23818
|
target: androidProfile.format === "aab" ? {
|
|
23152
23819
|
platform: "android",
|
|
@@ -23161,12 +23828,48 @@ const runAndroidPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
23161
23828
|
};
|
|
23162
23829
|
});
|
|
23163
23830
|
const runPlatformBuild = (input) => input.platform === "ios" ? runIosPlatformBuild(input) : runAndroidPlatformBuild(input);
|
|
23831
|
+
const dirExists = (root, name) => Effect.gen(function* () {
|
|
23832
|
+
return yield* (yield* FileSystem.FileSystem).exists(path.join(root, name)).pipe(Effect.orElseSucceed(() => false));
|
|
23833
|
+
});
|
|
23834
|
+
/**
|
|
23835
|
+
* Expo metadata path: read app.json (with the env overlay so dynamic configs
|
|
23836
|
+
* resolve), apply autoIncrement to the user's tree, re-read, then derive the OTA
|
|
23837
|
+
* runtimeVersion. Mirrors the original managed flow.
|
|
23838
|
+
*/
|
|
23839
|
+
const resolveExpoBuildMeta = (params) => Effect.gen(function* () {
|
|
23840
|
+
const { userCwd, platform, profile, envVars } = params;
|
|
23841
|
+
yield* applyAutoIncrement({
|
|
23842
|
+
projectRoot: userCwd,
|
|
23843
|
+
platform,
|
|
23844
|
+
config: yield* readExpoConfig(userCwd, envVars),
|
|
23845
|
+
...platform === "ios" && profile.ios?.autoIncrement !== void 0 ? { iosMode: profile.ios.autoIncrement } : {},
|
|
23846
|
+
...platform === "android" && profile.android?.autoIncrement !== void 0 ? { androidMode: profile.android.autoIncrement } : {}
|
|
23847
|
+
});
|
|
23848
|
+
const bumpedConfig = yield* readExpoConfig(userCwd, envVars);
|
|
23849
|
+
const appMeta = yield* resolveAppMeta({
|
|
23850
|
+
projectType: "expo",
|
|
23851
|
+
platform,
|
|
23852
|
+
projectRoot: userCwd,
|
|
23853
|
+
profile,
|
|
23854
|
+
expoAppMeta: yield* readAppMeta(bumpedConfig, platform)
|
|
23855
|
+
});
|
|
23856
|
+
return {
|
|
23857
|
+
appMeta,
|
|
23858
|
+
runtimeVersion: yield* resolveRuntimeVersion({
|
|
23859
|
+
raw: appMeta.rawRuntimeVersion,
|
|
23860
|
+
appVersion: appMeta.appVersion,
|
|
23861
|
+
projectRoot: userCwd,
|
|
23862
|
+
platform,
|
|
23863
|
+
buildNumber: appMeta.buildNumber,
|
|
23864
|
+
sdkVersion: bumpedConfig.sdkVersion
|
|
23865
|
+
})
|
|
23866
|
+
};
|
|
23867
|
+
});
|
|
23164
23868
|
const resolveProfileName = (projectRoot, requested) => Effect.gen(function* () {
|
|
23165
|
-
const
|
|
23166
|
-
const available = Object.keys(easConfig.build ?? {});
|
|
23869
|
+
const available = yield* listBuildProfileNames(projectRoot);
|
|
23167
23870
|
if (available.includes(requested)) return requested;
|
|
23168
23871
|
if (!(yield* InteractiveMode).allow || available.length === 0) return requested;
|
|
23169
|
-
yield* printHuman(`Build profile "${requested}" not found in
|
|
23872
|
+
yield* printHuman(`Build profile "${requested}" not found in better-update.json.`);
|
|
23170
23873
|
return yield* promptSelect("Pick a build profile:", available.map((name) => ({
|
|
23171
23874
|
value: name,
|
|
23172
23875
|
label: name
|
|
@@ -23180,11 +23883,19 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23180
23883
|
allowDirty: options.allowDirty ?? false,
|
|
23181
23884
|
label: "build"
|
|
23182
23885
|
});
|
|
23183
|
-
const
|
|
23184
|
-
|
|
23185
|
-
|
|
23886
|
+
const projectType = yield* detectProjectType({
|
|
23887
|
+
projectRoot: userCwd,
|
|
23888
|
+
override: asProjectType((yield* readBetterUpdateConfig(userCwd))?.["projectType"])
|
|
23889
|
+
});
|
|
23890
|
+
const isExpo = projectType === "expo";
|
|
23891
|
+
const projectId = yield* readProjectId;
|
|
23186
23892
|
const profile = yield* readBuildProfile(userCwd, yield* resolveProfileName(userCwd, options.profileName));
|
|
23187
23893
|
if (profile.developmentClient === true) yield* warnIfDevClientMissing(userCwd);
|
|
23894
|
+
const platform = isExpo ? yield* detectPlatform(options.platform, yield* readExpoConfig(userCwd)) : yield* detectPlatformGeneric(options.platform, {
|
|
23895
|
+
profile,
|
|
23896
|
+
hasAndroidDir: yield* dirExists(userCwd, "android"),
|
|
23897
|
+
hasIosDir: yield* dirExists(userCwd, "ios")
|
|
23898
|
+
});
|
|
23188
23899
|
const envVars = {
|
|
23189
23900
|
...yield* pullEnvVars(api, {
|
|
23190
23901
|
projectId,
|
|
@@ -23192,36 +23903,35 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23192
23903
|
}),
|
|
23193
23904
|
...profile.env
|
|
23194
23905
|
};
|
|
23195
|
-
yield*
|
|
23196
|
-
|
|
23197
|
-
platform,
|
|
23198
|
-
config: yield* readExpoConfig(userCwd, envVars),
|
|
23199
|
-
...platform === "ios" && profile.ios?.autoIncrement !== void 0 ? { iosMode: profile.ios.autoIncrement } : {},
|
|
23200
|
-
...platform === "android" && profile.android?.autoIncrement !== void 0 ? { androidMode: profile.android.autoIncrement } : {}
|
|
23201
|
-
});
|
|
23202
|
-
const bumpedConfig = yield* readExpoConfig(userCwd, envVars);
|
|
23203
|
-
const appMeta = yield* readAppMeta(bumpedConfig, platform);
|
|
23204
|
-
const runtimeVersion = yield* resolveRuntimeVersion({
|
|
23205
|
-
raw: appMeta.rawRuntimeVersion,
|
|
23206
|
-
appVersion: appMeta.appVersion,
|
|
23207
|
-
projectRoot: userCwd,
|
|
23906
|
+
const { appMeta, runtimeVersion } = isExpo ? yield* resolveExpoBuildMeta({
|
|
23907
|
+
userCwd,
|
|
23208
23908
|
platform,
|
|
23209
|
-
|
|
23210
|
-
|
|
23211
|
-
})
|
|
23909
|
+
profile,
|
|
23910
|
+
envVars
|
|
23911
|
+
}) : {
|
|
23912
|
+
appMeta: yield* resolveAppMeta({
|
|
23913
|
+
projectType,
|
|
23914
|
+
platform,
|
|
23915
|
+
projectRoot: userCwd,
|
|
23916
|
+
profile
|
|
23917
|
+
}),
|
|
23918
|
+
runtimeVersion: void 0
|
|
23919
|
+
};
|
|
23212
23920
|
if (options.clearCache) yield* clearBuildCaches(userCwd);
|
|
23213
23921
|
const tempDir = yield* acquireBuildTempDir;
|
|
23214
23922
|
const staging = yield* prepareStagingProject({
|
|
23215
23923
|
userCwd,
|
|
23216
23924
|
tempDir,
|
|
23217
|
-
envVars
|
|
23925
|
+
envVars,
|
|
23926
|
+
projectType
|
|
23218
23927
|
});
|
|
23219
|
-
yield* printHuman(`Building ${platform} artifact for profile "${profile.name}" (runtimeVersion=${runtimeVersion})`);
|
|
23928
|
+
yield* printHuman(`Building ${platform} artifact for profile "${profile.name}"${runtimeVersion === void 0 ? "" : ` (runtimeVersion=${runtimeVersion})`}`);
|
|
23220
23929
|
const { build, target, bundleId } = yield* runPlatformBuild({
|
|
23221
23930
|
api,
|
|
23222
23931
|
options,
|
|
23223
23932
|
platform,
|
|
23224
23933
|
profile,
|
|
23934
|
+
projectType,
|
|
23225
23935
|
appMeta,
|
|
23226
23936
|
envVars,
|
|
23227
23937
|
projectId,
|
|
@@ -23255,18 +23965,18 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23255
23965
|
commit: rawGitContext.commit,
|
|
23256
23966
|
dirty: rawGitContext.dirty
|
|
23257
23967
|
});
|
|
23258
|
-
const fingerprintHash = yield* runFingerprintForPlatform(userCwd, platform).pipe(Effect.map((entry) => entry.hash), Effect.
|
|
23968
|
+
const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(userCwd, platform).pipe(Effect.map((entry) => entry.hash), Effect.orElseSucceed(() => void 0)) : void 0;
|
|
23259
23969
|
const result = yield* reserveAndUpload(api, {
|
|
23260
23970
|
target,
|
|
23261
23971
|
projectId,
|
|
23262
23972
|
profileName: profile.name,
|
|
23263
|
-
runtimeVersion,
|
|
23264
23973
|
bundleId,
|
|
23265
23974
|
gitContext,
|
|
23266
23975
|
artifactPath: build.artifactPath,
|
|
23267
23976
|
sha256: build.sha256,
|
|
23268
23977
|
byteSize: build.byteSize,
|
|
23269
23978
|
...compact({
|
|
23979
|
+
runtimeVersion,
|
|
23270
23980
|
appVersion: appMeta.appVersion,
|
|
23271
23981
|
buildNumber: appMeta.buildNumber,
|
|
23272
23982
|
message: options.message,
|
|
@@ -23279,7 +23989,7 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23279
23989
|
["Status", result.status],
|
|
23280
23990
|
["Platform", platform],
|
|
23281
23991
|
["Profile", profile.name],
|
|
23282
|
-
["Runtime version", runtimeVersion],
|
|
23992
|
+
...runtimeVersion === void 0 ? [] : [["Runtime version", runtimeVersion]],
|
|
23283
23993
|
["Artifact", build.artifactPath],
|
|
23284
23994
|
["SHA-256", build.sha256],
|
|
23285
23995
|
["Bytes", String(build.byteSize)]
|
|
@@ -23324,7 +24034,7 @@ const DEFAULT_PROFILES = [
|
|
|
23324
24034
|
"preview",
|
|
23325
24035
|
"production"
|
|
23326
24036
|
];
|
|
23327
|
-
const writeEasJson
|
|
24037
|
+
const writeEasJson = (filePath, value) => Effect.gen(function* () {
|
|
23328
24038
|
yield* (yield* FileSystem.FileSystem).writeFileString(filePath, `${JSON.stringify(value, null, 2)}\n`).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to write eas.json: ${cause.message}` })));
|
|
23329
24039
|
});
|
|
23330
24040
|
const configureBuildCommand = defineCommand({
|
|
@@ -23342,7 +24052,7 @@ const configureBuildCommand = defineCommand({
|
|
|
23342
24052
|
const easJsonPath = path.join(projectRoot, "eas.json");
|
|
23343
24053
|
const fs = yield* FileSystem.FileSystem;
|
|
23344
24054
|
if (!(yield* fs.exists(easJsonPath))) {
|
|
23345
|
-
yield* writeEasJson
|
|
24055
|
+
yield* writeEasJson(easJsonPath, DEFAULT_EAS_JSON);
|
|
23346
24056
|
yield* printHuman(`Wrote eas.json with default profiles to ${easJsonPath}.`);
|
|
23347
24057
|
yield* printHumanKeyValue([["Profiles", DEFAULT_PROFILES.join(", ")], ["Path", easJsonPath]]);
|
|
23348
24058
|
return {
|
|
@@ -23359,7 +24069,7 @@ const configureBuildCommand = defineCommand({
|
|
|
23359
24069
|
path: easJsonPath
|
|
23360
24070
|
};
|
|
23361
24071
|
}
|
|
23362
|
-
yield* writeEasJson
|
|
24072
|
+
yield* writeEasJson(easJsonPath, DEFAULT_EAS_JSON);
|
|
23363
24073
|
yield* printHuman(`Overwrote eas.json with default profiles.`);
|
|
23364
24074
|
return {
|
|
23365
24075
|
action: "overwritten",
|
|
@@ -23387,7 +24097,7 @@ const configureBuildCommand = defineCommand({
|
|
|
23387
24097
|
};
|
|
23388
24098
|
}
|
|
23389
24099
|
const additions = Object.fromEntries(missing.map((name) => [name, DEFAULT_EAS_JSON.build[name]]));
|
|
23390
|
-
yield* writeEasJson
|
|
24100
|
+
yield* writeEasJson(easJsonPath, {
|
|
23391
24101
|
build: {
|
|
23392
24102
|
...config.build,
|
|
23393
24103
|
...additions
|
|
@@ -23554,7 +24264,7 @@ const compatibilityMatrixCommand = defineCommand({
|
|
|
23554
24264
|
|
|
23555
24265
|
//#endregion
|
|
23556
24266
|
//#region src/commands/builds/delete.ts
|
|
23557
|
-
const deleteCommand$
|
|
24267
|
+
const deleteCommand$6 = defineCommand({
|
|
23558
24268
|
meta: {
|
|
23559
24269
|
name: "delete",
|
|
23560
24270
|
description: "Delete a build"
|
|
@@ -23610,10 +24320,7 @@ const downloadCommand$1 = defineCommand({
|
|
|
23610
24320
|
const fs = yield* FileSystem.FileSystem;
|
|
23611
24321
|
const cwd = yield* (yield* CliRuntime).cwd;
|
|
23612
24322
|
const { artifact } = yield* api.builds.get({ path: { id: args.id } });
|
|
23613
|
-
if (!artifact) {
|
|
23614
|
-
yield* Effect.fail(new UploadFailedError({ message: `Build ${args.id} has no artifact yet.` }));
|
|
23615
|
-
return;
|
|
23616
|
-
}
|
|
24323
|
+
if (!artifact) return yield* new UploadFailedError({ message: `Build ${args.id} has no artifact yet.` });
|
|
23617
24324
|
const link = yield* api.builds.getInstallLink({ path: { id: args.id } });
|
|
23618
24325
|
const ext = artifact.format;
|
|
23619
24326
|
const outputPath = args.output ?? path.join(cwd, `${args.id}.${ext}`);
|
|
@@ -23703,7 +24410,7 @@ const DISTRIBUTION_OPTIONS$1 = [
|
|
|
23703
24410
|
"play-store",
|
|
23704
24411
|
"direct"
|
|
23705
24412
|
];
|
|
23706
|
-
const listCommand$
|
|
24413
|
+
const listCommand$10 = defineCommand({
|
|
23707
24414
|
meta: {
|
|
23708
24415
|
name: "list",
|
|
23709
24416
|
description: "List builds for the linked project"
|
|
@@ -23886,7 +24593,7 @@ const runInherit = (step, bin, ...args) => Command.exitCode(Command.make(bin, ..
|
|
|
23886
24593
|
* Locate a tool on PATH. Returns the absolute path or fails with NativeRunError.
|
|
23887
24594
|
*/
|
|
23888
24595
|
const which = (bin) => Effect.gen(function* () {
|
|
23889
|
-
const trimmed = (yield* Command.string(Command.make("which", bin)).pipe(Effect.
|
|
24596
|
+
const trimmed = (yield* Command.string(Command.make("which", bin)).pipe(Effect.mapError(() => new NativeRunError({ message: `${bin} not found in PATH` })))).trim();
|
|
23890
24597
|
if (trimmed === "") return yield* new NativeRunError({ message: `${bin} not found in PATH` });
|
|
23891
24598
|
return trimmed;
|
|
23892
24599
|
});
|
|
@@ -23905,7 +24612,7 @@ const findAppBundle = (root) => Effect.gen(function* () {
|
|
|
23905
24612
|
const fs = yield* FileSystem.FileSystem;
|
|
23906
24613
|
const candidates = [root, path.join(root, "Payload")];
|
|
23907
24614
|
for (const candidate of candidates) {
|
|
23908
|
-
const app = (yield* fs.readDirectory(candidate).pipe(Effect.
|
|
24615
|
+
const app = (yield* fs.readDirectory(candidate).pipe(Effect.orElseSucceed(() => []))).find((entry) => entry.endsWith(".app"));
|
|
23909
24616
|
if (app) return path.join(candidate, app);
|
|
23910
24617
|
}
|
|
23911
24618
|
return yield* new NativeRunError({ message: `No .app bundle found inside ${root} or ${path.join(root, "Payload")}.` });
|
|
@@ -23984,10 +24691,10 @@ const installAndLaunchIosDevice = (params) => Effect.gen(function* () {
|
|
|
23984
24691
|
* fall back to a CLI flag.
|
|
23985
24692
|
*/
|
|
23986
24693
|
const tryReadApkPackageWith = (bin, apkPath) => Effect.gen(function* () {
|
|
23987
|
-
if (!(yield* which(bin).pipe(Effect.
|
|
23988
|
-
const raw = yield* execCapture(`${bin} dump`, bin, "dump", "badging", apkPath).pipe(Effect.
|
|
24694
|
+
if (!(yield* which(bin).pipe(Effect.orElseSucceed(() => null)))) return;
|
|
24695
|
+
const raw = yield* execCapture(`${bin} dump`, bin, "dump", "badging", apkPath).pipe(Effect.orElseSucceed(() => null));
|
|
23989
24696
|
if (!raw) return;
|
|
23990
|
-
return /package: name='([^']+)'/u.exec(raw)?.[1];
|
|
24697
|
+
return /package: name='(?<packageName>[^']+)'/u.exec(raw)?.[1];
|
|
23991
24698
|
});
|
|
23992
24699
|
const readApkPackageName = (apkPath) => Effect.gen(function* () {
|
|
23993
24700
|
return yield* Effect.reduce(["aapt2", "aapt"], void 0, (acc, bin) => acc === void 0 ? tryReadApkPackageWith(bin, apkPath) : Effect.succeed(acc));
|
|
@@ -24057,10 +24764,7 @@ const runIosSimulator = (params) => Effect.gen(function* () {
|
|
|
24057
24764
|
});
|
|
24058
24765
|
const runIosDevice = (params) => Effect.gen(function* () {
|
|
24059
24766
|
const { deviceSelector } = params;
|
|
24060
|
-
if (deviceSelector === void 0) {
|
|
24061
|
-
yield* Effect.fail(new InvalidArgumentError({ message: "Pass --device-id <udid>. Run `xcrun devicectl list devices` to list connected devices." }));
|
|
24062
|
-
return;
|
|
24063
|
-
}
|
|
24767
|
+
if (deviceSelector === void 0) return yield* new InvalidArgumentError({ message: "Pass --device-id <udid>. Run `xcrun devicectl list devices` to list connected devices." });
|
|
24064
24768
|
const bundleId = yield* readBundleIdFromApp(yield* findAppBundle(yield* extractIosArtifact({
|
|
24065
24769
|
tempDir: params.tempDir,
|
|
24066
24770
|
artifactPath: params.artifactPath,
|
|
@@ -24085,21 +24789,12 @@ const runIos = (params) => {
|
|
|
24085
24789
|
return Effect.fail(new NativeRunError({ message: `Cannot install ${params.format} on iOS; only tar.gz (simulator) or ipa are supported.` }));
|
|
24086
24790
|
};
|
|
24087
24791
|
const runAndroid = (params) => Effect.gen(function* () {
|
|
24088
|
-
if (params.format === "aab") {
|
|
24089
|
-
|
|
24090
|
-
return;
|
|
24091
|
-
}
|
|
24092
|
-
if (params.format !== "apk") {
|
|
24093
|
-
yield* Effect.fail(new NativeRunError({ message: `Cannot install ${params.format} on Android; only apk is supported.` }));
|
|
24094
|
-
return;
|
|
24095
|
-
}
|
|
24792
|
+
if (params.format === "aab") return yield* new InvalidArgumentError({ message: ".aab artifacts cannot be installed directly. Use bundletool to convert to apks, or download the play-store APK." });
|
|
24793
|
+
if (params.format !== "apk") return yield* new NativeRunError({ message: `Cannot install ${params.format} on Android; only apk is supported.` });
|
|
24096
24794
|
const device = yield* pickAndroidDevice(params.emulatorSelector);
|
|
24097
24795
|
const detected = yield* readApkPackageName(params.artifactPath);
|
|
24098
24796
|
const packageName = params.packageOverride ?? detected;
|
|
24099
|
-
if (!packageName) {
|
|
24100
|
-
yield* Effect.fail(new InvalidArgumentError({ message: "Could not detect APK package name (aapt/aapt2 not on PATH). Pass --package <name> explicitly." }));
|
|
24101
|
-
return;
|
|
24102
|
-
}
|
|
24797
|
+
if (!packageName) return yield* new InvalidArgumentError({ message: "Could not detect APK package name (aapt/aapt2 not on PATH). Pass --package <name> explicitly." });
|
|
24103
24798
|
yield* printHuman(`Installing on Android device ${device.serial}...`);
|
|
24104
24799
|
yield* installAndLaunchAndroid({
|
|
24105
24800
|
serial: device.serial,
|
|
@@ -24164,7 +24859,7 @@ const runCommand$1 = defineCommand({
|
|
|
24164
24859
|
projectId
|
|
24165
24860
|
});
|
|
24166
24861
|
const { artifact } = build;
|
|
24167
|
-
if (!artifact) return yield*
|
|
24862
|
+
if (!artifact) return yield* new UploadFailedError({ message: `Build ${build.id} has no artifact yet.` });
|
|
24168
24863
|
const link = yield* api.builds.getInstallLink({ path: { id: build.id } });
|
|
24169
24864
|
const tempDir = yield* acquireBuildTempDir;
|
|
24170
24865
|
const artifactPath = path.join(tempDir, `artifact.${artifact.format}`);
|
|
@@ -24200,7 +24895,7 @@ const runCommand$1 = defineCommand({
|
|
|
24200
24895
|
//#region src/application/upload-workflow.ts
|
|
24201
24896
|
const resolveIosTarget = (profile, appMeta) => Effect.gen(function* () {
|
|
24202
24897
|
if (!profile.ios) return yield* new BuildProfileError({ message: `Profile "${profile.name}" has no ios section.` });
|
|
24203
|
-
if (!appMeta.bundleId) return yield* new BuildProfileError({ message: "Missing ios.bundleIdentifier
|
|
24898
|
+
if (!appMeta.bundleId) return yield* new BuildProfileError({ message: "Missing iOS bundle identifier (set ios.bundleIdentifier or your Expo config)." });
|
|
24204
24899
|
return {
|
|
24205
24900
|
target: {
|
|
24206
24901
|
platform: "ios",
|
|
@@ -24212,7 +24907,7 @@ const resolveIosTarget = (profile, appMeta) => Effect.gen(function* () {
|
|
|
24212
24907
|
});
|
|
24213
24908
|
const resolveAndroidTarget = (profile, appMeta, projectRoot) => Effect.gen(function* () {
|
|
24214
24909
|
if (!profile.android) return yield* new BuildProfileError({ message: `Profile "${profile.name}" has no android section.` });
|
|
24215
|
-
if (!appMeta.androidPackage) return yield* new BuildProfileError({ message: "Missing android.
|
|
24910
|
+
if (!appMeta.androidPackage) return yield* new BuildProfileError({ message: "Missing Android applicationId (set android.applicationId or your Expo config)." });
|
|
24216
24911
|
const gradleConfig = yield* readGradleConfig(`${projectRoot}/android`);
|
|
24217
24912
|
yield* warnOnGradleMismatch(gradleConfig, appMeta.androidPackage);
|
|
24218
24913
|
const bundleId = gradleConfig?.applicationId ?? appMeta.androidPackage;
|
|
@@ -24229,27 +24924,56 @@ const resolveAndroidTarget = (profile, appMeta, projectRoot) => Effect.gen(funct
|
|
|
24229
24924
|
bundleId
|
|
24230
24925
|
};
|
|
24231
24926
|
});
|
|
24927
|
+
/** Resolve app metadata + OTA runtimeVersion for an upload (project-type aware). */
|
|
24928
|
+
const resolveUploadMeta = (params) => Effect.gen(function* () {
|
|
24929
|
+
const { projectType, platform, projectRoot, profile, envVars } = params;
|
|
24930
|
+
const expoConfig = projectType === "expo" ? yield* readExpoConfig(projectRoot, envVars) : void 0;
|
|
24931
|
+
const appMeta = yield* resolveAppMeta({
|
|
24932
|
+
projectType,
|
|
24933
|
+
platform,
|
|
24934
|
+
projectRoot,
|
|
24935
|
+
profile,
|
|
24936
|
+
...compact({
|
|
24937
|
+
expoConfig,
|
|
24938
|
+
expoAppMeta: expoConfig === void 0 ? void 0 : yield* readAppMeta(expoConfig, platform)
|
|
24939
|
+
})
|
|
24940
|
+
});
|
|
24941
|
+
return {
|
|
24942
|
+
appMeta,
|
|
24943
|
+
runtimeVersion: expoConfig === void 0 ? void 0 : yield* resolveRuntimeVersion({
|
|
24944
|
+
raw: appMeta.rawRuntimeVersion,
|
|
24945
|
+
appVersion: appMeta.appVersion,
|
|
24946
|
+
projectRoot,
|
|
24947
|
+
platform,
|
|
24948
|
+
buildNumber: appMeta.buildNumber,
|
|
24949
|
+
sdkVersion: expoConfig.sdkVersion
|
|
24950
|
+
}),
|
|
24951
|
+
isExpo: expoConfig !== void 0
|
|
24952
|
+
};
|
|
24953
|
+
});
|
|
24232
24954
|
const runUploadWorkflow = (options) => Effect.gen(function* () {
|
|
24233
24955
|
const api = yield* apiClient;
|
|
24234
24956
|
const projectRoot = yield* (yield* CliRuntime).cwd;
|
|
24235
|
-
if (!(yield* (yield* FileSystem.FileSystem).exists(options.artifactPath).pipe(Effect.orElseSucceed(() => false)))) yield* new ArtifactNotFoundError({ message: `Artifact not found at ${options.artifactPath}.` });
|
|
24236
|
-
const
|
|
24957
|
+
if (!(yield* (yield* FileSystem.FileSystem).exists(options.artifactPath).pipe(Effect.orElseSucceed(() => false)))) return yield* new ArtifactNotFoundError({ message: `Artifact not found at ${options.artifactPath}.` });
|
|
24958
|
+
const projectType = yield* detectProjectType({
|
|
24959
|
+
projectRoot,
|
|
24960
|
+
override: asProjectType((yield* readBetterUpdateConfig(projectRoot))?.["projectType"])
|
|
24961
|
+
});
|
|
24962
|
+
const projectId = yield* readProjectId;
|
|
24237
24963
|
const profile = yield* readBuildProfile(projectRoot, options.profileName);
|
|
24238
|
-
const
|
|
24964
|
+
const envVars = {
|
|
24239
24965
|
...yield* pullEnvVars(api, {
|
|
24240
24966
|
projectId,
|
|
24241
24967
|
environment: profile.environment
|
|
24242
24968
|
}),
|
|
24243
24969
|
...profile.env
|
|
24244
|
-
}
|
|
24245
|
-
const appMeta = yield*
|
|
24246
|
-
|
|
24247
|
-
raw: appMeta.rawRuntimeVersion,
|
|
24248
|
-
appVersion: appMeta.appVersion,
|
|
24249
|
-
projectRoot,
|
|
24970
|
+
};
|
|
24971
|
+
const { appMeta, runtimeVersion, isExpo } = yield* resolveUploadMeta({
|
|
24972
|
+
projectType,
|
|
24250
24973
|
platform: options.platform,
|
|
24251
|
-
|
|
24252
|
-
|
|
24974
|
+
projectRoot,
|
|
24975
|
+
profile,
|
|
24976
|
+
envVars
|
|
24253
24977
|
});
|
|
24254
24978
|
const { target, bundleId } = options.platform === "ios" ? yield* resolveIosTarget(profile, appMeta) : yield* resolveAndroidTarget(profile, appMeta, projectRoot);
|
|
24255
24979
|
yield* printHuman(`Hashing ${options.artifactPath}...`);
|
|
@@ -24260,7 +24984,7 @@ const runUploadWorkflow = (options) => Effect.gen(function* () {
|
|
|
24260
24984
|
commit: rawGitContext.commit,
|
|
24261
24985
|
dirty: rawGitContext.dirty
|
|
24262
24986
|
});
|
|
24263
|
-
const fingerprintHash = yield* runFingerprintForPlatform(projectRoot, options.platform).pipe(Effect.map((entry) => entry.hash), Effect.
|
|
24987
|
+
const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(projectRoot, options.platform).pipe(Effect.map((entry) => entry.hash), Effect.orElseSucceed(() => void 0)) : void 0;
|
|
24264
24988
|
const result = yield* reserveAndUpload(api, compact({
|
|
24265
24989
|
target,
|
|
24266
24990
|
projectId,
|
|
@@ -24282,7 +25006,7 @@ const runUploadWorkflow = (options) => Effect.gen(function* () {
|
|
|
24282
25006
|
["Status", result.status],
|
|
24283
25007
|
["Platform", options.platform],
|
|
24284
25008
|
["Profile", profile.name],
|
|
24285
|
-
["Runtime version", runtimeVersion],
|
|
25009
|
+
...runtimeVersion === void 0 ? [] : [["Runtime version", runtimeVersion]],
|
|
24286
25010
|
["Artifact", options.artifactPath],
|
|
24287
25011
|
["SHA-256", sha256],
|
|
24288
25012
|
["Bytes", String(byteSize)]
|
|
@@ -24344,9 +25068,9 @@ const buildsCommand = defineCommand({
|
|
|
24344
25068
|
description: "Manage builds"
|
|
24345
25069
|
},
|
|
24346
25070
|
subCommands: {
|
|
24347
|
-
list: listCommand$
|
|
25071
|
+
list: listCommand$10,
|
|
24348
25072
|
get: getCommand$2,
|
|
24349
|
-
delete: deleteCommand$
|
|
25073
|
+
delete: deleteCommand$6,
|
|
24350
25074
|
download: downloadCommand$1,
|
|
24351
25075
|
run: runCommand$1,
|
|
24352
25076
|
"install-link": installLinkCommand,
|
|
@@ -24372,7 +25096,7 @@ const resolveNamedResourceId$1 = (params) => resolveNamedResourceId$2(params, (m
|
|
|
24372
25096
|
|
|
24373
25097
|
//#endregion
|
|
24374
25098
|
//#region src/commands/channels/create.ts
|
|
24375
|
-
const createCommand$
|
|
25099
|
+
const createCommand$4 = defineCommand({
|
|
24376
25100
|
meta: {
|
|
24377
25101
|
name: "create",
|
|
24378
25102
|
description: "Create a channel"
|
|
@@ -24417,7 +25141,7 @@ const createCommand$3 = defineCommand({
|
|
|
24417
25141
|
|
|
24418
25142
|
//#endregion
|
|
24419
25143
|
//#region src/commands/channels/delete.ts
|
|
24420
|
-
const deleteCommand$
|
|
25144
|
+
const deleteCommand$5 = defineCommand({
|
|
24421
25145
|
meta: {
|
|
24422
25146
|
name: "delete",
|
|
24423
25147
|
description: "Delete a channel"
|
|
@@ -24440,6 +25164,193 @@ const deleteCommand$4 = defineCommand({
|
|
|
24440
25164
|
})
|
|
24441
25165
|
});
|
|
24442
25166
|
|
|
25167
|
+
//#endregion
|
|
25168
|
+
//#region src/commands/channels/grants/helpers.ts
|
|
25169
|
+
var GrantCommandError = class extends Data.TaggedError("GrantCommandError") {};
|
|
25170
|
+
const grantErrorExtras = { GrantCommandError: 2 };
|
|
25171
|
+
|
|
25172
|
+
//#endregion
|
|
25173
|
+
//#region src/commands/channels/grants/list.ts
|
|
25174
|
+
const resolveChannel = (channels, target) => Effect.gen(function* () {
|
|
25175
|
+
const channel = channels.find((ch) => ch.id === target) ?? channels.find((ch) => ch.name === target);
|
|
25176
|
+
if (!channel) return yield* new ChannelCommandError({ message: `Channel "${target}" not found by ID or name.` });
|
|
25177
|
+
return channel;
|
|
25178
|
+
});
|
|
25179
|
+
const listCommand$9 = defineCommand({
|
|
25180
|
+
meta: {
|
|
25181
|
+
name: "list",
|
|
25182
|
+
description: "List per-member grants on a channel"
|
|
25183
|
+
},
|
|
25184
|
+
args: { channel: {
|
|
25185
|
+
type: "positional",
|
|
25186
|
+
required: true,
|
|
25187
|
+
description: "Channel ID or channel name"
|
|
25188
|
+
} },
|
|
25189
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
25190
|
+
const projectId = yield* readProjectId;
|
|
25191
|
+
const api = yield* apiClient;
|
|
25192
|
+
const channel = yield* resolveChannel(yield* drainPages((page) => api.channels.list({ urlParams: {
|
|
25193
|
+
projectId,
|
|
25194
|
+
limit: 100,
|
|
25195
|
+
page
|
|
25196
|
+
} })), args.channel);
|
|
25197
|
+
yield* printList([
|
|
25198
|
+
"ID",
|
|
25199
|
+
"Member ID",
|
|
25200
|
+
"Effect",
|
|
25201
|
+
"Actions",
|
|
25202
|
+
"Created"
|
|
25203
|
+
], (yield* api.channelGrants.list({
|
|
25204
|
+
path: { id: channel.id },
|
|
25205
|
+
urlParams: {}
|
|
25206
|
+
})).map((grant) => [
|
|
25207
|
+
grant.id,
|
|
25208
|
+
grant.memberId,
|
|
25209
|
+
grant.effect,
|
|
25210
|
+
grant.actions.join(", "),
|
|
25211
|
+
grant.createdAt
|
|
25212
|
+
]), "No grants found for this channel.");
|
|
25213
|
+
}), { exits: {
|
|
25214
|
+
...channelErrorExtras,
|
|
25215
|
+
...grantErrorExtras
|
|
25216
|
+
} })
|
|
25217
|
+
});
|
|
25218
|
+
|
|
25219
|
+
//#endregion
|
|
25220
|
+
//#region src/commands/channels/grants/revoke.ts
|
|
25221
|
+
const revokeCommand$2 = defineCommand({
|
|
25222
|
+
meta: {
|
|
25223
|
+
name: "revoke",
|
|
25224
|
+
description: "Revoke all grants for a member on a channel"
|
|
25225
|
+
},
|
|
25226
|
+
args: {
|
|
25227
|
+
channel: {
|
|
25228
|
+
type: "positional",
|
|
25229
|
+
required: true,
|
|
25230
|
+
description: "Channel ID or channel name"
|
|
25231
|
+
},
|
|
25232
|
+
member: {
|
|
25233
|
+
type: "string",
|
|
25234
|
+
required: true,
|
|
25235
|
+
description: "Member ID whose grants to revoke"
|
|
25236
|
+
},
|
|
25237
|
+
yes: {
|
|
25238
|
+
type: "boolean",
|
|
25239
|
+
description: "Skip confirmation prompt"
|
|
25240
|
+
}
|
|
25241
|
+
},
|
|
25242
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
25243
|
+
if (!args.yes) {
|
|
25244
|
+
if (!(yield* promptConfirm(`Revoke all grants for member ${args.member} on channel ${args.channel}?`, { initialValue: false }))) {
|
|
25245
|
+
yield* printHuman("Cancelled.");
|
|
25246
|
+
return { deleted: 0 };
|
|
25247
|
+
}
|
|
25248
|
+
}
|
|
25249
|
+
const projectId = yield* readProjectId;
|
|
25250
|
+
const api = yield* apiClient;
|
|
25251
|
+
const channels = yield* drainPages((page) => api.channels.list({ urlParams: {
|
|
25252
|
+
projectId,
|
|
25253
|
+
limit: 100,
|
|
25254
|
+
page
|
|
25255
|
+
} }));
|
|
25256
|
+
const channel = channels.find((ch) => ch.id === args.channel) ?? channels.find((ch) => ch.name === args.channel);
|
|
25257
|
+
if (!channel) return yield* new ChannelCommandError({ message: `Channel "${args.channel}" not found by ID or name.` });
|
|
25258
|
+
const result = yield* api.channelGrants.delete({ path: {
|
|
25259
|
+
id: channel.id,
|
|
25260
|
+
memberId: args.member
|
|
25261
|
+
} });
|
|
25262
|
+
yield* printHuman(`Revoked grants for member ${args.member} on channel "${channel.name}".`);
|
|
25263
|
+
return result;
|
|
25264
|
+
}), {
|
|
25265
|
+
exits: {
|
|
25266
|
+
...channelErrorExtras,
|
|
25267
|
+
...grantErrorExtras
|
|
25268
|
+
},
|
|
25269
|
+
json: "value"
|
|
25270
|
+
})
|
|
25271
|
+
});
|
|
25272
|
+
|
|
25273
|
+
//#endregion
|
|
25274
|
+
//#region src/commands/channels/grants/set.ts
|
|
25275
|
+
const setCommand$3 = defineCommand({
|
|
25276
|
+
meta: {
|
|
25277
|
+
name: "set",
|
|
25278
|
+
description: "Create or replace a member's grant on a channel"
|
|
25279
|
+
},
|
|
25280
|
+
args: {
|
|
25281
|
+
channel: {
|
|
25282
|
+
type: "positional",
|
|
25283
|
+
required: true,
|
|
25284
|
+
description: "Channel ID or channel name"
|
|
25285
|
+
},
|
|
25286
|
+
member: {
|
|
25287
|
+
type: "string",
|
|
25288
|
+
required: true,
|
|
25289
|
+
description: "Member ID to grant permissions to"
|
|
25290
|
+
},
|
|
25291
|
+
actions: {
|
|
25292
|
+
type: "string",
|
|
25293
|
+
required: true,
|
|
25294
|
+
description: "Permission action tokens in resource:action format, comma-separated (e.g. update:create,rollout:update)"
|
|
25295
|
+
},
|
|
25296
|
+
effect: {
|
|
25297
|
+
type: "string",
|
|
25298
|
+
description: "Grant effect: allow (default) or deny"
|
|
25299
|
+
}
|
|
25300
|
+
},
|
|
25301
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
25302
|
+
const effectValue = args.effect ?? "allow";
|
|
25303
|
+
if (effectValue !== "allow" && effectValue !== "deny") return yield* new GrantCommandError({ message: `Invalid effect "${effectValue}" — must be "allow" or "deny".` });
|
|
25304
|
+
const actionTokens = args.actions.split(",").map((tok) => tok.trim()).filter((tok) => tok.length > 0);
|
|
25305
|
+
if (actionTokens.length === 0) return yield* new GrantCommandError({ message: "At least one action token is required." });
|
|
25306
|
+
const projectId = yield* readProjectId;
|
|
25307
|
+
const api = yield* apiClient;
|
|
25308
|
+
const channels = yield* drainPages((page) => api.channels.list({ urlParams: {
|
|
25309
|
+
projectId,
|
|
25310
|
+
limit: 100,
|
|
25311
|
+
page
|
|
25312
|
+
} }));
|
|
25313
|
+
const channel = channels.find((ch) => ch.id === args.channel) ?? channels.find((ch) => ch.name === args.channel);
|
|
25314
|
+
if (!channel) return yield* new ChannelCommandError({ message: `Channel "${args.channel}" not found by ID or name.` });
|
|
25315
|
+
const grant = yield* api.channelGrants.upsert({
|
|
25316
|
+
path: {
|
|
25317
|
+
id: channel.id,
|
|
25318
|
+
memberId: args.member
|
|
25319
|
+
},
|
|
25320
|
+
payload: {
|
|
25321
|
+
effect: effectValue,
|
|
25322
|
+
actions: actionTokens
|
|
25323
|
+
}
|
|
25324
|
+
});
|
|
25325
|
+
yield* printHumanKeyValue([
|
|
25326
|
+
["ID", grant.id],
|
|
25327
|
+
["Member ID", grant.memberId],
|
|
25328
|
+
["Channel ID", grant.scopeId],
|
|
25329
|
+
["Effect", grant.effect],
|
|
25330
|
+
["Actions", grant.actions.join(", ")],
|
|
25331
|
+
["Created", grant.createdAt]
|
|
25332
|
+
]);
|
|
25333
|
+
return grant;
|
|
25334
|
+
}), { exits: {
|
|
25335
|
+
...channelErrorExtras,
|
|
25336
|
+
...grantErrorExtras
|
|
25337
|
+
} })
|
|
25338
|
+
});
|
|
25339
|
+
|
|
25340
|
+
//#endregion
|
|
25341
|
+
//#region src/commands/channels/grants/index.ts
|
|
25342
|
+
const grantsCommand$1 = defineCommand({
|
|
25343
|
+
meta: {
|
|
25344
|
+
name: "grants",
|
|
25345
|
+
description: "Manage per-member permission grants on a channel"
|
|
25346
|
+
},
|
|
25347
|
+
subCommands: {
|
|
25348
|
+
list: listCommand$9,
|
|
25349
|
+
set: setCommand$3,
|
|
25350
|
+
revoke: revokeCommand$2
|
|
25351
|
+
}
|
|
25352
|
+
});
|
|
25353
|
+
|
|
24443
25354
|
//#endregion
|
|
24444
25355
|
//#region src/commands/channels/insights.ts
|
|
24445
25356
|
const insightsCommand$1 = defineCommand({
|
|
@@ -24486,7 +25397,7 @@ const insightsCommand$1 = defineCommand({
|
|
|
24486
25397
|
|
|
24487
25398
|
//#endregion
|
|
24488
25399
|
//#region src/commands/channels/list.ts
|
|
24489
|
-
const listCommand$
|
|
25400
|
+
const listCommand$8 = defineCommand({
|
|
24490
25401
|
meta: {
|
|
24491
25402
|
name: "list",
|
|
24492
25403
|
description: "List channels for the linked project"
|
|
@@ -24590,7 +25501,7 @@ const completeCommand$1 = defineCommand({
|
|
|
24590
25501
|
|
|
24591
25502
|
//#endregion
|
|
24592
25503
|
//#region src/commands/channels/rollout/create.ts
|
|
24593
|
-
const createCommand$
|
|
25504
|
+
const createCommand$3 = defineCommand({
|
|
24594
25505
|
meta: {
|
|
24595
25506
|
name: "create",
|
|
24596
25507
|
description: "Start a branch rollout on a channel"
|
|
@@ -24671,7 +25582,7 @@ const revertCommand$2 = defineCommand({
|
|
|
24671
25582
|
|
|
24672
25583
|
//#endregion
|
|
24673
25584
|
//#region src/commands/channels/rollout/update.ts
|
|
24674
|
-
const updateCommand$
|
|
25585
|
+
const updateCommand$4 = defineCommand({
|
|
24675
25586
|
meta: {
|
|
24676
25587
|
name: "update",
|
|
24677
25588
|
description: "Update the rollout percentage on a channel"
|
|
@@ -24710,8 +25621,8 @@ const rolloutCommand$1 = defineCommand({
|
|
|
24710
25621
|
description: "Manage channel branch rollouts"
|
|
24711
25622
|
},
|
|
24712
25623
|
subCommands: {
|
|
24713
|
-
create: createCommand$
|
|
24714
|
-
update: updateCommand$
|
|
25624
|
+
create: createCommand$3,
|
|
25625
|
+
update: updateCommand$4,
|
|
24715
25626
|
complete: completeCommand$1,
|
|
24716
25627
|
revert: revertCommand$2
|
|
24717
25628
|
}
|
|
@@ -24719,7 +25630,7 @@ const rolloutCommand$1 = defineCommand({
|
|
|
24719
25630
|
|
|
24720
25631
|
//#endregion
|
|
24721
25632
|
//#region src/commands/channels/update.ts
|
|
24722
|
-
const updateCommand$
|
|
25633
|
+
const updateCommand$3 = defineCommand({
|
|
24723
25634
|
meta: {
|
|
24724
25635
|
name: "update",
|
|
24725
25636
|
description: "Relink a channel to a different branch"
|
|
@@ -24762,7 +25673,7 @@ const updateCommand$2 = defineCommand({
|
|
|
24762
25673
|
|
|
24763
25674
|
//#endregion
|
|
24764
25675
|
//#region src/commands/channels/view.ts
|
|
24765
|
-
const viewCommand$
|
|
25676
|
+
const viewCommand$3 = defineCommand({
|
|
24766
25677
|
meta: {
|
|
24767
25678
|
name: "view",
|
|
24768
25679
|
description: "Show a channel by ID or name"
|
|
@@ -24785,7 +25696,7 @@ const viewCommand$2 = defineCommand({
|
|
|
24785
25696
|
page
|
|
24786
25697
|
} }))]);
|
|
24787
25698
|
const channel = channels.find((entry) => entry.id === args.target) ?? channels.find((entry) => entry.name === args.target);
|
|
24788
|
-
if (!channel) return yield*
|
|
25699
|
+
if (!channel) return yield* new ChannelCommandError({ message: `Channel "${args.target}" not found by ID or name.` });
|
|
24789
25700
|
const branchName = new Map(branches.map((branch) => [branch.id, branch.name])).get(channel.branchId) ?? channel.branchId;
|
|
24790
25701
|
yield* printHumanKeyValue([
|
|
24791
25702
|
["ID", channel.id],
|
|
@@ -24822,14 +25733,15 @@ const channelsCommand = defineCommand({
|
|
|
24822
25733
|
description: "Manage channels"
|
|
24823
25734
|
},
|
|
24824
25735
|
subCommands: {
|
|
24825
|
-
list: listCommand$
|
|
24826
|
-
view: viewCommand$
|
|
24827
|
-
create: createCommand$
|
|
24828
|
-
update: updateCommand$
|
|
25736
|
+
list: listCommand$8,
|
|
25737
|
+
view: viewCommand$3,
|
|
25738
|
+
create: createCommand$4,
|
|
25739
|
+
update: updateCommand$3,
|
|
24829
25740
|
pause: pauseCommand,
|
|
24830
25741
|
resume: resumeCommand,
|
|
24831
|
-
delete: deleteCommand$
|
|
25742
|
+
delete: deleteCommand$5,
|
|
24832
25743
|
rollout: rolloutCommand$1,
|
|
25744
|
+
grants: grantsCommand$1,
|
|
24833
25745
|
insights: insightsCommand$1
|
|
24834
25746
|
}
|
|
24835
25747
|
});
|
|
@@ -24891,7 +25803,7 @@ const parseGoogleServiceAccountKey = (jsonText) => Effect.gen(function* () {
|
|
|
24891
25803
|
const APPLE_TEAM_ID_RE = /^[A-Z0-9]{10}$/u;
|
|
24892
25804
|
const extractTeamId = (params) => {
|
|
24893
25805
|
if (params.orgUnit && APPLE_TEAM_ID_RE.test(params.orgUnit)) return params.orgUnit;
|
|
24894
|
-
return /\(([A-Z0-9]{10})\)\s*$/u.exec(params.signingIdentity)?.[1];
|
|
25806
|
+
return /\((?<team>[A-Z0-9]{10})\)\s*$/u.exec(params.signingIdentity)?.[1];
|
|
24895
25807
|
};
|
|
24896
25808
|
/**
|
|
24897
25809
|
* Parse a PKCS#12 (.p12) buffer and extract certificate metadata.
|
|
@@ -25210,7 +26122,7 @@ const announce = (heading) => Effect.gen(function* () {
|
|
|
25210
26122
|
});
|
|
25211
26123
|
const reportError = (label, cause) => Console.log(`✗ ${label}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
25212
26124
|
const safely = (label, effect) => effect.pipe(Effect.catchAll((cause) => reportError(label, cause)), Effect.asVoid);
|
|
25213
|
-
const safePrompt = (effect) => effect.pipe(Effect.
|
|
26125
|
+
const safePrompt = (effect) => effect.pipe(Effect.orElseSucceed(() => BACK));
|
|
25214
26126
|
const promptForBundleConfig = (ctx) => Effect.gen(function* () {
|
|
25215
26127
|
const list = yield* ctx.api.iosBundleConfigurations.list({ path: { projectId: ctx.projectId } });
|
|
25216
26128
|
if (list.items.length === 0) return yield* new MissingCredentialsError({
|
|
@@ -26243,7 +27155,7 @@ const toRecipientView = (userEncryptionKeyId, key) => ({
|
|
|
26243
27155
|
fingerprint: key?.fingerprint
|
|
26244
27156
|
})
|
|
26245
27157
|
});
|
|
26246
|
-
const listCommand$
|
|
27158
|
+
const listCommand$7 = defineCommand({
|
|
26247
27159
|
meta: {
|
|
26248
27160
|
name: "list",
|
|
26249
27161
|
description: "List recipients that currently hold the org vault key"
|
|
@@ -26470,7 +27382,7 @@ const accessCommand = defineCommand({
|
|
|
26470
27382
|
description: "Inspect, grant, rotate, revoke, and recover access to the org credential vault"
|
|
26471
27383
|
},
|
|
26472
27384
|
subCommands: {
|
|
26473
|
-
list: listCommand$
|
|
27385
|
+
list: listCommand$7,
|
|
26474
27386
|
grant: grantCommand,
|
|
26475
27387
|
rotate: rotateCommand,
|
|
26476
27388
|
revoke: revokeCommand$1,
|
|
@@ -26679,7 +27591,7 @@ const CREDENTIAL_TYPES$3 = [
|
|
|
26679
27591
|
"keystore",
|
|
26680
27592
|
"google-service-account-key"
|
|
26681
27593
|
];
|
|
26682
|
-
const deleteCommand$
|
|
27594
|
+
const deleteCommand$4 = defineCommand({
|
|
26683
27595
|
meta: {
|
|
26684
27596
|
name: "delete",
|
|
26685
27597
|
description: "Delete a credential"
|
|
@@ -26721,7 +27633,7 @@ const deleteCommand$3 = defineCommand({
|
|
|
26721
27633
|
//#region src/commands/credentials/device.ts
|
|
26722
27634
|
/** Self-linking is for your own device keys; recovery/machine keys go through `access grant`. */
|
|
26723
27635
|
const requireDeviceKind = (target) => target.kind === "device" ? Effect.void : new IdentityError({ message: `Key ${target.id} is a ${target.kind} key, not a device. Use \`better-update credentials access grant\` for recovery/machine keys.` });
|
|
26724
|
-
const listCommand$
|
|
27636
|
+
const listCommand$6 = defineCommand({
|
|
26725
27637
|
meta: {
|
|
26726
27638
|
name: "list",
|
|
26727
27639
|
description: "List your registered device keys (the active one is marked)"
|
|
@@ -26779,7 +27691,7 @@ const deviceCommand = defineCommand({
|
|
|
26779
27691
|
description: "Manage your vault device keys"
|
|
26780
27692
|
},
|
|
26781
27693
|
subCommands: {
|
|
26782
|
-
list: listCommand$
|
|
27694
|
+
list: listCommand$6,
|
|
26783
27695
|
link: linkCommand
|
|
26784
27696
|
},
|
|
26785
27697
|
default: "list"
|
|
@@ -27141,7 +28053,7 @@ const handleCertLimitInteractive = (api, ascApiKeyId, certificateType) => Effect
|
|
|
27141
28053
|
ascApiKeyId,
|
|
27142
28054
|
certificateType
|
|
27143
28055
|
});
|
|
27144
|
-
if (certs.length === 0) return yield*
|
|
28056
|
+
if (certs.length === 0) return yield* new CertificateLimitError({ message: "Apple says the certificate limit is hit but no existing certificates were returned — try again later." });
|
|
27145
28057
|
const toRevoke = yield* promptMultiSelect("Select one or more certificates to revoke before retrying", certs.map((entry) => ({
|
|
27146
28058
|
value: entry.id,
|
|
27147
28059
|
label: `${entry.serialNumber.slice(0, 12)}… (${entry.displayName ?? entry.certificateType}, exp ${entry.expirationDate.slice(0, 10)})`
|
|
@@ -27434,7 +28346,7 @@ const printRecipient = (key) => printKeyValue([
|
|
|
27434
28346
|
["Recipient (public key)", key.publicKey],
|
|
27435
28347
|
["Fingerprint", key.fingerprint]
|
|
27436
28348
|
]);
|
|
27437
|
-
const createCommand$
|
|
28349
|
+
const createCommand$2 = defineCommand({
|
|
27438
28350
|
meta: {
|
|
27439
28351
|
name: "create",
|
|
27440
28352
|
description: "Create this device's encryption identity and register it as a recipient"
|
|
@@ -27537,7 +28449,7 @@ const identityCommand = defineCommand({
|
|
|
27537
28449
|
description: "Manage this device's end-to-end encryption identity"
|
|
27538
28450
|
},
|
|
27539
28451
|
subCommands: {
|
|
27540
|
-
create: createCommand$
|
|
28452
|
+
create: createCommand$2,
|
|
27541
28453
|
init: initCommand$1,
|
|
27542
28454
|
register: registerCommand,
|
|
27543
28455
|
show: showCommand
|
|
@@ -27547,7 +28459,7 @@ const identityCommand = defineCommand({
|
|
|
27547
28459
|
|
|
27548
28460
|
//#endregion
|
|
27549
28461
|
//#region src/commands/credentials/list.ts
|
|
27550
|
-
const listCommand$
|
|
28462
|
+
const listCommand$5 = defineCommand({
|
|
27551
28463
|
meta: {
|
|
27552
28464
|
name: "list",
|
|
27553
28465
|
description: "List credentials across platforms"
|
|
@@ -27903,7 +28815,7 @@ const writeArtifact = (fs, projectRoot, relPath, bytes) => Effect.gen(function*
|
|
|
27903
28815
|
const writeText = (fs, projectRoot, relPath, text) => writeArtifact(fs, projectRoot, relPath, new TextEncoder().encode(text));
|
|
27904
28816
|
const ensureGitignoreEntries = (fs, projectRoot, paths) => Effect.gen(function* () {
|
|
27905
28817
|
const filePath = path.join(projectRoot, ".gitignore");
|
|
27906
|
-
const lines = ((yield* fs.exists(filePath).pipe(Effect.
|
|
28818
|
+
const lines = ((yield* fs.exists(filePath).pipe(Effect.orElseSucceed(() => false))) ? yield* fs.readFileString(filePath).pipe(Effect.orElseSucceed(() => "")) : "").split("\n");
|
|
27907
28819
|
const added = [];
|
|
27908
28820
|
const next = [...lines];
|
|
27909
28821
|
for (const entry of paths) if (!lines.includes(entry)) {
|
|
@@ -28702,7 +29614,7 @@ const lookupByType = (api, id, type) => {
|
|
|
28702
29614
|
default: return Effect.fail(new CredentialValidationError({ message: `Unsupported credential type: ${String(type)}` }));
|
|
28703
29615
|
}
|
|
28704
29616
|
};
|
|
28705
|
-
const viewCommand$
|
|
29617
|
+
const viewCommand$2 = defineCommand({
|
|
28706
29618
|
meta: {
|
|
28707
29619
|
name: "view",
|
|
28708
29620
|
description: "Show details for a single credential (without secrets)"
|
|
@@ -28749,14 +29661,14 @@ const credentialsCommand = defineCommand({
|
|
|
28749
29661
|
unlock: unlockCommand,
|
|
28750
29662
|
lock: lockCommand,
|
|
28751
29663
|
status: statusCommand$1,
|
|
28752
|
-
list: listCommand$
|
|
28753
|
-
view: viewCommand$
|
|
29664
|
+
list: listCommand$5,
|
|
29665
|
+
view: viewCommand$2,
|
|
28754
29666
|
download: downloadCommand,
|
|
28755
29667
|
upload: uploadCommand,
|
|
28756
29668
|
"upload-asc-key": uploadAscKeyCommand,
|
|
28757
29669
|
generate: generateCommand$1,
|
|
28758
29670
|
"regenerate-profile": regenerateProfileCommand,
|
|
28759
|
-
delete: deleteCommand$
|
|
29671
|
+
delete: deleteCommand$4,
|
|
28760
29672
|
remove: removeCommand,
|
|
28761
29673
|
revoke: revokeCommand,
|
|
28762
29674
|
configure: configureCommand$1,
|
|
@@ -28776,7 +29688,7 @@ const DEVICE_CLASS_VALUES = [
|
|
|
28776
29688
|
const isDeviceClass = (value) => DEVICE_CLASS_VALUES.includes(value);
|
|
28777
29689
|
const ttlHours = (value) => {
|
|
28778
29690
|
if (value === void 0) return;
|
|
28779
|
-
const match = /^([0-9]+)([hd])?$/u.exec(value);
|
|
29691
|
+
const match = /^(?<value>[0-9]+)(?<unit>[hd])?$/u.exec(value);
|
|
28780
29692
|
if (!match?.[1]) return;
|
|
28781
29693
|
const num = Number.parseInt(match[1], 10);
|
|
28782
29694
|
return match[2] === "d" ? num * 24 : num;
|
|
@@ -29123,6 +30035,7 @@ const devicesCommand = defineCommand({
|
|
|
29123
30035
|
|
|
29124
30036
|
//#endregion
|
|
29125
30037
|
//#region src/commands/doctor.ts
|
|
30038
|
+
var HealthCheckError = class extends Data.TaggedError("HealthCheckError") {};
|
|
29126
30039
|
const pass = (id, name, message) => ({
|
|
29127
30040
|
id,
|
|
29128
30041
|
name,
|
|
@@ -29161,7 +30074,10 @@ const checkServerHealth = Effect.gen(function* () {
|
|
|
29161
30074
|
const url = `${yield* (yield* ConfigStore).getBaseUrl}/api/health`;
|
|
29162
30075
|
const response = yield* Effect.tryPromise({
|
|
29163
30076
|
try: async () => fetch(url, { signal: AbortSignal.timeout(3e3) }),
|
|
29164
|
-
catch: (cause) => new
|
|
30077
|
+
catch: (cause) => new HealthCheckError({
|
|
30078
|
+
message: String(cause),
|
|
30079
|
+
cause
|
|
30080
|
+
})
|
|
29165
30081
|
}).pipe(Effect.either);
|
|
29166
30082
|
if (response._tag === "Left") return fail("health", "Server reachable", `${url} unreachable: ${response.left.message}`);
|
|
29167
30083
|
const res = response.right;
|
|
@@ -29186,11 +30102,18 @@ const checkProjectLink = Effect.gen(function* () {
|
|
|
29186
30102
|
const source = (yield* readLinkedProjectId(root)) === void 0 ? "Expo config" : "better-update.json";
|
|
29187
30103
|
return pass("project-linked", "Project linked", `projectId=${resolved.right} (via ${source})`);
|
|
29188
30104
|
});
|
|
29189
|
-
const
|
|
29190
|
-
const
|
|
29191
|
-
|
|
29192
|
-
|
|
29193
|
-
|
|
30105
|
+
const checkProjectType = Effect.gen(function* () {
|
|
30106
|
+
const root = yield* (yield* CliRuntime).cwd;
|
|
30107
|
+
const override = asProjectType((yield* readBetterUpdateConfig(root))?.["projectType"]);
|
|
30108
|
+
return pass("project-type", "Project type", `${yield* detectProjectType({
|
|
30109
|
+
projectRoot: root,
|
|
30110
|
+
override
|
|
30111
|
+
})} (${override === void 0 ? "auto-detected" : "better-update.json override"})`);
|
|
30112
|
+
});
|
|
30113
|
+
const checkBuildConfig = Effect.gen(function* () {
|
|
30114
|
+
const names = yield* listBuildProfileNames(yield* (yield* CliRuntime).cwd);
|
|
30115
|
+
if (names.length === 0) return warn("build-config", "Build config", "No build profiles found. Add a \"build\" section to better-update.json.");
|
|
30116
|
+
return pass("build-config", "Build config", `${names.length} profile(s) defined`);
|
|
29194
30117
|
});
|
|
29195
30118
|
const runChecks = Effect.gen(function* () {
|
|
29196
30119
|
const xcode = (yield* CliRuntime).platform === "darwin" ? [yield* checkCommand("xcode", "Xcode CLI tools", "xcode-select", ["-p"])] : [];
|
|
@@ -29201,7 +30124,8 @@ const runChecks = Effect.gen(function* () {
|
|
|
29201
30124
|
yield* checkServerHealth,
|
|
29202
30125
|
yield* checkAuth,
|
|
29203
30126
|
yield* checkProjectLink,
|
|
29204
|
-
yield*
|
|
30127
|
+
yield* checkProjectType,
|
|
30128
|
+
yield* checkBuildConfig
|
|
29205
30129
|
];
|
|
29206
30130
|
});
|
|
29207
30131
|
const statusIcon = (status) => {
|
|
@@ -29244,23 +30168,23 @@ const envErrorExtras = {
|
|
|
29244
30168
|
SystemError: 6,
|
|
29245
30169
|
BadArgument: 6
|
|
29246
30170
|
};
|
|
29247
|
-
const isEnvironmentName = (value) => value === "development" || value === "preview" || value === "production";
|
|
30171
|
+
const isEnvironmentName$1 = (value) => value === "development" || value === "preview" || value === "production";
|
|
29248
30172
|
const parseEnvironmentsArg = (raw) => Effect.gen(function* () {
|
|
29249
30173
|
const tokens = raw.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
|
|
29250
30174
|
if (tokens.length === 0) return yield* new InvalidArgumentError({ message: "Provide at least one environment (development, preview, production)." });
|
|
29251
30175
|
const seen = /* @__PURE__ */ new Set();
|
|
29252
30176
|
yield* Effect.forEach(tokens, (token) => Effect.gen(function* () {
|
|
29253
|
-
if (!isEnvironmentName(token)) return yield* new InvalidArgumentError({ message: `Invalid environment "${token}". Must be one of: development, preview, production.` });
|
|
30177
|
+
if (!isEnvironmentName$1(token)) return yield* new InvalidArgumentError({ message: `Invalid environment "${token}". Must be one of: development, preview, production.` });
|
|
29254
30178
|
seen.add(token);
|
|
29255
30179
|
}), { discard: true });
|
|
29256
30180
|
return [...seen];
|
|
29257
30181
|
});
|
|
29258
30182
|
const parseSingleEnvironmentArg = (raw) => Effect.gen(function* () {
|
|
29259
|
-
if (!isEnvironmentName(raw)) return yield* new InvalidArgumentError({ message: `Invalid environment "${raw}". Must be one of: development, preview, production.` });
|
|
30183
|
+
if (!isEnvironmentName$1(raw)) return yield* new InvalidArgumentError({ message: `Invalid environment "${raw}". Must be one of: development, preview, production.` });
|
|
29260
30184
|
return raw;
|
|
29261
30185
|
});
|
|
29262
30186
|
const formatEnvironments = (environments) => [...environments].toSorted((left, right) => left.localeCompare(right)).join(",");
|
|
29263
|
-
const DOTENV_LINE = /^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=\s*(
|
|
30187
|
+
const DOTENV_LINE = /^\s*(?:export\s+)?(?<key>[A-Z][A-Z0-9_]*)\s*=\s*(?<value>.*?)\s*$/u;
|
|
29264
30188
|
const stripQuotes = (raw) => {
|
|
29265
30189
|
if (raw.length < 2) return raw;
|
|
29266
30190
|
const [first] = raw;
|
|
@@ -29295,7 +30219,7 @@ const findProjectEnvVar = (api, projectId, key, environment) => Effect.gen(funct
|
|
|
29295
30219
|
|
|
29296
30220
|
//#endregion
|
|
29297
30221
|
//#region src/commands/env/delete.ts
|
|
29298
|
-
const deleteCommand$
|
|
30222
|
+
const deleteCommand$3 = defineCommand({
|
|
29299
30223
|
meta: {
|
|
29300
30224
|
name: "delete",
|
|
29301
30225
|
description: "Delete a project env var (one environment, or every environment by default)"
|
|
@@ -29364,7 +30288,7 @@ const getExecTrailingArgv = () => trailing$1;
|
|
|
29364
30288
|
const pullForExec = (api, projectId, environment) => pullEnvVars(api, {
|
|
29365
30289
|
projectId,
|
|
29366
30290
|
environment
|
|
29367
|
-
}).pipe(Effect.
|
|
30291
|
+
}).pipe(Effect.orElseSucceed(() => ({})));
|
|
29368
30292
|
const splitTrailing = (trailing) => {
|
|
29369
30293
|
if (!trailing || trailing.length === 0) return Effect.fail(new InvalidArgumentError({ message: "Pass the command after `--`. Example: `better-update env exec production -- bun run dev`." }));
|
|
29370
30294
|
const [bin, ...rest] = trailing;
|
|
@@ -29467,6 +30391,191 @@ const getCommand$1 = defineCommand({
|
|
|
29467
30391
|
}), envErrorExtras)
|
|
29468
30392
|
});
|
|
29469
30393
|
|
|
30394
|
+
//#endregion
|
|
30395
|
+
//#region src/commands/env/grants/helpers.ts
|
|
30396
|
+
var EnvGrantCommandError = class extends Data.TaggedError("EnvGrantCommandError") {};
|
|
30397
|
+
const envGrantErrorExtras = { EnvGrantCommandError: 2 };
|
|
30398
|
+
/** Sentinel project token for the org-global env-var scope (mirrors server). */
|
|
30399
|
+
const ENV_GRANT_GLOBAL = "global";
|
|
30400
|
+
const ENVIRONMENTS = [
|
|
30401
|
+
"development",
|
|
30402
|
+
"preview",
|
|
30403
|
+
"production"
|
|
30404
|
+
];
|
|
30405
|
+
/**
|
|
30406
|
+
* Type guard narrowing a raw arg to an {@link EnvironmentName}. Lets set/unset
|
|
30407
|
+
* validate `args.environment` AND narrow it without an unsafe `as` assertion.
|
|
30408
|
+
*/
|
|
30409
|
+
const isEnvironmentName = (value) => ENVIRONMENTS.includes(value);
|
|
30410
|
+
|
|
30411
|
+
//#endregion
|
|
30412
|
+
//#region src/commands/env/grants/list.ts
|
|
30413
|
+
const listCommand$4 = defineCommand({
|
|
30414
|
+
meta: {
|
|
30415
|
+
name: "list",
|
|
30416
|
+
description: "List env-var grants on a (project × environment) scope"
|
|
30417
|
+
},
|
|
30418
|
+
args: {
|
|
30419
|
+
project: {
|
|
30420
|
+
type: "string",
|
|
30421
|
+
description: `Project id, or "${ENV_GRANT_GLOBAL}" for the org-global scope (default: linked project)`
|
|
30422
|
+
},
|
|
30423
|
+
global: {
|
|
30424
|
+
type: "boolean",
|
|
30425
|
+
default: false,
|
|
30426
|
+
description: "Target the org-global env-var scope instead of a project"
|
|
30427
|
+
}
|
|
30428
|
+
},
|
|
30429
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
30430
|
+
const projectId = args.global ? ENV_GRANT_GLOBAL : args.project ?? (yield* readProjectId);
|
|
30431
|
+
yield* printList([
|
|
30432
|
+
"Member ID",
|
|
30433
|
+
"Environment",
|
|
30434
|
+
"Effect",
|
|
30435
|
+
"Actions"
|
|
30436
|
+
], (yield* (yield* apiClient).envGrants.list({ urlParams: { projectId } })).map((row) => [
|
|
30437
|
+
row.memberId,
|
|
30438
|
+
row.environment,
|
|
30439
|
+
row.effect,
|
|
30440
|
+
row.actions.join(", ")
|
|
30441
|
+
]), "No env-var grants found for this scope.");
|
|
30442
|
+
}), { exits: { ...envGrantErrorExtras } })
|
|
30443
|
+
});
|
|
30444
|
+
|
|
30445
|
+
//#endregion
|
|
30446
|
+
//#region src/commands/env/grants/set.ts
|
|
30447
|
+
const setCommand$2 = defineCommand({
|
|
30448
|
+
meta: {
|
|
30449
|
+
name: "set",
|
|
30450
|
+
description: "Create or replace a member's env-var grant on a scope"
|
|
30451
|
+
},
|
|
30452
|
+
args: {
|
|
30453
|
+
member: {
|
|
30454
|
+
type: "string",
|
|
30455
|
+
required: true,
|
|
30456
|
+
description: "Member ID to grant"
|
|
30457
|
+
},
|
|
30458
|
+
environment: {
|
|
30459
|
+
type: "string",
|
|
30460
|
+
required: true,
|
|
30461
|
+
description: "Environment: development | preview | production"
|
|
30462
|
+
},
|
|
30463
|
+
actions: {
|
|
30464
|
+
type: "string",
|
|
30465
|
+
description: "Comma-separated envVar:* tokens (default: envVar:read)"
|
|
30466
|
+
},
|
|
30467
|
+
effect: {
|
|
30468
|
+
type: "string",
|
|
30469
|
+
description: "allow (default) or deny"
|
|
30470
|
+
},
|
|
30471
|
+
project: {
|
|
30472
|
+
type: "string",
|
|
30473
|
+
description: `Project id (default: linked project)`
|
|
30474
|
+
},
|
|
30475
|
+
global: {
|
|
30476
|
+
type: "boolean",
|
|
30477
|
+
default: false,
|
|
30478
|
+
description: "Target the org-global env-var scope"
|
|
30479
|
+
}
|
|
30480
|
+
},
|
|
30481
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
30482
|
+
const effectValue = args.effect ?? "allow";
|
|
30483
|
+
if (effectValue !== "allow" && effectValue !== "deny") return yield* new EnvGrantCommandError({ message: `Invalid effect "${effectValue}".` });
|
|
30484
|
+
const { environment } = args;
|
|
30485
|
+
if (!isEnvironmentName(environment)) return yield* new EnvGrantCommandError({ message: `Invalid environment "${environment}". One of: ${ENVIRONMENTS.join(", ")}.` });
|
|
30486
|
+
const actionTokens = (args.actions ?? "envVar:read").split(",").map((tok) => tok.trim()).filter((tok) => tok.length > 0);
|
|
30487
|
+
if (actionTokens.length === 0) return yield* new EnvGrantCommandError({ message: "At least one action token is required." });
|
|
30488
|
+
const projectId = args.global ? null : args.project ?? (yield* readProjectId);
|
|
30489
|
+
const grant = yield* (yield* apiClient).envGrants.upsert({ payload: {
|
|
30490
|
+
memberId: args.member,
|
|
30491
|
+
projectId,
|
|
30492
|
+
environment,
|
|
30493
|
+
effect: effectValue,
|
|
30494
|
+
actions: actionTokens
|
|
30495
|
+
} });
|
|
30496
|
+
yield* printHumanKeyValue([
|
|
30497
|
+
["ID", grant.id],
|
|
30498
|
+
["Member ID", grant.memberId],
|
|
30499
|
+
["Scope", grant.scopeId],
|
|
30500
|
+
["Effect", grant.effect],
|
|
30501
|
+
["Actions", grant.actions.join(", ")],
|
|
30502
|
+
["Created", grant.createdAt]
|
|
30503
|
+
]);
|
|
30504
|
+
return grant;
|
|
30505
|
+
}), { exits: { ...envGrantErrorExtras } })
|
|
30506
|
+
});
|
|
30507
|
+
|
|
30508
|
+
//#endregion
|
|
30509
|
+
//#region src/commands/env/grants/unset.ts
|
|
30510
|
+
const unsetCommand = defineCommand({
|
|
30511
|
+
meta: {
|
|
30512
|
+
name: "unset",
|
|
30513
|
+
description: "Revoke a member's env-var grants on a scope"
|
|
30514
|
+
},
|
|
30515
|
+
args: {
|
|
30516
|
+
member: {
|
|
30517
|
+
type: "string",
|
|
30518
|
+
required: true,
|
|
30519
|
+
description: "Member ID whose grants to revoke"
|
|
30520
|
+
},
|
|
30521
|
+
environment: {
|
|
30522
|
+
type: "string",
|
|
30523
|
+
required: true,
|
|
30524
|
+
description: "Environment"
|
|
30525
|
+
},
|
|
30526
|
+
project: {
|
|
30527
|
+
type: "string",
|
|
30528
|
+
description: "Project id (default: linked project)"
|
|
30529
|
+
},
|
|
30530
|
+
global: {
|
|
30531
|
+
type: "boolean",
|
|
30532
|
+
default: false,
|
|
30533
|
+
description: "Target the org-global scope"
|
|
30534
|
+
},
|
|
30535
|
+
yes: {
|
|
30536
|
+
type: "boolean",
|
|
30537
|
+
default: false,
|
|
30538
|
+
description: "Skip confirmation prompt"
|
|
30539
|
+
}
|
|
30540
|
+
},
|
|
30541
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
30542
|
+
const { environment } = args;
|
|
30543
|
+
if (!isEnvironmentName(environment)) return yield* new EnvGrantCommandError({ message: `Invalid environment "${environment}".` });
|
|
30544
|
+
if (!args.yes) {
|
|
30545
|
+
const scopeLabel = args.global ? ENV_GRANT_GLOBAL : args.project ?? "linked project";
|
|
30546
|
+
if (!(yield* promptConfirm(`Revoke env-var grants for member ${args.member} on ${scopeLabel}/${args.environment}?`, { initialValue: false }))) {
|
|
30547
|
+
yield* printHuman("Cancelled.");
|
|
30548
|
+
return { deleted: 0 };
|
|
30549
|
+
}
|
|
30550
|
+
}
|
|
30551
|
+
const projectId = args.global ? null : args.project ?? (yield* readProjectId);
|
|
30552
|
+
const result = yield* (yield* apiClient).envGrants.delete({ payload: {
|
|
30553
|
+
memberId: args.member,
|
|
30554
|
+
projectId,
|
|
30555
|
+
environment
|
|
30556
|
+
} });
|
|
30557
|
+
yield* printHuman(`Revoked env-var grants for member ${args.member}.`);
|
|
30558
|
+
return result;
|
|
30559
|
+
}), {
|
|
30560
|
+
exits: { ...envGrantErrorExtras },
|
|
30561
|
+
json: "value"
|
|
30562
|
+
})
|
|
30563
|
+
});
|
|
30564
|
+
|
|
30565
|
+
//#endregion
|
|
30566
|
+
//#region src/commands/env/grants/index.ts
|
|
30567
|
+
const grantsCommand = defineCommand({
|
|
30568
|
+
meta: {
|
|
30569
|
+
name: "grants",
|
|
30570
|
+
description: "Manage per-member env-var access grants on a (project × environment) scope"
|
|
30571
|
+
},
|
|
30572
|
+
subCommands: {
|
|
30573
|
+
list: listCommand$4,
|
|
30574
|
+
set: setCommand$2,
|
|
30575
|
+
unset: unsetCommand
|
|
30576
|
+
}
|
|
30577
|
+
});
|
|
30578
|
+
|
|
29470
30579
|
//#endregion
|
|
29471
30580
|
//#region src/commands/env/history.ts
|
|
29472
30581
|
const historyCommand = defineCommand({
|
|
@@ -29565,7 +30674,7 @@ const importCommand = defineCommand({
|
|
|
29565
30674
|
|
|
29566
30675
|
//#endregion
|
|
29567
30676
|
//#region src/commands/env/list.ts
|
|
29568
|
-
const listCommand$
|
|
30677
|
+
const listCommand$3 = defineCommand({
|
|
29569
30678
|
meta: {
|
|
29570
30679
|
name: "list",
|
|
29571
30680
|
description: "List environment variable metadata. Values are end-to-end encrypted — read them with `env pull`, `env export`, or `env get`."
|
|
@@ -29831,7 +30940,7 @@ const setCommand$1 = defineCommand({
|
|
|
29831
30940
|
|
|
29832
30941
|
//#endregion
|
|
29833
30942
|
//#region src/commands/env/update.ts
|
|
29834
|
-
const updateCommand$
|
|
30943
|
+
const updateCommand$2 = defineCommand({
|
|
29835
30944
|
meta: {
|
|
29836
30945
|
name: "update",
|
|
29837
30946
|
description: "Update a project env var's value or visibility for an environment"
|
|
@@ -29912,18 +31021,19 @@ const envCommand = defineCommand({
|
|
|
29912
31021
|
description: "Manage environment variables"
|
|
29913
31022
|
},
|
|
29914
31023
|
subCommands: {
|
|
29915
|
-
list: listCommand$
|
|
31024
|
+
list: listCommand$3,
|
|
29916
31025
|
get: getCommand$1,
|
|
29917
31026
|
set: setCommand$1,
|
|
29918
|
-
update: updateCommand$
|
|
29919
|
-
delete: deleteCommand$
|
|
31027
|
+
update: updateCommand$2,
|
|
31028
|
+
delete: deleteCommand$3,
|
|
29920
31029
|
history: historyCommand,
|
|
29921
31030
|
rollback: rollbackCommand$1,
|
|
29922
31031
|
import: importCommand,
|
|
29923
31032
|
push: pushCommand,
|
|
29924
31033
|
export: exportCommand,
|
|
29925
31034
|
pull: pullCommand,
|
|
29926
|
-
exec: execCommand
|
|
31035
|
+
exec: execCommand,
|
|
31036
|
+
grants: grantsCommand
|
|
29927
31037
|
}
|
|
29928
31038
|
});
|
|
29929
31039
|
|
|
@@ -30194,7 +31304,7 @@ const fingerprintCommand = defineCommand({
|
|
|
30194
31304
|
const checkExistingLink = (api, config, localSlug) => Effect.gen(function* () {
|
|
30195
31305
|
const existingId = config.extra?.betterUpdate?.projectId;
|
|
30196
31306
|
if (typeof existingId !== "string" || existingId.length === 0) return "no-link";
|
|
30197
|
-
const project = yield* api.projects.get({ path: { id: existingId } }).pipe(Effect.
|
|
31307
|
+
const project = yield* api.projects.get({ path: { id: existingId } }).pipe(Effect.orElseSucceed(() => void 0));
|
|
30198
31308
|
if (project === void 0) {
|
|
30199
31309
|
yield* printHuman(`Existing projectId "${existingId}" not found on server. Re-linking by local slug "${localSlug}".`);
|
|
30200
31310
|
return "stale";
|
|
@@ -30214,9 +31324,9 @@ const checkExistingLink = (api, config, localSlug) => Effect.gen(function* () {
|
|
|
30214
31324
|
const slugify = (value) => value.toLowerCase().replaceAll(/[^a-z0-9]+/gu, "-").replaceAll(/^-+|-+$/gu, "");
|
|
30215
31325
|
/** Best-effort `name` from the local package.json, or undefined when absent. */
|
|
30216
31326
|
const readPackageJsonName = (projectRoot) => Effect.gen(function* () {
|
|
30217
|
-
const content = yield* (yield* FileSystem.FileSystem).readFileString(path.join(projectRoot, "package.json")).pipe(Effect.
|
|
31327
|
+
const content = yield* (yield* FileSystem.FileSystem).readFileString(path.join(projectRoot, "package.json")).pipe(Effect.orElseSucceed(() => ""));
|
|
30218
31328
|
if (content.length === 0) return;
|
|
30219
|
-
const parsed = yield* Effect.try(() => JSON.parse(content)).pipe(Effect.
|
|
31329
|
+
const parsed = yield* Effect.try(() => JSON.parse(content)).pipe(Effect.orElseSucceed(() => void 0));
|
|
30220
31330
|
const name = isRecord(parsed) ? parsed["name"] : void 0;
|
|
30221
31331
|
return typeof name === "string" && name.length > 0 ? name : void 0;
|
|
30222
31332
|
});
|
|
@@ -30369,49 +31479,40 @@ const logoutCommand = defineCommand({
|
|
|
30369
31479
|
|
|
30370
31480
|
//#endregion
|
|
30371
31481
|
//#region src/commands/migrate-config.ts
|
|
30372
|
-
const readAppJson = (projectRoot) => {
|
|
30373
|
-
const path$1 = path.join(projectRoot, "app.json");
|
|
30374
|
-
if (!existsSync(path$1)) return null;
|
|
30375
|
-
const raw = readFileSync(path$1, "utf8");
|
|
30376
|
-
return JSON.parse(raw);
|
|
30377
|
-
};
|
|
30378
|
-
const writeAppJson = (projectRoot, content) => {
|
|
30379
|
-
writeFileSync(path.join(projectRoot, "app.json"), `${JSON.stringify(content, null, 2)}\n`);
|
|
30380
|
-
};
|
|
30381
|
-
const writeEasJson = (projectRoot, profiles) => {
|
|
30382
|
-
writeFileSync(path.join(projectRoot, "eas.json"), `${JSON.stringify({ build: profiles }, null, 2)}\n`);
|
|
30383
|
-
};
|
|
30384
31482
|
const migrateConfigCommand = defineCommand({
|
|
30385
31483
|
meta: {
|
|
30386
31484
|
name: "migrate-config",
|
|
30387
|
-
description: "Migrate
|
|
31485
|
+
description: "Migrate `build`/`submit` profiles from a legacy eas.json into better-update.json"
|
|
30388
31486
|
},
|
|
30389
31487
|
args: { yes: {
|
|
30390
31488
|
type: "boolean",
|
|
30391
31489
|
description: "Skip the confirmation prompt"
|
|
30392
31490
|
} },
|
|
30393
31491
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
30394
|
-
const
|
|
30395
|
-
const
|
|
30396
|
-
|
|
30397
|
-
const
|
|
30398
|
-
if (
|
|
30399
|
-
|
|
31492
|
+
const runtime = yield* CliRuntime;
|
|
31493
|
+
const fs = yield* FileSystem.FileSystem;
|
|
31494
|
+
const root = yield* runtime.cwd;
|
|
31495
|
+
const easPath = path.join(root, "eas.json");
|
|
31496
|
+
if (!(yield* fs.exists(easPath).pipe(Effect.orElseSucceed(() => false)))) return yield* new InvalidArgumentError({ message: `No eas.json found at ${root}.` });
|
|
31497
|
+
const config = yield* readEasJson(root);
|
|
31498
|
+
const patch = compact({
|
|
31499
|
+
build: config.build,
|
|
31500
|
+
submit: config.submit,
|
|
31501
|
+
cli: config.cli
|
|
31502
|
+
});
|
|
31503
|
+
if (Object.keys(patch).length === 0) {
|
|
31504
|
+
yield* printHuman("eas.json has no build/submit/cli sections — nothing to migrate.");
|
|
30400
31505
|
return;
|
|
30401
31506
|
}
|
|
30402
|
-
|
|
30403
|
-
if (!args.yes) {
|
|
30404
|
-
if (!(yield* promptConfirm(
|
|
31507
|
+
const existingBuild = (yield* readBetterUpdateConfig(root))?.["build"];
|
|
31508
|
+
if (typeof existingBuild === "object" && existingBuild !== null && config.build !== void 0 && !args.yes) {
|
|
31509
|
+
if (!(yield* promptConfirm("better-update.json already has a build section — overwrite it from eas.json?", { initialValue: false }))) {
|
|
30405
31510
|
yield* printHuman("Cancelled.");
|
|
30406
31511
|
return;
|
|
30407
31512
|
}
|
|
30408
31513
|
}
|
|
30409
|
-
|
|
30410
|
-
|
|
30411
|
-
const betterUpdate = (clone.expo?.extra)?.betterUpdate;
|
|
30412
|
-
if (betterUpdate) delete betterUpdate["profiles"];
|
|
30413
|
-
writeAppJson(root, clone);
|
|
30414
|
-
yield* printHuman("Migrated profiles into eas.json. Legacy field removed from app.json.");
|
|
31514
|
+
yield* writeBetterUpdateConfig(root, patch);
|
|
31515
|
+
yield* printHuman("Merged eas.json build/submit into better-update.json. You can now delete eas.json.");
|
|
30415
31516
|
}))
|
|
30416
31517
|
});
|
|
30417
31518
|
|
|
@@ -30469,7 +31570,7 @@ const openCommand = defineCommand({
|
|
|
30469
31570
|
|
|
30470
31571
|
//#endregion
|
|
30471
31572
|
//#region src/commands/projects.ts
|
|
30472
|
-
const listCommand$
|
|
31573
|
+
const listCommand$2 = defineCommand({
|
|
30473
31574
|
meta: {
|
|
30474
31575
|
name: "list",
|
|
30475
31576
|
description: "List projects (most recently active first)"
|
|
@@ -30520,7 +31621,7 @@ const listCommand$1 = defineCommand({
|
|
|
30520
31621
|
yield* printHuman(`Page ${result.page} · ${result.items.length} of ${result.total} project(s)`);
|
|
30521
31622
|
}))
|
|
30522
31623
|
});
|
|
30523
|
-
const createCommand = defineCommand({
|
|
31624
|
+
const createCommand$1 = defineCommand({
|
|
30524
31625
|
meta: {
|
|
30525
31626
|
name: "create",
|
|
30526
31627
|
description: "Create a new project"
|
|
@@ -30598,7 +31699,7 @@ const renameCommand = defineCommand({
|
|
|
30598
31699
|
return project;
|
|
30599
31700
|
}), { json: "value" })
|
|
30600
31701
|
});
|
|
30601
|
-
const deleteCommand$
|
|
31702
|
+
const deleteCommand$2 = defineCommand({
|
|
30602
31703
|
meta: {
|
|
30603
31704
|
name: "delete",
|
|
30604
31705
|
description: "Delete a project"
|
|
@@ -30623,10 +31724,229 @@ const projectsCommand = defineCommand({
|
|
|
30623
31724
|
description: "Manage projects"
|
|
30624
31725
|
},
|
|
30625
31726
|
subCommands: {
|
|
30626
|
-
list: listCommand$
|
|
30627
|
-
create: createCommand,
|
|
31727
|
+
list: listCommand$2,
|
|
31728
|
+
create: createCommand$1,
|
|
30628
31729
|
get: getCommand,
|
|
30629
31730
|
rename: renameCommand,
|
|
31731
|
+
delete: deleteCommand$2
|
|
31732
|
+
}
|
|
31733
|
+
});
|
|
31734
|
+
|
|
31735
|
+
//#endregion
|
|
31736
|
+
//#region src/commands/roles/helpers.ts
|
|
31737
|
+
var RoleCommandError = class extends Data.TaggedError("RoleCommandError") {};
|
|
31738
|
+
const roleErrorExtras = { RoleCommandError: 2 };
|
|
31739
|
+
/**
|
|
31740
|
+
* Parse comma-separated "resource:action" tokens into the PermissionGrant array
|
|
31741
|
+
* the API expects. Multiple actions for the same resource are grouped.
|
|
31742
|
+
*
|
|
31743
|
+
* Input examples:
|
|
31744
|
+
* "channel:read,channel:update"
|
|
31745
|
+
* "channel:read, rollout:create, rollout:update"
|
|
31746
|
+
*/
|
|
31747
|
+
const parsePermissionTokens = (raw) => Effect.gen(function* () {
|
|
31748
|
+
const tokens = raw.split(",").map((tok) => tok.trim()).filter((tok) => tok.length > 0);
|
|
31749
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
31750
|
+
for (const token of tokens) {
|
|
31751
|
+
const colonIdx = token.indexOf(":");
|
|
31752
|
+
if (colonIdx === -1) return yield* new RoleCommandError({ message: `Invalid permission token "${token}" — expected "resource:action" format.` });
|
|
31753
|
+
const resource = token.slice(0, colonIdx).trim();
|
|
31754
|
+
const action = token.slice(colonIdx + 1).trim();
|
|
31755
|
+
if (!resource || !action) return yield* new RoleCommandError({ message: `Invalid permission token "${token}" — resource and action must be non-empty.` });
|
|
31756
|
+
const actions = grouped.get(resource) ?? /* @__PURE__ */ new Set();
|
|
31757
|
+
actions.add(action);
|
|
31758
|
+
grouped.set(resource, actions);
|
|
31759
|
+
}
|
|
31760
|
+
return [...grouped.entries()].map(([resource, actions]) => ({
|
|
31761
|
+
resource,
|
|
31762
|
+
actions: [...actions]
|
|
31763
|
+
}));
|
|
31764
|
+
});
|
|
31765
|
+
|
|
31766
|
+
//#endregion
|
|
31767
|
+
//#region src/commands/roles/create.ts
|
|
31768
|
+
const createCommand = defineCommand({
|
|
31769
|
+
meta: {
|
|
31770
|
+
name: "create",
|
|
31771
|
+
description: "Create a custom role"
|
|
31772
|
+
},
|
|
31773
|
+
args: {
|
|
31774
|
+
name: {
|
|
31775
|
+
type: "string",
|
|
31776
|
+
required: true,
|
|
31777
|
+
description: "Role name (unique per organization)"
|
|
31778
|
+
},
|
|
31779
|
+
permission: {
|
|
31780
|
+
type: "string",
|
|
31781
|
+
required: true,
|
|
31782
|
+
description: "Permission tokens in resource:action format, comma-separated (e.g. channel:read,channel:update)"
|
|
31783
|
+
}
|
|
31784
|
+
},
|
|
31785
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31786
|
+
const permissions = yield* parsePermissionTokens(args.permission);
|
|
31787
|
+
const role = yield* (yield* apiClient).roles.create({ payload: {
|
|
31788
|
+
name: args.name,
|
|
31789
|
+
permissions
|
|
31790
|
+
} });
|
|
31791
|
+
yield* printHumanKeyValue([
|
|
31792
|
+
["ID", role.id],
|
|
31793
|
+
["Name", role.role],
|
|
31794
|
+
["Permissions", role.permissions.map((perm) => `${perm.resource}:[${perm.actions.join(",")}]`).join("; ")],
|
|
31795
|
+
["Created", role.createdAt]
|
|
31796
|
+
]);
|
|
31797
|
+
return role;
|
|
31798
|
+
}), {
|
|
31799
|
+
exits: roleErrorExtras,
|
|
31800
|
+
json: "value"
|
|
31801
|
+
})
|
|
31802
|
+
});
|
|
31803
|
+
|
|
31804
|
+
//#endregion
|
|
31805
|
+
//#region src/commands/roles/delete.ts
|
|
31806
|
+
const deleteCommand$1 = defineCommand({
|
|
31807
|
+
meta: {
|
|
31808
|
+
name: "delete",
|
|
31809
|
+
description: "Delete a custom role"
|
|
31810
|
+
},
|
|
31811
|
+
args: {
|
|
31812
|
+
id: {
|
|
31813
|
+
type: "positional",
|
|
31814
|
+
required: true,
|
|
31815
|
+
description: "Role ID"
|
|
31816
|
+
},
|
|
31817
|
+
yes: {
|
|
31818
|
+
type: "boolean",
|
|
31819
|
+
description: "Skip confirmation prompt"
|
|
31820
|
+
}
|
|
31821
|
+
},
|
|
31822
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31823
|
+
if (!args.yes) {
|
|
31824
|
+
if (!(yield* promptConfirm(`Delete role ${args.id}?`, { initialValue: false }))) {
|
|
31825
|
+
yield* printHuman("Cancelled.");
|
|
31826
|
+
return { deleted: 0 };
|
|
31827
|
+
}
|
|
31828
|
+
}
|
|
31829
|
+
const result = yield* (yield* apiClient).roles.delete({ path: { id: args.id } });
|
|
31830
|
+
yield* printHuman(`Role ${args.id} deleted.`);
|
|
31831
|
+
return result;
|
|
31832
|
+
}), {
|
|
31833
|
+
exits: roleErrorExtras,
|
|
31834
|
+
json: "value"
|
|
31835
|
+
})
|
|
31836
|
+
});
|
|
31837
|
+
|
|
31838
|
+
//#endregion
|
|
31839
|
+
//#region src/commands/roles/list.ts
|
|
31840
|
+
const listCommand$1 = defineCommand({
|
|
31841
|
+
meta: {
|
|
31842
|
+
name: "list",
|
|
31843
|
+
description: "List custom roles for the active organization"
|
|
31844
|
+
},
|
|
31845
|
+
args: { "organization-id": {
|
|
31846
|
+
type: "string",
|
|
31847
|
+
required: true,
|
|
31848
|
+
description: "Organization ID to list roles for"
|
|
31849
|
+
} },
|
|
31850
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31851
|
+
yield* printList([
|
|
31852
|
+
"ID",
|
|
31853
|
+
"Name",
|
|
31854
|
+
"Permissions",
|
|
31855
|
+
"Created"
|
|
31856
|
+
], (yield* (yield* apiClient).roles.list({ urlParams: { organizationId: args["organization-id"] } })).map((role) => [
|
|
31857
|
+
role.id,
|
|
31858
|
+
role.role,
|
|
31859
|
+
role.permissions.map((perm) => `${perm.resource}:[${perm.actions.join(",")}]`).join("; "),
|
|
31860
|
+
role.createdAt
|
|
31861
|
+
]), "No custom roles found.");
|
|
31862
|
+
}), roleErrorExtras)
|
|
31863
|
+
});
|
|
31864
|
+
|
|
31865
|
+
//#endregion
|
|
31866
|
+
//#region src/commands/roles/update.ts
|
|
31867
|
+
const updateCommand$1 = defineCommand({
|
|
31868
|
+
meta: {
|
|
31869
|
+
name: "update",
|
|
31870
|
+
description: "Update a custom role's name or permissions"
|
|
31871
|
+
},
|
|
31872
|
+
args: {
|
|
31873
|
+
id: {
|
|
31874
|
+
type: "positional",
|
|
31875
|
+
required: true,
|
|
31876
|
+
description: "Role ID"
|
|
31877
|
+
},
|
|
31878
|
+
name: {
|
|
31879
|
+
type: "string",
|
|
31880
|
+
description: "New role name"
|
|
31881
|
+
},
|
|
31882
|
+
permission: {
|
|
31883
|
+
type: "string",
|
|
31884
|
+
description: "Replacement permission tokens in resource:action format, comma-separated (e.g. channel:read,channel:update)"
|
|
31885
|
+
}
|
|
31886
|
+
},
|
|
31887
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31888
|
+
const permissions = args.permission === void 0 ? void 0 : yield* parsePermissionTokens(args.permission);
|
|
31889
|
+
const role = yield* (yield* apiClient).roles.update({
|
|
31890
|
+
path: { id: args.id },
|
|
31891
|
+
payload: compact({
|
|
31892
|
+
name: args.name,
|
|
31893
|
+
permissions
|
|
31894
|
+
})
|
|
31895
|
+
});
|
|
31896
|
+
yield* printHumanKeyValue([
|
|
31897
|
+
["ID", role.id],
|
|
31898
|
+
["Name", role.role],
|
|
31899
|
+
["Permissions", role.permissions.map((perm) => `${perm.resource}:[${perm.actions.join(",")}]`).join("; ")],
|
|
31900
|
+
["Updated", role.updatedAt ?? "-"]
|
|
31901
|
+
]);
|
|
31902
|
+
return role;
|
|
31903
|
+
}), {
|
|
31904
|
+
exits: roleErrorExtras,
|
|
31905
|
+
json: "value"
|
|
31906
|
+
})
|
|
31907
|
+
});
|
|
31908
|
+
|
|
31909
|
+
//#endregion
|
|
31910
|
+
//#region src/commands/roles/view.ts
|
|
31911
|
+
const viewCommand$1 = defineCommand({
|
|
31912
|
+
meta: {
|
|
31913
|
+
name: "view",
|
|
31914
|
+
description: "Show a custom role by ID"
|
|
31915
|
+
},
|
|
31916
|
+
args: { id: {
|
|
31917
|
+
type: "positional",
|
|
31918
|
+
required: true,
|
|
31919
|
+
description: "Role ID"
|
|
31920
|
+
} },
|
|
31921
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31922
|
+
const role = yield* (yield* apiClient).roles.get({ path: { id: args.id } });
|
|
31923
|
+
yield* printHumanKeyValue([
|
|
31924
|
+
["ID", role.id],
|
|
31925
|
+
["Name", role.role],
|
|
31926
|
+
["Organization ID", role.organizationId],
|
|
31927
|
+
["Permissions", role.permissions.map((perm) => `${perm.resource}:[${perm.actions.join(",")}]`).join("; ")],
|
|
31928
|
+
["Created", role.createdAt],
|
|
31929
|
+
["Updated", role.updatedAt ?? "-"]
|
|
31930
|
+
]);
|
|
31931
|
+
return role;
|
|
31932
|
+
}), {
|
|
31933
|
+
exits: roleErrorExtras,
|
|
31934
|
+
json: "value"
|
|
31935
|
+
})
|
|
31936
|
+
});
|
|
31937
|
+
|
|
31938
|
+
//#endregion
|
|
31939
|
+
//#region src/commands/roles/index.ts
|
|
31940
|
+
const rolesCommand = defineCommand({
|
|
31941
|
+
meta: {
|
|
31942
|
+
name: "roles",
|
|
31943
|
+
description: "Manage custom organization roles"
|
|
31944
|
+
},
|
|
31945
|
+
subCommands: {
|
|
31946
|
+
list: listCommand$1,
|
|
31947
|
+
view: viewCommand$1,
|
|
31948
|
+
create: createCommand,
|
|
31949
|
+
update: updateCommand$1,
|
|
30630
31950
|
delete: deleteCommand$1
|
|
30631
31951
|
}
|
|
30632
31952
|
});
|
|
@@ -30856,7 +32176,7 @@ const submitCommand = defineCommand({
|
|
|
30856
32176
|
}
|
|
30857
32177
|
const projectId = yield* readProjectId;
|
|
30858
32178
|
const api = yield* apiClient;
|
|
30859
|
-
const easProfile = yield*
|
|
32179
|
+
const easProfile = yield* readSubmitProfile(yield* (yield* CliRuntime).cwd, args.profile);
|
|
30860
32180
|
const archive = yield* resolveArchive(api, projectId, platform, {
|
|
30861
32181
|
id: args.id,
|
|
30862
32182
|
path: args.path,
|
|
@@ -32275,7 +33595,7 @@ const runPatchPhase = (input) => Effect.gen(function* () {
|
|
|
32275
33595
|
runtimeVersion: input.runtimeVersion,
|
|
32276
33596
|
platform: input.platform,
|
|
32277
33597
|
limit: Math.max(1, input.baseWindow)
|
|
32278
|
-
} }).pipe(Effect.
|
|
33598
|
+
} }).pipe(Effect.orElseSucceed(() => []));
|
|
32279
33599
|
const bases = selectBaseWindow(candidates, {
|
|
32280
33600
|
newUpdateId: input.newUpdateId,
|
|
32281
33601
|
maxRecent: input.baseWindow
|
|
@@ -32291,7 +33611,7 @@ const runPatchPhase = (input) => Effect.gen(function* () {
|
|
|
32291
33611
|
bestSavingsPct: void 0
|
|
32292
33612
|
};
|
|
32293
33613
|
}
|
|
32294
|
-
const newBundleBytes = yield* sha256File(input.newLaunchPath).pipe(Effect.map((result) => result.byteSize), Effect.
|
|
33614
|
+
const newBundleBytes = yield* sha256File(input.newLaunchPath).pipe(Effect.map((result) => result.byteSize), Effect.orElseSucceed(() => void 0));
|
|
32295
33615
|
yield* printHuman(`Diffing against ${bases.length} base(s) (window=${input.baseWindow}; ${candidates.length} candidate(s) available).`);
|
|
32296
33616
|
const uploadedOutcomes = (yield* Effect.forEach(bases, (base, index) => Effect.gen(function* () {
|
|
32297
33617
|
const basePath = path.join(input.workDir, `base-${index}.bundle`);
|
|
@@ -32386,7 +33706,7 @@ const dedupeAssetsByHash = (assets) => uniqBy(assets, (asset) => asset.hash);
|
|
|
32386
33706
|
* is informational, so a fingerprint failure resolves to `undefined` rather than
|
|
32387
33707
|
* failing the publish.
|
|
32388
33708
|
*/
|
|
32389
|
-
const resolvePlatformFingerprintHash = (projectRoot, platform) => runFingerprintForPlatform(projectRoot, platform).pipe(Effect.map((result) => result.hash), Effect.
|
|
33709
|
+
const resolvePlatformFingerprintHash = (projectRoot, platform) => runFingerprintForPlatform(projectRoot, platform).pipe(Effect.map((result) => result.hash), Effect.orElseSucceed(() => void 0));
|
|
32390
33710
|
const preparePlatformAssets = ({ exportDir, platform }) => Effect.gen(function* () {
|
|
32391
33711
|
const exportedAssets = yield* readExpoExportAssets({
|
|
32392
33712
|
exportDir,
|
|
@@ -32482,7 +33802,7 @@ const publishPlatform = (params) => Effect.gen(function* () {
|
|
|
32482
33802
|
const uploadDetailsByHash = new Map(assetRegistration.uploaded.map((asset) => [asset.hash, asset]));
|
|
32483
33803
|
yield* Effect.forEach(uniqueAssets.filter((asset) => uploadDetailsByHash.has(asset.hash)), (asset) => Effect.gen(function* () {
|
|
32484
33804
|
const detail = uploadDetailsByHash.get(asset.hash);
|
|
32485
|
-
if (!detail) return yield*
|
|
33805
|
+
if (!detail) return yield* new UpdatePublishError({ message: `Missing upload details for asset ${asset.hash}` });
|
|
32486
33806
|
return yield* assetUploader.uploadAssetBinary({
|
|
32487
33807
|
path: asset.path,
|
|
32488
33808
|
hash: asset.hash,
|
|
@@ -33867,6 +35187,7 @@ const commandRegistry = {
|
|
|
33867
35187
|
projects: projectsCommand,
|
|
33868
35188
|
branches: branchesCommand,
|
|
33869
35189
|
channels: channelsCommand,
|
|
35190
|
+
roles: rolesCommand,
|
|
33870
35191
|
build: buildCommand,
|
|
33871
35192
|
builds: buildsCommand,
|
|
33872
35193
|
credentials: credentialsCommand,
|