@better-update/cli 0.26.1 → 0.28.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.28.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,
@@ -526,6 +527,49 @@ var AndroidUploadKeystoresGroup = class extends HttpApiGroup.make("androidUpload
526
527
  description: "Manage Android signing keystores"
527
528
  })) {};
528
529
 
530
+ //#endregion
531
+ //#region ../../packages/api/src/domain/api-key.ts
532
+ var ApiKey = class extends Schema.Class("ApiKey")({
533
+ id: Id,
534
+ name: Schema.NullOr(Schema.String),
535
+ start: Schema.NullOr(Schema.String),
536
+ prefix: Schema.NullOr(Schema.String),
537
+ enabled: Schema.Boolean,
538
+ createdAt: DateTimeString,
539
+ expiresAt: Schema.NullOr(DateTimeString)
540
+ }) {};
541
+ var CreatedApiKey = class extends Schema.Class("CreatedApiKey")({
542
+ id: Id,
543
+ name: Schema.NullOr(Schema.String),
544
+ start: Schema.NullOr(Schema.String),
545
+ prefix: Schema.NullOr(Schema.String),
546
+ enabled: Schema.Boolean,
547
+ createdAt: DateTimeString,
548
+ expiresAt: Schema.NullOr(DateTimeString),
549
+ key: Schema.String
550
+ }) {};
551
+ const CreateApiKeyBody = Schema.Struct({
552
+ name: Name120,
553
+ expiresInDays: Schema.optional(Schema.Number.pipe(Schema.int(), Schema.positive()))
554
+ });
555
+ const ApiKeyList = Schema.Struct({ items: Schema.Array(ApiKey) });
556
+
557
+ //#endregion
558
+ //#region ../../packages/api/src/groups/api-keys.ts
559
+ var ApiKeysGroup = class extends HttpApiGroup.make("api-keys").add(HttpApiEndpoint.get("list", "/api/api-keys").addSuccess(ApiKeyList).annotateContext(OpenApi.annotations({
560
+ title: "List API keys",
561
+ description: "List the active organization's API keys (hashed secret never exposed; only the `start` prefix for identification)"
562
+ }))).add(HttpApiEndpoint.post("create", "/api/api-keys").setPayload(CreateApiKeyBody).addSuccess(CreatedApiKey, { status: 201 }).annotateContext(OpenApi.annotations({
563
+ title: "Create API key",
564
+ description: "Mint a new API key for the active organization. The plaintext key is returned ONCE; only its hash is stored"
565
+ }))).add(HttpApiEndpoint.del("revoke")`/api/api-keys/${idParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
566
+ title: "Revoke API key",
567
+ description: "Delete an API key by id (org-scoped; no cross-organization deletes)"
568
+ }))).addError(NotFound).addError(Forbidden).annotateContext(OpenApi.annotations({
569
+ title: "API Keys",
570
+ description: "IAM-gated organization API key mint / list / revoke"
571
+ })) {};
572
+
529
573
  //#endregion
530
574
  //#region ../../packages/api/src/domain/apple-team.ts
531
575
  const AppleTeamType = Schema.Literal("IN_HOUSE", "COMPANY_ORGANIZATION", "INDIVIDUAL");
@@ -860,7 +904,7 @@ var AssetsGroup = class extends HttpApiGroup.make("assets").add(HttpApiEndpoint.
860
904
 
861
905
  //#endregion
862
906
  //#region ../../packages/api/src/domain/audit-log.ts
863
- const AuditLogResourceType = Schema.Literal("project", "branch", "channel", "update", "build", "appleCredential", "androidCredential", "iosBundleConfiguration", "envVar", "device", "webhook", "iosAppMetadata", "submission", "vaultAccess");
907
+ const AuditLogResourceType = Schema.Literal("project", "branch", "channel", "update", "build", "appleCredential", "androidCredential", "iosBundleConfiguration", "envVar", "device", "webhook", "iosAppMetadata", "submission", "vaultAccess", "policy", "group", "policyAttachment", "apiKey", "invitation", "member", "organization");
864
908
  const AuditLogSource = Schema.Literal("session", "api-key");
865
909
  var AuditLog = class extends Schema.Class("AuditLog")({
866
910
  id: Id,
@@ -1656,6 +1700,97 @@ var GoogleServiceAccountKeysGroup = class extends HttpApiGroup.make("googleServi
1656
1700
  description: "Manage Google Play + FCM service account JSON keys"
1657
1701
  })) {};
1658
1702
 
1703
+ //#endregion
1704
+ //#region ../../packages/api/src/domain/group.ts
1705
+ var Group = class extends Schema.Class("Group")({
1706
+ id: Id,
1707
+ organizationId: Id,
1708
+ name: Schema.NonEmptyString,
1709
+ description: Schema.NullOr(Schema.String),
1710
+ createdAt: DateTimeString,
1711
+ updatedAt: Schema.NullOr(DateTimeString)
1712
+ }) {};
1713
+ const CreateGroupBody = Schema.Struct({
1714
+ name: Schema.NonEmptyString,
1715
+ description: Schema.optional(Schema.String)
1716
+ });
1717
+ const UpdateGroupBody = Schema.Struct({
1718
+ name: Schema.optional(Schema.NonEmptyString),
1719
+ description: Schema.optional(Schema.NullOr(Schema.String))
1720
+ });
1721
+ var GroupMember = class extends Schema.Class("GroupMember")({
1722
+ memberId: Id,
1723
+ createdAt: DateTimeString
1724
+ }) {};
1725
+ const AddGroupMemberBody = Schema.Struct({ memberId: Id });
1726
+
1727
+ //#endregion
1728
+ //#region ../../packages/api/src/groups/groups.ts
1729
+ /** `:memberId` path parameter — the `member.id` of a group member. */
1730
+ const memberIdParam = HttpApiSchema.param("memberId", Id);
1731
+ var GroupsGroup = class extends HttpApiGroup.make("groups").add(HttpApiEndpoint.get("list", "/api/groups").addSuccess(Schema.Struct({ items: Schema.Array(Group) })).annotateContext(OpenApi.annotations({
1732
+ title: "List groups",
1733
+ description: "List member groups in the active organization"
1734
+ }))).add(HttpApiEndpoint.post("create", "/api/groups").setPayload(CreateGroupBody).addSuccess(Group, { status: 201 }).annotateContext(OpenApi.annotations({
1735
+ title: "Create group",
1736
+ description: "Create a member group for the active organization"
1737
+ }))).add(HttpApiEndpoint.get("get")`/api/groups/${idParam}`.addSuccess(Group).annotateContext(OpenApi.annotations({
1738
+ title: "Get group",
1739
+ description: "Fetch a single group by id"
1740
+ }))).add(HttpApiEndpoint.patch("update")`/api/groups/${idParam}`.setPayload(UpdateGroupBody).addSuccess(Group).annotateContext(OpenApi.annotations({
1741
+ title: "Update group",
1742
+ description: "Update a group's name or description"
1743
+ }))).add(HttpApiEndpoint.del("delete")`/api/groups/${idParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
1744
+ title: "Delete group",
1745
+ description: "Delete a group and sweep its memberships and policy attachments"
1746
+ }))).add(HttpApiEndpoint.get("listMembers")`/api/groups/${idParam}/members`.addSuccess(Schema.Struct({ items: Schema.Array(GroupMember) })).annotateContext(OpenApi.annotations({
1747
+ title: "List group members",
1748
+ description: "List the members belonging to a group"
1749
+ }))).add(HttpApiEndpoint.post("addMember")`/api/groups/${idParam}/members`.setPayload(AddGroupMemberBody).addSuccess(GroupMember, { status: 201 }).annotateContext(OpenApi.annotations({
1750
+ title: "Add group member",
1751
+ description: "Add an organization member to a group"
1752
+ }))).add(HttpApiEndpoint.del("removeMember")`/api/groups/${idParam}/members/${memberIdParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
1753
+ title: "Remove group member",
1754
+ description: "Remove a member from a group"
1755
+ }))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
1756
+ title: "Groups",
1757
+ description: "Member groups for collective policy attachment"
1758
+ })) {};
1759
+
1760
+ //#endregion
1761
+ //#region ../../packages/api/src/domain/invitation.ts
1762
+ var Invitation = class extends Schema.Class("Invitation")({
1763
+ id: Id,
1764
+ email: Schema.String,
1765
+ role: Schema.NullOr(Schema.String),
1766
+ status: Schema.String,
1767
+ expiresAt: DateTimeString,
1768
+ createdAt: DateTimeString
1769
+ }) {};
1770
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
1771
+ const InvitableRole = Schema.Literal("member");
1772
+ const CreateInvitationBody = Schema.Struct({
1773
+ email: Schema.String.pipe(Schema.minLength(1), Schema.maxLength(320), Schema.pattern(EMAIL_PATTERN)),
1774
+ role: Schema.optional(InvitableRole)
1775
+ });
1776
+ const InvitationList = Schema.Struct({ items: Schema.Array(Invitation) });
1777
+
1778
+ //#endregion
1779
+ //#region ../../packages/api/src/groups/invitations.ts
1780
+ var InvitationsGroup = class extends HttpApiGroup.make("invitations").add(HttpApiEndpoint.get("list", "/api/invitations").addSuccess(InvitationList).annotateContext(OpenApi.annotations({
1781
+ title: "List invitations",
1782
+ description: "List the active organization's invitations (all statuses, newest first)"
1783
+ }))).add(HttpApiEndpoint.post("create", "/api/invitations").setPayload(CreateInvitationBody).addSuccess(Invitation, { status: 201 }).annotateContext(OpenApi.annotations({
1784
+ title: "Create invitation",
1785
+ description: "Invite a member to the active organization. Writes a pending `invitation` row (better-auth's accept-invitation consumes it) and sends the invite email"
1786
+ }))).add(HttpApiEndpoint.del("cancel")`/api/invitations/${idParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
1787
+ title: "Cancel invitation",
1788
+ description: "Cancel a pending invitation by id (org-scoped). A canceled invitation can no longer be accepted"
1789
+ }))).addError(NotFound).addError(Forbidden).annotateContext(OpenApi.annotations({
1790
+ title: "Invitations",
1791
+ description: "IAM-gated organization invitation create / list / cancel"
1792
+ })) {};
1793
+
1659
1794
  //#endregion
1660
1795
  //#region ../../packages/api/src/domain/ios-app-metadata.ts
1661
1796
  const AscAppId = Schema.String.pipe(Schema.pattern(/^[0-9]{1,30}$/u, { message: () => "ASC App ID must be 1-30 digits" }));
@@ -1792,7 +1927,13 @@ const Me = Schema.Struct({
1792
1927
  /** Authentication source — "session" for browser + CLI sessions, "api-key" for API-key (CI) bearer tokens. */
1793
1928
  source: Schema.Literal("session", "api-key"),
1794
1929
  /** Email or descriptor identifying the actor — useful when `user` is null (api-key auth). */
1795
- actorEmail: Schema.String
1930
+ actorEmail: Schema.String,
1931
+ /** Holds `invitation:create` on `org` — gates the Invite button. */
1932
+ canInviteMembers: Schema.Boolean,
1933
+ /** Holds `member:delete` on `org` — gates the per-member Remove action. */
1934
+ canRemoveMembers: Schema.Boolean,
1935
+ /** Holds `policy:update` on `org` — gates the per-member Manage-policies action. */
1936
+ canManagePolicies: Schema.Boolean
1796
1937
  });
1797
1938
 
1798
1939
  //#endregion
@@ -1805,6 +1946,16 @@ var MeGroup = class extends HttpApiGroup.make("me").add(HttpApiEndpoint.get("get
1805
1946
  description: "Current authenticated actor information"
1806
1947
  })) {};
1807
1948
 
1949
+ //#endregion
1950
+ //#region ../../packages/api/src/groups/members.ts
1951
+ var MembersGroup = class extends HttpApiGroup.make("members").add(HttpApiEndpoint.del("remove")`/api/members/${idParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
1952
+ title: "Remove member",
1953
+ description: "Remove a member from the active organization by member id (org-scoped; no cross-organization removes). Rejects removing the last owner (409). Membership role is `owner | member`; admin/developer/viewer access comes from policy attachments, not the role"
1954
+ }))).addError(NotFound).addError(Conflict).addError(Forbidden).annotateContext(OpenApi.annotations({
1955
+ title: "Members",
1956
+ description: "IAM-gated organization member removal"
1957
+ })) {};
1958
+
1808
1959
  //#endregion
1809
1960
  //#region ../../packages/api/src/groups/org-vault.ts
1810
1961
  /** `:keyId` path parameter — a registered recipient's `user_encryption_keys.id`. */
@@ -1835,6 +1986,134 @@ var OrgVaultGroup = class extends HttpApiGroup.make("orgVault").add(HttpApiEndpo
1835
1986
  description: "Manage the organization's end-to-end encrypted vault key wraps"
1836
1987
  })) {};
1837
1988
 
1989
+ //#endregion
1990
+ //#region ../../packages/api/src/domain/organization.ts
1991
+ var Organization = class extends Schema.Class("Organization")({
1992
+ id: Id,
1993
+ name: Schema.String,
1994
+ slug: Schema.String
1995
+ }) {};
1996
+ const UpdateOrganizationBody = Schema.Struct({
1997
+ name: Schema.optional(Schema.String.pipe(Schema.minLength(1), Schema.maxLength(120))),
1998
+ slug: Schema.optional(Schema.String.pipe(Schema.minLength(1), Schema.maxLength(120)))
1999
+ });
2000
+
2001
+ //#endregion
2002
+ //#region ../../packages/api/src/groups/organization.ts
2003
+ var OrganizationGroup = class extends HttpApiGroup.make("organization").add(HttpApiEndpoint.patch("update", "/api/organization").setPayload(UpdateOrganizationBody).addSuccess(Organization).annotateContext(OpenApi.annotations({
2004
+ title: "Update organization",
2005
+ description: "Rename / re-slug the active organization (IAM-gated by organization:update)"
2006
+ }))).addError(NotFound).addError(Conflict).addError(Forbidden).annotateContext(OpenApi.annotations({
2007
+ title: "Organization",
2008
+ description: "IAM-gated active-organization settings"
2009
+ })) {};
2010
+
2011
+ //#endregion
2012
+ //#region ../../packages/api/src/domain/policy.ts
2013
+ /** Whether a statement grants or denies the matched actions. */
2014
+ const PolicyEffect = Schema.Literal("allow", "deny");
2015
+ const PolicyStatement = Schema.Struct({
2016
+ effect: PolicyEffect,
2017
+ actions: Schema.Array(Schema.String).pipe(Schema.minItems(1)),
2018
+ resources: Schema.Array(Schema.String).pipe(Schema.minItems(1))
2019
+ });
2020
+ const PolicyDocument = Schema.Struct({ statements: Schema.Array(PolicyStatement) });
2021
+ var Policy = class extends Schema.Class("Policy")({
2022
+ id: Id,
2023
+ organizationId: Id,
2024
+ name: Schema.NonEmptyString,
2025
+ description: Schema.NullOr(Schema.String),
2026
+ document: PolicyDocument,
2027
+ createdAt: DateTimeString,
2028
+ updatedAt: Schema.NullOr(DateTimeString)
2029
+ }) {};
2030
+ const CreatePolicyBody = Schema.Struct({
2031
+ name: Schema.NonEmptyString,
2032
+ description: Schema.optional(Schema.String),
2033
+ document: PolicyDocument
2034
+ });
2035
+ const UpdatePolicyBody = Schema.Struct({
2036
+ name: Schema.optional(Schema.NonEmptyString),
2037
+ description: Schema.optional(Schema.NullOr(Schema.String)),
2038
+ document: Schema.optional(PolicyDocument)
2039
+ });
2040
+
2041
+ //#endregion
2042
+ //#region ../../packages/api/src/groups/policies.ts
2043
+ var PoliciesGroup = class extends HttpApiGroup.make("policies").add(HttpApiEndpoint.get("list", "/api/policies").addSuccess(Schema.Struct({ items: Schema.Array(Policy) })).annotateContext(OpenApi.annotations({
2044
+ title: "List policies",
2045
+ description: "List policies in the active organization, merging the read-only managed presets (admin/developer/viewer) into the list"
2046
+ }))).add(HttpApiEndpoint.post("create", "/api/policies").setPayload(CreatePolicyBody).addSuccess(Policy, { status: 201 }).annotateContext(OpenApi.annotations({
2047
+ title: "Create policy",
2048
+ description: "Create a named IAM policy document for the active organization"
2049
+ }))).add(HttpApiEndpoint.get("get")`/api/policies/${idParam}`.addSuccess(Policy).annotateContext(OpenApi.annotations({
2050
+ title: "Get policy",
2051
+ description: "Fetch a single policy by id, resolving real ids or managed:* preset ids"
2052
+ }))).add(HttpApiEndpoint.patch("update")`/api/policies/${idParam}`.setPayload(UpdatePolicyBody).addSuccess(Policy).annotateContext(OpenApi.annotations({
2053
+ title: "Update policy",
2054
+ description: "Update a policy's name, description, or document; managed:* ids are rejected"
2055
+ }))).add(HttpApiEndpoint.del("delete")`/api/policies/${idParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
2056
+ title: "Delete policy",
2057
+ description: "Delete a policy and sweep its attachments; managed:* ids are rejected"
2058
+ }))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
2059
+ title: "Policies",
2060
+ description: "IAM policy documents (named, reusable permission grants)"
2061
+ })) {};
2062
+
2063
+ //#endregion
2064
+ //#region ../../packages/api/src/domain/policy-attachment.ts
2065
+ /** Whether an attachment binds a policy to a member, a group, or an api-key. */
2066
+ const PrincipalType = Schema.Literal("member", "group", "apikey");
2067
+ var PolicyAttachment = class extends Schema.Class("PolicyAttachment")({
2068
+ id: Id,
2069
+ organizationId: Id,
2070
+ policyId: Schema.String,
2071
+ principalType: PrincipalType,
2072
+ principalId: Id,
2073
+ createdAt: DateTimeString
2074
+ }) {};
2075
+ const AttachPolicyBody = Schema.Struct({ policyId: Schema.String });
2076
+
2077
+ //#endregion
2078
+ //#region ../../packages/api/src/groups/policy-attachments.ts
2079
+ /**
2080
+ * `:policyId` path parameter — a real `policy.id` or a managed preset id. Managed
2081
+ * ids contain a colon (`managed:admin`); the single path segment matches it as-is,
2082
+ * and clients URL-encode the colon when building the path.
2083
+ */
2084
+ const policyIdParam = HttpApiSchema.param("policyId", Schema.String);
2085
+ var PolicyAttachmentsGroup = class extends HttpApiGroup.make("policy-attachments").add(HttpApiEndpoint.get("listForMember")`/api/members/${idParam}/policies`.addSuccess(Schema.Struct({ items: Schema.Array(PolicyAttachment) })).annotateContext(OpenApi.annotations({
2086
+ title: "List member policy attachments",
2087
+ description: "List policies attached directly to an organization member"
2088
+ }))).add(HttpApiEndpoint.post("attachToMember")`/api/members/${idParam}/policies`.setPayload(AttachPolicyBody).addSuccess(PolicyAttachment, { status: 201 }).annotateContext(OpenApi.annotations({
2089
+ title: "Attach policy to member",
2090
+ description: "Attach a policy (real or managed) directly to a member"
2091
+ }))).add(HttpApiEndpoint.del("detachFromMember")`/api/members/${idParam}/policies/${policyIdParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
2092
+ title: "Detach policy from member",
2093
+ description: "Remove a policy attachment from a member"
2094
+ }))).add(HttpApiEndpoint.get("listForGroup")`/api/groups/${idParam}/policies`.addSuccess(Schema.Struct({ items: Schema.Array(PolicyAttachment) })).annotateContext(OpenApi.annotations({
2095
+ title: "List group policy attachments",
2096
+ description: "List policies attached to a group"
2097
+ }))).add(HttpApiEndpoint.post("attachToGroup")`/api/groups/${idParam}/policies`.setPayload(AttachPolicyBody).addSuccess(PolicyAttachment, { status: 201 }).annotateContext(OpenApi.annotations({
2098
+ title: "Attach policy to group",
2099
+ description: "Attach a policy (real or managed) to a group; members inherit it"
2100
+ }))).add(HttpApiEndpoint.del("detachFromGroup")`/api/groups/${idParam}/policies/${policyIdParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
2101
+ title: "Detach policy from group",
2102
+ description: "Remove a policy attachment from a group"
2103
+ }))).add(HttpApiEndpoint.get("listForApiKey")`/api/api-keys/${idParam}/policies`.addSuccess(Schema.Struct({ items: Schema.Array(PolicyAttachment) })).annotateContext(OpenApi.annotations({
2104
+ title: "List api-key policy attachments",
2105
+ description: "List policies attached to an api-key principal"
2106
+ }))).add(HttpApiEndpoint.post("attachToApiKey")`/api/api-keys/${idParam}/policies`.setPayload(AttachPolicyBody).addSuccess(PolicyAttachment, { status: 201 }).annotateContext(OpenApi.annotations({
2107
+ title: "Attach policy to api-key",
2108
+ description: "Attach a policy (real or managed) to an api-key principal"
2109
+ }))).add(HttpApiEndpoint.del("detachFromApiKey")`/api/api-keys/${idParam}/policies/${policyIdParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
2110
+ title: "Detach policy from api-key",
2111
+ description: "Remove a policy attachment from an api-key principal"
2112
+ }))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
2113
+ title: "Policy Attachments",
2114
+ description: "Bindings of policies to member, group, and api-key principals"
2115
+ })) {};
2116
+
1838
2117
  //#endregion
1839
2118
  //#region ../../packages/api/src/domain/project.ts
1840
2119
  var Project = class extends Schema.Class("Project")({
@@ -2155,7 +2434,7 @@ var WebhooksGroup = class extends HttpApiGroup.make("webhooks").add(HttpApiEndpo
2155
2434
 
2156
2435
  //#endregion
2157
2436
  //#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({
2437
+ 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(PoliciesGroup).add(GroupsGroup).add(PolicyAttachmentsGroup).add(ApiKeysGroup).add(InvitationsGroup).add(MembersGroup).add(OrganizationGroup).add(AdminGroup).middleware(Authentication).annotateContext(OpenApi.annotations({
2159
2438
  title: "Better Update Management API",
2160
2439
  version: "1.0.0",
2161
2440
  description: "Management API for OTA update publishing, deployment, and analytics"
@@ -2185,6 +2464,151 @@ var ProtocolApi = class extends HttpApi.make("protocol-api").add(ManifestGroup).
2185
2464
  description: "Expo Updates protocol endpoints (unauthenticated)"
2186
2465
  })) {};
2187
2466
 
2467
+ //#endregion
2468
+ //#region ../../packages/api/src/domain/policy-selector.ts
2469
+ /**
2470
+ * Pure, framework-agnostic validators for the IAM path-glob SELECTOR GRAMMAR and
2471
+ * the action-token shape. Shared by web / CLI / server so all three reject bad
2472
+ * input identically. This is INPUT-SHAPE validation only — it is distinct from
2473
+ * the server matching algorithm (`selectorMatches`), which is NOT duplicated here.
2474
+ */
2475
+ /** A selector segment: `*` or a non-empty token of `[A-Za-z0-9._:-]`. */
2476
+ const SEGMENT_PATTERN = /^[A-Za-z0-9._:-]+$/u;
2477
+ /**
2478
+ * An action token of `"<word>:<word>"` or `"<word>:*"`, where a word is a
2479
+ * non-empty run of `[A-Za-z0-9_-]`. The standalone `"*"` token is handled
2480
+ * separately in `isValidActionTokenShape`.
2481
+ */
2482
+ const ACTION_TOKEN_PATTERN = /^[A-Za-z0-9_-]+:(?:\*|[A-Za-z0-9_-]+)$/u;
2483
+ const isValidSegment = (segment) => segment === "*" || SEGMENT_PATTERN.test(segment);
2484
+ /**
2485
+ * A resource selector is valid when it is `"*"` OR is slash-joined segments where
2486
+ * each segment is `"*"` or a non-empty token of `[A-Za-z0-9._:-]`. Empty segments
2487
+ * (leading/trailing/double slashes) are rejected.
2488
+ */
2489
+ const isValidSelector = (selector) => {
2490
+ if (selector === "*") return true;
2491
+ if (selector.length === 0) return false;
2492
+ return selector.split("/").every(isValidSegment);
2493
+ };
2494
+ /**
2495
+ * An action token is valid in SHAPE when it is `"*"`, `"<word>:<word>"`, or
2496
+ * `"<word>:*"`. The server still validates the token against the real
2497
+ * resource/action vocabulary — this only guards the grammar.
2498
+ */
2499
+ const isValidActionTokenShape = (token) => token === "*" || ACTION_TOKEN_PATTERN.test(token);
2500
+ const ID = "@";
2501
+ const CANONICAL_TEMPLATES = [
2502
+ ["org"],
2503
+ ["project", ID],
2504
+ [
2505
+ "project",
2506
+ ID,
2507
+ "build"
2508
+ ],
2509
+ [
2510
+ "project",
2511
+ ID,
2512
+ "build",
2513
+ ID
2514
+ ],
2515
+ [
2516
+ "project",
2517
+ ID,
2518
+ "credential"
2519
+ ],
2520
+ [
2521
+ "project",
2522
+ ID,
2523
+ "credential",
2524
+ ID
2525
+ ],
2526
+ [
2527
+ "project",
2528
+ ID,
2529
+ "submission"
2530
+ ],
2531
+ [
2532
+ "project",
2533
+ ID,
2534
+ "submission",
2535
+ ID
2536
+ ],
2537
+ [
2538
+ "project",
2539
+ ID,
2540
+ "env",
2541
+ ID
2542
+ ],
2543
+ [
2544
+ "project",
2545
+ ID,
2546
+ "env",
2547
+ ID,
2548
+ "envVar"
2549
+ ],
2550
+ [
2551
+ "project",
2552
+ ID,
2553
+ "env",
2554
+ ID,
2555
+ "envVar",
2556
+ ID
2557
+ ],
2558
+ [
2559
+ "project",
2560
+ ID,
2561
+ "channel",
2562
+ ID
2563
+ ],
2564
+ [
2565
+ "project",
2566
+ ID,
2567
+ "channel",
2568
+ ID,
2569
+ "update"
2570
+ ],
2571
+ [
2572
+ "project",
2573
+ ID,
2574
+ "channel",
2575
+ ID,
2576
+ "update",
2577
+ ID
2578
+ ],
2579
+ [
2580
+ "project",
2581
+ ID,
2582
+ "channel",
2583
+ ID,
2584
+ "rollout"
2585
+ ],
2586
+ [
2587
+ "project",
2588
+ ID,
2589
+ "channel",
2590
+ ID,
2591
+ "rollout",
2592
+ ID
2593
+ ]
2594
+ ];
2595
+ const matchesTemplate = (template, segments) => template.length === segments.length && template.every((slot, index) => {
2596
+ const segment = segments[index];
2597
+ return segment !== void 0 && (slot === ID || segment === slot || segment === "*");
2598
+ });
2599
+ /**
2600
+ * True when a (shape-valid) selector matches one of the canonical resource-path
2601
+ * templates the server can actually produce — so a typo'd or pluralised segment
2602
+ * (e.g. `"project/A/channels/X"`) is caught at policy-write time instead of being
2603
+ * stored as a silently inert policy that can never match. The standalone `"*"`
2604
+ * matches everything and is always canonical.
2605
+ */
2606
+ const isCanonicalSelector = (selector) => {
2607
+ if (selector === "*") return true;
2608
+ const segments = selector.split("/");
2609
+ return CANONICAL_TEMPLATES.some((template) => matchesTemplate(template, segments));
2610
+ };
2611
+
2188
2612
  //#endregion
2189
2613
  //#region src/lib/exit-codes.ts
2190
2614
  var AuthRequiredError = class extends Data.TaggedError("AuthRequiredError") {};
@@ -2221,8 +2645,8 @@ var FingerprintMismatchError = class extends Data.TaggedError("FingerprintMismat
2221
2645
 
2222
2646
  //#endregion
2223
2647
  //#region ../../packages/type-guards/src/index.ts
2224
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2225
- const asRecord = (value) => isRecord(value) ? value : void 0;
2648
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2649
+ const asRecord = (value) => isRecord$1(value) ? value : void 0;
2226
2650
  const toOptional = (value) => value ?? void 0;
2227
2651
  const toDbNull = (value) => value ?? null;
2228
2652
  const compact = (obj) => Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== void 0));
@@ -2280,7 +2704,7 @@ const AuthStoreLive = Layer.effect(AuthStore, Effect.gen(function* () {
2280
2704
  try: () => JSON.parse(content),
2281
2705
  catch: () => new AuthRequiredError({ message: "Corrupted auth file. Run `better-update login` to re-authenticate." })
2282
2706
  });
2283
- if (!isRecord(parsed)) return yield* new AuthRequiredError({ message: "Invalid auth file. Run `better-update login` to re-authenticate." });
2707
+ if (!isRecord$1(parsed)) return yield* new AuthRequiredError({ message: "Invalid auth file. Run `better-update login` to re-authenticate." });
2284
2708
  const { token } = parsed;
2285
2709
  if (typeof token !== "string") return yield* new AuthRequiredError({ message: "Invalid auth file. Run `better-update login` to re-authenticate." });
2286
2710
  return token;
@@ -2308,13 +2732,13 @@ const ConfigStoreLive = Layer.effect(ConfigStore, Effect.gen(function* () {
2308
2732
  const runtime = yield* CliRuntime;
2309
2733
  const homeDirectory = yield* runtime.homeDirectory;
2310
2734
  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({
2735
+ const readConfig = fs.readFileString(configFile).pipe(Effect.orElseSucceed(() => ""), Effect.flatMap((content) => content.length === 0 ? Effect.void : Effect.try({
2312
2736
  try: () => JSON.parse(content),
2313
2737
  catch: (cause) => new ConfigStoreParseError({
2314
2738
  message: "Config file contains invalid JSON",
2315
2739
  cause
2316
2740
  })
2317
- }).pipe(Effect.map((parsed) => isRecord(parsed) ? parsed : void 0), Effect.catchAll(() => Effect.succeed(void 0)))));
2741
+ }).pipe(Effect.map((parsed) => isRecord$1(parsed) ? parsed : void 0), Effect.orElseSucceed(() => void 0))));
2318
2742
  return {
2319
2743
  getBaseUrl: Effect.gen(function* () {
2320
2744
  const envUrl = yield* runtime.getEnv("BETTER_UPDATE_URL");
@@ -2537,10 +2961,10 @@ const AppleSessionStoreLive = Layer.effect(AppleSessionStore, Effect.gen(functio
2537
2961
  const usernameFile = path.join(sessionDir, "apple-username.json");
2538
2962
  return {
2539
2963
  loadSession: Effect.gen(function* () {
2540
- const content = yield* fs.readFileString(sessionFile).pipe(Effect.catchAll(() => Effect.succeed(null)));
2964
+ const content = yield* fs.readFileString(sessionFile).pipe(Effect.orElseSucceed(() => null));
2541
2965
  if (!content) return null;
2542
2966
  const parsed = safeJsonParse(content);
2543
- if (!isRecord(parsed)) return null;
2967
+ if (!isRecord$1(parsed)) return null;
2544
2968
  if (typeof parsed["username"] !== "string" || !parsed["cookies"]) return null;
2545
2969
  return {
2546
2970
  cookies: parsed["cookies"],
@@ -2555,10 +2979,10 @@ const AppleSessionStoreLive = Layer.effect(AppleSessionStore, Effect.gen(functio
2555
2979
  }).pipe(Effect.mapError((cause) => new AppleAuthError$1({ message: `Failed to save Apple session: ${formatCause(cause)}` }))),
2556
2980
  clearSession: fs.remove(sessionFile).pipe(Effect.catchAll(() => Effect.void)),
2557
2981
  loadLastUsername: Effect.gen(function* () {
2558
- const content = yield* fs.readFileString(usernameFile).pipe(Effect.catchAll(() => Effect.succeed(null)));
2982
+ const content = yield* fs.readFileString(usernameFile).pipe(Effect.orElseSucceed(() => null));
2559
2983
  if (!content) return null;
2560
2984
  const parsed = safeJsonParse(content);
2561
- if (!isRecord(parsed) || typeof parsed["username"] !== "string") return null;
2985
+ if (!isRecord$1(parsed) || typeof parsed["username"] !== "string") return null;
2562
2986
  return parsed["username"];
2563
2987
  }),
2564
2988
  saveLastUsername: (username) => Effect.gen(function* () {
@@ -2651,7 +3075,7 @@ const interactiveLogin = (appleUtils, options, cachedUsername) => Effect.gen(fun
2651
3075
  const tryRestore = (appleUtils, store) => Effect.gen(function* () {
2652
3076
  const stored = yield* store.loadSession;
2653
3077
  if (stored === null) return null;
2654
- const restored = yield* restoreFromCookies(appleUtils, stored.cookies).pipe(Effect.catchAll(() => Effect.succeed(null)));
3078
+ const restored = yield* restoreFromCookies(appleUtils, stored.cookies).pipe(Effect.orElseSucceed(() => null));
2655
3079
  if (restored === null) return null;
2656
3080
  return yield* resolveSessionTeam(appleUtils, restored);
2657
3081
  });
@@ -2670,7 +3094,7 @@ const makeAppleAuthLive = (appleUtils = defaultAppleUtils) => Layer.effect(Apple
2670
3094
  whoami: Effect.gen(function* () {
2671
3095
  const stored = yield* store.loadSession;
2672
3096
  if (stored === null) return null;
2673
- const restored = yield* restoreFromCookies(appleUtils, stored.cookies).pipe(Effect.catchAll(() => Effect.succeed(null)));
3097
+ const restored = yield* restoreFromCookies(appleUtils, stored.cookies).pipe(Effect.orElseSucceed(() => null));
2674
3098
  if (restored !== null) return sessionFromAuthState(restored);
2675
3099
  const info = appleUtils.Session.getAnySessionInfo();
2676
3100
  return info === null ? null : sessionFromInfo(stored.username, info);
@@ -2741,13 +3165,13 @@ const BsdiffServiceLive = Layer.succeed(BsdiffService, { diff: (input) => Effect
2741
3165
 
2742
3166
  //#endregion
2743
3167
  //#region src/services/identity-store.ts
2744
- const isArgon2Params = (value) => isRecord(value) && typeof value["time"] === "number" && typeof value["memory"] === "number" && typeof value["parallelism"] === "number";
3168
+ const isArgon2Params = (value) => isRecord$1(value) && typeof value["time"] === "number" && typeof value["memory"] === "number" && typeof value["parallelism"] === "number";
2745
3169
  /**
2746
3170
  * Structural guard for the on-disk identity envelope. A corrupt or foreign file
2747
3171
  * reads as "absent" so the CLI prompts to (re)create rather than crashing — the
2748
3172
  * AAD-bound `openIdentity` still fails loudly if a well-formed file was tampered.
2749
3173
  */
2750
- const isIdentityFile = (value) => isRecord(value) && value["version"] === 1 && typeof value["publicKey"] === "string" && typeof value["fingerprint"] === "string" && value["kdf"] === "argon2id" && isArgon2Params(value["kdfParams"]) && typeof value["salt"] === "string" && value["cipher"] === "xchacha20poly1305" && typeof value["ct"] === "string";
3174
+ const isIdentityFile = (value) => isRecord$1(value) && value["version"] === 1 && typeof value["publicKey"] === "string" && typeof value["fingerprint"] === "string" && value["kdf"] === "argon2id" && isArgon2Params(value["kdfParams"]) && typeof value["salt"] === "string" && value["cipher"] === "xchacha20poly1305" && typeof value["ct"] === "string";
2751
3175
  var IdentityStore = class extends Context.Tag("cli/IdentityStore")() {};
2752
3176
  const IdentityStoreLive = Layer.effect(IdentityStore, Effect.gen(function* () {
2753
3177
  const fs = yield* FileSystem.FileSystem;
@@ -2756,7 +3180,7 @@ const IdentityStoreLive = Layer.effect(IdentityStore, Effect.gen(function* () {
2756
3180
  const identityFile = path.join(identityDir, "identity.json");
2757
3181
  return {
2758
3182
  load: Effect.gen(function* () {
2759
- const content = yield* fs.readFileString(identityFile).pipe(Effect.catchAll(() => Effect.succeed("")));
3183
+ const content = yield* fs.readFileString(identityFile).pipe(Effect.orElseSucceed(() => ""));
2760
3184
  if (content.length === 0) return null;
2761
3185
  const parsed = safeJsonParse(content);
2762
3186
  return isIdentityFile(parsed) ? parsed : null;
@@ -2948,7 +3372,7 @@ const UpdateAssetUploaderLive = Layer.effect(UpdateAssetUploader, Effect.gen(fun
2948
3372
  const VAULT_CACHE_TTL_MS = 900 * 1e3;
2949
3373
  /** Keychain service name; the account is the recipient's public key. */
2950
3374
  const KEYCHAIN_SERVICE = "better-update-vault";
2951
- const isCachedVaultEntry = (value) => isRecord(value) && typeof value["vaultKey"] === "string" && typeof value["vaultVersion"] === "number" && typeof value["keyId"] === "string" && typeof value["exp"] === "number";
3375
+ const isCachedVaultEntry = (value) => isRecord$1(value) && typeof value["vaultKey"] === "string" && typeof value["vaultVersion"] === "number" && typeof value["keyId"] === "string" && typeof value["exp"] === "number";
2952
3376
  /** Serialize an unlocked vault into a keychain blob, stamping a TTL from `now`. */
2953
3377
  const encodeCacheEntry = (vault, now, ttlMs = VAULT_CACHE_TTL_MS) => JSON.stringify({
2954
3378
  vaultKey: toBase64(vault.vaultKey),
@@ -2980,7 +3404,7 @@ const VaultCacheLive = Layer.effect(VaultCache, Effect.gen(function* () {
2980
3404
  const flag = yield* runtime.getEnv("BETTER_UPDATE_NO_CACHE");
2981
3405
  return flag !== void 0 && flag.length > 0 && flag !== "0" && flag !== "false";
2982
3406
  });
2983
- const readRaw = (publicKey) => Effect.try(() => new Entry(KEYCHAIN_SERVICE, publicKey).getPassword()).pipe(Effect.catchAll(() => Effect.succeed(null)));
3407
+ const readRaw = (publicKey) => Effect.try(() => new Entry(KEYCHAIN_SERVICE, publicKey).getPassword()).pipe(Effect.orElseSucceed(() => null));
2984
3408
  const writeRaw = (publicKey, blob) => Effect.try(() => {
2985
3409
  new Entry(KEYCHAIN_SERVICE, publicKey).setPassword(blob);
2986
3410
  }).pipe(Effect.ignore);
@@ -3018,13 +3442,13 @@ const VersionCheckLive = Layer.effect(VersionCheck, Effect.gen(function* () {
3018
3442
  const cacheDir = path.join(homeDirectory, ".better-update");
3019
3443
  const cacheFile = path.join(cacheDir, "version-check.json");
3020
3444
  const readCache = Effect.gen(function* () {
3021
- const content = yield* fs.readFileString(cacheFile).pipe(Effect.catchAll(() => Effect.succeed("")));
3445
+ const content = yield* fs.readFileString(cacheFile).pipe(Effect.orElseSucceed(() => ""));
3022
3446
  if (content.length === 0) return;
3023
3447
  const parsed = yield* Effect.try({
3024
3448
  try: () => JSON.parse(content),
3025
3449
  catch: () => "parse-error"
3026
- }).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
3027
- if (isRecord(parsed) && typeof parsed["latest"] === "string" && typeof parsed["checkedAt"] === "number") return {
3450
+ }).pipe(Effect.orElseSucceed(() => void 0));
3451
+ if (isRecord$1(parsed) && typeof parsed["latest"] === "string" && typeof parsed["checkedAt"] === "number") return {
3028
3452
  latest: parsed["latest"],
3029
3453
  checkedAt: parsed["checkedAt"]
3030
3454
  };
@@ -3041,7 +3465,7 @@ const VersionCheckLive = Layer.effect(VersionCheck, Effect.gen(function* () {
3041
3465
  const response = yield* httpClient.execute(request);
3042
3466
  if (response.status < 200 || response.status >= 300) return;
3043
3467
  const body = yield* response.json;
3044
- if (!isRecord(body) || typeof body["version"] !== "string") return;
3468
+ if (!isRecord$1(body) || typeof body["version"] !== "string") return;
3045
3469
  const latest = body["version"];
3046
3470
  yield* fs.makeDirectory(cacheDir, { recursive: true });
3047
3471
  yield* fs.writeFileString(cacheFile, `${JSON.stringify({
@@ -3142,7 +3566,7 @@ const createBrowserLoginSession = (options = {}) => {
3142
3566
  if (request.method === "GET" && url.pathname === "/callback") return new Response(CALLBACK_PAGE, { headers: { "content-type": "text/html; charset=utf-8" } });
3143
3567
  if (request.method === "POST" && url.pathname === "/callback/token") try {
3144
3568
  const body = await request.json();
3145
- if (!isRecord(body)) return new Response("Invalid callback payload", { status: 400 });
3569
+ if (!isRecord$1(body)) return new Response("Invalid callback payload", { status: 400 });
3146
3570
  const token = typeof body["token"] === "string" ? body["token"].trim() : "";
3147
3571
  if (token.length === 0) return new Response("Missing token", { status: 400 });
3148
3572
  Effect.runSync(Deferred.succeed(tokenDeferred, token));
@@ -3224,7 +3648,7 @@ const buildOpenBrowserCommand = (platform, url) => {
3224
3648
  };
3225
3649
  const openBrowser = (url) => Effect.gen(function* () {
3226
3650
  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}`);
3651
+ if (!(yield* Command.exitCode(command).pipe(Effect.map((code) => code === 0), Effect.orElseSucceed(() => false)))) yield* Console.log(`Open this URL manually:\n${url}`);
3228
3652
  });
3229
3653
  const browserLogin = Effect.scoped(Effect.gen(function* () {
3230
3654
  const configStore = yield* ConfigStore;
@@ -3623,9 +4047,9 @@ const configPath = (projectRoot) => path.join(projectRoot, BETTER_UPDATE_CONFIG_
3623
4047
  * graceful `readConfig`.
3624
4048
  */
3625
4049
  const readBetterUpdateConfig = (projectRoot) => Effect.gen(function* () {
3626
- const content = yield* (yield* FileSystem.FileSystem).readFileString(configPath(projectRoot)).pipe(Effect.catchAll(() => Effect.succeed("")));
4050
+ const content = yield* (yield* FileSystem.FileSystem).readFileString(configPath(projectRoot)).pipe(Effect.orElseSucceed(() => ""));
3627
4051
  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)));
4052
+ return yield* Effect.try(() => JSON.parse(content)).pipe(Effect.map((parsed) => isRecord$1(parsed) ? parsed : void 0), Effect.orElseSucceed(() => void 0));
3629
4053
  });
3630
4054
  /**
3631
4055
  * Resolve the linked project id from `better-update.json`, or `undefined` when
@@ -4354,14 +4778,8 @@ const KeyValueFromString = Schema.transformOrFail(Schema.String, KeyValuePair, {
4354
4778
  },
4355
4779
  encode: ({ key, value }) => ParseResult.succeed(`${key}=${value}`)
4356
4780
  });
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
- });
4781
+ 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}".` })));
4782
+ const parseKeyValue = (raw) => Schema.decodeUnknown(KeyValueFromString)(raw).pipe(Effect.mapError(() => new InvalidArgumentError({ message: "Invalid format. Use KEY=VALUE (e.g. API_KEY=abc123)" })));
4365
4783
  const parseLimit = (raw, defaultValue) => {
4366
4784
  if (raw === void 0) return Effect.succeed(defaultValue);
4367
4785
  const parsed = Number(raw);
@@ -18709,7 +19127,7 @@ const asArrayBuffer$1 = (bytes) => {
18709
19127
  };
18710
19128
  const signAscJwt = (credentials) => Effect.gen(function* () {
18711
19129
  const der = pemToPkcs8Der(credentials.p8Pem);
18712
- if (der === null) return yield* Effect.fail(new AppleAuthError({ cause: /* @__PURE__ */ new Error("Invalid .p8 PEM") }));
19130
+ if (der === null) return yield* new AppleAuthError({ cause: /* @__PURE__ */ new Error("Invalid .p8 PEM") });
18713
19131
  const header = {
18714
19132
  alg: "ES256",
18715
19133
  kid: credentials.keyId,
@@ -18760,8 +19178,8 @@ var AscApiError = class extends Data.TaggedError("AscApiError") {};
18760
19178
  var AscNetworkError = class extends Data.TaggedError("AscNetworkError") {};
18761
19179
  const API_BASE = "https://api.appstoreconnect.apple.com";
18762
19180
  const extractErrors = (body) => {
18763
- if (!isRecord(body) || !Array.isArray(body["errors"])) return [];
18764
- return body["errors"].filter((value) => isRecord(value));
19181
+ if (!isRecord$1(body) || !Array.isArray(body["errors"])) return [];
19182
+ return body["errors"].filter((value) => isRecord$1(value));
18765
19183
  };
18766
19184
  const parseApiError = (response, body, raw) => {
18767
19185
  const [first] = extractErrors(body);
@@ -18793,13 +19211,13 @@ const fetchRaw = (jwt, path, init) => Effect.gen(function* () {
18793
19211
  try: () => text.length === 0 ? {} : JSON.parse(text),
18794
19212
  catch: (cause) => new AscNetworkError({ cause })
18795
19213
  });
18796
- if (!response.ok) return yield* Effect.fail(parseApiError(response, body, text));
19214
+ if (!response.ok) return yield* parseApiError(response, body, text);
18797
19215
  return body;
18798
19216
  });
18799
19217
  const toAscCertificate = (value) => {
18800
- if (!isRecord(value)) return null;
19218
+ if (!isRecord$1(value)) return null;
18801
19219
  const { id, attributes } = value;
18802
- if (typeof id !== "string" || !isRecord(attributes)) return null;
19220
+ if (typeof id !== "string" || !isRecord$1(attributes)) return null;
18803
19221
  const { serialNumber, certificateType, expirationDate, certificateContent, displayName } = attributes;
18804
19222
  if (typeof serialNumber !== "string" || typeof certificateType !== "string" || typeof expirationDate !== "string") return null;
18805
19223
  return {
@@ -18812,9 +19230,9 @@ const toAscCertificate = (value) => {
18812
19230
  };
18813
19231
  };
18814
19232
  const toAscBundleId = (value) => {
18815
- if (!isRecord(value)) return null;
19233
+ if (!isRecord$1(value)) return null;
18816
19234
  const { id, attributes } = value;
18817
- if (typeof id !== "string" || !isRecord(attributes)) return null;
19235
+ if (typeof id !== "string" || !isRecord$1(attributes)) return null;
18818
19236
  const { identifier, name } = attributes;
18819
19237
  if (typeof identifier !== "string" || typeof name !== "string") return null;
18820
19238
  return {
@@ -18834,9 +19252,9 @@ const asProfileType = (value) => {
18834
19252
  return match === void 0 ? null : match;
18835
19253
  };
18836
19254
  const toAscProfile = (value) => {
18837
- if (!isRecord(value)) return null;
19255
+ if (!isRecord$1(value)) return null;
18838
19256
  const { id, attributes } = value;
18839
- if (typeof id !== "string" || !isRecord(attributes)) return null;
19257
+ if (typeof id !== "string" || !isRecord$1(attributes)) return null;
18840
19258
  const { name, uuid, expirationDate, profileContent } = attributes;
18841
19259
  const profileType = asProfileType(attributes["profileType"]);
18842
19260
  if (typeof name !== "string" || typeof uuid !== "string" || typeof expirationDate !== "string" || typeof profileContent !== "string" || profileType === null) return null;
@@ -18850,9 +19268,9 @@ const toAscProfile = (value) => {
18850
19268
  };
18851
19269
  };
18852
19270
  const toAscDevice = (value) => {
18853
- if (!isRecord(value)) return null;
19271
+ if (!isRecord$1(value)) return null;
18854
19272
  const { id, attributes } = value;
18855
- if (typeof id !== "string" || !isRecord(attributes)) return null;
19273
+ if (typeof id !== "string" || !isRecord$1(attributes)) return null;
18856
19274
  const { udid, name } = attributes;
18857
19275
  if (typeof udid !== "string" || typeof name !== "string") return null;
18858
19276
  return {
@@ -18862,11 +19280,11 @@ const toAscDevice = (value) => {
18862
19280
  };
18863
19281
  };
18864
19282
  const extractList = (body, map) => {
18865
- if (!isRecord(body) || !Array.isArray(body["data"])) return [];
19283
+ if (!isRecord$1(body) || !Array.isArray(body["data"])) return [];
18866
19284
  return body["data"].map(map).filter((value) => value !== null);
18867
19285
  };
18868
19286
  const extractSingle = (body, map) => {
18869
- if (!isRecord(body)) return null;
19287
+ if (!isRecord$1(body)) return null;
18870
19288
  return map(body["data"]);
18871
19289
  };
18872
19290
  const malformed = (resource) => new AscApiError({
@@ -18893,12 +19311,10 @@ const createCertificate = (credentials, params) => withJwt(credentials, (jwt) =>
18893
19311
  }
18894
19312
  } })
18895
19313
  }), toAscCertificate);
18896
- if (resource === null) return yield* Effect.fail(malformed("certificate"));
19314
+ if (resource === null) return yield* malformed("certificate");
18897
19315
  return resource;
18898
19316
  }));
18899
- const deleteCertificate = (credentials, id) => withJwt(credentials, (jwt) => Effect.gen(function* () {
18900
- yield* fetchRaw(jwt, `/v1/certificates/${encodeURIComponent(id)}`, { method: "DELETE" });
18901
- }));
19317
+ const deleteCertificate = (credentials, id) => withJwt(credentials, (jwt) => Effect.asVoid(fetchRaw(jwt, `/v1/certificates/${encodeURIComponent(id)}`, { method: "DELETE" })));
18902
19318
  const listBundleIds = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
18903
19319
  return extractList(yield* fetchRaw(jwt, "/v1/bundleIds?limit=200"), toAscBundleId);
18904
19320
  }));
@@ -18914,7 +19330,7 @@ const createBundleId = (credentials, params) => withJwt(credentials, (jwt) => Ef
18914
19330
  }
18915
19331
  } })
18916
19332
  }), toAscBundleId);
18917
- if (resource === null) return yield* Effect.fail(malformed("bundleId"));
19333
+ if (resource === null) return yield* malformed("bundleId");
18918
19334
  return resource;
18919
19335
  }));
18920
19336
  const listDevices = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
@@ -18946,7 +19362,7 @@ const createProvisioningProfile = (credentials, params) => withJwt(credentials,
18946
19362
  relationships
18947
19363
  } })
18948
19364
  }), toAscProfile);
18949
- if (resource === null) return yield* Effect.fail(malformed("profile"));
19365
+ if (resource === null) return yield* malformed("profile");
18950
19366
  return resource;
18951
19367
  }));
18952
19368
  const isCertificateLimitError = (error) => {
@@ -18982,7 +19398,7 @@ const parseCert = (certDerBytes) => {
18982
19398
  const generatePassword = () => forge.util.encode64(forge.random.getBytesSync(16));
18983
19399
  const extractCertMetadata = (cert) => Effect.gen(function* () {
18984
19400
  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" }));
19401
+ if (appleTeamId === null) return yield* new CertParseError({ message: "Could not extract Apple team identifier from certificate subject" });
18986
19402
  return {
18987
19403
  serialNumber: cert.serialNumber.toUpperCase(),
18988
19404
  validFrom: cert.validity.notBefore.toISOString(),
@@ -19000,7 +19416,7 @@ const extractCertMetadata = (cert) => Effect.gen(function* () {
19000
19416
  */
19001
19417
  const extractMetadataFromP12 = (params) => Effect.gen(function* () {
19002
19418
  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" }));
19419
+ if (certBagOid === void 0) return yield* new CertParseError({ message: "PKCS#12 OID lookup for certBag failed" });
19004
19420
  const [first] = yield* Effect.try({
19005
19421
  try: () => {
19006
19422
  const p12Der = forge.util.decode64(params.p12Base64);
@@ -19009,7 +19425,7 @@ const extractMetadataFromP12 = (params) => Effect.gen(function* () {
19009
19425
  },
19010
19426
  catch: (error) => new CertParseError({ message: `Failed to parse PKCS#12 bundle: ${error instanceof Error ? error.message : String(error)}` })
19011
19427
  });
19012
- if (first?.cert === void 0) return yield* Effect.fail(new CertParseError({ message: "PKCS#12 bundle does not contain a certificate" }));
19428
+ if (first?.cert === void 0) return yield* new CertParseError({ message: "PKCS#12 bundle does not contain a certificate" });
19013
19429
  return yield* extractCertMetadata(first.cert);
19014
19430
  });
19015
19431
  const buildDistributionCertP12 = (params) => Effect.gen(function* () {
@@ -19190,10 +19606,10 @@ const generateAndUploadDistributionCertificate = (api, input) => Effect.gen(func
19190
19606
  csrPem: csrResult.csrPem,
19191
19607
  certificateType
19192
19608
  }).pipe(Effect.mapError(wrapAscError("apple-create-certificate")));
19193
- if (apple.certificateContent === null) return yield* Effect.fail(new GenerateFailedError({
19609
+ if (apple.certificateContent === null) return yield* new GenerateFailedError({
19194
19610
  step: "apple-create-certificate",
19195
19611
  message: "Apple response missing certificateContent"
19196
- }));
19612
+ });
19197
19613
  const bundle = yield* buildDistributionCertP12({
19198
19614
  certificateContentBase64: apple.certificateContent,
19199
19615
  privateKey: csrResult.privateKey
@@ -19243,10 +19659,10 @@ const revokeAppleCertificate = (api, input) => Effect.gen(function* () {
19243
19659
  });
19244
19660
  const revokeLocalDistributionCertificate = (api, input) => Effect.gen(function* () {
19245
19661
  const local = (yield* api.appleDistributionCertificates.list()).items.find((entry) => entry.id === input.distributionCertificateId);
19246
- if (local === void 0) return yield* Effect.fail(new GenerateFailedError({
19662
+ if (local === void 0) return yield* new GenerateFailedError({
19247
19663
  step: "load-distribution-certificate",
19248
19664
  message: `Distribution certificate ${input.distributionCertificateId} not found on this account`
19249
- }));
19665
+ });
19250
19666
  const creds = yield* fetchAscCredentials(api, input.ascApiKeyId);
19251
19667
  const ascCreds = {
19252
19668
  keyId: creds.keyId,
@@ -19283,10 +19699,10 @@ const listAppleCertificates = (api, input) => Effect.gen(function* () {
19283
19699
  });
19284
19700
  const resolveCertAscId = (creds, serialNumber, certificateType) => Effect.gen(function* () {
19285
19701
  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({
19702
+ if (match === void 0) return yield* new GenerateFailedError({
19287
19703
  step: "match-apple-certificate",
19288
19704
  message: `Distribution certificate ${serialNumber} not present on Apple Developer Portal; upload or re-generate it`
19289
- }));
19705
+ });
19290
19706
  return match.id;
19291
19707
  });
19292
19708
  const ensureBundleId = (creds, bundleIdentifier) => Effect.gen(function* () {
@@ -19319,10 +19735,10 @@ const generateAndUploadProvisioningProfile = (api, input) => Effect.gen(function
19319
19735
  const [certAscId, bundleIdAscId] = yield* Effect.all([resolveCertAscId(ascCreds, cert.serialNumber.toUpperCase(), certificateType), ensureBundleId(ascCreds, input.bundleIdentifier)], { concurrency: 2 });
19320
19736
  const useDevices = input.distributionType === "AD_HOC" || input.distributionType === "DEVELOPMENT";
19321
19737
  const { ids: deviceAscIds } = useDevices ? yield* collectDeviceAscIds(ascCreds, cert.appleTeamId, input.deviceIds) : { ids: [] };
19322
- if (useDevices && deviceAscIds.length === 0) return yield* Effect.fail(new GenerateFailedError({
19738
+ if (useDevices && deviceAscIds.length === 0) return yield* new GenerateFailedError({
19323
19739
  step: "collect-devices",
19324
19740
  message: "No registered devices to attach to the provisioning profile"
19325
- }));
19741
+ });
19326
19742
  const profileBytes = fromBase64((yield* createProvisioningProfile(ascCreds, {
19327
19743
  profileName: `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`,
19328
19744
  profileType: DISTRIBUTION_TO_PROFILE_TYPE$1[input.distributionType],
@@ -19633,10 +20049,10 @@ const downloadAndroidCredentials = (api, options) => Effect.gen(function* () {
19633
20049
  ...compact({ buildProfile: options.buildProfile })
19634
20050
  }
19635
20051
  }).pipe(Effect.mapError((cause) => resolveErrorToMissingCredentials(cause, "android")));
19636
- if (resolved.platform !== "android") return yield* Effect.fail(new MissingCredentialsError({
20052
+ if (resolved.platform !== "android") return yield* new MissingCredentialsError({
19637
20053
  message: "Server returned non-Android credentials for an Android build request",
19638
20054
  hint: androidBindHint
19639
- }));
20055
+ });
19640
20056
  const secret = yield* decryptResolveSecret({
19641
20057
  session: yield* openVaultSessionForBuild(api, androidBindHint),
19642
20058
  credentialType: "keystore",
@@ -19757,7 +20173,7 @@ const credentialsJsonPath = (projectRoot) => path.join(projectRoot, CREDENTIALS_
19757
20173
  const readCredentialsJson = (projectRoot) => Effect.gen(function* () {
19758
20174
  const fs = yield* FileSystem.FileSystem;
19759
20175
  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}.` });
20176
+ if (!(yield* fs.exists(filePath).pipe(Effect.orElseSucceed(() => false)))) return yield* new CredentialsJsonError({ message: `credentials.json not found at ${filePath}.` });
19761
20177
  return yield* parseCredentialsJson(yield* fs.readFileString(filePath).pipe(Effect.mapError((cause) => new CredentialsJsonError({ message: `Failed to read credentials.json: ${String(cause)}` }))));
19762
20178
  });
19763
20179
  const writeCredentialsJson = (projectRoot, data) => Effect.gen(function* () {
@@ -19774,7 +20190,7 @@ const resolveCredentialPath = (projectRoot, candidate) => path.isAbsolute(candid
19774
20190
 
19775
20191
  //#endregion
19776
20192
  //#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({
20193
+ const requirePath = (fs, absolutePath, label) => fs.exists(absolutePath).pipe(Effect.orElseSucceed(() => false), Effect.flatMap((exists) => exists ? Effect.void : Effect.fail(new MissingCredentialsError({
19778
20194
  message: `Local credentials.json: ${label} not found at ${absolutePath}.`,
19779
20195
  hint: "Run `better-update credentials sync pull` to materialize files, or fix the path in credentials.json."
19780
20196
  }))));
@@ -20029,7 +20445,7 @@ const runStepFormatted = (cmd, step, formatter) => Effect.gen(function* () {
20029
20445
  if (code !== 0) {
20030
20446
  const summary = formatter.getBuildSummary();
20031
20447
  if (summary.length > 0) process$1.stderr.write(`${summary}\n`);
20032
- return yield* Effect.fail(buildFailed(step, code, `${step} exited with code ${code}`));
20448
+ return yield* buildFailed(step, code, `${step} exited with code ${code}`);
20033
20449
  }
20034
20450
  });
20035
20451
 
@@ -20265,10 +20681,10 @@ const findAscCertificateId = (ctx, serialNumber, certificateType) => Effect.gen(
20265
20681
  const certs = yield* wrap("apple-list-certificates", async () => AppleUtils.Certificate.getAsync(ctx, { query: { filter: { certificateType } } }));
20266
20682
  const upper = serialNumber.toUpperCase();
20267
20683
  const match = certs.find((entry) => entry.attributes.serialNumber.toUpperCase() === upper);
20268
- if (match === void 0) return yield* Effect.fail(new AppleIdGenerateFailedError({
20684
+ if (match === void 0) return yield* new AppleIdGenerateFailedError({
20269
20685
  step: "match-apple-certificate",
20270
20686
  message: `Distribution certificate ${serialNumber} not present on Apple Developer Portal; upload or re-generate it`
20271
- }));
20687
+ });
20272
20688
  return match.id;
20273
20689
  });
20274
20690
  const collectIosDeviceIds = (ctx, deviceIds) => Effect.gen(function* () {
@@ -20287,10 +20703,10 @@ const generateAndUploadProvisioningProfileViaAppleId = (api, input) => Effect.ge
20287
20703
  const [certAscId, bundleIdAscId] = yield* Effect.all([findAscCertificateId(ctx, cert.serialNumber, certificateType), findOrCreateBundleId(ctx, input.bundleIdentifier)], { concurrency: 2 });
20288
20704
  const useDevices = input.distributionType === "AD_HOC" || input.distributionType === "DEVELOPMENT";
20289
20705
  const deviceIds = useDevices ? yield* collectIosDeviceIds(ctx, input.deviceIds) : [];
20290
- if (useDevices && deviceIds.length === 0) return yield* Effect.fail(new AppleIdGenerateFailedError({
20706
+ if (useDevices && deviceIds.length === 0) return yield* new AppleIdGenerateFailedError({
20291
20707
  step: "collect-devices",
20292
20708
  message: "No registered devices to attach to the provisioning profile"
20293
- }));
20709
+ });
20294
20710
  const profileName = `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`;
20295
20711
  const { profileContent } = (yield* wrap("apple-create-profile", async () => AppleUtils.Profile.createAsync(ctx, {
20296
20712
  bundleId: bundleIdAscId,
@@ -20299,10 +20715,10 @@ const generateAndUploadProvisioningProfileViaAppleId = (api, input) => Effect.ge
20299
20715
  name: profileName,
20300
20716
  profileType: DISTRIBUTION_TO_PROFILE_TYPE[input.distributionType]
20301
20717
  }))).attributes;
20302
- if (profileContent === null) return yield* Effect.fail(new AppleIdGenerateFailedError({
20718
+ if (profileContent === null) return yield* new AppleIdGenerateFailedError({
20303
20719
  step: "extract-profile-content",
20304
20720
  message: "Apple returned a profile with no content (likely expired/invalid)"
20305
- }));
20721
+ });
20306
20722
  const profileBytes = fromBase64(profileContent);
20307
20723
  const rosterHash = useDevices ? computeDeviceRosterHashHex(deviceIds) : void 0;
20308
20724
  const created = yield* api.appleProvisioningProfiles.upload({ payload: {
@@ -20482,10 +20898,10 @@ const interactiveCertLimitRecover = (api, ascApiKeyId) => Effect.gen(function* (
20482
20898
  ascApiKeyId,
20483
20899
  certificateType: "IOS_DISTRIBUTION"
20484
20900
  });
20485
- if (certs.length === 0) return yield* Effect.fail(new MissingCredentialsError({
20901
+ if (certs.length === 0) return yield* new MissingCredentialsError({
20486
20902
  message: "Apple says the certificate limit is hit but no existing certificates were returned.",
20487
20903
  hint: "Try again later or check the Apple Developer portal."
20488
- }));
20904
+ });
20489
20905
  const toRevoke = yield* promptMultiSelect("Select one or more certificates to revoke before retrying", certs.map((entry) => ({
20490
20906
  value: entry.id,
20491
20907
  label: `${entry.serialNumber.slice(0, 12)}… (${entry.displayName ?? entry.certificateType}, exp ${entry.expirationDate.slice(0, 10)})`
@@ -20498,10 +20914,10 @@ const interactiveCertLimitRecover = (api, ascApiKeyId) => Effect.gen(function* (
20498
20914
  });
20499
20915
  const generateDistributionCertInteractive = (api) => Effect.gen(function* () {
20500
20916
  const teamAscKeys = (yield* api.ascApiKeys.list()).items.filter((key) => key.appleTeamId !== null);
20501
- if (teamAscKeys.length === 0) return yield* Effect.fail(new MissingCredentialsError({
20917
+ if (teamAscKeys.length === 0) return yield* new MissingCredentialsError({
20502
20918
  message: "No ASC API key linked to an Apple team in this organization.",
20503
20919
  hint: "Upload an ASC API key with a team assignment via the dashboard, then retry."
20504
- }));
20920
+ });
20505
20921
  const ascKeyId = yield* promptSelect("Select an ASC API key to issue the certificate against", teamAscKeys.map((key) => ({
20506
20922
  value: key.id,
20507
20923
  label: `${key.name} (${key.keyId})`
@@ -20520,10 +20936,10 @@ const chooseIosCertificateId = (api) => Effect.gen(function* () {
20520
20936
  }, {
20521
20937
  value: "abort",
20522
20938
  label: "Abort — I'll upload one manually"
20523
- }])) === "abort") return yield* Effect.fail(new MissingCredentialsError({
20939
+ }])) === "abort") return yield* new MissingCredentialsError({
20524
20940
  message: "Build aborted — no distribution certificate available.",
20525
20941
  hint: "Run `better-update credentials generate distribution-certificate --asc-key-id <id>` or upload via the dashboard."
20526
- }));
20942
+ });
20527
20943
  return (yield* generateDistributionCertInteractive(api)).id;
20528
20944
  }
20529
20945
  const choice = yield* promptSelect("Select a distribution certificate (or 'generate' for a fresh one)", [{
@@ -20539,10 +20955,10 @@ const chooseIosCertificateId = (api) => Effect.gen(function* () {
20539
20955
  const pickIosCertificate = (api) => Effect.gen(function* () {
20540
20956
  const chosenId = yield* chooseIosCertificateId(api);
20541
20957
  const cert = (yield* api.appleDistributionCertificates.list()).items.find((entry) => entry.id === chosenId);
20542
- if (cert === void 0) return yield* Effect.fail(new MissingCredentialsError({
20958
+ if (cert === void 0) return yield* new MissingCredentialsError({
20543
20959
  message: "Selected certificate not found after generation.",
20544
20960
  hint: "Retry."
20545
- }));
20961
+ });
20546
20962
  return {
20547
20963
  certId: chosenId,
20548
20964
  cert
@@ -20550,10 +20966,10 @@ const pickIosCertificate = (api) => Effect.gen(function* () {
20550
20966
  });
20551
20967
  const pickIosAscKey = (api, appleTeamId) => Effect.gen(function* () {
20552
20968
  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({
20969
+ if (teamAscKeys.length === 0) return yield* new MissingCredentialsError({
20554
20970
  message: `No ASC API key linked to Apple team ${appleTeamId}.`,
20555
20971
  hint: "Upload an ASC API key for that team via the dashboard, then retry."
20556
- }));
20972
+ });
20557
20973
  return yield* promptSelect("Select an ASC API key", teamAscKeys.map((key) => ({
20558
20974
  value: key.id,
20559
20975
  label: `${key.name} (${key.keyId})`
@@ -20634,10 +21050,10 @@ const generateKeystoreInteractive = (api) => Effect.gen(function* () {
20634
21050
  });
20635
21051
  const pickExistingKeystore = (api) => Effect.gen(function* () {
20636
21052
  const keystores = yield* api.androidUploadKeystores.list();
20637
- if (keystores.items.length === 0) return yield* Effect.fail(new MissingCredentialsError({
21053
+ if (keystores.items.length === 0) return yield* new MissingCredentialsError({
20638
21054
  message: "No existing keystores in this organization.",
20639
21055
  hint: "Re-run and choose 'Generate new keystore'."
20640
- }));
21056
+ });
20641
21057
  return yield* promptSelect("Select a keystore", keystores.items.map((item) => ({
20642
21058
  value: item.id,
20643
21059
  label: item.keyAlias
@@ -20670,10 +21086,10 @@ const setupAndroidInteractive = (api, input) => Effect.gen(function* () {
20670
21086
  label: "Abort — I'll configure it in the dashboard"
20671
21087
  }
20672
21088
  ]);
20673
- if (choice === "abort") return yield* Effect.fail(new MissingCredentialsError({
21089
+ if (choice === "abort") return yield* new MissingCredentialsError({
20674
21090
  message: `Build aborted — no keystore bound to ${input.applicationIdentifier}.`,
20675
21091
  hint: "Run `better-update credentials generate keystore` or upload via the dashboard."
20676
- }));
21092
+ });
20677
21093
  const keystoreId = yield* choice === "generate" ? generateKeystoreAuto(api, input.applicationIdentifier) : pickExistingKeystore(api);
20678
21094
  yield* api.androidBuildCredentials.create({
20679
21095
  path: { applicationIdentifierId: appId },
@@ -20694,10 +21110,10 @@ const ensureAndroidCredentialsAvailable = (api, input) => api.buildCredentials.r
20694
21110
  }).pipe(Effect.asVoid);
20695
21111
  const ensureAndroidCredentials = (api, input, options) => ensureAndroidCredentialsAvailable(api, input).pipe(Effect.catchIf(isMissingResolveError, () => Effect.gen(function* () {
20696
21112
  const mode = yield* InteractiveMode;
20697
- if (options.freezeCredentials || !mode.allow) return yield* Effect.fail(new MissingCredentialsError({
21113
+ if (options.freezeCredentials || !mode.allow) return yield* new MissingCredentialsError({
20698
21114
  message: `No Android build credentials for ${input.applicationIdentifier}.`,
20699
21115
  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
- }));
21116
+ });
20701
21117
  yield* setupAndroidInteractive(api, input);
20702
21118
  return yield* ensureAndroidCredentialsAvailable(api, input);
20703
21119
  })));
@@ -20718,10 +21134,10 @@ const resolveIosBuildCredentials = (api, input) => api.buildCredentials.resolve(
20718
21134
  const findBoundIosConfig = (api, input) => Effect.gen(function* () {
20719
21135
  const distributionType = IOS_DISTRIBUTION_TO_TYPE[input.distribution];
20720
21136
  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({
21137
+ if (match === void 0) return yield* new MissingCredentialsError({
20722
21138
  message: `iOS bundle configuration vanished while regenerating stale profile for ${input.bundleIdentifier}`,
20723
21139
  hint: "Retry; the configuration must exist before regeneration"
20724
- }));
21140
+ });
20725
21141
  return match;
20726
21142
  });
20727
21143
  const regenerateProvisioningProfile = (api, input) => Effect.gen(function* () {
@@ -20752,19 +21168,19 @@ const regenerateProvisioningProfile = (api, input) => Effect.gen(function* () {
20752
21168
  });
20753
21169
  const ensureIosCredentials = (api, input, options) => resolveIosBuildCredentials(api, input).pipe(Effect.catchIf(isMissingResolveError, () => Effect.gen(function* () {
20754
21170
  const mode = yield* InteractiveMode;
20755
- if (options.freezeCredentials || !mode.allow) return yield* Effect.fail(new MissingCredentialsError({
21171
+ if (options.freezeCredentials || !mode.allow) return yield* new MissingCredentialsError({
20756
21172
  message: `No iOS build credentials for ${input.bundleIdentifier} (${input.distribution}).`,
20757
21173
  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
- }));
21174
+ });
20759
21175
  yield* setupIosInteractive(api, input);
20760
21176
  return yield* resolveIosBuildCredentials(api, input);
20761
21177
  })), Effect.flatMap((resolved) => Effect.gen(function* () {
20762
21178
  if (resolved.platform !== "ios" || !resolved.profileStale) return;
20763
21179
  const mode = yield* InteractiveMode;
20764
- if (options.freezeCredentials || !mode.allow) return yield* Effect.fail(new MissingCredentialsError({
21180
+ if (options.freezeCredentials || !mode.allow) return yield* new MissingCredentialsError({
20765
21181
  message: `Stale provisioning profile for ${input.bundleIdentifier}; cannot regenerate without an interactive session.`,
20766
21182
  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
- }));
21183
+ });
20768
21184
  yield* Console.log(`Stale provisioning profile for ${input.bundleIdentifier} (device roster changed). Regenerating...`);
20769
21185
  yield* regenerateProvisioningProfile(api, input);
20770
21186
  })));
@@ -20814,7 +21230,7 @@ const applyTargetSigning = (options) => Effect.gen(function* () {
20814
21230
  const projectDir = yield* findXcodeProjectDir$1(options.iosDir);
20815
21231
  const pbxprojPath = path.join(projectDir, "project.pbxproj");
20816
21232
  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}.` });
21233
+ 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
21234
  const serialized = yield* Effect.try({
20819
21235
  try: () => project.writeSync(),
20820
21236
  catch: (cause) => new XcodeProjectError({ message: `Failed to serialize ${pbxprojPath}: ${cause instanceof Error ? cause.message : String(cause)}` })
@@ -21002,7 +21418,7 @@ const installProvisioningProfile = ({ profilePath }) => Effect.acquireRelease(Ef
21002
21418
  //#endregion
21003
21419
  //#region src/lib/post-build-validation.ts
21004
21420
  const validateOneBundle = (bundleDir, expectedByBundleId, expectedTeamId) => Effect.gen(function* () {
21005
- const bundleId = yield* readBundleId(bundleDir).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
21421
+ const bundleId = yield* readBundleId(bundleDir).pipe(Effect.orElseSucceed(() => void 0));
21006
21422
  if (!bundleId) return {
21007
21423
  bundleId: void 0,
21008
21424
  warnings: [`Missing CFBundleIdentifier in Info.plist at ${bundleDir}`]
@@ -21014,16 +21430,16 @@ const validateOneBundle = (bundleDir, expectedByBundleId, expectedTeamId) => Eff
21014
21430
  };
21015
21431
  return {
21016
21432
  bundleId,
21017
- warnings: yield* validateEmbeddedProfile(bundleDir, expected.profileUuid, expectedTeamId, bundleId).pipe(Effect.catchAll(() => Effect.succeed([])))
21433
+ warnings: yield* validateEmbeddedProfile(bundleDir, expected.profileUuid, expectedTeamId, bundleId).pipe(Effect.orElseSucceed(() => []))
21018
21434
  };
21019
21435
  });
21020
21436
  const validateIosBuild = (params) => Effect.gen(function* () {
21021
- const appDir = yield* findAppDirectory$1(params.archivePath).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
21437
+ const appDir = yield* findAppDirectory$1(params.archivePath).pipe(Effect.orElseSucceed(() => void 0));
21022
21438
  if (!appDir) return {
21023
21439
  passed: false,
21024
21440
  warnings: ["Could not locate .app bundle in archive — skipping post-build validation"]
21025
21441
  };
21026
- const bundleDirs = yield* listSignedBundleDirs(appDir).pipe(Effect.catchAll(() => Effect.succeed([appDir])));
21442
+ const bundleDirs = yield* listSignedBundleDirs(appDir).pipe(Effect.orElseSucceed(() => [appDir]));
21027
21443
  const expectedByBundleId = new Map(params.expectedTargets.map((target) => [target.bundleId, target]));
21028
21444
  const perBundle = yield* Effect.forEach(bundleDirs, (bundleDir) => validateOneBundle(bundleDir, expectedByBundleId, params.expectedTeamId));
21029
21445
  const warnings = perBundle.flatMap((entry) => [...entry.warnings]);
@@ -21053,7 +21469,7 @@ const findAppDirectory$1 = (archivePath) => Effect.gen(function* () {
21053
21469
  const listSignedBundleDirs = (appDir) => Effect.gen(function* () {
21054
21470
  const fs = yield* FileSystem.FileSystem;
21055
21471
  const plugInsDir = path.join(appDir, "PlugIns");
21056
- if (!(yield* fs.exists(plugInsDir).pipe(Effect.catchAll(() => Effect.succeed(false))))) return [appDir];
21472
+ if (!(yield* fs.exists(plugInsDir).pipe(Effect.orElseSucceed(() => false)))) return [appDir];
21057
21473
  return [appDir, ...(yield* fs.readDirectory(plugInsDir)).filter((entry) => entry.endsWith(".appex")).map((entry) => path.join(plugInsDir, entry))];
21058
21474
  });
21059
21475
  const readBundleId = (bundleDir) => Effect.gen(function* () {
@@ -21996,7 +22412,7 @@ const easJsonPath = (projectRoot) => Effect.gen(function* () {
21996
22412
  const readEasJson = (projectRoot) => Effect.gen(function* () {
21997
22413
  const fs = yield* FileSystem.FileSystem;
21998
22414
  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}` })))));
22415
+ 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
22416
  });
22001
22417
  const mergeCustom = (base, overlay) => {
22002
22418
  const merged = shallowMerge(base, overlay);
@@ -22208,8 +22624,8 @@ const clearBuildCaches = (projectRoot) => Effect.gen(function* () {
22208
22624
  const removed = [];
22209
22625
  yield* Effect.forEach(CACHE_DIRS, (rel) => Effect.gen(function* () {
22210
22626
  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)));
22627
+ if (!(yield* fs.exists(target).pipe(Effect.orElseSucceed(() => false)))) return;
22628
+ yield* fs.remove(target, { recursive: true }).pipe(Effect.orElseSucceed(() => void 0));
22213
22629
  removed.push(rel);
22214
22630
  }), { concurrency: 4 });
22215
22631
  if (removed.length > 0) yield* Console.error(`Cleared caches: ${removed.join(", ")}`);
@@ -22503,7 +22919,7 @@ const runFingerprintFull = (projectRoot, options = {}) => Effect.gen(function* (
22503
22919
  try: () => JSON.parse(stdout),
22504
22920
  catch: () => new FingerprintError({ message: "Failed to parse @expo/fingerprint output as JSON." })
22505
22921
  });
22506
- if (!isRecord(parsed)) return yield* new FingerprintError({ message: "@expo/fingerprint output was not a JSON object." });
22922
+ if (!isRecord$1(parsed)) return yield* new FingerprintError({ message: "@expo/fingerprint output was not a JSON object." });
22507
22923
  const { hash } = parsed;
22508
22924
  if (typeof hash !== "string" || hash.length === 0) return yield* new FingerprintError({ message: "@expo/fingerprint output did not contain a \"hash\" string field." });
22509
22925
  const sourcesRaw = parsed["sources"];
@@ -22538,10 +22954,10 @@ const runString = (cmd, cwd) => Command.string(Command.workingDirectory(cmd, cwd
22538
22954
  */
22539
22955
  const readGitContext = (projectRoot) => Effect.gen(function* () {
22540
22956
  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("")))
22957
+ runString(Command.make("git", "rev-parse", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.orElseSucceed(() => "")),
22958
+ runString(Command.make("git", "symbolic-ref", "--short", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.orElseSucceed(() => "")),
22959
+ runString(Command.make("git", "log", "-1", "--format=%s"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.orElseSucceed(() => "")),
22960
+ runString(Command.make("git", "status", "--porcelain"), projectRoot).pipe(Effect.orElseSucceed(() => ""))
22545
22961
  ], { concurrency: "unbounded" });
22546
22962
  return {
22547
22963
  ref: ref.length > 0 ? ref : void 0,
@@ -22570,14 +22986,14 @@ const readGradleConfig = (androidDir) => Effect.gen(function* () {
22570
22986
  const hasKts = yield* fs.exists(ktsPath).pipe(Effect.orElseSucceed(() => false));
22571
22987
  if (!hasGroovy && hasKts) return;
22572
22988
  if (!hasGroovy) return;
22573
- const content = yield* fs.readFileString(gradlePath).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
22989
+ const content = yield* fs.readFileString(gradlePath).pipe(Effect.orElseSucceed(() => void 0));
22574
22990
  if (!content) return;
22575
22991
  return yield* Effect.tryPromise({
22576
22992
  try: async () => {
22577
22993
  return __require("gradle-to-js").parseText(stripGroovyComments(content));
22578
22994
  },
22579
22995
  catch: () => void 0
22580
- }).pipe(Effect.map(extractGradleConfig), Effect.catchAll(() => Effect.succeed(void 0)));
22996
+ }).pipe(Effect.map(extractGradleConfig), Effect.orElseSucceed(() => void 0));
22581
22997
  });
22582
22998
  /**
22583
22999
  * Log a warning if Gradle applicationId differs from app.json package name.
@@ -22697,7 +23113,7 @@ const ALWAYS_IGNORE = [...[
22697
23113
  "dist"
22698
23114
  ], ...NATIVE_BUILD_OUTPUTS];
22699
23115
  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;
23116
+ for (const [name, pm] of LOCKFILES) if (yield* fs.exists(path.join(dir, name)).pipe(Effect.orElseSucceed(() => false))) return pm;
22701
23117
  });
22702
23118
  const walkUpForLockfile = (startCwd, dir) => Effect.gen(function* () {
22703
23119
  const pm = yield* findLockfile(yield* FileSystem.FileSystem, dir);
@@ -22733,13 +23149,13 @@ const buildIgnoreInstance = (workspaceRoot, options = {}) => Effect.gen(function
22733
23149
  const ig = ignore();
22734
23150
  ig.add([...ALWAYS_IGNORE]);
22735
23151
  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("")));
23152
+ if (yield* fs.exists(easignorePath).pipe(Effect.orElseSucceed(() => false))) {
23153
+ const content = yield* fs.readFileString(easignorePath).pipe(Effect.orElseSucceed(() => ""));
22738
23154
  ig.add(content);
22739
23155
  } else {
22740
23156
  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("")));
23157
+ if (yield* fs.exists(gitignorePath).pipe(Effect.orElseSucceed(() => false))) {
23158
+ const content = yield* fs.readFileString(gitignorePath).pipe(Effect.orElseSucceed(() => ""));
22743
23159
  ig.add(content);
22744
23160
  }
22745
23161
  }
@@ -22810,7 +23226,7 @@ const prepareStagingProject = (input) => Effect.gen(function* () {
22810
23226
  })
22811
23227
  });
22812
23228
  yield* initGitRepo(stagingRoot);
22813
- if (yield* fs.exists(path.join(workspaceRoot, "package.json")).pipe(Effect.catchAll(() => Effect.succeed(false)))) yield* runInstall({
23229
+ if (yield* fs.exists(path.join(workspaceRoot, "package.json")).pipe(Effect.orElseSucceed(() => false))) yield* runInstall({
22814
23230
  stagingRoot,
22815
23231
  packageManager,
22816
23232
  env: yield* runtime.commandEnvironment(input.envVars)
@@ -22827,7 +23243,7 @@ const prepareStagingProject = (input) => Effect.gen(function* () {
22827
23243
  //#endregion
22828
23244
  //#region src/lib/repo-clean.ts
22829
23245
  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([])));
23246
+ 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
23247
  /**
22832
23248
  * Refuse to proceed when the working tree has uncommitted changes. Skipped when
22833
23249
  * `allowDirty` is true. In interactive mode, prompts the user to confirm; in
@@ -22840,11 +23256,8 @@ const ensureRepoClean = ({ projectRoot, allowDirty, label }) => Effect.gen(funct
22840
23256
  const preview = dirty.slice(0, MAX_FILES_SHOWN).join("\n ");
22841
23257
  const overflow = dirty.length > MAX_FILES_SHOWN ? `\n ... and ${String(dirty.length - MAX_FILES_SHOWN)} more` : "";
22842
23258
  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.` });
23259
+ if (!(yield* InteractiveMode).allow) return yield* new DirtyRepoError({ message: `Refusing to ${label} with a dirty working tree. Commit your changes or pass --allow-dirty.` });
23260
+ if (!(yield* promptConfirm(`Continue ${label} with uncommitted changes?`, { initialValue: false }))) return yield* new DirtyRepoError({ message: `${label} cancelled by user.` });
22848
23261
  });
22849
23262
 
22850
23263
  //#endregion
@@ -22984,7 +23397,7 @@ const postTokenRequest = (tokenUri, jwt) => Effect.tryPromise({
22984
23397
  });
22985
23398
  const exchangeJwtForAccessToken = (tokenUri, jwt) => Effect.gen(function* () {
22986
23399
  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}` }));
23400
+ if (!result.ok) return yield* new GooglePlayAuthError({ message: `OAuth token exchange failed: ${String(result.status)} ${result.text}` });
22988
23401
  const json = yield* Effect.try({
22989
23402
  try: () => JSON.parse(result.text),
22990
23403
  catch: (cause) => new GooglePlayAuthError({
@@ -23009,7 +23422,7 @@ const acquireGooglePlayAccessToken = (serviceAccountJson) => Effect.gen(function
23009
23422
  message: "Service account JSON missing required fields (type, client_email, private_key)",
23010
23423
  cause
23011
23424
  })));
23012
- if (parsed.type !== "service_account") return yield* Effect.fail(new GooglePlayAuthError({ message: `Service account JSON has wrong type: ${parsed.type}` }));
23425
+ if (parsed.type !== "service_account") return yield* new GooglePlayAuthError({ message: `Service account JSON has wrong type: ${parsed.type}` });
23013
23426
  const tokenUri = parsed.token_uri ?? GOOGLE_OAUTH_TOKEN_URL;
23014
23427
  return {
23015
23428
  accessToken: yield* exchangeJwtForAccessToken(tokenUri, yield* signJwt(yield* importPrivateKey(parsed.private_key), buildJwtAssertion({
@@ -23049,10 +23462,10 @@ const performFetch = (params) => Effect.tryPromise({
23049
23462
  });
23050
23463
  const callJsonRaw = (params) => Effect.gen(function* () {
23051
23464
  const result = yield* performFetch(params);
23052
- if (!result.ok) return yield* Effect.fail(new GooglePlayApiError({
23465
+ if (!result.ok) return yield* new GooglePlayApiError({
23053
23466
  message: `${params.label} failed: ${String(result.status)} ${result.text}`,
23054
23467
  httpStatus: result.status
23055
- }));
23468
+ });
23056
23469
  return yield* Effect.try({
23057
23470
  try: () => result.text === "" ? {} : JSON.parse(result.text),
23058
23471
  catch: (cause) => new GooglePlayApiError({
@@ -23107,10 +23520,10 @@ const performBundleUpload = (params) => Effect.tryPromise({
23107
23520
  });
23108
23521
  const uploadBundle = (params) => Effect.gen(function* () {
23109
23522
  const result = yield* performBundleUpload(params);
23110
- if (!result.ok) return yield* Effect.fail(new GooglePlayApiError({
23523
+ if (!result.ok) return yield* new GooglePlayApiError({
23111
23524
  message: `edits.bundles.upload failed: ${String(result.status)} ${result.text}`,
23112
23525
  httpStatus: result.status
23113
- }));
23526
+ });
23114
23527
  const raw = yield* Effect.try({
23115
23528
  try: () => JSON.parse(result.text),
23116
23529
  catch: (cause) => new GooglePlayApiError({
@@ -23301,10 +23714,10 @@ const fetchArchiveOverHttp = (url) => Effect.gen(function* () {
23301
23714
  message: `Failed to download AAB from ${url}: ${cause instanceof Error ? cause.message : String(cause)}`
23302
23715
  })
23303
23716
  });
23304
- if (!result.ok || result.bytes === null) return yield* Effect.fail(new CliSubmitError({
23717
+ if (!result.ok || result.bytes === null) return yield* new CliSubmitError({
23305
23718
  code: "SUBMISSION_ARCHIVE_DOWNLOAD_FAILED",
23306
23719
  message: `HTTP ${String(result.status)} fetching archive at ${url}`
23307
- }));
23720
+ });
23308
23721
  return result.bytes;
23309
23722
  });
23310
23723
  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 +23792,10 @@ const runGooglePlayPipeline = (params) => Effect.gen(function* () {
23379
23792
  });
23380
23793
  const runAndroidGooglePlayUpload = (inputs) => Effect.gen(function* () {
23381
23794
  const { applicationId } = inputs.androidProfile;
23382
- if (applicationId === void 0) return yield* Effect.fail(new CliSubmitError({
23795
+ if (applicationId === void 0) return yield* new CliSubmitError({
23383
23796
  code: "SUBMISSION_ANDROID_APP_ID_MISSING",
23384
23797
  message: "Android submit profile requires applicationId — set submit.<profile>.android.applicationId in eas.json"
23385
- }));
23798
+ });
23386
23799
  const serviceAccountJson = yield* resolveServiceAccountJson({
23387
23800
  api: inputs.api,
23388
23801
  serviceAccountKeyId: inputs.serviceAccountKeyId,
@@ -23407,7 +23820,7 @@ const runAndroidGooglePlayUpload = (inputs) => Effect.gen(function* () {
23407
23820
  errorCode: engineError.code,
23408
23821
  errorMessage: engineError.message
23409
23822
  });
23410
- return yield* Effect.fail(engineError);
23823
+ return yield* engineError;
23411
23824
  })));
23412
23825
  yield* patchSubmissionStatus(inputs.api, inputs.submissionId, { status: "FINISHED" });
23413
23826
  yield* printHuman(`Google Play bundle uploaded (versionCode ${String(result.versionCode)})`);
@@ -23824,7 +24237,7 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
23824
24237
  commit: rawGitContext.commit,
23825
24238
  dirty: rawGitContext.dirty
23826
24239
  });
23827
- const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(userCwd, platform).pipe(Effect.map((entry) => entry.hash), Effect.catchAll(() => Effect.succeed(void 0))) : void 0;
24240
+ const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(userCwd, platform).pipe(Effect.map((entry) => entry.hash), Effect.orElseSucceed(() => void 0)) : void 0;
23828
24241
  const result = yield* reserveAndUpload(api, {
23829
24242
  target,
23830
24243
  projectId,
@@ -24179,10 +24592,7 @@ const downloadCommand$1 = defineCommand({
24179
24592
  const fs = yield* FileSystem.FileSystem;
24180
24593
  const cwd = yield* (yield* CliRuntime).cwd;
24181
24594
  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
- }
24595
+ if (!artifact) return yield* new UploadFailedError({ message: `Build ${args.id} has no artifact yet.` });
24186
24596
  const link = yield* api.builds.getInstallLink({ path: { id: args.id } });
24187
24597
  const ext = artifact.format;
24188
24598
  const outputPath = args.output ?? path.join(cwd, `${args.id}.${ext}`);
@@ -24455,7 +24865,7 @@ const runInherit = (step, bin, ...args) => Command.exitCode(Command.make(bin, ..
24455
24865
  * Locate a tool on PATH. Returns the absolute path or fails with NativeRunError.
24456
24866
  */
24457
24867
  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();
24868
+ const trimmed = (yield* Command.string(Command.make("which", bin)).pipe(Effect.mapError(() => new NativeRunError({ message: `${bin} not found in PATH` })))).trim();
24459
24869
  if (trimmed === "") return yield* new NativeRunError({ message: `${bin} not found in PATH` });
24460
24870
  return trimmed;
24461
24871
  });
@@ -24474,7 +24884,7 @@ const findAppBundle = (root) => Effect.gen(function* () {
24474
24884
  const fs = yield* FileSystem.FileSystem;
24475
24885
  const candidates = [root, path.join(root, "Payload")];
24476
24886
  for (const candidate of candidates) {
24477
- const app = (yield* fs.readDirectory(candidate).pipe(Effect.catchAll(() => Effect.succeed([])))).find((entry) => entry.endsWith(".app"));
24887
+ const app = (yield* fs.readDirectory(candidate).pipe(Effect.orElseSucceed(() => []))).find((entry) => entry.endsWith(".app"));
24478
24888
  if (app) return path.join(candidate, app);
24479
24889
  }
24480
24890
  return yield* new NativeRunError({ message: `No .app bundle found inside ${root} or ${path.join(root, "Payload")}.` });
@@ -24553,8 +24963,8 @@ const installAndLaunchIosDevice = (params) => Effect.gen(function* () {
24553
24963
  * fall back to a CLI flag.
24554
24964
  */
24555
24965
  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)));
24966
+ if (!(yield* which(bin).pipe(Effect.orElseSucceed(() => null)))) return;
24967
+ const raw = yield* execCapture(`${bin} dump`, bin, "dump", "badging", apkPath).pipe(Effect.orElseSucceed(() => null));
24558
24968
  if (!raw) return;
24559
24969
  return /package: name='(?<packageName>[^']+)'/u.exec(raw)?.[1];
24560
24970
  });
@@ -24626,10 +25036,7 @@ const runIosSimulator = (params) => Effect.gen(function* () {
24626
25036
  });
24627
25037
  const runIosDevice = (params) => Effect.gen(function* () {
24628
25038
  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
- }
25039
+ if (deviceSelector === void 0) return yield* new InvalidArgumentError({ message: "Pass --device-id <udid>. Run `xcrun devicectl list devices` to list connected devices." });
24633
25040
  const bundleId = yield* readBundleIdFromApp(yield* findAppBundle(yield* extractIosArtifact({
24634
25041
  tempDir: params.tempDir,
24635
25042
  artifactPath: params.artifactPath,
@@ -24654,21 +25061,12 @@ const runIos = (params) => {
24654
25061
  return Effect.fail(new NativeRunError({ message: `Cannot install ${params.format} on iOS; only tar.gz (simulator) or ipa are supported.` }));
24655
25062
  };
24656
25063
  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
- }
25064
+ 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." });
25065
+ if (params.format !== "apk") return yield* new NativeRunError({ message: `Cannot install ${params.format} on Android; only apk is supported.` });
24665
25066
  const device = yield* pickAndroidDevice(params.emulatorSelector);
24666
25067
  const detected = yield* readApkPackageName(params.artifactPath);
24667
25068
  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
- }
25069
+ if (!packageName) return yield* new InvalidArgumentError({ message: "Could not detect APK package name (aapt/aapt2 not on PATH). Pass --package <name> explicitly." });
24672
25070
  yield* printHuman(`Installing on Android device ${device.serial}...`);
24673
25071
  yield* installAndLaunchAndroid({
24674
25072
  serial: device.serial,
@@ -24733,7 +25131,7 @@ const runCommand$1 = defineCommand({
24733
25131
  projectId
24734
25132
  });
24735
25133
  const { artifact } = build;
24736
- if (!artifact) return yield* Effect.fail(new UploadFailedError({ message: `Build ${build.id} has no artifact yet.` }));
25134
+ if (!artifact) return yield* new UploadFailedError({ message: `Build ${build.id} has no artifact yet.` });
24737
25135
  const link = yield* api.builds.getInstallLink({ path: { id: build.id } });
24738
25136
  const tempDir = yield* acquireBuildTempDir;
24739
25137
  const artifactPath = path.join(tempDir, `artifact.${artifact.format}`);
@@ -24828,7 +25226,7 @@ const resolveUploadMeta = (params) => Effect.gen(function* () {
24828
25226
  const runUploadWorkflow = (options) => Effect.gen(function* () {
24829
25227
  const api = yield* apiClient;
24830
25228
  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}.` });
25229
+ if (!(yield* (yield* FileSystem.FileSystem).exists(options.artifactPath).pipe(Effect.orElseSucceed(() => false)))) return yield* new ArtifactNotFoundError({ message: `Artifact not found at ${options.artifactPath}.` });
24832
25230
  const projectType = yield* detectProjectType({
24833
25231
  projectRoot,
24834
25232
  override: asProjectType((yield* readBetterUpdateConfig(projectRoot))?.["projectType"])
@@ -24858,7 +25256,7 @@ const runUploadWorkflow = (options) => Effect.gen(function* () {
24858
25256
  commit: rawGitContext.commit,
24859
25257
  dirty: rawGitContext.dirty
24860
25258
  });
24861
- const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(projectRoot, options.platform).pipe(Effect.map((entry) => entry.hash), Effect.catchAll(() => Effect.succeed(void 0))) : void 0;
25259
+ const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(projectRoot, options.platform).pipe(Effect.map((entry) => entry.hash), Effect.orElseSucceed(() => void 0)) : void 0;
24862
25260
  const result = yield* reserveAndUpload(api, compact({
24863
25261
  target,
24864
25262
  projectId,
@@ -25383,7 +25781,7 @@ const viewCommand$2 = defineCommand({
25383
25781
  page
25384
25782
  } }))]);
25385
25783
  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.` }));
25784
+ if (!channel) return yield* new ChannelCommandError({ message: `Channel "${args.target}" not found by ID or name.` });
25387
25785
  const branchName = new Map(branches.map((branch) => [branch.id, branch.name])).get(channel.branchId) ?? channel.branchId;
25388
25786
  yield* printHumanKeyValue([
25389
25787
  ["ID", channel.id],
@@ -25808,7 +26206,7 @@ const announce = (heading) => Effect.gen(function* () {
25808
26206
  });
25809
26207
  const reportError = (label, cause) => Console.log(`✗ ${label}: ${cause instanceof Error ? cause.message : String(cause)}`);
25810
26208
  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)));
26209
+ const safePrompt = (effect) => effect.pipe(Effect.orElseSucceed(() => BACK));
25812
26210
  const promptForBundleConfig = (ctx) => Effect.gen(function* () {
25813
26211
  const list = yield* ctx.api.iosBundleConfigurations.list({ path: { projectId: ctx.projectId } });
25814
26212
  if (list.items.length === 0) return yield* new MissingCredentialsError({
@@ -27739,7 +28137,7 @@ const handleCertLimitInteractive = (api, ascApiKeyId, certificateType) => Effect
27739
28137
  ascApiKeyId,
27740
28138
  certificateType
27741
28139
  });
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." }));
28140
+ 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
28141
  const toRevoke = yield* promptMultiSelect("Select one or more certificates to revoke before retrying", certs.map((entry) => ({
27744
28142
  value: entry.id,
27745
28143
  label: `${entry.serialNumber.slice(0, 12)}… (${entry.displayName ?? entry.certificateType}, exp ${entry.expirationDate.slice(0, 10)})`
@@ -28501,7 +28899,7 @@ const writeArtifact = (fs, projectRoot, relPath, bytes) => Effect.gen(function*
28501
28899
  const writeText = (fs, projectRoot, relPath, text) => writeArtifact(fs, projectRoot, relPath, new TextEncoder().encode(text));
28502
28900
  const ensureGitignoreEntries = (fs, projectRoot, paths) => Effect.gen(function* () {
28503
28901
  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");
28902
+ const lines = ((yield* fs.exists(filePath).pipe(Effect.orElseSucceed(() => false))) ? yield* fs.readFileString(filePath).pipe(Effect.orElseSucceed(() => "")) : "").split("\n");
28505
28903
  const added = [];
28506
28904
  const next = [...lines];
28507
28905
  for (const entry of paths) if (!lines.includes(entry)) {
@@ -29721,6 +30119,7 @@ const devicesCommand = defineCommand({
29721
30119
 
29722
30120
  //#endregion
29723
30121
  //#region src/commands/doctor.ts
30122
+ var HealthCheckError = class extends Data.TaggedError("HealthCheckError") {};
29724
30123
  const pass = (id, name, message) => ({
29725
30124
  id,
29726
30125
  name,
@@ -29759,7 +30158,10 @@ const checkServerHealth = Effect.gen(function* () {
29759
30158
  const url = `${yield* (yield* ConfigStore).getBaseUrl}/api/health`;
29760
30159
  const response = yield* Effect.tryPromise({
29761
30160
  try: async () => fetch(url, { signal: AbortSignal.timeout(3e3) }),
29762
- catch: (cause) => new Error(String(cause))
30161
+ catch: (cause) => new HealthCheckError({
30162
+ message: String(cause),
30163
+ cause
30164
+ })
29763
30165
  }).pipe(Effect.either);
29764
30166
  if (response._tag === "Left") return fail("health", "Server reachable", `${url} unreachable: ${response.left.message}`);
29765
30167
  const res = response.right;
@@ -29970,7 +30372,7 @@ const getExecTrailingArgv = () => trailing$1;
29970
30372
  const pullForExec = (api, projectId, environment) => pullEnvVars(api, {
29971
30373
  projectId,
29972
30374
  environment
29973
- }).pipe(Effect.catchAll(() => Effect.succeed({})));
30375
+ }).pipe(Effect.orElseSucceed(() => ({})));
29974
30376
  const splitTrailing = (trailing) => {
29975
30377
  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
30378
  const [bin, ...rest] = trailing;
@@ -30795,12 +31197,382 @@ const fingerprintCommand = defineCommand({
30795
31197
  }
30796
31198
  });
30797
31199
 
31200
+ //#endregion
31201
+ //#region src/commands/groups/helpers.ts
31202
+ const groupErrorExtras = { GroupCommandError: 2 };
31203
+
31204
+ //#endregion
31205
+ //#region src/commands/groups/attach.ts
31206
+ const attachGroupPolicyCommand = defineCommand({
31207
+ meta: {
31208
+ name: "attach",
31209
+ description: "Attach a policy (real or managed:*) to a group; members inherit it"
31210
+ },
31211
+ args: {
31212
+ id: {
31213
+ type: "positional",
31214
+ required: true,
31215
+ description: "Group ID"
31216
+ },
31217
+ "policy-id": {
31218
+ type: "string",
31219
+ required: true,
31220
+ description: "Policy ID to attach (real id or managed preset like managed:admin)"
31221
+ }
31222
+ },
31223
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
31224
+ const attachment = yield* (yield* apiClient)["policy-attachments"].attachToGroup({
31225
+ path: { id: args.id },
31226
+ payload: { policyId: args["policy-id"] }
31227
+ });
31228
+ yield* printHumanKeyValue([
31229
+ ["Attachment ID", attachment.id],
31230
+ ["Group ID", attachment.principalId],
31231
+ ["Policy ID", attachment.policyId],
31232
+ ["Created", attachment.createdAt]
31233
+ ]);
31234
+ return attachment;
31235
+ }), {
31236
+ exits: groupErrorExtras,
31237
+ json: "value"
31238
+ })
31239
+ });
31240
+
31241
+ //#endregion
31242
+ //#region src/commands/groups/create.ts
31243
+ const createGroupCommand = defineCommand({
31244
+ meta: {
31245
+ name: "create",
31246
+ description: "Create a member group"
31247
+ },
31248
+ args: {
31249
+ name: {
31250
+ type: "string",
31251
+ required: true,
31252
+ description: "Group display name"
31253
+ },
31254
+ description: {
31255
+ type: "string",
31256
+ description: "Optional human description"
31257
+ }
31258
+ },
31259
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
31260
+ const group = yield* (yield* apiClient).groups.create({ payload: {
31261
+ name: args.name,
31262
+ ...compact({ description: args.description })
31263
+ } });
31264
+ yield* printHumanKeyValue([
31265
+ ["ID", group.id],
31266
+ ["Name", group.name],
31267
+ ["Description", group.description ?? "-"],
31268
+ ["Created", group.createdAt]
31269
+ ]);
31270
+ return group;
31271
+ }), {
31272
+ exits: groupErrorExtras,
31273
+ json: "value"
31274
+ })
31275
+ });
31276
+
31277
+ //#endregion
31278
+ //#region src/commands/groups/delete.ts
31279
+ const deleteGroupCommand = defineCommand({
31280
+ meta: {
31281
+ name: "delete",
31282
+ description: "Delete a group and sweep its memberships and policy attachments"
31283
+ },
31284
+ args: {
31285
+ id: {
31286
+ type: "positional",
31287
+ required: true,
31288
+ description: "Group ID"
31289
+ },
31290
+ yes: {
31291
+ type: "boolean",
31292
+ description: "Skip confirmation prompt"
31293
+ }
31294
+ },
31295
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
31296
+ if (!args.yes) {
31297
+ if (!(yield* promptConfirm(`Delete group ${args.id}?`, { initialValue: false }))) {
31298
+ yield* printHuman("Cancelled.");
31299
+ return;
31300
+ }
31301
+ }
31302
+ yield* (yield* apiClient).groups.delete({ path: { id: args.id } });
31303
+ yield* printHuman(`Deleted group ${args.id}.`);
31304
+ return {
31305
+ id: args.id,
31306
+ deleted: true
31307
+ };
31308
+ }), {
31309
+ exits: groupErrorExtras,
31310
+ json: "value"
31311
+ })
31312
+ });
31313
+
31314
+ //#endregion
31315
+ //#region src/commands/groups/detach.ts
31316
+ const detachGroupPolicyCommand = defineCommand({
31317
+ meta: {
31318
+ name: "detach",
31319
+ description: "Remove a policy attachment from a group"
31320
+ },
31321
+ args: {
31322
+ id: {
31323
+ type: "positional",
31324
+ required: true,
31325
+ description: "Group ID"
31326
+ },
31327
+ "policy-id": {
31328
+ type: "string",
31329
+ required: true,
31330
+ description: "Policy ID to detach (real id or managed preset like managed:admin)"
31331
+ }
31332
+ },
31333
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
31334
+ yield* (yield* apiClient)["policy-attachments"].detachFromGroup({ path: {
31335
+ id: args.id,
31336
+ policyId: encodeURIComponent(args["policy-id"])
31337
+ } });
31338
+ yield* printHuman(`Detached policy ${args["policy-id"]} from group ${args.id}.`);
31339
+ return {
31340
+ groupId: args.id,
31341
+ policyId: args["policy-id"],
31342
+ detached: true
31343
+ };
31344
+ }), {
31345
+ exits: groupErrorExtras,
31346
+ json: "value"
31347
+ })
31348
+ });
31349
+
31350
+ //#endregion
31351
+ //#region src/commands/groups/list.ts
31352
+ const listGroupsCommand = defineCommand({
31353
+ meta: {
31354
+ name: "list",
31355
+ description: "List member groups in the active organization"
31356
+ },
31357
+ run: async () => runEffect(Effect.gen(function* () {
31358
+ const result = yield* (yield* apiClient).groups.list();
31359
+ yield* printHumanTable([
31360
+ "ID",
31361
+ "Name",
31362
+ "Description",
31363
+ "Created"
31364
+ ], result.items.map((group) => [
31365
+ group.id,
31366
+ group.name,
31367
+ group.description ?? "-",
31368
+ group.createdAt
31369
+ ]));
31370
+ return result;
31371
+ }), {
31372
+ exits: groupErrorExtras,
31373
+ json: "value"
31374
+ })
31375
+ });
31376
+
31377
+ //#endregion
31378
+ //#region src/commands/groups/members/add.ts
31379
+ const addGroupMemberCommand = defineCommand({
31380
+ meta: {
31381
+ name: "add",
31382
+ description: "Add an organization member to a group"
31383
+ },
31384
+ args: {
31385
+ id: {
31386
+ type: "positional",
31387
+ required: true,
31388
+ description: "Group ID"
31389
+ },
31390
+ "member-id": {
31391
+ type: "string",
31392
+ required: true,
31393
+ description: "Organization member ID to add"
31394
+ }
31395
+ },
31396
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
31397
+ const member = yield* (yield* apiClient).groups.addMember({
31398
+ path: { id: args.id },
31399
+ payload: { memberId: args["member-id"] }
31400
+ });
31401
+ yield* printHumanKeyValue([["Member ID", member.memberId], ["Added", member.createdAt]]);
31402
+ return member;
31403
+ }), {
31404
+ exits: groupErrorExtras,
31405
+ json: "value"
31406
+ })
31407
+ });
31408
+
31409
+ //#endregion
31410
+ //#region src/commands/groups/members/remove.ts
31411
+ const removeGroupMemberCommand = defineCommand({
31412
+ meta: {
31413
+ name: "remove",
31414
+ description: "Remove a member from a group"
31415
+ },
31416
+ args: {
31417
+ id: {
31418
+ type: "positional",
31419
+ required: true,
31420
+ description: "Group ID"
31421
+ },
31422
+ "member-id": {
31423
+ type: "string",
31424
+ required: true,
31425
+ description: "Organization member ID to remove"
31426
+ }
31427
+ },
31428
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
31429
+ yield* (yield* apiClient).groups.removeMember({ path: {
31430
+ id: args.id,
31431
+ memberId: args["member-id"]
31432
+ } });
31433
+ yield* printHuman(`Removed member ${args["member-id"]} from group ${args.id}.`);
31434
+ return {
31435
+ groupId: args.id,
31436
+ memberId: args["member-id"],
31437
+ removed: true
31438
+ };
31439
+ }), {
31440
+ exits: groupErrorExtras,
31441
+ json: "value"
31442
+ })
31443
+ });
31444
+
31445
+ //#endregion
31446
+ //#region src/commands/groups/members/index.ts
31447
+ const listGroupMembersCommand = defineCommand({
31448
+ meta: {
31449
+ name: "list",
31450
+ description: "List the members belonging to a group"
31451
+ },
31452
+ args: { id: {
31453
+ type: "positional",
31454
+ required: true,
31455
+ description: "Group ID"
31456
+ } },
31457
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
31458
+ const result = yield* (yield* apiClient).groups.listMembers({ path: { id: args.id } });
31459
+ yield* printHumanTable(["Member ID", "Added"], result.items.map((member) => [member.memberId, member.createdAt]));
31460
+ return result;
31461
+ }), {
31462
+ exits: groupErrorExtras,
31463
+ json: "value"
31464
+ })
31465
+ });
31466
+ const membersCommand = defineCommand({
31467
+ meta: {
31468
+ name: "members",
31469
+ description: "Manage the members of a group"
31470
+ },
31471
+ subCommands: {
31472
+ list: listGroupMembersCommand,
31473
+ add: addGroupMemberCommand,
31474
+ remove: removeGroupMemberCommand
31475
+ }
31476
+ });
31477
+
31478
+ //#endregion
31479
+ //#region src/commands/groups/policies.ts
31480
+ const listGroupPoliciesCommand = defineCommand({
31481
+ meta: {
31482
+ name: "policies",
31483
+ description: "List policies attached to a group"
31484
+ },
31485
+ args: { id: {
31486
+ type: "positional",
31487
+ required: true,
31488
+ description: "Group ID"
31489
+ } },
31490
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
31491
+ const result = yield* (yield* apiClient)["policy-attachments"].listForGroup({ path: { id: args.id } });
31492
+ yield* printHumanTable([
31493
+ "Attachment ID",
31494
+ "Policy ID",
31495
+ "Created"
31496
+ ], result.items.map((attachment) => [
31497
+ attachment.id,
31498
+ attachment.policyId,
31499
+ attachment.createdAt
31500
+ ]));
31501
+ return result;
31502
+ }), {
31503
+ exits: groupErrorExtras,
31504
+ json: "value"
31505
+ })
31506
+ });
31507
+
31508
+ //#endregion
31509
+ //#region src/commands/groups/update.ts
31510
+ const updateGroupCommand = defineCommand({
31511
+ meta: {
31512
+ name: "update",
31513
+ description: "Update a group's name or description"
31514
+ },
31515
+ args: {
31516
+ id: {
31517
+ type: "positional",
31518
+ required: true,
31519
+ description: "Group ID"
31520
+ },
31521
+ name: {
31522
+ type: "string",
31523
+ description: "New display name"
31524
+ },
31525
+ description: {
31526
+ type: "string",
31527
+ description: "New description"
31528
+ }
31529
+ },
31530
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
31531
+ const group = yield* (yield* apiClient).groups.update({
31532
+ path: { id: args.id },
31533
+ payload: compact({
31534
+ name: args.name,
31535
+ description: args.description
31536
+ })
31537
+ });
31538
+ yield* printHumanKeyValue([
31539
+ ["ID", group.id],
31540
+ ["Name", group.name],
31541
+ ["Description", group.description ?? "-"],
31542
+ ["Updated", group.updatedAt ?? "-"]
31543
+ ]);
31544
+ return group;
31545
+ }), {
31546
+ exits: groupErrorExtras,
31547
+ json: "value"
31548
+ })
31549
+ });
31550
+
31551
+ //#endregion
31552
+ //#region src/commands/groups/index.ts
31553
+ const groupsCommand = defineCommand({
31554
+ meta: {
31555
+ name: "groups",
31556
+ description: "Manage member groups for collective policy attachment"
31557
+ },
31558
+ subCommands: {
31559
+ list: listGroupsCommand,
31560
+ create: createGroupCommand,
31561
+ update: updateGroupCommand,
31562
+ delete: deleteGroupCommand,
31563
+ members: membersCommand,
31564
+ policies: listGroupPoliciesCommand,
31565
+ attach: attachGroupPolicyCommand,
31566
+ detach: detachGroupPolicyCommand
31567
+ }
31568
+ });
31569
+
30798
31570
  //#endregion
30799
31571
  //#region src/commands/init.ts
30800
31572
  const checkExistingLink = (api, config, localSlug) => Effect.gen(function* () {
30801
31573
  const existingId = config.extra?.betterUpdate?.projectId;
30802
31574
  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)));
31575
+ const project = yield* api.projects.get({ path: { id: existingId } }).pipe(Effect.orElseSucceed(() => void 0));
30804
31576
  if (project === void 0) {
30805
31577
  yield* printHuman(`Existing projectId "${existingId}" not found on server. Re-linking by local slug "${localSlug}".`);
30806
31578
  return "stale";
@@ -30820,10 +31592,10 @@ const checkExistingLink = (api, config, localSlug) => Effect.gen(function* () {
30820
31592
  const slugify = (value) => value.toLowerCase().replaceAll(/[^a-z0-9]+/gu, "-").replaceAll(/^-+|-+$/gu, "");
30821
31593
  /** Best-effort `name` from the local package.json, or undefined when absent. */
30822
31594
  const readPackageJsonName = (projectRoot) => Effect.gen(function* () {
30823
- const content = yield* (yield* FileSystem.FileSystem).readFileString(path.join(projectRoot, "package.json")).pipe(Effect.catchAll(() => Effect.succeed("")));
31595
+ const content = yield* (yield* FileSystem.FileSystem).readFileString(path.join(projectRoot, "package.json")).pipe(Effect.orElseSucceed(() => ""));
30824
31596
  if (content.length === 0) return;
30825
- const parsed = yield* Effect.try(() => JSON.parse(content)).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
30826
- const name = isRecord(parsed) ? parsed["name"] : void 0;
31597
+ const parsed = yield* Effect.try(() => JSON.parse(content)).pipe(Effect.orElseSucceed(() => void 0));
31598
+ const name = isRecord$1(parsed) ? parsed["name"] : void 0;
30827
31599
  return typeof name === "string" && name.length > 0 ? name : void 0;
30828
31600
  });
30829
31601
  /**
@@ -31064,6 +31836,236 @@ const openCommand = defineCommand({
31064
31836
  }))
31065
31837
  });
31066
31838
 
31839
+ //#endregion
31840
+ //#region src/commands/policies/helpers.ts
31841
+ var PolicyCommandError = class extends Data.TaggedError("PolicyCommandError") {};
31842
+ const policyErrorExtras = { PolicyCommandError: 2 };
31843
+ /** A managed preset id is virtual + read-only; its id is prefixed with `managed:`. */
31844
+ const isManagedPolicyId = (id) => id.startsWith("managed:");
31845
+ const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
31846
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
31847
+ /**
31848
+ * Parse + shape-validate a `--document` JSON string client-side so a malformed
31849
+ * document fails with a clear local message before any network round-trip. The
31850
+ * server still re-validates action tokens against the real vocabulary and the
31851
+ * selectors against the shared grammar; this only guards the JSON shape and the
31852
+ * action/selector token SHAPE via the contract's pure validators.
31853
+ *
31854
+ * Expected shape:
31855
+ * { "statements": [ { "effect": "allow"|"deny", "actions": ["project:read"|"*"], "resources": ["*"|"project/A"] } ] }
31856
+ */
31857
+ const parsePolicyDocument = (raw) => Effect.gen(function* () {
31858
+ const parsed = yield* Effect.try({
31859
+ try: () => JSON.parse(raw),
31860
+ catch: () => new PolicyCommandError({ message: "The --document value is not valid JSON. Pass a JSON object string." })
31861
+ });
31862
+ if (!isRecord(parsed) || !Array.isArray(parsed["statements"])) return yield* new PolicyCommandError({ message: "The document must be a JSON object with a \"statements\" array. Example: {\"statements\":[{\"effect\":\"allow\",\"actions\":[\"project:read\"],\"resources\":[\"*\"]}]}" });
31863
+ const statements = [];
31864
+ for (const [index, entry] of parsed["statements"].entries()) {
31865
+ const statement = yield* validateStatement(entry, index);
31866
+ statements.push(statement);
31867
+ }
31868
+ if (statements.length === 0) return yield* new PolicyCommandError({ message: "The document must contain at least one statement." });
31869
+ return { statements };
31870
+ });
31871
+ const validateStatement = (entry, index) => Effect.gen(function* () {
31872
+ const at = `statements[${index}]`;
31873
+ if (!isRecord(entry)) return yield* new PolicyCommandError({ message: `${at} must be a JSON object.` });
31874
+ const { effect } = entry;
31875
+ if (effect !== "allow" && effect !== "deny") return yield* new PolicyCommandError({ message: `${at}.effect must be "allow" or "deny".` });
31876
+ const { actions } = entry;
31877
+ if (!isStringArray(actions) || actions.length === 0) return yield* new PolicyCommandError({ message: `${at}.actions must be a non-empty array of action-token strings.` });
31878
+ const badAction = actions.find((token) => !isValidActionTokenShape(token));
31879
+ if (badAction !== void 0) return yield* new PolicyCommandError({ message: `${at}.actions has an invalid token "${badAction}". Use "*", "<resource>:*", or "<resource>:<action>".` });
31880
+ const { resources } = entry;
31881
+ if (!isStringArray(resources) || resources.length === 0) return yield* new PolicyCommandError({ message: `${at}.resources must be a non-empty array of selector strings.` });
31882
+ const badResource = resources.find((selector) => !isValidSelector(selector));
31883
+ if (badResource !== void 0) return yield* new PolicyCommandError({ message: `${at}.resources has an invalid selector "${badResource}". Use "*" or slash-joined segments like "project/A".` });
31884
+ const inertResource = resources.find((selector) => !isCanonicalSelector(selector));
31885
+ if (inertResource !== void 0) return yield* new PolicyCommandError({ message: `${at}.resources selector "${inertResource}" matches no known resource path. Use segments like "project/{id}/channel/{id}" or "project/{id}/env/{env}".` });
31886
+ return {
31887
+ effect,
31888
+ actions,
31889
+ resources
31890
+ };
31891
+ });
31892
+
31893
+ //#endregion
31894
+ //#region src/commands/policies/create.ts
31895
+ const DOCUMENT_HELP = "JSON document string: {\"statements\":[{\"effect\":\"allow\"|\"deny\",\"actions\":[\"<resource>:<action>\"|\"<resource>:*\"|\"*\"],\"resources\":[\"*\"|\"project/A\"|\"project/*/env/production\"]}]}";
31896
+ const createPolicyCommand = defineCommand({
31897
+ meta: {
31898
+ name: "create",
31899
+ description: "Create a named IAM policy from a JSON document"
31900
+ },
31901
+ args: {
31902
+ name: {
31903
+ type: "string",
31904
+ required: true,
31905
+ description: "Policy display name"
31906
+ },
31907
+ description: {
31908
+ type: "string",
31909
+ description: "Optional human description"
31910
+ },
31911
+ document: {
31912
+ type: "string",
31913
+ required: true,
31914
+ description: DOCUMENT_HELP
31915
+ }
31916
+ },
31917
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
31918
+ const document = yield* parsePolicyDocument(args.document);
31919
+ const policy = yield* (yield* apiClient).policies.create({ payload: {
31920
+ name: args.name,
31921
+ document,
31922
+ ...compact({ description: args.description })
31923
+ } });
31924
+ yield* printHumanKeyValue([
31925
+ ["ID", policy.id],
31926
+ ["Name", policy.name],
31927
+ ["Description", policy.description ?? "-"],
31928
+ ["Statements", String(policy.document.statements.length)],
31929
+ ["Created", policy.createdAt]
31930
+ ]);
31931
+ return policy;
31932
+ }), {
31933
+ exits: policyErrorExtras,
31934
+ json: "value"
31935
+ })
31936
+ });
31937
+
31938
+ //#endregion
31939
+ //#region src/commands/policies/delete.ts
31940
+ const deletePolicyCommand = defineCommand({
31941
+ meta: {
31942
+ name: "delete",
31943
+ description: "Delete a policy and sweep its attachments (managed presets cannot be deleted)"
31944
+ },
31945
+ args: {
31946
+ id: {
31947
+ type: "positional",
31948
+ required: true,
31949
+ description: "Policy ID"
31950
+ },
31951
+ yes: {
31952
+ type: "boolean",
31953
+ description: "Skip confirmation prompt"
31954
+ }
31955
+ },
31956
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
31957
+ if (isManagedPolicyId(args.id)) return yield* new PolicyCommandError({ message: `Policy "${args.id}" is a managed preset and cannot be deleted.` });
31958
+ if (!args.yes) {
31959
+ if (!(yield* promptConfirm(`Delete policy ${args.id}?`, { initialValue: false }))) {
31960
+ yield* printHuman("Cancelled.");
31961
+ return;
31962
+ }
31963
+ }
31964
+ yield* (yield* apiClient).policies.delete({ path: { id: args.id } });
31965
+ yield* printHuman(`Deleted policy ${args.id}.`);
31966
+ return {
31967
+ id: args.id,
31968
+ deleted: true
31969
+ };
31970
+ }), {
31971
+ exits: policyErrorExtras,
31972
+ json: "value"
31973
+ })
31974
+ });
31975
+
31976
+ //#endregion
31977
+ //#region src/commands/policies/list.ts
31978
+ const listPoliciesCommand = defineCommand({
31979
+ meta: {
31980
+ name: "list",
31981
+ description: "List policies in the active organization (includes read-only managed presets)"
31982
+ },
31983
+ run: async () => runEffect(Effect.gen(function* () {
31984
+ const result = yield* (yield* apiClient).policies.list();
31985
+ yield* printHumanTable([
31986
+ "ID",
31987
+ "Name",
31988
+ "Statements",
31989
+ "Managed"
31990
+ ], result.items.map((policy) => [
31991
+ policy.id,
31992
+ policy.name,
31993
+ String(policy.document.statements.length),
31994
+ isManagedPolicyId(policy.id) ? "yes" : "no"
31995
+ ]));
31996
+ return result;
31997
+ }), {
31998
+ exits: policyErrorExtras,
31999
+ json: "value"
32000
+ })
32001
+ });
32002
+
32003
+ //#endregion
32004
+ //#region src/commands/policies/update.ts
32005
+ const updatePolicyCommand = defineCommand({
32006
+ meta: {
32007
+ name: "update",
32008
+ description: "Update a policy's name, description, or document (managed presets are read-only)"
32009
+ },
32010
+ args: {
32011
+ id: {
32012
+ type: "positional",
32013
+ required: true,
32014
+ description: "Policy ID"
32015
+ },
32016
+ name: {
32017
+ type: "string",
32018
+ description: "New display name"
32019
+ },
32020
+ description: {
32021
+ type: "string",
32022
+ description: "New description"
32023
+ },
32024
+ document: {
32025
+ type: "string",
32026
+ description: "Replacement JSON document string"
32027
+ }
32028
+ },
32029
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
32030
+ if (isManagedPolicyId(args.id)) return yield* new PolicyCommandError({ message: `Policy "${args.id}" is a managed preset and is read-only. Create a custom policy instead.` });
32031
+ const document = args.document === void 0 ? void 0 : yield* parsePolicyDocument(args.document);
32032
+ const policy = yield* (yield* apiClient).policies.update({
32033
+ path: { id: args.id },
32034
+ payload: compact({
32035
+ name: args.name,
32036
+ description: args.description,
32037
+ document
32038
+ })
32039
+ });
32040
+ yield* printHumanKeyValue([
32041
+ ["ID", policy.id],
32042
+ ["Name", policy.name],
32043
+ ["Description", policy.description ?? "-"],
32044
+ ["Statements", String(policy.document.statements.length)],
32045
+ ["Updated", policy.updatedAt ?? "-"]
32046
+ ]);
32047
+ return policy;
32048
+ }), {
32049
+ exits: policyErrorExtras,
32050
+ json: "value"
32051
+ })
32052
+ });
32053
+
32054
+ //#endregion
32055
+ //#region src/commands/policies/index.ts
32056
+ const policiesCommand = defineCommand({
32057
+ meta: {
32058
+ name: "policies",
32059
+ description: "Manage IAM policies (named, reusable permission grants)"
32060
+ },
32061
+ subCommands: {
32062
+ list: listPoliciesCommand,
32063
+ create: createPolicyCommand,
32064
+ update: updatePolicyCommand,
32065
+ delete: deletePolicyCommand
32066
+ }
32067
+ });
32068
+
31067
32069
  //#endregion
31068
32070
  //#region src/commands/projects.ts
31069
32071
  const listCommand$1 = defineCommand({
@@ -32700,10 +33702,10 @@ const assertSignedManifestBundleUrl = (params) => Effect.gen(function* () {
32700
33702
  try: () => JSON.parse(params.manifestBody),
32701
33703
  catch: () => params.makeError(`Signed ${params.platform} manifestBody is not valid JSON.`)
32702
33704
  });
32703
- if (!isRecord(parsed)) return yield* Effect.fail(params.makeError(`Signed ${params.platform} manifestBody must be a JSON object.`));
33705
+ if (!isRecord$1(parsed)) return yield* Effect.fail(params.makeError(`Signed ${params.platform} manifestBody must be a JSON object.`));
32704
33706
  const { id } = parsed;
32705
33707
  const { launchAsset } = parsed;
32706
- if (typeof id !== "string" || !isRecord(launchAsset)) return yield* Effect.fail(params.makeError(`Signed ${params.platform} manifestBody must carry a string "id" and an object "launchAsset".`));
33708
+ if (typeof id !== "string" || !isRecord$1(launchAsset)) return yield* Effect.fail(params.makeError(`Signed ${params.platform} manifestBody must carry a string "id" and an object "launchAsset".`));
32707
33709
  const { url } = launchAsset;
32708
33710
  const expectedPrefix = `${params.serverBaseUrl}/manifest/${params.projectId}/bundle/${id}/`;
32709
33711
  if (typeof url !== "string" || !url.startsWith(expectedPrefix)) return yield* Effect.fail(params.makeError(`Signed ${params.platform} manifestBody launchAsset.url must point at the Worker bundle route (${expectedPrefix}…) so bsdiff patches apply; got ${typeof url === "string" ? url : "a non-string value"}. Re-render the signed manifest with the Worker bundle URL.`));
@@ -32872,7 +33874,7 @@ const runPatchPhase = (input) => Effect.gen(function* () {
32872
33874
  runtimeVersion: input.runtimeVersion,
32873
33875
  platform: input.platform,
32874
33876
  limit: Math.max(1, input.baseWindow)
32875
- } }).pipe(Effect.catchAll(() => Effect.succeed([])));
33877
+ } }).pipe(Effect.orElseSucceed(() => []));
32876
33878
  const bases = selectBaseWindow(candidates, {
32877
33879
  newUpdateId: input.newUpdateId,
32878
33880
  maxRecent: input.baseWindow
@@ -32888,7 +33890,7 @@ const runPatchPhase = (input) => Effect.gen(function* () {
32888
33890
  bestSavingsPct: void 0
32889
33891
  };
32890
33892
  }
32891
- const newBundleBytes = yield* sha256File(input.newLaunchPath).pipe(Effect.map((result) => result.byteSize), Effect.catchAll(() => Effect.succeed(void 0)));
33893
+ const newBundleBytes = yield* sha256File(input.newLaunchPath).pipe(Effect.map((result) => result.byteSize), Effect.orElseSucceed(() => void 0));
32892
33894
  yield* printHuman(`Diffing against ${bases.length} base(s) (window=${input.baseWindow}; ${candidates.length} candidate(s) available).`);
32893
33895
  const uploadedOutcomes = (yield* Effect.forEach(bases, (base, index) => Effect.gen(function* () {
32894
33896
  const basePath = path.join(input.workDir, `base-${index}.bundle`);
@@ -32983,7 +33985,7 @@ const dedupeAssetsByHash = (assets) => uniqBy(assets, (asset) => asset.hash);
32983
33985
  * is informational, so a fingerprint failure resolves to `undefined` rather than
32984
33986
  * failing the publish.
32985
33987
  */
32986
- const resolvePlatformFingerprintHash = (projectRoot, platform) => runFingerprintForPlatform(projectRoot, platform).pipe(Effect.map((result) => result.hash), Effect.catchAll(() => Effect.succeed(void 0)));
33988
+ const resolvePlatformFingerprintHash = (projectRoot, platform) => runFingerprintForPlatform(projectRoot, platform).pipe(Effect.map((result) => result.hash), Effect.orElseSucceed(() => void 0));
32987
33989
  const preparePlatformAssets = ({ exportDir, platform }) => Effect.gen(function* () {
32988
33990
  const exportedAssets = yield* readExpoExportAssets({
32989
33991
  exportDir,
@@ -33079,7 +34081,7 @@ const publishPlatform = (params) => Effect.gen(function* () {
33079
34081
  const uploadDetailsByHash = new Map(assetRegistration.uploaded.map((asset) => [asset.hash, asset]));
33080
34082
  yield* Effect.forEach(uniqueAssets.filter((asset) => uploadDetailsByHash.has(asset.hash)), (asset) => Effect.gen(function* () {
33081
34083
  const detail = uploadDetailsByHash.get(asset.hash);
33082
- if (!detail) return yield* Effect.fail(new UpdatePublishError({ message: `Missing upload details for asset ${asset.hash}` }));
34084
+ if (!detail) return yield* new UpdatePublishError({ message: `Missing upload details for asset ${asset.hash}` });
33083
34085
  return yield* assetUploader.uploadAssetBinary({
33084
34086
  path: asset.path,
33085
34087
  hash: asset.hash,
@@ -33631,10 +34633,10 @@ const extractDirectiveCommitTime = (directiveBody) => Effect.gen(function* () {
33631
34633
  try: () => JSON.parse(directiveBody),
33632
34634
  catch: () => new UpdateRollbackError({ message: "directiveBody must be valid JSON." })
33633
34635
  });
33634
- if (!isRecord(directive)) return yield* new UpdateRollbackError({ message: "directiveBody must decode to a JSON object." });
34636
+ if (!isRecord$1(directive)) return yield* new UpdateRollbackError({ message: "directiveBody must decode to a JSON object." });
33635
34637
  if (directive["type"] !== "rollBackToEmbedded") return yield* new UpdateRollbackError({ message: "directiveBody.type must be \"rollBackToEmbedded\"." });
33636
34638
  const { parameters } = directive;
33637
- if (!isRecord(parameters)) return yield* new UpdateRollbackError({ message: "directiveBody.parameters must be an object." });
34639
+ if (!isRecord$1(parameters)) return yield* new UpdateRollbackError({ message: "directiveBody.parameters must be an object." });
33638
34640
  const { commitTime } = parameters;
33639
34641
  if (typeof commitTime !== "string" || Number.isNaN(Date.parse(commitTime))) return yield* new UpdateRollbackError({ message: "directiveBody.parameters.commitTime must be a valid ISO 8601 timestamp." });
33640
34642
  if (new Date(Date.parse(commitTime)).toISOString() !== commitTime) return yield* new UpdateRollbackError({ message: "directiveBody.parameters.commitTime must be canonical ISO 8601 with milliseconds and a trailing Z (e.g. 2026-05-06T14:00:00.000Z). The expo-updates client cannot parse other forms (no milliseconds, numeric timezone offsets), so the signed rollback would be rejected on-device." });
@@ -34462,6 +35464,8 @@ const commandRegistry = {
34462
35464
  init: initCommand,
34463
35465
  status: statusCommand,
34464
35466
  projects: projectsCommand,
35467
+ policies: policiesCommand,
35468
+ groups: groupsCommand,
34465
35469
  branches: branchesCommand,
34466
35470
  channels: channelsCommand,
34467
35471
  build: buildCommand,