@better-update/cli 0.26.1 → 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 CHANGED
@@ -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.26.1";
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,
@@ -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.catchAll(() => Effect.succeed("")), Effect.flatMap((content) => content.length === 0 ? Effect.succeed(void 0) : Effect.try({
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.catchAll(() => Effect.succeed(void 0)))));
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.catchAll(() => Effect.succeed(null)));
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.catchAll(() => Effect.succeed(null)));
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.catchAll(() => Effect.succeed(null)));
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.catchAll(() => Effect.succeed(null)));
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.catchAll(() => Effect.succeed("")));
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.catchAll(() => Effect.succeed(null)));
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.catchAll(() => Effect.succeed("")));
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.catchAll(() => Effect.succeed(void 0)));
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.catchAll(() => Effect.succeed(false))))) yield* Console.log(`Open this URL manually:\n${url}`);
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.catchAll(() => Effect.succeed("")));
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.catchAll(() => Effect.succeed(void 0)));
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.try({
4358
- try: () => Schema.decodeUnknownSync(RolloutPercentage)(Number(raw)),
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$9 = defineCommand({
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$9 }
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$8 = defineCommand({
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$4 = defineCommand({
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$3 = defineCommand({
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$6 = defineCommand({
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$8,
4670
- view: viewCommand$3,
4671
- create: createCommand$4,
4815
+ list: listCommand$11,
4816
+ view: viewCommand$4,
4817
+ create: createCommand$5,
4672
4818
  rename: renameCommand$1,
4673
- delete: deleteCommand$6
4819
+ delete: deleteCommand$7
4674
4820
  }
4675
4821
  });
4676
4822
 
@@ -18709,7 +18855,7 @@ const asArrayBuffer$1 = (bytes) => {
18709
18855
  };
18710
18856
  const signAscJwt = (credentials) => Effect.gen(function* () {
18711
18857
  const der = pemToPkcs8Der(credentials.p8Pem);
18712
- if (der === null) return yield* Effect.fail(new AppleAuthError({ cause: /* @__PURE__ */ new Error("Invalid .p8 PEM") }));
18858
+ if (der === null) return yield* new AppleAuthError({ cause: /* @__PURE__ */ new Error("Invalid .p8 PEM") });
18713
18859
  const header = {
18714
18860
  alg: "ES256",
18715
18861
  kid: credentials.keyId,
@@ -18793,7 +18939,7 @@ const fetchRaw = (jwt, path, init) => Effect.gen(function* () {
18793
18939
  try: () => text.length === 0 ? {} : JSON.parse(text),
18794
18940
  catch: (cause) => new AscNetworkError({ cause })
18795
18941
  });
18796
- if (!response.ok) return yield* Effect.fail(parseApiError(response, body, text));
18942
+ if (!response.ok) return yield* parseApiError(response, body, text);
18797
18943
  return body;
18798
18944
  });
18799
18945
  const toAscCertificate = (value) => {
@@ -18893,12 +19039,10 @@ const createCertificate = (credentials, params) => withJwt(credentials, (jwt) =>
18893
19039
  }
18894
19040
  } })
18895
19041
  }), toAscCertificate);
18896
- if (resource === null) return yield* Effect.fail(malformed("certificate"));
19042
+ if (resource === null) return yield* malformed("certificate");
18897
19043
  return resource;
18898
19044
  }));
18899
- const deleteCertificate = (credentials, id) => withJwt(credentials, (jwt) => Effect.gen(function* () {
18900
- yield* fetchRaw(jwt, `/v1/certificates/${encodeURIComponent(id)}`, { method: "DELETE" });
18901
- }));
19045
+ const deleteCertificate = (credentials, id) => withJwt(credentials, (jwt) => Effect.asVoid(fetchRaw(jwt, `/v1/certificates/${encodeURIComponent(id)}`, { method: "DELETE" })));
18902
19046
  const listBundleIds = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
18903
19047
  return extractList(yield* fetchRaw(jwt, "/v1/bundleIds?limit=200"), toAscBundleId);
18904
19048
  }));
@@ -18914,7 +19058,7 @@ const createBundleId = (credentials, params) => withJwt(credentials, (jwt) => Ef
18914
19058
  }
18915
19059
  } })
18916
19060
  }), toAscBundleId);
18917
- if (resource === null) return yield* Effect.fail(malformed("bundleId"));
19061
+ if (resource === null) return yield* malformed("bundleId");
18918
19062
  return resource;
18919
19063
  }));
18920
19064
  const listDevices = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
@@ -18946,7 +19090,7 @@ const createProvisioningProfile = (credentials, params) => withJwt(credentials,
18946
19090
  relationships
18947
19091
  } })
18948
19092
  }), toAscProfile);
18949
- if (resource === null) return yield* Effect.fail(malformed("profile"));
19093
+ if (resource === null) return yield* malformed("profile");
18950
19094
  return resource;
18951
19095
  }));
18952
19096
  const isCertificateLimitError = (error) => {
@@ -18982,7 +19126,7 @@ const parseCert = (certDerBytes) => {
18982
19126
  const generatePassword = () => forge.util.encode64(forge.random.getBytesSync(16));
18983
19127
  const extractCertMetadata = (cert) => Effect.gen(function* () {
18984
19128
  const appleTeamId = extractTeamId$1(cert);
18985
- if (appleTeamId === null) return yield* Effect.fail(new CertParseError({ message: "Could not extract Apple team identifier from certificate subject" }));
19129
+ if (appleTeamId === null) return yield* new CertParseError({ message: "Could not extract Apple team identifier from certificate subject" });
18986
19130
  return {
18987
19131
  serialNumber: cert.serialNumber.toUpperCase(),
18988
19132
  validFrom: cert.validity.notBefore.toISOString(),
@@ -19000,7 +19144,7 @@ const extractCertMetadata = (cert) => Effect.gen(function* () {
19000
19144
  */
19001
19145
  const extractMetadataFromP12 = (params) => Effect.gen(function* () {
19002
19146
  const certBagOid = forge.pki.oids["certBag"];
19003
- if (certBagOid === void 0) return yield* Effect.fail(new CertParseError({ message: "PKCS#12 OID lookup for certBag failed" }));
19147
+ if (certBagOid === void 0) return yield* new CertParseError({ message: "PKCS#12 OID lookup for certBag failed" });
19004
19148
  const [first] = yield* Effect.try({
19005
19149
  try: () => {
19006
19150
  const p12Der = forge.util.decode64(params.p12Base64);
@@ -19009,7 +19153,7 @@ const extractMetadataFromP12 = (params) => Effect.gen(function* () {
19009
19153
  },
19010
19154
  catch: (error) => new CertParseError({ message: `Failed to parse PKCS#12 bundle: ${error instanceof Error ? error.message : String(error)}` })
19011
19155
  });
19012
- if (first?.cert === void 0) return yield* Effect.fail(new CertParseError({ message: "PKCS#12 bundle does not contain a certificate" }));
19156
+ if (first?.cert === void 0) return yield* new CertParseError({ message: "PKCS#12 bundle does not contain a certificate" });
19013
19157
  return yield* extractCertMetadata(first.cert);
19014
19158
  });
19015
19159
  const buildDistributionCertP12 = (params) => Effect.gen(function* () {
@@ -19190,10 +19334,10 @@ const generateAndUploadDistributionCertificate = (api, input) => Effect.gen(func
19190
19334
  csrPem: csrResult.csrPem,
19191
19335
  certificateType
19192
19336
  }).pipe(Effect.mapError(wrapAscError("apple-create-certificate")));
19193
- if (apple.certificateContent === null) return yield* Effect.fail(new GenerateFailedError({
19337
+ if (apple.certificateContent === null) return yield* new GenerateFailedError({
19194
19338
  step: "apple-create-certificate",
19195
19339
  message: "Apple response missing certificateContent"
19196
- }));
19340
+ });
19197
19341
  const bundle = yield* buildDistributionCertP12({
19198
19342
  certificateContentBase64: apple.certificateContent,
19199
19343
  privateKey: csrResult.privateKey
@@ -19243,10 +19387,10 @@ const revokeAppleCertificate = (api, input) => Effect.gen(function* () {
19243
19387
  });
19244
19388
  const revokeLocalDistributionCertificate = (api, input) => Effect.gen(function* () {
19245
19389
  const local = (yield* api.appleDistributionCertificates.list()).items.find((entry) => entry.id === input.distributionCertificateId);
19246
- if (local === void 0) return yield* Effect.fail(new GenerateFailedError({
19390
+ if (local === void 0) return yield* new GenerateFailedError({
19247
19391
  step: "load-distribution-certificate",
19248
19392
  message: `Distribution certificate ${input.distributionCertificateId} not found on this account`
19249
- }));
19393
+ });
19250
19394
  const creds = yield* fetchAscCredentials(api, input.ascApiKeyId);
19251
19395
  const ascCreds = {
19252
19396
  keyId: creds.keyId,
@@ -19283,10 +19427,10 @@ const listAppleCertificates = (api, input) => Effect.gen(function* () {
19283
19427
  });
19284
19428
  const resolveCertAscId = (creds, serialNumber, certificateType) => Effect.gen(function* () {
19285
19429
  const match = (yield* listCertificates(creds, { certificateType }).pipe(Effect.mapError(wrapAscError("apple-list-certificates")))).find((entry) => entry.serialNumber.toUpperCase() === serialNumber);
19286
- if (match === void 0) return yield* Effect.fail(new GenerateFailedError({
19430
+ if (match === void 0) return yield* new GenerateFailedError({
19287
19431
  step: "match-apple-certificate",
19288
19432
  message: `Distribution certificate ${serialNumber} not present on Apple Developer Portal; upload or re-generate it`
19289
- }));
19433
+ });
19290
19434
  return match.id;
19291
19435
  });
19292
19436
  const ensureBundleId = (creds, bundleIdentifier) => Effect.gen(function* () {
@@ -19319,10 +19463,10 @@ const generateAndUploadProvisioningProfile = (api, input) => Effect.gen(function
19319
19463
  const [certAscId, bundleIdAscId] = yield* Effect.all([resolveCertAscId(ascCreds, cert.serialNumber.toUpperCase(), certificateType), ensureBundleId(ascCreds, input.bundleIdentifier)], { concurrency: 2 });
19320
19464
  const useDevices = input.distributionType === "AD_HOC" || input.distributionType === "DEVELOPMENT";
19321
19465
  const { ids: deviceAscIds } = useDevices ? yield* collectDeviceAscIds(ascCreds, cert.appleTeamId, input.deviceIds) : { ids: [] };
19322
- if (useDevices && deviceAscIds.length === 0) return yield* Effect.fail(new GenerateFailedError({
19466
+ if (useDevices && deviceAscIds.length === 0) return yield* new GenerateFailedError({
19323
19467
  step: "collect-devices",
19324
19468
  message: "No registered devices to attach to the provisioning profile"
19325
- }));
19469
+ });
19326
19470
  const profileBytes = fromBase64((yield* createProvisioningProfile(ascCreds, {
19327
19471
  profileName: `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`,
19328
19472
  profileType: DISTRIBUTION_TO_PROFILE_TYPE$1[input.distributionType],
@@ -19633,10 +19777,10 @@ const downloadAndroidCredentials = (api, options) => Effect.gen(function* () {
19633
19777
  ...compact({ buildProfile: options.buildProfile })
19634
19778
  }
19635
19779
  }).pipe(Effect.mapError((cause) => resolveErrorToMissingCredentials(cause, "android")));
19636
- if (resolved.platform !== "android") return yield* Effect.fail(new MissingCredentialsError({
19780
+ if (resolved.platform !== "android") return yield* new MissingCredentialsError({
19637
19781
  message: "Server returned non-Android credentials for an Android build request",
19638
19782
  hint: androidBindHint
19639
- }));
19783
+ });
19640
19784
  const secret = yield* decryptResolveSecret({
19641
19785
  session: yield* openVaultSessionForBuild(api, androidBindHint),
19642
19786
  credentialType: "keystore",
@@ -19757,7 +19901,7 @@ const credentialsJsonPath = (projectRoot) => path.join(projectRoot, CREDENTIALS_
19757
19901
  const readCredentialsJson = (projectRoot) => Effect.gen(function* () {
19758
19902
  const fs = yield* FileSystem.FileSystem;
19759
19903
  const filePath = credentialsJsonPath(projectRoot);
19760
- if (!(yield* fs.exists(filePath).pipe(Effect.catchAll(() => Effect.succeed(false))))) return yield* new CredentialsJsonError({ message: `credentials.json not found at ${filePath}.` });
19904
+ if (!(yield* fs.exists(filePath).pipe(Effect.orElseSucceed(() => false)))) return yield* new CredentialsJsonError({ message: `credentials.json not found at ${filePath}.` });
19761
19905
  return yield* parseCredentialsJson(yield* fs.readFileString(filePath).pipe(Effect.mapError((cause) => new CredentialsJsonError({ message: `Failed to read credentials.json: ${String(cause)}` }))));
19762
19906
  });
19763
19907
  const writeCredentialsJson = (projectRoot, data) => Effect.gen(function* () {
@@ -19774,7 +19918,7 @@ const resolveCredentialPath = (projectRoot, candidate) => path.isAbsolute(candid
19774
19918
 
19775
19919
  //#endregion
19776
19920
  //#region src/lib/local-credentials.ts
19777
- const requirePath = (fs, absolutePath, label) => fs.exists(absolutePath).pipe(Effect.catchAll(() => Effect.succeed(false)), Effect.flatMap((exists) => exists ? Effect.void : Effect.fail(new MissingCredentialsError({
19921
+ const requirePath = (fs, absolutePath, label) => fs.exists(absolutePath).pipe(Effect.orElseSucceed(() => false), Effect.flatMap((exists) => exists ? Effect.void : Effect.fail(new MissingCredentialsError({
19778
19922
  message: `Local credentials.json: ${label} not found at ${absolutePath}.`,
19779
19923
  hint: "Run `better-update credentials sync pull` to materialize files, or fix the path in credentials.json."
19780
19924
  }))));
@@ -20029,7 +20173,7 @@ const runStepFormatted = (cmd, step, formatter) => Effect.gen(function* () {
20029
20173
  if (code !== 0) {
20030
20174
  const summary = formatter.getBuildSummary();
20031
20175
  if (summary.length > 0) process$1.stderr.write(`${summary}\n`);
20032
- return yield* Effect.fail(buildFailed(step, code, `${step} exited with code ${code}`));
20176
+ return yield* buildFailed(step, code, `${step} exited with code ${code}`);
20033
20177
  }
20034
20178
  });
20035
20179
 
@@ -20265,10 +20409,10 @@ const findAscCertificateId = (ctx, serialNumber, certificateType) => Effect.gen(
20265
20409
  const certs = yield* wrap("apple-list-certificates", async () => AppleUtils.Certificate.getAsync(ctx, { query: { filter: { certificateType } } }));
20266
20410
  const upper = serialNumber.toUpperCase();
20267
20411
  const match = certs.find((entry) => entry.attributes.serialNumber.toUpperCase() === upper);
20268
- if (match === void 0) return yield* Effect.fail(new AppleIdGenerateFailedError({
20412
+ if (match === void 0) return yield* new AppleIdGenerateFailedError({
20269
20413
  step: "match-apple-certificate",
20270
20414
  message: `Distribution certificate ${serialNumber} not present on Apple Developer Portal; upload or re-generate it`
20271
- }));
20415
+ });
20272
20416
  return match.id;
20273
20417
  });
20274
20418
  const collectIosDeviceIds = (ctx, deviceIds) => Effect.gen(function* () {
@@ -20287,10 +20431,10 @@ const generateAndUploadProvisioningProfileViaAppleId = (api, input) => Effect.ge
20287
20431
  const [certAscId, bundleIdAscId] = yield* Effect.all([findAscCertificateId(ctx, cert.serialNumber, certificateType), findOrCreateBundleId(ctx, input.bundleIdentifier)], { concurrency: 2 });
20288
20432
  const useDevices = input.distributionType === "AD_HOC" || input.distributionType === "DEVELOPMENT";
20289
20433
  const deviceIds = useDevices ? yield* collectIosDeviceIds(ctx, input.deviceIds) : [];
20290
- if (useDevices && deviceIds.length === 0) return yield* Effect.fail(new AppleIdGenerateFailedError({
20434
+ if (useDevices && deviceIds.length === 0) return yield* new AppleIdGenerateFailedError({
20291
20435
  step: "collect-devices",
20292
20436
  message: "No registered devices to attach to the provisioning profile"
20293
- }));
20437
+ });
20294
20438
  const profileName = `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`;
20295
20439
  const { profileContent } = (yield* wrap("apple-create-profile", async () => AppleUtils.Profile.createAsync(ctx, {
20296
20440
  bundleId: bundleIdAscId,
@@ -20299,10 +20443,10 @@ const generateAndUploadProvisioningProfileViaAppleId = (api, input) => Effect.ge
20299
20443
  name: profileName,
20300
20444
  profileType: DISTRIBUTION_TO_PROFILE_TYPE[input.distributionType]
20301
20445
  }))).attributes;
20302
- if (profileContent === null) return yield* Effect.fail(new AppleIdGenerateFailedError({
20446
+ if (profileContent === null) return yield* new AppleIdGenerateFailedError({
20303
20447
  step: "extract-profile-content",
20304
20448
  message: "Apple returned a profile with no content (likely expired/invalid)"
20305
- }));
20449
+ });
20306
20450
  const profileBytes = fromBase64(profileContent);
20307
20451
  const rosterHash = useDevices ? computeDeviceRosterHashHex(deviceIds) : void 0;
20308
20452
  const created = yield* api.appleProvisioningProfiles.upload({ payload: {
@@ -20482,10 +20626,10 @@ const interactiveCertLimitRecover = (api, ascApiKeyId) => Effect.gen(function* (
20482
20626
  ascApiKeyId,
20483
20627
  certificateType: "IOS_DISTRIBUTION"
20484
20628
  });
20485
- if (certs.length === 0) return yield* Effect.fail(new MissingCredentialsError({
20629
+ if (certs.length === 0) return yield* new MissingCredentialsError({
20486
20630
  message: "Apple says the certificate limit is hit but no existing certificates were returned.",
20487
20631
  hint: "Try again later or check the Apple Developer portal."
20488
- }));
20632
+ });
20489
20633
  const toRevoke = yield* promptMultiSelect("Select one or more certificates to revoke before retrying", certs.map((entry) => ({
20490
20634
  value: entry.id,
20491
20635
  label: `${entry.serialNumber.slice(0, 12)}… (${entry.displayName ?? entry.certificateType}, exp ${entry.expirationDate.slice(0, 10)})`
@@ -20498,10 +20642,10 @@ const interactiveCertLimitRecover = (api, ascApiKeyId) => Effect.gen(function* (
20498
20642
  });
20499
20643
  const generateDistributionCertInteractive = (api) => Effect.gen(function* () {
20500
20644
  const teamAscKeys = (yield* api.ascApiKeys.list()).items.filter((key) => key.appleTeamId !== null);
20501
- if (teamAscKeys.length === 0) return yield* Effect.fail(new MissingCredentialsError({
20645
+ if (teamAscKeys.length === 0) return yield* new MissingCredentialsError({
20502
20646
  message: "No ASC API key linked to an Apple team in this organization.",
20503
20647
  hint: "Upload an ASC API key with a team assignment via the dashboard, then retry."
20504
- }));
20648
+ });
20505
20649
  const ascKeyId = yield* promptSelect("Select an ASC API key to issue the certificate against", teamAscKeys.map((key) => ({
20506
20650
  value: key.id,
20507
20651
  label: `${key.name} (${key.keyId})`
@@ -20520,10 +20664,10 @@ const chooseIosCertificateId = (api) => Effect.gen(function* () {
20520
20664
  }, {
20521
20665
  value: "abort",
20522
20666
  label: "Abort — I'll upload one manually"
20523
- }])) === "abort") return yield* Effect.fail(new MissingCredentialsError({
20667
+ }])) === "abort") return yield* new MissingCredentialsError({
20524
20668
  message: "Build aborted — no distribution certificate available.",
20525
20669
  hint: "Run `better-update credentials generate distribution-certificate --asc-key-id <id>` or upload via the dashboard."
20526
- }));
20670
+ });
20527
20671
  return (yield* generateDistributionCertInteractive(api)).id;
20528
20672
  }
20529
20673
  const choice = yield* promptSelect("Select a distribution certificate (or 'generate' for a fresh one)", [{
@@ -20539,10 +20683,10 @@ const chooseIosCertificateId = (api) => Effect.gen(function* () {
20539
20683
  const pickIosCertificate = (api) => Effect.gen(function* () {
20540
20684
  const chosenId = yield* chooseIosCertificateId(api);
20541
20685
  const cert = (yield* api.appleDistributionCertificates.list()).items.find((entry) => entry.id === chosenId);
20542
- if (cert === void 0) return yield* Effect.fail(new MissingCredentialsError({
20686
+ if (cert === void 0) return yield* new MissingCredentialsError({
20543
20687
  message: "Selected certificate not found after generation.",
20544
20688
  hint: "Retry."
20545
- }));
20689
+ });
20546
20690
  return {
20547
20691
  certId: chosenId,
20548
20692
  cert
@@ -20550,10 +20694,10 @@ const pickIosCertificate = (api) => Effect.gen(function* () {
20550
20694
  });
20551
20695
  const pickIosAscKey = (api, appleTeamId) => Effect.gen(function* () {
20552
20696
  const teamAscKeys = (yield* api.ascApiKeys.list()).items.filter((key) => key.appleTeamId !== null && key.appleTeamId === appleTeamId);
20553
- if (teamAscKeys.length === 0) return yield* Effect.fail(new MissingCredentialsError({
20697
+ if (teamAscKeys.length === 0) return yield* new MissingCredentialsError({
20554
20698
  message: `No ASC API key linked to Apple team ${appleTeamId}.`,
20555
20699
  hint: "Upload an ASC API key for that team via the dashboard, then retry."
20556
- }));
20700
+ });
20557
20701
  return yield* promptSelect("Select an ASC API key", teamAscKeys.map((key) => ({
20558
20702
  value: key.id,
20559
20703
  label: `${key.name} (${key.keyId})`
@@ -20634,10 +20778,10 @@ const generateKeystoreInteractive = (api) => Effect.gen(function* () {
20634
20778
  });
20635
20779
  const pickExistingKeystore = (api) => Effect.gen(function* () {
20636
20780
  const keystores = yield* api.androidUploadKeystores.list();
20637
- if (keystores.items.length === 0) return yield* Effect.fail(new MissingCredentialsError({
20781
+ if (keystores.items.length === 0) return yield* new MissingCredentialsError({
20638
20782
  message: "No existing keystores in this organization.",
20639
20783
  hint: "Re-run and choose 'Generate new keystore'."
20640
- }));
20784
+ });
20641
20785
  return yield* promptSelect("Select a keystore", keystores.items.map((item) => ({
20642
20786
  value: item.id,
20643
20787
  label: item.keyAlias
@@ -20670,10 +20814,10 @@ const setupAndroidInteractive = (api, input) => Effect.gen(function* () {
20670
20814
  label: "Abort — I'll configure it in the dashboard"
20671
20815
  }
20672
20816
  ]);
20673
- if (choice === "abort") return yield* Effect.fail(new MissingCredentialsError({
20817
+ if (choice === "abort") return yield* new MissingCredentialsError({
20674
20818
  message: `Build aborted — no keystore bound to ${input.applicationIdentifier}.`,
20675
20819
  hint: "Run `better-update credentials generate keystore` or upload via the dashboard."
20676
- }));
20820
+ });
20677
20821
  const keystoreId = yield* choice === "generate" ? generateKeystoreAuto(api, input.applicationIdentifier) : pickExistingKeystore(api);
20678
20822
  yield* api.androidBuildCredentials.create({
20679
20823
  path: { applicationIdentifierId: appId },
@@ -20694,10 +20838,10 @@ const ensureAndroidCredentialsAvailable = (api, input) => api.buildCredentials.r
20694
20838
  }).pipe(Effect.asVoid);
20695
20839
  const ensureAndroidCredentials = (api, input, options) => ensureAndroidCredentialsAvailable(api, input).pipe(Effect.catchIf(isMissingResolveError, () => Effect.gen(function* () {
20696
20840
  const mode = yield* InteractiveMode;
20697
- if (options.freezeCredentials || !mode.allow) return yield* Effect.fail(new MissingCredentialsError({
20841
+ if (options.freezeCredentials || !mode.allow) return yield* new MissingCredentialsError({
20698
20842
  message: `No Android build credentials for ${input.applicationIdentifier}.`,
20699
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."
20700
- }));
20844
+ });
20701
20845
  yield* setupAndroidInteractive(api, input);
20702
20846
  return yield* ensureAndroidCredentialsAvailable(api, input);
20703
20847
  })));
@@ -20718,10 +20862,10 @@ const resolveIosBuildCredentials = (api, input) => api.buildCredentials.resolve(
20718
20862
  const findBoundIosConfig = (api, input) => Effect.gen(function* () {
20719
20863
  const distributionType = IOS_DISTRIBUTION_TO_TYPE[input.distribution];
20720
20864
  const match = (yield* api.iosBundleConfigurations.list({ path: { projectId: input.projectId } })).items.find((config) => config.bundleIdentifier === input.bundleIdentifier && config.distributionType === distributionType);
20721
- if (match === void 0) return yield* Effect.fail(new MissingCredentialsError({
20865
+ if (match === void 0) return yield* new MissingCredentialsError({
20722
20866
  message: `iOS bundle configuration vanished while regenerating stale profile for ${input.bundleIdentifier}`,
20723
20867
  hint: "Retry; the configuration must exist before regeneration"
20724
- }));
20868
+ });
20725
20869
  return match;
20726
20870
  });
20727
20871
  const regenerateProvisioningProfile = (api, input) => Effect.gen(function* () {
@@ -20752,19 +20896,19 @@ const regenerateProvisioningProfile = (api, input) => Effect.gen(function* () {
20752
20896
  });
20753
20897
  const ensureIosCredentials = (api, input, options) => resolveIosBuildCredentials(api, input).pipe(Effect.catchIf(isMissingResolveError, () => Effect.gen(function* () {
20754
20898
  const mode = yield* InteractiveMode;
20755
- if (options.freezeCredentials || !mode.allow) return yield* Effect.fail(new MissingCredentialsError({
20899
+ if (options.freezeCredentials || !mode.allow) return yield* new MissingCredentialsError({
20756
20900
  message: `No iOS build credentials for ${input.bundleIdentifier} (${input.distribution}).`,
20757
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."
20758
- }));
20902
+ });
20759
20903
  yield* setupIosInteractive(api, input);
20760
20904
  return yield* resolveIosBuildCredentials(api, input);
20761
20905
  })), Effect.flatMap((resolved) => Effect.gen(function* () {
20762
20906
  if (resolved.platform !== "ios" || !resolved.profileStale) return;
20763
20907
  const mode = yield* InteractiveMode;
20764
- if (options.freezeCredentials || !mode.allow) return yield* Effect.fail(new MissingCredentialsError({
20908
+ if (options.freezeCredentials || !mode.allow) return yield* new MissingCredentialsError({
20765
20909
  message: `Stale provisioning profile for ${input.bundleIdentifier}; cannot regenerate without an interactive session.`,
20766
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."
20767
- }));
20911
+ });
20768
20912
  yield* Console.log(`Stale provisioning profile for ${input.bundleIdentifier} (device roster changed). Regenerating...`);
20769
20913
  yield* regenerateProvisioningProfile(api, input);
20770
20914
  })));
@@ -20814,7 +20958,7 @@ const applyTargetSigning = (options) => Effect.gen(function* () {
20814
20958
  const projectDir = yield* findXcodeProjectDir$1(options.iosDir);
20815
20959
  const pbxprojPath = path.join(projectDir, "project.pbxproj");
20816
20960
  const project = yield* parseProject$1(pbxprojPath);
20817
- 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}.` });
20818
20962
  const serialized = yield* Effect.try({
20819
20963
  try: () => project.writeSync(),
20820
20964
  catch: (cause) => new XcodeProjectError({ message: `Failed to serialize ${pbxprojPath}: ${cause instanceof Error ? cause.message : String(cause)}` })
@@ -21002,7 +21146,7 @@ const installProvisioningProfile = ({ profilePath }) => Effect.acquireRelease(Ef
21002
21146
  //#endregion
21003
21147
  //#region src/lib/post-build-validation.ts
21004
21148
  const validateOneBundle = (bundleDir, expectedByBundleId, expectedTeamId) => Effect.gen(function* () {
21005
- const bundleId = yield* readBundleId(bundleDir).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
21149
+ const bundleId = yield* readBundleId(bundleDir).pipe(Effect.orElseSucceed(() => void 0));
21006
21150
  if (!bundleId) return {
21007
21151
  bundleId: void 0,
21008
21152
  warnings: [`Missing CFBundleIdentifier in Info.plist at ${bundleDir}`]
@@ -21014,16 +21158,16 @@ const validateOneBundle = (bundleDir, expectedByBundleId, expectedTeamId) => Eff
21014
21158
  };
21015
21159
  return {
21016
21160
  bundleId,
21017
- warnings: yield* validateEmbeddedProfile(bundleDir, expected.profileUuid, expectedTeamId, bundleId).pipe(Effect.catchAll(() => Effect.succeed([])))
21161
+ warnings: yield* validateEmbeddedProfile(bundleDir, expected.profileUuid, expectedTeamId, bundleId).pipe(Effect.orElseSucceed(() => []))
21018
21162
  };
21019
21163
  });
21020
21164
  const validateIosBuild = (params) => Effect.gen(function* () {
21021
- const appDir = yield* findAppDirectory$1(params.archivePath).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
21165
+ const appDir = yield* findAppDirectory$1(params.archivePath).pipe(Effect.orElseSucceed(() => void 0));
21022
21166
  if (!appDir) return {
21023
21167
  passed: false,
21024
21168
  warnings: ["Could not locate .app bundle in archive — skipping post-build validation"]
21025
21169
  };
21026
- const bundleDirs = yield* listSignedBundleDirs(appDir).pipe(Effect.catchAll(() => Effect.succeed([appDir])));
21170
+ const bundleDirs = yield* listSignedBundleDirs(appDir).pipe(Effect.orElseSucceed(() => [appDir]));
21027
21171
  const expectedByBundleId = new Map(params.expectedTargets.map((target) => [target.bundleId, target]));
21028
21172
  const perBundle = yield* Effect.forEach(bundleDirs, (bundleDir) => validateOneBundle(bundleDir, expectedByBundleId, params.expectedTeamId));
21029
21173
  const warnings = perBundle.flatMap((entry) => [...entry.warnings]);
@@ -21053,7 +21197,7 @@ const findAppDirectory$1 = (archivePath) => Effect.gen(function* () {
21053
21197
  const listSignedBundleDirs = (appDir) => Effect.gen(function* () {
21054
21198
  const fs = yield* FileSystem.FileSystem;
21055
21199
  const plugInsDir = path.join(appDir, "PlugIns");
21056
- if (!(yield* fs.exists(plugInsDir).pipe(Effect.catchAll(() => Effect.succeed(false))))) return [appDir];
21200
+ if (!(yield* fs.exists(plugInsDir).pipe(Effect.orElseSucceed(() => false)))) return [appDir];
21057
21201
  return [appDir, ...(yield* fs.readDirectory(plugInsDir)).filter((entry) => entry.endsWith(".appex")).map((entry) => path.join(plugInsDir, entry))];
21058
21202
  });
21059
21203
  const readBundleId = (bundleDir) => Effect.gen(function* () {
@@ -21996,7 +22140,7 @@ const easJsonPath = (projectRoot) => Effect.gen(function* () {
21996
22140
  const readEasJson = (projectRoot) => Effect.gen(function* () {
21997
22141
  const fs = yield* FileSystem.FileSystem;
21998
22142
  const filePath = yield* easJsonPath(projectRoot);
21999
- return yield* parseEasConfig(yield* fs.readFileString(filePath).pipe(Effect.catchAll((cause) => Effect.fail(new BuildProfileError({ message: cause._tag === "SystemError" && cause.reason === "NotFound" ? `No eas.json found at ${filePath}. Create one with a "build" section.` : `Failed to read eas.json: ${cause.message}` })))));
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}` }))));
22000
22144
  });
22001
22145
  const mergeCustom = (base, overlay) => {
22002
22146
  const merged = shallowMerge(base, overlay);
@@ -22208,8 +22352,8 @@ const clearBuildCaches = (projectRoot) => Effect.gen(function* () {
22208
22352
  const removed = [];
22209
22353
  yield* Effect.forEach(CACHE_DIRS, (rel) => Effect.gen(function* () {
22210
22354
  const target = path.join(projectRoot, rel);
22211
- if (!(yield* fs.exists(target).pipe(Effect.catchAll(() => Effect.succeed(false))))) return;
22212
- yield* fs.remove(target, { recursive: true }).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
22355
+ if (!(yield* fs.exists(target).pipe(Effect.orElseSucceed(() => false)))) return;
22356
+ yield* fs.remove(target, { recursive: true }).pipe(Effect.orElseSucceed(() => void 0));
22213
22357
  removed.push(rel);
22214
22358
  }), { concurrency: 4 });
22215
22359
  if (removed.length > 0) yield* Console.error(`Cleared caches: ${removed.join(", ")}`);
@@ -22538,10 +22682,10 @@ const runString = (cmd, cwd) => Command.string(Command.workingDirectory(cmd, cwd
22538
22682
  */
22539
22683
  const readGitContext = (projectRoot) => Effect.gen(function* () {
22540
22684
  const [commit, ref, commitMessage, status] = yield* Effect.all([
22541
- runString(Command.make("git", "rev-parse", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.catchAll(() => Effect.succeed(""))),
22542
- runString(Command.make("git", "symbolic-ref", "--short", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.catchAll(() => Effect.succeed(""))),
22543
- runString(Command.make("git", "log", "-1", "--format=%s"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.catchAll(() => Effect.succeed(""))),
22544
- runString(Command.make("git", "status", "--porcelain"), projectRoot).pipe(Effect.catchAll(() => Effect.succeed("")))
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(() => ""))
22545
22689
  ], { concurrency: "unbounded" });
22546
22690
  return {
22547
22691
  ref: ref.length > 0 ? ref : void 0,
@@ -22570,14 +22714,14 @@ const readGradleConfig = (androidDir) => Effect.gen(function* () {
22570
22714
  const hasKts = yield* fs.exists(ktsPath).pipe(Effect.orElseSucceed(() => false));
22571
22715
  if (!hasGroovy && hasKts) return;
22572
22716
  if (!hasGroovy) return;
22573
- const content = yield* fs.readFileString(gradlePath).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
22717
+ const content = yield* fs.readFileString(gradlePath).pipe(Effect.orElseSucceed(() => void 0));
22574
22718
  if (!content) return;
22575
22719
  return yield* Effect.tryPromise({
22576
22720
  try: async () => {
22577
22721
  return __require("gradle-to-js").parseText(stripGroovyComments(content));
22578
22722
  },
22579
22723
  catch: () => void 0
22580
- }).pipe(Effect.map(extractGradleConfig), Effect.catchAll(() => Effect.succeed(void 0)));
22724
+ }).pipe(Effect.map(extractGradleConfig), Effect.orElseSucceed(() => void 0));
22581
22725
  });
22582
22726
  /**
22583
22727
  * Log a warning if Gradle applicationId differs from app.json package name.
@@ -22697,7 +22841,7 @@ const ALWAYS_IGNORE = [...[
22697
22841
  "dist"
22698
22842
  ], ...NATIVE_BUILD_OUTPUTS];
22699
22843
  const findLockfile = (fs, dir) => Effect.gen(function* () {
22700
- for (const [name, pm] of LOCKFILES) if (yield* fs.exists(path.join(dir, name)).pipe(Effect.catchAll(() => Effect.succeed(false)))) return pm;
22844
+ for (const [name, pm] of LOCKFILES) if (yield* fs.exists(path.join(dir, name)).pipe(Effect.orElseSucceed(() => false))) return pm;
22701
22845
  });
22702
22846
  const walkUpForLockfile = (startCwd, dir) => Effect.gen(function* () {
22703
22847
  const pm = yield* findLockfile(yield* FileSystem.FileSystem, dir);
@@ -22733,13 +22877,13 @@ const buildIgnoreInstance = (workspaceRoot, options = {}) => Effect.gen(function
22733
22877
  const ig = ignore();
22734
22878
  ig.add([...ALWAYS_IGNORE]);
22735
22879
  const easignorePath = path.join(workspaceRoot, ".easignore");
22736
- if (yield* fs.exists(easignorePath).pipe(Effect.catchAll(() => Effect.succeed(false)))) {
22737
- const content = yield* fs.readFileString(easignorePath).pipe(Effect.catchAll(() => Effect.succeed("")));
22880
+ if (yield* fs.exists(easignorePath).pipe(Effect.orElseSucceed(() => false))) {
22881
+ const content = yield* fs.readFileString(easignorePath).pipe(Effect.orElseSucceed(() => ""));
22738
22882
  ig.add(content);
22739
22883
  } else {
22740
22884
  const gitignorePath = path.join(workspaceRoot, ".gitignore");
22741
- if (yield* fs.exists(gitignorePath).pipe(Effect.catchAll(() => Effect.succeed(false)))) {
22742
- const content = yield* fs.readFileString(gitignorePath).pipe(Effect.catchAll(() => Effect.succeed("")));
22885
+ if (yield* fs.exists(gitignorePath).pipe(Effect.orElseSucceed(() => false))) {
22886
+ const content = yield* fs.readFileString(gitignorePath).pipe(Effect.orElseSucceed(() => ""));
22743
22887
  ig.add(content);
22744
22888
  }
22745
22889
  }
@@ -22810,7 +22954,7 @@ const prepareStagingProject = (input) => Effect.gen(function* () {
22810
22954
  })
22811
22955
  });
22812
22956
  yield* initGitRepo(stagingRoot);
22813
- if (yield* fs.exists(path.join(workspaceRoot, "package.json")).pipe(Effect.catchAll(() => Effect.succeed(false)))) yield* runInstall({
22957
+ if (yield* fs.exists(path.join(workspaceRoot, "package.json")).pipe(Effect.orElseSucceed(() => false))) yield* runInstall({
22814
22958
  stagingRoot,
22815
22959
  packageManager,
22816
22960
  env: yield* runtime.commandEnvironment(input.envVars)
@@ -22827,7 +22971,7 @@ const prepareStagingProject = (input) => Effect.gen(function* () {
22827
22971
  //#endregion
22828
22972
  //#region src/lib/repo-clean.ts
22829
22973
  const MAX_FILES_SHOWN = 10;
22830
- 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.catchAll(() => Effect.succeed([])));
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(() => []));
22831
22975
  /**
22832
22976
  * Refuse to proceed when the working tree has uncommitted changes. Skipped when
22833
22977
  * `allowDirty` is true. In interactive mode, prompts the user to confirm; in
@@ -22840,11 +22984,8 @@ const ensureRepoClean = ({ projectRoot, allowDirty, label }) => Effect.gen(funct
22840
22984
  const preview = dirty.slice(0, MAX_FILES_SHOWN).join("\n ");
22841
22985
  const overflow = dirty.length > MAX_FILES_SHOWN ? `\n ... and ${String(dirty.length - MAX_FILES_SHOWN)} more` : "";
22842
22986
  yield* Console.error(`Uncommitted changes (${String(dirty.length)} file(s)):\n ${preview}${overflow}`);
22843
- if (!(yield* InteractiveMode).allow) {
22844
- yield* new DirtyRepoError({ message: `Refusing to ${label} with a dirty working tree. Commit your changes or pass --allow-dirty.` });
22845
- return;
22846
- }
22847
- 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.` });
22848
22989
  });
22849
22990
 
22850
22991
  //#endregion
@@ -22984,7 +23125,7 @@ const postTokenRequest = (tokenUri, jwt) => Effect.tryPromise({
22984
23125
  });
22985
23126
  const exchangeJwtForAccessToken = (tokenUri, jwt) => Effect.gen(function* () {
22986
23127
  const result = yield* postTokenRequest(tokenUri, jwt);
22987
- if (!result.ok) return yield* Effect.fail(new GooglePlayAuthError({ message: `OAuth token exchange failed: ${String(result.status)} ${result.text}` }));
23128
+ if (!result.ok) return yield* new GooglePlayAuthError({ message: `OAuth token exchange failed: ${String(result.status)} ${result.text}` });
22988
23129
  const json = yield* Effect.try({
22989
23130
  try: () => JSON.parse(result.text),
22990
23131
  catch: (cause) => new GooglePlayAuthError({
@@ -23009,7 +23150,7 @@ const acquireGooglePlayAccessToken = (serviceAccountJson) => Effect.gen(function
23009
23150
  message: "Service account JSON missing required fields (type, client_email, private_key)",
23010
23151
  cause
23011
23152
  })));
23012
- if (parsed.type !== "service_account") return yield* Effect.fail(new GooglePlayAuthError({ message: `Service account JSON has wrong type: ${parsed.type}` }));
23153
+ if (parsed.type !== "service_account") return yield* new GooglePlayAuthError({ message: `Service account JSON has wrong type: ${parsed.type}` });
23013
23154
  const tokenUri = parsed.token_uri ?? GOOGLE_OAUTH_TOKEN_URL;
23014
23155
  return {
23015
23156
  accessToken: yield* exchangeJwtForAccessToken(tokenUri, yield* signJwt(yield* importPrivateKey(parsed.private_key), buildJwtAssertion({
@@ -23049,10 +23190,10 @@ const performFetch = (params) => Effect.tryPromise({
23049
23190
  });
23050
23191
  const callJsonRaw = (params) => Effect.gen(function* () {
23051
23192
  const result = yield* performFetch(params);
23052
- if (!result.ok) return yield* Effect.fail(new GooglePlayApiError({
23193
+ if (!result.ok) return yield* new GooglePlayApiError({
23053
23194
  message: `${params.label} failed: ${String(result.status)} ${result.text}`,
23054
23195
  httpStatus: result.status
23055
- }));
23196
+ });
23056
23197
  return yield* Effect.try({
23057
23198
  try: () => result.text === "" ? {} : JSON.parse(result.text),
23058
23199
  catch: (cause) => new GooglePlayApiError({
@@ -23107,10 +23248,10 @@ const performBundleUpload = (params) => Effect.tryPromise({
23107
23248
  });
23108
23249
  const uploadBundle = (params) => Effect.gen(function* () {
23109
23250
  const result = yield* performBundleUpload(params);
23110
- if (!result.ok) return yield* Effect.fail(new GooglePlayApiError({
23251
+ if (!result.ok) return yield* new GooglePlayApiError({
23111
23252
  message: `edits.bundles.upload failed: ${String(result.status)} ${result.text}`,
23112
23253
  httpStatus: result.status
23113
- }));
23254
+ });
23114
23255
  const raw = yield* Effect.try({
23115
23256
  try: () => JSON.parse(result.text),
23116
23257
  catch: (cause) => new GooglePlayApiError({
@@ -23301,10 +23442,10 @@ const fetchArchiveOverHttp = (url) => Effect.gen(function* () {
23301
23442
  message: `Failed to download AAB from ${url}: ${cause instanceof Error ? cause.message : String(cause)}`
23302
23443
  })
23303
23444
  });
23304
- if (!result.ok || result.bytes === null) return yield* Effect.fail(new CliSubmitError({
23445
+ if (!result.ok || result.bytes === null) return yield* new CliSubmitError({
23305
23446
  code: "SUBMISSION_ARCHIVE_DOWNLOAD_FAILED",
23306
23447
  message: `HTTP ${String(result.status)} fetching archive at ${url}`
23307
- }));
23448
+ });
23308
23449
  return result.bytes;
23309
23450
  });
23310
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);
@@ -23379,10 +23520,10 @@ const runGooglePlayPipeline = (params) => Effect.gen(function* () {
23379
23520
  });
23380
23521
  const runAndroidGooglePlayUpload = (inputs) => Effect.gen(function* () {
23381
23522
  const { applicationId } = inputs.androidProfile;
23382
- if (applicationId === void 0) return yield* Effect.fail(new CliSubmitError({
23523
+ if (applicationId === void 0) return yield* new CliSubmitError({
23383
23524
  code: "SUBMISSION_ANDROID_APP_ID_MISSING",
23384
23525
  message: "Android submit profile requires applicationId — set submit.<profile>.android.applicationId in eas.json"
23385
- }));
23526
+ });
23386
23527
  const serviceAccountJson = yield* resolveServiceAccountJson({
23387
23528
  api: inputs.api,
23388
23529
  serviceAccountKeyId: inputs.serviceAccountKeyId,
@@ -23407,7 +23548,7 @@ const runAndroidGooglePlayUpload = (inputs) => Effect.gen(function* () {
23407
23548
  errorCode: engineError.code,
23408
23549
  errorMessage: engineError.message
23409
23550
  });
23410
- return yield* Effect.fail(engineError);
23551
+ return yield* engineError;
23411
23552
  })));
23412
23553
  yield* patchSubmissionStatus(inputs.api, inputs.submissionId, { status: "FINISHED" });
23413
23554
  yield* printHuman(`Google Play bundle uploaded (versionCode ${String(result.versionCode)})`);
@@ -23824,7 +23965,7 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
23824
23965
  commit: rawGitContext.commit,
23825
23966
  dirty: rawGitContext.dirty
23826
23967
  });
23827
- const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(userCwd, platform).pipe(Effect.map((entry) => entry.hash), Effect.catchAll(() => Effect.succeed(void 0))) : void 0;
23968
+ const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(userCwd, platform).pipe(Effect.map((entry) => entry.hash), Effect.orElseSucceed(() => void 0)) : void 0;
23828
23969
  const result = yield* reserveAndUpload(api, {
23829
23970
  target,
23830
23971
  projectId,
@@ -24123,7 +24264,7 @@ const compatibilityMatrixCommand = defineCommand({
24123
24264
 
24124
24265
  //#endregion
24125
24266
  //#region src/commands/builds/delete.ts
24126
- const deleteCommand$5 = defineCommand({
24267
+ const deleteCommand$6 = defineCommand({
24127
24268
  meta: {
24128
24269
  name: "delete",
24129
24270
  description: "Delete a build"
@@ -24179,10 +24320,7 @@ const downloadCommand$1 = defineCommand({
24179
24320
  const fs = yield* FileSystem.FileSystem;
24180
24321
  const cwd = yield* (yield* CliRuntime).cwd;
24181
24322
  const { artifact } = yield* api.builds.get({ path: { id: args.id } });
24182
- if (!artifact) {
24183
- yield* Effect.fail(new UploadFailedError({ message: `Build ${args.id} has no artifact yet.` }));
24184
- return;
24185
- }
24323
+ if (!artifact) return yield* new UploadFailedError({ message: `Build ${args.id} has no artifact yet.` });
24186
24324
  const link = yield* api.builds.getInstallLink({ path: { id: args.id } });
24187
24325
  const ext = artifact.format;
24188
24326
  const outputPath = args.output ?? path.join(cwd, `${args.id}.${ext}`);
@@ -24272,7 +24410,7 @@ const DISTRIBUTION_OPTIONS$1 = [
24272
24410
  "play-store",
24273
24411
  "direct"
24274
24412
  ];
24275
- const listCommand$7 = defineCommand({
24413
+ const listCommand$10 = defineCommand({
24276
24414
  meta: {
24277
24415
  name: "list",
24278
24416
  description: "List builds for the linked project"
@@ -24455,7 +24593,7 @@ const runInherit = (step, bin, ...args) => Command.exitCode(Command.make(bin, ..
24455
24593
  * Locate a tool on PATH. Returns the absolute path or fails with NativeRunError.
24456
24594
  */
24457
24595
  const which = (bin) => Effect.gen(function* () {
24458
- const trimmed = (yield* Command.string(Command.make("which", bin)).pipe(Effect.catchAll(() => Effect.fail(new NativeRunError({ message: `${bin} not found in PATH` }))))).trim();
24596
+ const trimmed = (yield* Command.string(Command.make("which", bin)).pipe(Effect.mapError(() => new NativeRunError({ message: `${bin} not found in PATH` })))).trim();
24459
24597
  if (trimmed === "") return yield* new NativeRunError({ message: `${bin} not found in PATH` });
24460
24598
  return trimmed;
24461
24599
  });
@@ -24474,7 +24612,7 @@ const findAppBundle = (root) => Effect.gen(function* () {
24474
24612
  const fs = yield* FileSystem.FileSystem;
24475
24613
  const candidates = [root, path.join(root, "Payload")];
24476
24614
  for (const candidate of candidates) {
24477
- const app = (yield* fs.readDirectory(candidate).pipe(Effect.catchAll(() => Effect.succeed([])))).find((entry) => entry.endsWith(".app"));
24615
+ const app = (yield* fs.readDirectory(candidate).pipe(Effect.orElseSucceed(() => []))).find((entry) => entry.endsWith(".app"));
24478
24616
  if (app) return path.join(candidate, app);
24479
24617
  }
24480
24618
  return yield* new NativeRunError({ message: `No .app bundle found inside ${root} or ${path.join(root, "Payload")}.` });
@@ -24553,8 +24691,8 @@ const installAndLaunchIosDevice = (params) => Effect.gen(function* () {
24553
24691
  * fall back to a CLI flag.
24554
24692
  */
24555
24693
  const tryReadApkPackageWith = (bin, apkPath) => Effect.gen(function* () {
24556
- if (!(yield* which(bin).pipe(Effect.catchAll(() => Effect.succeed(null))))) return;
24557
- const raw = yield* execCapture(`${bin} dump`, bin, "dump", "badging", apkPath).pipe(Effect.catchAll(() => Effect.succeed(null)));
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));
24558
24696
  if (!raw) return;
24559
24697
  return /package: name='(?<packageName>[^']+)'/u.exec(raw)?.[1];
24560
24698
  });
@@ -24626,10 +24764,7 @@ const runIosSimulator = (params) => Effect.gen(function* () {
24626
24764
  });
24627
24765
  const runIosDevice = (params) => Effect.gen(function* () {
24628
24766
  const { deviceSelector } = params;
24629
- if (deviceSelector === void 0) {
24630
- yield* Effect.fail(new InvalidArgumentError({ message: "Pass --device-id <udid>. Run `xcrun devicectl list devices` to list connected devices." }));
24631
- return;
24632
- }
24767
+ if (deviceSelector === void 0) return yield* new InvalidArgumentError({ message: "Pass --device-id <udid>. Run `xcrun devicectl list devices` to list connected devices." });
24633
24768
  const bundleId = yield* readBundleIdFromApp(yield* findAppBundle(yield* extractIosArtifact({
24634
24769
  tempDir: params.tempDir,
24635
24770
  artifactPath: params.artifactPath,
@@ -24654,21 +24789,12 @@ const runIos = (params) => {
24654
24789
  return Effect.fail(new NativeRunError({ message: `Cannot install ${params.format} on iOS; only tar.gz (simulator) or ipa are supported.` }));
24655
24790
  };
24656
24791
  const runAndroid = (params) => Effect.gen(function* () {
24657
- if (params.format === "aab") {
24658
- yield* Effect.fail(new InvalidArgumentError({ message: ".aab artifacts cannot be installed directly. Use bundletool to convert to apks, or download the play-store APK." }));
24659
- return;
24660
- }
24661
- if (params.format !== "apk") {
24662
- yield* Effect.fail(new NativeRunError({ message: `Cannot install ${params.format} on Android; only apk is supported.` }));
24663
- return;
24664
- }
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.` });
24665
24794
  const device = yield* pickAndroidDevice(params.emulatorSelector);
24666
24795
  const detected = yield* readApkPackageName(params.artifactPath);
24667
24796
  const packageName = params.packageOverride ?? detected;
24668
- if (!packageName) {
24669
- yield* Effect.fail(new InvalidArgumentError({ message: "Could not detect APK package name (aapt/aapt2 not on PATH). Pass --package <name> explicitly." }));
24670
- return;
24671
- }
24797
+ if (!packageName) return yield* new InvalidArgumentError({ message: "Could not detect APK package name (aapt/aapt2 not on PATH). Pass --package <name> explicitly." });
24672
24798
  yield* printHuman(`Installing on Android device ${device.serial}...`);
24673
24799
  yield* installAndLaunchAndroid({
24674
24800
  serial: device.serial,
@@ -24733,7 +24859,7 @@ const runCommand$1 = defineCommand({
24733
24859
  projectId
24734
24860
  });
24735
24861
  const { artifact } = build;
24736
- if (!artifact) return yield* Effect.fail(new UploadFailedError({ message: `Build ${build.id} has no artifact yet.` }));
24862
+ if (!artifact) return yield* new UploadFailedError({ message: `Build ${build.id} has no artifact yet.` });
24737
24863
  const link = yield* api.builds.getInstallLink({ path: { id: build.id } });
24738
24864
  const tempDir = yield* acquireBuildTempDir;
24739
24865
  const artifactPath = path.join(tempDir, `artifact.${artifact.format}`);
@@ -24828,7 +24954,7 @@ const resolveUploadMeta = (params) => Effect.gen(function* () {
24828
24954
  const runUploadWorkflow = (options) => Effect.gen(function* () {
24829
24955
  const api = yield* apiClient;
24830
24956
  const projectRoot = yield* (yield* CliRuntime).cwd;
24831
- if (!(yield* (yield* FileSystem.FileSystem).exists(options.artifactPath).pipe(Effect.orElseSucceed(() => false)))) yield* new ArtifactNotFoundError({ message: `Artifact not found at ${options.artifactPath}.` });
24957
+ if (!(yield* (yield* FileSystem.FileSystem).exists(options.artifactPath).pipe(Effect.orElseSucceed(() => false)))) return yield* new ArtifactNotFoundError({ message: `Artifact not found at ${options.artifactPath}.` });
24832
24958
  const projectType = yield* detectProjectType({
24833
24959
  projectRoot,
24834
24960
  override: asProjectType((yield* readBetterUpdateConfig(projectRoot))?.["projectType"])
@@ -24858,7 +24984,7 @@ const runUploadWorkflow = (options) => Effect.gen(function* () {
24858
24984
  commit: rawGitContext.commit,
24859
24985
  dirty: rawGitContext.dirty
24860
24986
  });
24861
- const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(projectRoot, options.platform).pipe(Effect.map((entry) => entry.hash), Effect.catchAll(() => Effect.succeed(void 0))) : void 0;
24987
+ const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(projectRoot, options.platform).pipe(Effect.map((entry) => entry.hash), Effect.orElseSucceed(() => void 0)) : void 0;
24862
24988
  const result = yield* reserveAndUpload(api, compact({
24863
24989
  target,
24864
24990
  projectId,
@@ -24942,9 +25068,9 @@ const buildsCommand = defineCommand({
24942
25068
  description: "Manage builds"
24943
25069
  },
24944
25070
  subCommands: {
24945
- list: listCommand$7,
25071
+ list: listCommand$10,
24946
25072
  get: getCommand$2,
24947
- delete: deleteCommand$5,
25073
+ delete: deleteCommand$6,
24948
25074
  download: downloadCommand$1,
24949
25075
  run: runCommand$1,
24950
25076
  "install-link": installLinkCommand,
@@ -24970,7 +25096,7 @@ const resolveNamedResourceId$1 = (params) => resolveNamedResourceId$2(params, (m
24970
25096
 
24971
25097
  //#endregion
24972
25098
  //#region src/commands/channels/create.ts
24973
- const createCommand$3 = defineCommand({
25099
+ const createCommand$4 = defineCommand({
24974
25100
  meta: {
24975
25101
  name: "create",
24976
25102
  description: "Create a channel"
@@ -25015,7 +25141,7 @@ const createCommand$3 = defineCommand({
25015
25141
 
25016
25142
  //#endregion
25017
25143
  //#region src/commands/channels/delete.ts
25018
- const deleteCommand$4 = defineCommand({
25144
+ const deleteCommand$5 = defineCommand({
25019
25145
  meta: {
25020
25146
  name: "delete",
25021
25147
  description: "Delete a channel"
@@ -25038,6 +25164,193 @@ const deleteCommand$4 = defineCommand({
25038
25164
  })
25039
25165
  });
25040
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
+
25041
25354
  //#endregion
25042
25355
  //#region src/commands/channels/insights.ts
25043
25356
  const insightsCommand$1 = defineCommand({
@@ -25084,7 +25397,7 @@ const insightsCommand$1 = defineCommand({
25084
25397
 
25085
25398
  //#endregion
25086
25399
  //#region src/commands/channels/list.ts
25087
- const listCommand$6 = defineCommand({
25400
+ const listCommand$8 = defineCommand({
25088
25401
  meta: {
25089
25402
  name: "list",
25090
25403
  description: "List channels for the linked project"
@@ -25188,7 +25501,7 @@ const completeCommand$1 = defineCommand({
25188
25501
 
25189
25502
  //#endregion
25190
25503
  //#region src/commands/channels/rollout/create.ts
25191
- const createCommand$2 = defineCommand({
25504
+ const createCommand$3 = defineCommand({
25192
25505
  meta: {
25193
25506
  name: "create",
25194
25507
  description: "Start a branch rollout on a channel"
@@ -25269,7 +25582,7 @@ const revertCommand$2 = defineCommand({
25269
25582
 
25270
25583
  //#endregion
25271
25584
  //#region src/commands/channels/rollout/update.ts
25272
- const updateCommand$3 = defineCommand({
25585
+ const updateCommand$4 = defineCommand({
25273
25586
  meta: {
25274
25587
  name: "update",
25275
25588
  description: "Update the rollout percentage on a channel"
@@ -25308,8 +25621,8 @@ const rolloutCommand$1 = defineCommand({
25308
25621
  description: "Manage channel branch rollouts"
25309
25622
  },
25310
25623
  subCommands: {
25311
- create: createCommand$2,
25312
- update: updateCommand$3,
25624
+ create: createCommand$3,
25625
+ update: updateCommand$4,
25313
25626
  complete: completeCommand$1,
25314
25627
  revert: revertCommand$2
25315
25628
  }
@@ -25317,7 +25630,7 @@ const rolloutCommand$1 = defineCommand({
25317
25630
 
25318
25631
  //#endregion
25319
25632
  //#region src/commands/channels/update.ts
25320
- const updateCommand$2 = defineCommand({
25633
+ const updateCommand$3 = defineCommand({
25321
25634
  meta: {
25322
25635
  name: "update",
25323
25636
  description: "Relink a channel to a different branch"
@@ -25360,7 +25673,7 @@ const updateCommand$2 = defineCommand({
25360
25673
 
25361
25674
  //#endregion
25362
25675
  //#region src/commands/channels/view.ts
25363
- const viewCommand$2 = defineCommand({
25676
+ const viewCommand$3 = defineCommand({
25364
25677
  meta: {
25365
25678
  name: "view",
25366
25679
  description: "Show a channel by ID or name"
@@ -25383,7 +25696,7 @@ const viewCommand$2 = defineCommand({
25383
25696
  page
25384
25697
  } }))]);
25385
25698
  const channel = channels.find((entry) => entry.id === args.target) ?? channels.find((entry) => entry.name === args.target);
25386
- if (!channel) return yield* Effect.fail(new ChannelCommandError({ message: `Channel "${args.target}" not found by ID or name.` }));
25699
+ if (!channel) return yield* new ChannelCommandError({ message: `Channel "${args.target}" not found by ID or name.` });
25387
25700
  const branchName = new Map(branches.map((branch) => [branch.id, branch.name])).get(channel.branchId) ?? channel.branchId;
25388
25701
  yield* printHumanKeyValue([
25389
25702
  ["ID", channel.id],
@@ -25420,14 +25733,15 @@ const channelsCommand = defineCommand({
25420
25733
  description: "Manage channels"
25421
25734
  },
25422
25735
  subCommands: {
25423
- list: listCommand$6,
25424
- view: viewCommand$2,
25425
- create: createCommand$3,
25426
- update: updateCommand$2,
25736
+ list: listCommand$8,
25737
+ view: viewCommand$3,
25738
+ create: createCommand$4,
25739
+ update: updateCommand$3,
25427
25740
  pause: pauseCommand,
25428
25741
  resume: resumeCommand,
25429
- delete: deleteCommand$4,
25742
+ delete: deleteCommand$5,
25430
25743
  rollout: rolloutCommand$1,
25744
+ grants: grantsCommand$1,
25431
25745
  insights: insightsCommand$1
25432
25746
  }
25433
25747
  });
@@ -25808,7 +26122,7 @@ const announce = (heading) => Effect.gen(function* () {
25808
26122
  });
25809
26123
  const reportError = (label, cause) => Console.log(`✗ ${label}: ${cause instanceof Error ? cause.message : String(cause)}`);
25810
26124
  const safely = (label, effect) => effect.pipe(Effect.catchAll((cause) => reportError(label, cause)), Effect.asVoid);
25811
- const safePrompt = (effect) => effect.pipe(Effect.catchAll(() => Effect.succeed(BACK)));
26125
+ const safePrompt = (effect) => effect.pipe(Effect.orElseSucceed(() => BACK));
25812
26126
  const promptForBundleConfig = (ctx) => Effect.gen(function* () {
25813
26127
  const list = yield* ctx.api.iosBundleConfigurations.list({ path: { projectId: ctx.projectId } });
25814
26128
  if (list.items.length === 0) return yield* new MissingCredentialsError({
@@ -26841,7 +27155,7 @@ const toRecipientView = (userEncryptionKeyId, key) => ({
26841
27155
  fingerprint: key?.fingerprint
26842
27156
  })
26843
27157
  });
26844
- const listCommand$5 = defineCommand({
27158
+ const listCommand$7 = defineCommand({
26845
27159
  meta: {
26846
27160
  name: "list",
26847
27161
  description: "List recipients that currently hold the org vault key"
@@ -27068,7 +27382,7 @@ const accessCommand = defineCommand({
27068
27382
  description: "Inspect, grant, rotate, revoke, and recover access to the org credential vault"
27069
27383
  },
27070
27384
  subCommands: {
27071
- list: listCommand$5,
27385
+ list: listCommand$7,
27072
27386
  grant: grantCommand,
27073
27387
  rotate: rotateCommand,
27074
27388
  revoke: revokeCommand$1,
@@ -27277,7 +27591,7 @@ const CREDENTIAL_TYPES$3 = [
27277
27591
  "keystore",
27278
27592
  "google-service-account-key"
27279
27593
  ];
27280
- const deleteCommand$3 = defineCommand({
27594
+ const deleteCommand$4 = defineCommand({
27281
27595
  meta: {
27282
27596
  name: "delete",
27283
27597
  description: "Delete a credential"
@@ -27319,7 +27633,7 @@ const deleteCommand$3 = defineCommand({
27319
27633
  //#region src/commands/credentials/device.ts
27320
27634
  /** Self-linking is for your own device keys; recovery/machine keys go through `access grant`. */
27321
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.` });
27322
- const listCommand$4 = defineCommand({
27636
+ const listCommand$6 = defineCommand({
27323
27637
  meta: {
27324
27638
  name: "list",
27325
27639
  description: "List your registered device keys (the active one is marked)"
@@ -27377,7 +27691,7 @@ const deviceCommand = defineCommand({
27377
27691
  description: "Manage your vault device keys"
27378
27692
  },
27379
27693
  subCommands: {
27380
- list: listCommand$4,
27694
+ list: listCommand$6,
27381
27695
  link: linkCommand
27382
27696
  },
27383
27697
  default: "list"
@@ -27739,7 +28053,7 @@ const handleCertLimitInteractive = (api, ascApiKeyId, certificateType) => Effect
27739
28053
  ascApiKeyId,
27740
28054
  certificateType
27741
28055
  });
27742
- if (certs.length === 0) return yield* Effect.fail(new CertificateLimitError({ message: "Apple says the certificate limit is hit but no existing certificates were returned — try again later." }));
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." });
27743
28057
  const toRevoke = yield* promptMultiSelect("Select one or more certificates to revoke before retrying", certs.map((entry) => ({
27744
28058
  value: entry.id,
27745
28059
  label: `${entry.serialNumber.slice(0, 12)}… (${entry.displayName ?? entry.certificateType}, exp ${entry.expirationDate.slice(0, 10)})`
@@ -28032,7 +28346,7 @@ const printRecipient = (key) => printKeyValue([
28032
28346
  ["Recipient (public key)", key.publicKey],
28033
28347
  ["Fingerprint", key.fingerprint]
28034
28348
  ]);
28035
- const createCommand$1 = defineCommand({
28349
+ const createCommand$2 = defineCommand({
28036
28350
  meta: {
28037
28351
  name: "create",
28038
28352
  description: "Create this device's encryption identity and register it as a recipient"
@@ -28135,7 +28449,7 @@ const identityCommand = defineCommand({
28135
28449
  description: "Manage this device's end-to-end encryption identity"
28136
28450
  },
28137
28451
  subCommands: {
28138
- create: createCommand$1,
28452
+ create: createCommand$2,
28139
28453
  init: initCommand$1,
28140
28454
  register: registerCommand,
28141
28455
  show: showCommand
@@ -28145,7 +28459,7 @@ const identityCommand = defineCommand({
28145
28459
 
28146
28460
  //#endregion
28147
28461
  //#region src/commands/credentials/list.ts
28148
- const listCommand$3 = defineCommand({
28462
+ const listCommand$5 = defineCommand({
28149
28463
  meta: {
28150
28464
  name: "list",
28151
28465
  description: "List credentials across platforms"
@@ -28501,7 +28815,7 @@ const writeArtifact = (fs, projectRoot, relPath, bytes) => Effect.gen(function*
28501
28815
  const writeText = (fs, projectRoot, relPath, text) => writeArtifact(fs, projectRoot, relPath, new TextEncoder().encode(text));
28502
28816
  const ensureGitignoreEntries = (fs, projectRoot, paths) => Effect.gen(function* () {
28503
28817
  const filePath = path.join(projectRoot, ".gitignore");
28504
- const lines = ((yield* fs.exists(filePath).pipe(Effect.catchAll(() => Effect.succeed(false)))) ? yield* fs.readFileString(filePath).pipe(Effect.catchAll(() => Effect.succeed(""))) : "").split("\n");
28818
+ const lines = ((yield* fs.exists(filePath).pipe(Effect.orElseSucceed(() => false))) ? yield* fs.readFileString(filePath).pipe(Effect.orElseSucceed(() => "")) : "").split("\n");
28505
28819
  const added = [];
28506
28820
  const next = [...lines];
28507
28821
  for (const entry of paths) if (!lines.includes(entry)) {
@@ -29300,7 +29614,7 @@ const lookupByType = (api, id, type) => {
29300
29614
  default: return Effect.fail(new CredentialValidationError({ message: `Unsupported credential type: ${String(type)}` }));
29301
29615
  }
29302
29616
  };
29303
- const viewCommand$1 = defineCommand({
29617
+ const viewCommand$2 = defineCommand({
29304
29618
  meta: {
29305
29619
  name: "view",
29306
29620
  description: "Show details for a single credential (without secrets)"
@@ -29347,14 +29661,14 @@ const credentialsCommand = defineCommand({
29347
29661
  unlock: unlockCommand,
29348
29662
  lock: lockCommand,
29349
29663
  status: statusCommand$1,
29350
- list: listCommand$3,
29351
- view: viewCommand$1,
29664
+ list: listCommand$5,
29665
+ view: viewCommand$2,
29352
29666
  download: downloadCommand,
29353
29667
  upload: uploadCommand,
29354
29668
  "upload-asc-key": uploadAscKeyCommand,
29355
29669
  generate: generateCommand$1,
29356
29670
  "regenerate-profile": regenerateProfileCommand,
29357
- delete: deleteCommand$3,
29671
+ delete: deleteCommand$4,
29358
29672
  remove: removeCommand,
29359
29673
  revoke: revokeCommand,
29360
29674
  configure: configureCommand$1,
@@ -29721,6 +30035,7 @@ const devicesCommand = defineCommand({
29721
30035
 
29722
30036
  //#endregion
29723
30037
  //#region src/commands/doctor.ts
30038
+ var HealthCheckError = class extends Data.TaggedError("HealthCheckError") {};
29724
30039
  const pass = (id, name, message) => ({
29725
30040
  id,
29726
30041
  name,
@@ -29759,7 +30074,10 @@ const checkServerHealth = Effect.gen(function* () {
29759
30074
  const url = `${yield* (yield* ConfigStore).getBaseUrl}/api/health`;
29760
30075
  const response = yield* Effect.tryPromise({
29761
30076
  try: async () => fetch(url, { signal: AbortSignal.timeout(3e3) }),
29762
- catch: (cause) => new Error(String(cause))
30077
+ catch: (cause) => new HealthCheckError({
30078
+ message: String(cause),
30079
+ cause
30080
+ })
29763
30081
  }).pipe(Effect.either);
29764
30082
  if (response._tag === "Left") return fail("health", "Server reachable", `${url} unreachable: ${response.left.message}`);
29765
30083
  const res = response.right;
@@ -29850,19 +30168,19 @@ const envErrorExtras = {
29850
30168
  SystemError: 6,
29851
30169
  BadArgument: 6
29852
30170
  };
29853
- const isEnvironmentName = (value) => value === "development" || value === "preview" || value === "production";
30171
+ const isEnvironmentName$1 = (value) => value === "development" || value === "preview" || value === "production";
29854
30172
  const parseEnvironmentsArg = (raw) => Effect.gen(function* () {
29855
30173
  const tokens = raw.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
29856
30174
  if (tokens.length === 0) return yield* new InvalidArgumentError({ message: "Provide at least one environment (development, preview, production)." });
29857
30175
  const seen = /* @__PURE__ */ new Set();
29858
30176
  yield* Effect.forEach(tokens, (token) => Effect.gen(function* () {
29859
- 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.` });
29860
30178
  seen.add(token);
29861
30179
  }), { discard: true });
29862
30180
  return [...seen];
29863
30181
  });
29864
30182
  const parseSingleEnvironmentArg = (raw) => Effect.gen(function* () {
29865
- 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.` });
29866
30184
  return raw;
29867
30185
  });
29868
30186
  const formatEnvironments = (environments) => [...environments].toSorted((left, right) => left.localeCompare(right)).join(",");
@@ -29901,7 +30219,7 @@ const findProjectEnvVar = (api, projectId, key, environment) => Effect.gen(funct
29901
30219
 
29902
30220
  //#endregion
29903
30221
  //#region src/commands/env/delete.ts
29904
- const deleteCommand$2 = defineCommand({
30222
+ const deleteCommand$3 = defineCommand({
29905
30223
  meta: {
29906
30224
  name: "delete",
29907
30225
  description: "Delete a project env var (one environment, or every environment by default)"
@@ -29970,7 +30288,7 @@ const getExecTrailingArgv = () => trailing$1;
29970
30288
  const pullForExec = (api, projectId, environment) => pullEnvVars(api, {
29971
30289
  projectId,
29972
30290
  environment
29973
- }).pipe(Effect.catchAll(() => Effect.succeed({})));
30291
+ }).pipe(Effect.orElseSucceed(() => ({})));
29974
30292
  const splitTrailing = (trailing) => {
29975
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`." }));
29976
30294
  const [bin, ...rest] = trailing;
@@ -30073,6 +30391,191 @@ const getCommand$1 = defineCommand({
30073
30391
  }), envErrorExtras)
30074
30392
  });
30075
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
+
30076
30579
  //#endregion
30077
30580
  //#region src/commands/env/history.ts
30078
30581
  const historyCommand = defineCommand({
@@ -30171,7 +30674,7 @@ const importCommand = defineCommand({
30171
30674
 
30172
30675
  //#endregion
30173
30676
  //#region src/commands/env/list.ts
30174
- const listCommand$2 = defineCommand({
30677
+ const listCommand$3 = defineCommand({
30175
30678
  meta: {
30176
30679
  name: "list",
30177
30680
  description: "List environment variable metadata. Values are end-to-end encrypted — read them with `env pull`, `env export`, or `env get`."
@@ -30437,7 +30940,7 @@ const setCommand$1 = defineCommand({
30437
30940
 
30438
30941
  //#endregion
30439
30942
  //#region src/commands/env/update.ts
30440
- const updateCommand$1 = defineCommand({
30943
+ const updateCommand$2 = defineCommand({
30441
30944
  meta: {
30442
30945
  name: "update",
30443
30946
  description: "Update a project env var's value or visibility for an environment"
@@ -30518,18 +31021,19 @@ const envCommand = defineCommand({
30518
31021
  description: "Manage environment variables"
30519
31022
  },
30520
31023
  subCommands: {
30521
- list: listCommand$2,
31024
+ list: listCommand$3,
30522
31025
  get: getCommand$1,
30523
31026
  set: setCommand$1,
30524
- update: updateCommand$1,
30525
- delete: deleteCommand$2,
31027
+ update: updateCommand$2,
31028
+ delete: deleteCommand$3,
30526
31029
  history: historyCommand,
30527
31030
  rollback: rollbackCommand$1,
30528
31031
  import: importCommand,
30529
31032
  push: pushCommand,
30530
31033
  export: exportCommand,
30531
31034
  pull: pullCommand,
30532
- exec: execCommand
31035
+ exec: execCommand,
31036
+ grants: grantsCommand
30533
31037
  }
30534
31038
  });
30535
31039
 
@@ -30800,7 +31304,7 @@ const fingerprintCommand = defineCommand({
30800
31304
  const checkExistingLink = (api, config, localSlug) => Effect.gen(function* () {
30801
31305
  const existingId = config.extra?.betterUpdate?.projectId;
30802
31306
  if (typeof existingId !== "string" || existingId.length === 0) return "no-link";
30803
- const project = yield* api.projects.get({ path: { id: existingId } }).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
31307
+ const project = yield* api.projects.get({ path: { id: existingId } }).pipe(Effect.orElseSucceed(() => void 0));
30804
31308
  if (project === void 0) {
30805
31309
  yield* printHuman(`Existing projectId "${existingId}" not found on server. Re-linking by local slug "${localSlug}".`);
30806
31310
  return "stale";
@@ -30820,9 +31324,9 @@ const checkExistingLink = (api, config, localSlug) => Effect.gen(function* () {
30820
31324
  const slugify = (value) => value.toLowerCase().replaceAll(/[^a-z0-9]+/gu, "-").replaceAll(/^-+|-+$/gu, "");
30821
31325
  /** Best-effort `name` from the local package.json, or undefined when absent. */
30822
31326
  const readPackageJsonName = (projectRoot) => Effect.gen(function* () {
30823
- const content = yield* (yield* FileSystem.FileSystem).readFileString(path.join(projectRoot, "package.json")).pipe(Effect.catchAll(() => Effect.succeed("")));
31327
+ const content = yield* (yield* FileSystem.FileSystem).readFileString(path.join(projectRoot, "package.json")).pipe(Effect.orElseSucceed(() => ""));
30824
31328
  if (content.length === 0) return;
30825
- const parsed = yield* Effect.try(() => JSON.parse(content)).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
31329
+ const parsed = yield* Effect.try(() => JSON.parse(content)).pipe(Effect.orElseSucceed(() => void 0));
30826
31330
  const name = isRecord(parsed) ? parsed["name"] : void 0;
30827
31331
  return typeof name === "string" && name.length > 0 ? name : void 0;
30828
31332
  });
@@ -31066,7 +31570,7 @@ const openCommand = defineCommand({
31066
31570
 
31067
31571
  //#endregion
31068
31572
  //#region src/commands/projects.ts
31069
- const listCommand$1 = defineCommand({
31573
+ const listCommand$2 = defineCommand({
31070
31574
  meta: {
31071
31575
  name: "list",
31072
31576
  description: "List projects (most recently active first)"
@@ -31117,7 +31621,7 @@ const listCommand$1 = defineCommand({
31117
31621
  yield* printHuman(`Page ${result.page} · ${result.items.length} of ${result.total} project(s)`);
31118
31622
  }))
31119
31623
  });
31120
- const createCommand = defineCommand({
31624
+ const createCommand$1 = defineCommand({
31121
31625
  meta: {
31122
31626
  name: "create",
31123
31627
  description: "Create a new project"
@@ -31195,7 +31699,7 @@ const renameCommand = defineCommand({
31195
31699
  return project;
31196
31700
  }), { json: "value" })
31197
31701
  });
31198
- const deleteCommand$1 = defineCommand({
31702
+ const deleteCommand$2 = defineCommand({
31199
31703
  meta: {
31200
31704
  name: "delete",
31201
31705
  description: "Delete a project"
@@ -31220,10 +31724,229 @@ const projectsCommand = defineCommand({
31220
31724
  description: "Manage projects"
31221
31725
  },
31222
31726
  subCommands: {
31223
- list: listCommand$1,
31224
- create: createCommand,
31727
+ list: listCommand$2,
31728
+ create: createCommand$1,
31225
31729
  get: getCommand,
31226
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,
31227
31950
  delete: deleteCommand$1
31228
31951
  }
31229
31952
  });
@@ -32872,7 +33595,7 @@ const runPatchPhase = (input) => Effect.gen(function* () {
32872
33595
  runtimeVersion: input.runtimeVersion,
32873
33596
  platform: input.platform,
32874
33597
  limit: Math.max(1, input.baseWindow)
32875
- } }).pipe(Effect.catchAll(() => Effect.succeed([])));
33598
+ } }).pipe(Effect.orElseSucceed(() => []));
32876
33599
  const bases = selectBaseWindow(candidates, {
32877
33600
  newUpdateId: input.newUpdateId,
32878
33601
  maxRecent: input.baseWindow
@@ -32888,7 +33611,7 @@ const runPatchPhase = (input) => Effect.gen(function* () {
32888
33611
  bestSavingsPct: void 0
32889
33612
  };
32890
33613
  }
32891
- const newBundleBytes = yield* sha256File(input.newLaunchPath).pipe(Effect.map((result) => result.byteSize), Effect.catchAll(() => Effect.succeed(void 0)));
33614
+ const newBundleBytes = yield* sha256File(input.newLaunchPath).pipe(Effect.map((result) => result.byteSize), Effect.orElseSucceed(() => void 0));
32892
33615
  yield* printHuman(`Diffing against ${bases.length} base(s) (window=${input.baseWindow}; ${candidates.length} candidate(s) available).`);
32893
33616
  const uploadedOutcomes = (yield* Effect.forEach(bases, (base, index) => Effect.gen(function* () {
32894
33617
  const basePath = path.join(input.workDir, `base-${index}.bundle`);
@@ -32983,7 +33706,7 @@ const dedupeAssetsByHash = (assets) => uniqBy(assets, (asset) => asset.hash);
32983
33706
  * is informational, so a fingerprint failure resolves to `undefined` rather than
32984
33707
  * failing the publish.
32985
33708
  */
32986
- const resolvePlatformFingerprintHash = (projectRoot, platform) => runFingerprintForPlatform(projectRoot, platform).pipe(Effect.map((result) => result.hash), Effect.catchAll(() => Effect.succeed(void 0)));
33709
+ const resolvePlatformFingerprintHash = (projectRoot, platform) => runFingerprintForPlatform(projectRoot, platform).pipe(Effect.map((result) => result.hash), Effect.orElseSucceed(() => void 0));
32987
33710
  const preparePlatformAssets = ({ exportDir, platform }) => Effect.gen(function* () {
32988
33711
  const exportedAssets = yield* readExpoExportAssets({
32989
33712
  exportDir,
@@ -33079,7 +33802,7 @@ const publishPlatform = (params) => Effect.gen(function* () {
33079
33802
  const uploadDetailsByHash = new Map(assetRegistration.uploaded.map((asset) => [asset.hash, asset]));
33080
33803
  yield* Effect.forEach(uniqueAssets.filter((asset) => uploadDetailsByHash.has(asset.hash)), (asset) => Effect.gen(function* () {
33081
33804
  const detail = uploadDetailsByHash.get(asset.hash);
33082
- if (!detail) return yield* Effect.fail(new UpdatePublishError({ message: `Missing upload details for asset ${asset.hash}` }));
33805
+ if (!detail) return yield* new UpdatePublishError({ message: `Missing upload details for asset ${asset.hash}` });
33083
33806
  return yield* assetUploader.uploadAssetBinary({
33084
33807
  path: asset.path,
33085
33808
  hash: asset.hash,
@@ -34464,6 +35187,7 @@ const commandRegistry = {
34464
35187
  projects: projectsCommand,
34465
35188
  branches: branchesCommand,
34466
35189
  channels: channelsCommand,
35190
+ roles: rolesCommand,
34467
35191
  build: buildCommand,
34468
35192
  builds: buildsCommand,
34469
35193
  credentials: credentialsCommand,