@better-update/cli 0.10.1 → 0.12.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
@@ -7,19 +7,20 @@ import { Command, FetchHttpClient, FileSystem, HttpApi, HttpApiClient, HttpApiEn
7
7
  import { NodeContext } from "@effect/platform-node";
8
8
  import path, { join } from "node:path";
9
9
  import process$1 from "node:process";
10
+ import * as AppleUtils from "@expo/apple-utils";
11
+ import { cancel, confirm, isCancel, multiselect, password, select, text } from "@clack/prompts";
12
+ import { once } from "node:events";
13
+ import { createServer } from "node:http";
10
14
  import { maxBy, uniqBy } from "es-toolkit";
11
15
  import { createHash, randomBytes, randomUUID } from "node:crypto";
12
16
  import { createReadStream, existsSync, readFileSync, writeFileSync } from "node:fs";
13
17
  import os from "node:os";
14
18
  import plist from "@expo/plist";
15
19
  import { ExpoRunFormatter } from "@expo/xcpretty";
16
- import { cancel, confirm, isCancel, multiselect, password, select, text } from "@clack/prompts";
17
20
  import forge from "node-forge";
18
21
  import { Buffer as Buffer$1 } from "node:buffer";
19
22
  import { getFormattedSerialNumber, getX509Certificate, parsePKCS12 } from "@expo/pkcs12";
20
23
  import qrcode from "qrcode-terminal";
21
- import { once } from "node:events";
22
- import { createServer } from "node:http";
23
24
  import { fileURLToPath } from "node:url";
24
25
 
25
26
  //#region \0rolldown/runtime.js
@@ -27,7 +28,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
27
28
 
28
29
  //#endregion
29
30
  //#region package.json
30
- var version = "0.10.1";
31
+ var version = "0.12.0";
31
32
 
32
33
  //#endregion
33
34
  //#region src/lib/interactive-mode.ts
@@ -1127,37 +1128,51 @@ var DevicesGroup = class extends HttpApiGroup.make("devices").add(HttpApiEndpoin
1127
1128
 
1128
1129
  //#endregion
1129
1130
  //#region ../../packages/api/src/domain/env-var.ts
1130
- const EnvVarVisibility = Schema.Literal("plaintext", "sensitive", "secret");
1131
+ const EnvVarVisibility = Schema.Literal("plaintext", "sensitive");
1132
+ const EnvVarScope = Schema.Literal("project", "global");
1133
+ const EnvVarEnvironment = Schema.Literal("development", "preview", "production");
1134
+ const EnvVarListScope = Schema.Literal("all", "project", "global");
1131
1135
  var EnvVar = class extends Schema.Class("EnvVar")({
1132
1136
  id: Id,
1133
1137
  organizationId: Id,
1134
- projectId: Id,
1135
- environment: Schema.String,
1138
+ projectId: Schema.NullOr(Id),
1139
+ scope: EnvVarScope,
1136
1140
  key: Schema.String,
1137
1141
  visibility: EnvVarVisibility,
1138
1142
  value: Schema.NullOr(Schema.String),
1143
+ environments: Schema.Array(EnvVarEnvironment),
1144
+ overridesGlobal: Schema.optional(Schema.Boolean),
1139
1145
  createdAt: DateTimeString,
1140
1146
  updatedAt: DateTimeString
1141
1147
  }) {};
1142
1148
  const EnvVarKey = Schema.String.pipe(Schema.pattern(/^[A-Z][A-Z0-9_]*$/u), Schema.maxLength(256));
1143
1149
  const EnvVarValue = Schema.String.pipe(Schema.maxLength(32768));
1144
- const EnvVarEnvironment = Schema.String.pipe(Schema.minLength(1), Schema.maxLength(64));
1150
+ const EnvVarEnvironmentArray = Schema.Array(EnvVarEnvironment).pipe(Schema.minItems(1));
1145
1151
  const CreateEnvVarBody = Schema.Struct({
1146
- projectId: Id,
1147
- environment: EnvVarEnvironment,
1152
+ scope: EnvVarScope,
1153
+ projectId: Schema.optional(Id),
1154
+ environments: EnvVarEnvironmentArray,
1148
1155
  key: EnvVarKey,
1149
1156
  value: EnvVarValue,
1150
1157
  visibility: EnvVarVisibility
1151
1158
  });
1152
1159
  const UpdateEnvVarBody = Schema.Struct({
1153
1160
  value: Schema.optional(EnvVarValue),
1161
+ visibility: Schema.optional(EnvVarVisibility),
1162
+ environments: Schema.optional(EnvVarEnvironmentArray)
1163
+ });
1164
+ const BulkImportEntry = Schema.Struct({
1165
+ key: EnvVarKey,
1166
+ value: EnvVarValue,
1154
1167
  visibility: Schema.optional(EnvVarVisibility)
1155
1168
  });
1156
1169
  const BulkImportEnvVarsBody = Schema.Struct({
1157
- projectId: Id,
1158
- environment: EnvVarEnvironment,
1159
- content: Schema.String.pipe(Schema.maxLength(4e6)),
1160
- visibility: EnvVarVisibility
1170
+ scope: EnvVarScope,
1171
+ projectId: Schema.optional(Id),
1172
+ environments: EnvVarEnvironmentArray,
1173
+ content: Schema.optional(Schema.String.pipe(Schema.maxLength(4e6))),
1174
+ entries: Schema.optional(Schema.Array(BulkImportEntry).pipe(Schema.maxItems(100))),
1175
+ visibility: Schema.optional(EnvVarVisibility)
1161
1176
  });
1162
1177
  const BulkImportResult = Schema.Struct({
1163
1178
  created: Schema.Number,
@@ -1172,7 +1187,7 @@ const EnvVarExportItem = Schema.Struct({
1172
1187
  });
1173
1188
  const EnvVarExportResult = Schema.Struct({
1174
1189
  items: Schema.Array(EnvVarExportItem),
1175
- environment: Schema.String
1190
+ environment: EnvVarEnvironment
1176
1191
  });
1177
1192
 
1178
1193
  //#endregion
@@ -1180,32 +1195,34 @@ const EnvVarExportResult = Schema.Struct({
1180
1195
  const idParam$5 = HttpApiSchema.param("id", Schema.String);
1181
1196
  var EnvVarsGroup = class extends HttpApiGroup.make("env-vars").add(HttpApiEndpoint.post("create", "/api/env-vars").setPayload(CreateEnvVarBody).addSuccess(EnvVar, { status: 201 }).addError(BadRequest).addError(Conflict).annotateContext(OpenApi.annotations({
1182
1197
  title: "Create environment variable",
1183
- description: "Create a new environment variable for a project"
1198
+ description: "Create a new environment variable. Scope can be 'project' (requires projectId) or 'global' (organization-wide)."
1184
1199
  }))).add(HttpApiEndpoint.get("list", "/api/env-vars").setUrlParams(Schema.Struct({
1185
- projectId: Id,
1186
- environment: Schema.optional(Schema.String),
1200
+ scope: Schema.optional(EnvVarListScope),
1201
+ projectId: Schema.optional(Id),
1202
+ environments: Schema.optional(Schema.String),
1203
+ search: Schema.optional(Schema.String),
1187
1204
  ...PaginationParams.fields
1188
- })).addSuccess(Schema.Struct({ items: Schema.Array(EnvVar) })).annotateContext(OpenApi.annotations({
1205
+ })).addSuccess(Schema.Struct({ items: Schema.Array(EnvVar) })).addError(BadRequest).annotateContext(OpenApi.annotations({
1189
1206
  title: "List environment variables",
1190
- description: "List environment variables with optional filters"
1207
+ description: "List environment variables. scope=all merges project + global vars with project overrides. environments is a comma-separated list. search matches key substring."
1191
1208
  }))).add(HttpApiEndpoint.get("get")`/api/env-vars/${idParam$5}`.addSuccess(EnvVar).annotateContext(OpenApi.annotations({
1192
1209
  title: "Get environment variable",
1193
1210
  description: "Get an environment variable by ID"
1194
1211
  }))).add(HttpApiEndpoint.patch("update")`/api/env-vars/${idParam$5}`.setPayload(UpdateEnvVarBody).addSuccess(EnvVar).addError(BadRequest).annotateContext(OpenApi.annotations({
1195
1212
  title: "Update environment variable",
1196
- description: "Update an environment variable's value or visibility"
1213
+ description: "Update value, visibility, or assigned environments"
1197
1214
  }))).add(HttpApiEndpoint.del("delete")`/api/env-vars/${idParam$5}`.addSuccess(DeleteEnvVarResult).annotateContext(OpenApi.annotations({
1198
1215
  title: "Delete environment variable",
1199
1216
  description: "Delete an environment variable"
1200
1217
  }))).add(HttpApiEndpoint.post("bulkImport", "/api/env-vars/bulk-import").setPayload(BulkImportEnvVarsBody).addSuccess(BulkImportResult).addError(BadRequest).annotateContext(OpenApi.annotations({
1201
1218
  title: "Bulk import environment variables",
1202
- description: "Import environment variables from a dotenv-formatted string. Supports KEY=VALUE format with # comments. Quoted values (single/double) are unquoted. Multiline values are not supported."
1219
+ description: "Import variables from a dotenv-formatted string. Applies to all selected environments. Supports KEY=VALUE format with # comments. Quoted values (single/double) are unquoted. Multiline values are not supported."
1203
1220
  }))).add(HttpApiEndpoint.get("export", "/api/env-vars/export").setUrlParams(Schema.Struct({
1204
1221
  projectId: Id,
1205
- environment: Schema.String
1222
+ environment: EnvVarEnvironment
1206
1223
  })).addSuccess(EnvVarExportResult).addError(Forbidden).annotateContext(OpenApi.annotations({
1207
1224
  title: "Export environment variables",
1208
- description: "Export environment variables for a project environment"
1225
+ description: "Export environment variables for a project environment. Global org-scoped vars are merged in; project values override globals on key collision."
1209
1226
  }))).addError(NotFound).addError(Forbidden).addError(BadRequest).annotateContext(OpenApi.annotations({
1210
1227
  title: "Environment Variables",
1211
1228
  description: "Manage environment variables for project builds and deployments"
@@ -1756,6 +1773,53 @@ const ApiClientLive = Layer.effect(ApiClientService, Effect.gen(function* () {
1756
1773
  }) };
1757
1774
  }));
1758
1775
 
1776
+ //#endregion
1777
+ //#region src/lib/prompts.ts
1778
+ const ensureInteractive = (promptName) => Effect.gen(function* () {
1779
+ if (!(yield* InteractiveMode).allow) return yield* new InteractiveProhibitedError({ message: `Interactive prompt "${promptName}" requested while running non-interactively. Provide the value via a flag, run with --interactive, or unset CI.` });
1780
+ });
1781
+ const handleCancel = (value) => {
1782
+ if (isCancel(value)) {
1783
+ cancel("Operation cancelled.");
1784
+ process.exit(130);
1785
+ }
1786
+ return value;
1787
+ };
1788
+ const promptPassword = (message) => Effect.gen(function* () {
1789
+ yield* ensureInteractive(message);
1790
+ return handleCancel(yield* Effect.promise(async () => password({ message })));
1791
+ });
1792
+ const promptSelect = (message, options) => Effect.gen(function* () {
1793
+ yield* ensureInteractive(message);
1794
+ return handleCancel(yield* Effect.promise(async () => select({
1795
+ message,
1796
+ options: [...options]
1797
+ })));
1798
+ });
1799
+ const promptMultiSelect = (message, options, config) => Effect.gen(function* () {
1800
+ yield* ensureInteractive(message);
1801
+ return handleCancel(yield* Effect.promise(async () => multiselect({
1802
+ message,
1803
+ options: [...options],
1804
+ required: config?.required ?? false
1805
+ })));
1806
+ });
1807
+ const promptText = (message, options) => Effect.gen(function* () {
1808
+ yield* ensureInteractive(message);
1809
+ return handleCancel(yield* Effect.promise(async () => text({
1810
+ message,
1811
+ ...options?.placeholder === void 0 ? {} : { placeholder: options.placeholder },
1812
+ ...options?.defaultValue === void 0 ? {} : { defaultValue: options.defaultValue }
1813
+ })));
1814
+ });
1815
+ const promptConfirm = (message, options) => Effect.gen(function* () {
1816
+ yield* ensureInteractive(message);
1817
+ return handleCancel(yield* Effect.promise(async () => confirm({
1818
+ message,
1819
+ ...options?.initialValue === void 0 ? {} : { initialValue: options.initialValue }
1820
+ })));
1821
+ });
1822
+
1759
1823
  //#endregion
1760
1824
  //#region ../../packages/safe-json/src/index.ts
1761
1825
  const parseJsonResult = (text) => {
@@ -1781,6 +1845,7 @@ const AppleSessionStoreLive = Layer.effect(AppleSessionStore, Effect.gen(functio
1781
1845
  const homeDirectory = yield* (yield* CliRuntime).homeDirectory;
1782
1846
  const sessionDir = path.join(homeDirectory, ".better-update");
1783
1847
  const sessionFile = path.join(sessionDir, "apple-session.json");
1848
+ const usernameFile = path.join(sessionDir, "apple-username.json");
1784
1849
  return {
1785
1850
  loadSession: Effect.gen(function* () {
1786
1851
  const content = yield* fs.readFileString(sessionFile).pipe(Effect.catchAll(() => Effect.succeed(null)));
@@ -1803,9 +1868,129 @@ const AppleSessionStoreLive = Layer.effect(AppleSessionStore, Effect.gen(functio
1803
1868
  yield* fs.writeFileString(sessionFile, `${JSON.stringify(session, null, 2)}\n`);
1804
1869
  yield* fs.chmod(sessionFile, 384);
1805
1870
  }).pipe(Effect.mapError((cause) => new AppleAuthError$1({ message: `Failed to save Apple session: ${formatCause(cause)}` }))),
1806
- clearSession: fs.remove(sessionFile).pipe(Effect.catchAll(() => Effect.void))
1871
+ clearSession: fs.remove(sessionFile).pipe(Effect.catchAll(() => Effect.void)),
1872
+ loadLastUsername: Effect.gen(function* () {
1873
+ const content = yield* fs.readFileString(usernameFile).pipe(Effect.catchAll(() => Effect.succeed(null)));
1874
+ if (!content) return null;
1875
+ const parsed = safeJsonParse(content);
1876
+ if (!isRecord(parsed) || typeof parsed["username"] !== "string") return null;
1877
+ return parsed["username"];
1878
+ }),
1879
+ saveLastUsername: (username) => Effect.gen(function* () {
1880
+ yield* fs.makeDirectory(sessionDir, { recursive: true });
1881
+ yield* fs.chmod(sessionDir, 448);
1882
+ yield* fs.writeFileString(usernameFile, `${JSON.stringify({ username }, null, 2)}\n`);
1883
+ yield* fs.chmod(usernameFile, 384);
1884
+ }).pipe(Effect.mapError((cause) => new AppleAuthError$1({ message: `Failed to save Apple username: ${formatCause(cause)}` })))
1885
+ };
1886
+ }));
1887
+
1888
+ //#endregion
1889
+ //#region src/services/apple-auth.ts
1890
+ const defaultAppleUtils = {
1891
+ Auth: AppleUtils.Auth,
1892
+ Session: AppleUtils.Session,
1893
+ CookieFileCache: AppleUtils.CookieFileCache
1894
+ };
1895
+ var AppleAuth = class extends Context.Tag("cli/AppleAuth")() {};
1896
+ const sessionFromAuthState = (state) => ({
1897
+ username: state.username,
1898
+ teamId: state.context.teamId ?? state.session.provider.publicProviderId,
1899
+ teamName: state.session.provider.name,
1900
+ providerId: state.context.providerId ?? state.session.provider.providerId
1901
+ });
1902
+ const sessionFromInfo = (username, info) => ({
1903
+ username,
1904
+ teamId: info.provider.publicProviderId,
1905
+ teamName: info.provider.name,
1906
+ providerId: info.provider.providerId
1907
+ });
1908
+ const restoreFromCookies = (appleUtils, cookies, providerId, teamId) => Effect.tryPromise({
1909
+ try: async () => {
1910
+ const input = {
1911
+ cookies,
1912
+ ...providerId === void 0 ? {} : { providerId },
1913
+ ...teamId === void 0 ? {} : { teamId }
1914
+ };
1915
+ return appleUtils.Auth.loginWithCookiesAsync(input);
1916
+ },
1917
+ catch: (cause) => new AppleAuthError$1({ message: `Failed to restore Apple session: ${formatCause(cause)}` })
1918
+ });
1919
+ const loginWithCredentials = (appleUtils, credentials) => Effect.tryPromise({
1920
+ try: async () => appleUtils.Auth.loginWithUserCredentialsAsync(credentials, { autoResolveProvider: true }),
1921
+ catch: (cause) => new AppleAuthError$1({ message: `Apple login failed: ${formatCause(cause)}` })
1922
+ });
1923
+ const readJarCookies = (appleUtils) => appleUtils.CookieFileCache.getCookiesJSON();
1924
+ const promptCredentials = (defaultUsername) => Effect.gen(function* () {
1925
+ const username = yield* promptText("Apple ID", defaultUsername === void 0 ? { placeholder: "you@example.com" } : {
1926
+ defaultValue: defaultUsername,
1927
+ placeholder: defaultUsername
1928
+ });
1929
+ return {
1930
+ username,
1931
+ password: yield* promptPassword(`Password for ${username}`)
1932
+ };
1933
+ });
1934
+ const interactiveLogin = (appleUtils, options, cachedUsername) => Effect.gen(function* () {
1935
+ const store = yield* AppleSessionStore;
1936
+ if (!(yield* InteractiveMode).allow) return yield* new InteractiveProhibitedError({ message: "Apple ID login requires an interactive terminal. Re-run with --interactive or provide an ASC API key (APPLE_ASC_KEY_ID, APPLE_ASC_ISSUER_ID, APPLE_ASC_KEY)." });
1937
+ const defaultUsername = options.username ?? cachedUsername;
1938
+ const { username, password } = yield* promptCredentials(defaultUsername === null ? void 0 : defaultUsername);
1939
+ yield* Effect.logInfo(`Authenticating with Apple as ${username}...`);
1940
+ const state = yield* loginWithCredentials(appleUtils, {
1941
+ username,
1942
+ password
1943
+ });
1944
+ if (state === null) return yield* new AppleAuthError$1({ message: "Apple login returned no session (unexpected)." });
1945
+ const session = sessionFromAuthState(state);
1946
+ yield* store.saveSession({
1947
+ cookies: readJarCookies(appleUtils),
1948
+ username: session.username,
1949
+ teamId: session.teamId,
1950
+ ...session.providerId === void 0 ? {} : { providerId: session.providerId }
1951
+ });
1952
+ yield* store.saveLastUsername(session.username);
1953
+ return session;
1954
+ });
1955
+ const tryRestore = (appleUtils, store) => Effect.gen(function* () {
1956
+ const stored = yield* store.loadSession;
1957
+ if (stored === null) return null;
1958
+ const restored = yield* restoreFromCookies(appleUtils, stored.cookies, stored.providerId, stored.teamId);
1959
+ if (restored === null) return null;
1960
+ return sessionFromAuthState(restored);
1961
+ });
1962
+ const makeAppleAuthLive = (appleUtils = defaultAppleUtils) => Layer.effect(AppleAuth, Effect.gen(function* () {
1963
+ const store = yield* AppleSessionStore;
1964
+ return {
1965
+ ensureLoggedIn: (options = {}) => Effect.gen(function* () {
1966
+ const restored = yield* tryRestore(appleUtils, store).pipe(Effect.catchAll(() => Effect.succeed(null)));
1967
+ if (restored !== null) return restored;
1968
+ return yield* interactiveLogin(appleUtils, options, yield* store.loadLastUsername).pipe(Effect.provideService(AppleSessionStore, store));
1969
+ }),
1970
+ logout: store.clearSession.pipe(Effect.flatMap(() => Effect.tryPromise({
1971
+ try: async () => appleUtils.Auth.logoutAsync(),
1972
+ catch: (cause) => new AppleAuthError$1({ message: formatCause(cause) })
1973
+ }).pipe(Effect.catchAll(() => Effect.void)))),
1974
+ whoami: Effect.gen(function* () {
1975
+ const stored = yield* store.loadSession;
1976
+ if (stored === null) return null;
1977
+ const restored = yield* restoreFromCookies(appleUtils, stored.cookies, stored.providerId, stored.teamId).pipe(Effect.catchAll(() => Effect.succeed(null)));
1978
+ if (restored !== null) return sessionFromAuthState(restored);
1979
+ const info = appleUtils.Session.getAnySessionInfo();
1980
+ return info === null ? {
1981
+ username: stored.username,
1982
+ teamId: stored.teamId,
1983
+ teamName: null,
1984
+ providerId: stored.providerId
1985
+ } : sessionFromInfo(stored.username, info);
1986
+ }),
1987
+ buildRequestContext: (session) => ({
1988
+ teamId: session.teamId,
1989
+ ...session.providerId === void 0 ? {} : { providerId: session.providerId }
1990
+ })
1807
1991
  };
1808
1992
  }));
1993
+ const AppleAuthLive = makeAppleAuthLive();
1809
1994
 
1810
1995
  //#endregion
1811
1996
  //#region src/services/presigned-upload.ts
@@ -1902,10 +2087,11 @@ const CliPlatformLayer = Layer.mergeAll(CliRuntimeLive, NodeContext.layer, Fetch
1902
2087
  const CliStoreLayer = Layer.mergeAll(AuthStoreLive, ConfigStoreLive, AppleSessionStoreLive).pipe(Layer.provide(CliPlatformLayer));
1903
2088
  const CliAdapterDependencies = Layer.mergeAll(CliPlatformLayer, CliStoreLayer);
1904
2089
  const ApiClientLayer = ApiClientLive.pipe(Layer.provide(CliAdapterDependencies));
2090
+ const AppleAuthLayer = AppleAuthLive.pipe(Layer.provide(CliAdapterDependencies));
1905
2091
  const PresignedUploadLayer = PresignedUploadClientLive.pipe(Layer.provide(CliPlatformLayer));
1906
2092
  const UpdateAssetUploaderLayer = UpdateAssetUploaderLive.pipe(Layer.provide(Layer.mergeAll(ApiClientLayer, PresignedUploadLayer)));
1907
2093
  const VersionCheckLayer = VersionCheckLive.pipe(Layer.provide(CliPlatformLayer));
1908
- const makeCliLive = (options) => Layer.mergeAll(CliAdapterDependencies, ApiClientLayer, PresignedUploadLayer, UpdateAssetUploaderLayer, VersionCheckLayer, makeOutputModeLayer(options.json), makeInteractiveModeLayer(options.interactive));
2094
+ const makeCliLive = (options) => Layer.mergeAll(CliAdapterDependencies, ApiClientLayer, AppleAuthLayer, PresignedUploadLayer, UpdateAssetUploaderLayer, VersionCheckLayer, makeOutputModeLayer(options.json), makeInteractiveModeLayer(options.interactive));
1909
2095
  /** Default CLI layer: human-readable, interactive. Override via flags at the entrypoint. */
1910
2096
  const CliLive = makeCliLive({
1911
2097
  json: false,
@@ -1913,55 +2099,256 @@ const CliLive = makeCliLive({
1913
2099
  });
1914
2100
 
1915
2101
  //#endregion
1916
- //#region src/application/command-exit.ts
1917
- const exitWith = (code, message) => Console.error(message).pipe(Effect.zipRight(Effect.gen(function* () {
1918
- yield* (yield* CliRuntime).setExitCode(code);
1919
- })));
2102
+ //#region src/lib/browser-login.ts
2103
+ var BrowserLoginTimeoutError = class extends Data.TaggedError("BrowserLoginTimeoutError") {};
2104
+ var BrowserLoginSessionClosedError = class extends Data.TaggedError("BrowserLoginSessionClosedError") {};
2105
+ const CALLBACK_PAGE = `<!doctype html>
2106
+ <html lang="en">
2107
+ <head>
2108
+ <meta charset="utf-8" />
2109
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2110
+ <title>better-update CLI Login</title>
2111
+ <style>
2112
+ :root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, sans-serif; }
2113
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px; }
2114
+ main { max-width: 32rem; line-height: 1.5; }
2115
+ code { font-family: ui-monospace, SFMono-Regular, monospace; }
2116
+ </style>
2117
+ </head>
2118
+ <body>
2119
+ <main>
2120
+ <h1>Completing CLI login...</h1>
2121
+ <p id="message">Finalizing the local session. You can keep this tab open.</p>
2122
+ </main>
2123
+ <script>
2124
+ const message = document.getElementById("message");
2125
+ const render = (text) => {
2126
+ if (message) message.textContent = text;
2127
+ };
1920
2128
 
1921
- //#endregion
1922
- //#region src/lib/command-errors.ts
1923
- const BASE_TAG_MAP = {
1924
- AuthRequiredError: 3,
1925
- ProjectNotLinkedError: 4,
1926
- NotFound: 1,
1927
- Conflict: 1,
1928
- Forbidden: 1,
1929
- BadRequest: 2,
1930
- InvalidArgumentError: 2,
1931
- InteractiveProhibitedError: 2
1932
- };
1933
- const SYSTEM_TAG_MESSAGE = {
1934
- SystemError: (error) => `Filesystem error: ${error.message}`,
1935
- BadArgument: (error) => `Invalid argument: ${error.message}`
2129
+ const params = new URLSearchParams(window.location.hash.slice(1));
2130
+ const token = params.get("token");
2131
+
2132
+ if (!token) {
2133
+ render("Missing token. Return to the CLI and run login again.");
2134
+ } else {
2135
+ fetch("/callback/token", {
2136
+ method: "POST",
2137
+ headers: { "content-type": "application/json" },
2138
+ body: JSON.stringify({ token }),
2139
+ })
2140
+ .then(async (response) => {
2141
+ if (!response.ok) {
2142
+ const body = await response.text();
2143
+ throw new Error(body || "Callback failed");
2144
+ }
2145
+ window.history.replaceState({}, document.title, window.location.pathname);
2146
+ render("CLI login complete. You can close this tab.");
2147
+ setTimeout(() => window.close(), 300);
2148
+ })
2149
+ .catch((error) => {
2150
+ render(error instanceof Error ? error.message : "Callback failed.");
2151
+ });
2152
+ }
2153
+ <\/script>
2154
+ </body>
2155
+ </html>`;
2156
+ const createBrowserLoginSession = (options = {}) => {
2157
+ const tokenDeferred = Effect.runSync(Deferred.make());
2158
+ const waitForToken = Deferred.await(tokenDeferred).pipe(Effect.timeoutFail({
2159
+ duration: options.timeoutMs === void 0 ? Duration.minutes(5) : Duration.millis(options.timeoutMs),
2160
+ onTimeout: () => new BrowserLoginTimeoutError({ message: "Timed out waiting for browser login to complete." })
2161
+ }));
2162
+ const dispose = () => {
2163
+ Effect.runSync(Deferred.fail(tokenDeferred, new BrowserLoginSessionClosedError({ message: "Browser login session closed." })));
2164
+ };
2165
+ return {
2166
+ callbackPath: "/callback",
2167
+ waitForToken,
2168
+ handleRequest: async (request) => {
2169
+ const url = new URL(request.url);
2170
+ if (request.method === "GET" && url.pathname === "/callback") return new Response(CALLBACK_PAGE, { headers: { "content-type": "text/html; charset=utf-8" } });
2171
+ if (request.method === "POST" && url.pathname === "/callback/token") try {
2172
+ const body = await request.json();
2173
+ if (!isRecord(body)) return new Response("Invalid callback payload", { status: 400 });
2174
+ const token = typeof body["token"] === "string" ? body["token"].trim() : "";
2175
+ if (token.length === 0) return new Response("Missing token", { status: 400 });
2176
+ Effect.runSync(Deferred.succeed(tokenDeferred, token));
2177
+ return Response.json({ ok: true });
2178
+ } catch {
2179
+ return new Response("Invalid callback payload", { status: 400 });
2180
+ }
2181
+ return new Response("Not found", { status: 404 });
2182
+ },
2183
+ dispose
2184
+ };
1936
2185
  };
1937
- const SYSTEM_TAG_CODE = {
1938
- SystemError: 6,
1939
- BadArgument: 6
2186
+ const readBody = async (req) => {
2187
+ const chunks = [];
2188
+ for await (const chunk of req) chunks.push(chunk);
2189
+ return Buffer.concat(chunks);
1940
2190
  };
1941
- const makeCommandErrorHandler = (extras = {}) => {
1942
- const combined = {
1943
- ...BASE_TAG_MAP,
1944
- ...extras
2191
+ const toFetchRequest = async (req, origin) => {
2192
+ const url = new URL(req.url ?? "/", origin);
2193
+ const method = req.method ?? "GET";
2194
+ const headers = new Headers();
2195
+ for (const [key, value] of Object.entries(req.headers)) {
2196
+ if (value === void 0) continue;
2197
+ if (Array.isArray(value)) for (const entry of value) headers.append(key, entry);
2198
+ else headers.append(key, value);
2199
+ }
2200
+ const init = {
2201
+ method,
2202
+ headers
1945
2203
  };
1946
- const handlers = {};
1947
- for (const [tag, code] of Object.entries(combined)) {
1948
- const systemFormat = SYSTEM_TAG_MESSAGE[tag];
1949
- const resolvedCode = SYSTEM_TAG_CODE[tag] ?? code;
1950
- handlers[tag] = (error) => exitWith(resolvedCode, systemFormat ? systemFormat(error) : error.message);
2204
+ if (method !== "GET" && method !== "HEAD") {
2205
+ const body = await readBody(req);
2206
+ init.body = new Uint8Array(body);
1951
2207
  }
1952
- return (effect) => {
1953
- return effect.pipe(Effect.catchTags(handlers), Effect.catchAll((cause) => exitWith(1, formatCause(cause))));
2208
+ return new Request(url, init);
2209
+ };
2210
+ const writeFetchResponse = async (res, response) => {
2211
+ res.statusCode = response.status;
2212
+ response.headers.forEach((value, key) => {
2213
+ res.setHeader(key, value);
2214
+ });
2215
+ const body = await response.arrayBuffer();
2216
+ res.end(Buffer.from(body));
2217
+ };
2218
+ const handleIncoming = async (req, res, session) => {
2219
+ try {
2220
+ const request = await toFetchRequest(req, "http://127.0.0.1");
2221
+ await writeFetchResponse(res, await session.handleRequest(request));
2222
+ } catch {
2223
+ res.statusCode = 500;
2224
+ res.end("Local callback failed");
2225
+ }
2226
+ };
2227
+ const createBrowserLoginServer = async (options = {}) => {
2228
+ const session = createBrowserLoginSession(options);
2229
+ const server = createServer((req, res) => {
2230
+ handleIncoming(req, res, session).catch(() => void 0);
2231
+ });
2232
+ server.listen(0, "127.0.0.1");
2233
+ await once(server, "listening");
2234
+ const address = server.address();
2235
+ return {
2236
+ callbackUrl: `http://127.0.0.1:${address !== null && typeof address === "object" ? address.port : 0}${session.callbackPath}`,
2237
+ waitForToken: session.waitForToken,
2238
+ stop: () => {
2239
+ session.dispose();
2240
+ server.close();
2241
+ }
1954
2242
  };
1955
2243
  };
1956
2244
 
1957
2245
  //#endregion
1958
- //#region src/lib/citty-effect.ts
1959
- let activeCliLayer = CliLive;
2246
+ //#region src/application/login.ts
2247
+ const buildOpenBrowserCommand = (platform, url) => {
2248
+ if (platform === "darwin") return Command.make("open", url);
2249
+ if (platform === "win32") return Command.make("cmd", "/c", "start", "", url);
2250
+ return Command.make("xdg-open", url);
2251
+ };
2252
+ const openBrowser = (url) => Effect.gen(function* () {
2253
+ const command = buildOpenBrowserCommand((yield* CliRuntime).platform, url);
2254
+ 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}`);
2255
+ });
2256
+ const browserLogin = Effect.scoped(Effect.gen(function* () {
2257
+ const configStore = yield* ConfigStore;
2258
+ const authStore = yield* AuthStore;
2259
+ const webUrl = yield* configStore.getWebUrl;
2260
+ const loginServer = yield* Effect.acquireRelease(Effect.promise(async () => createBrowserLoginServer()), (server) => Effect.sync(server.stop));
2261
+ const loginUrl = `${webUrl}/auth/cli-login?callbackUrl=${encodeURIComponent(loginServer.callbackUrl)}`;
2262
+ yield* Console.log("Opening browser for better-update login...");
2263
+ yield* Console.log("");
2264
+ yield* openBrowser(loginUrl);
2265
+ const token = yield* loginServer.waitForToken;
2266
+ yield* authStore.saveToken(token);
2267
+ yield* Console.log("");
2268
+ yield* Console.log("Logged in successfully. Token saved to ~/.better-update/auth.json");
2269
+ }));
2270
+ const manualLogin = Effect.gen(function* () {
2271
+ yield* Console.log("Log in to better-update with an existing API key");
2272
+ yield* Console.log("Get your API key from the dashboard > API Keys page");
2273
+ yield* Console.log("");
2274
+ const token = yield* promptPassword("Paste your API key (from dashboard > API Keys):");
2275
+ yield* (yield* AuthStore).saveToken(token);
2276
+ yield* Console.log("");
2277
+ yield* Console.log("Logged in successfully. Token saved to ~/.better-update/auth.json");
2278
+ });
2279
+ const runLogin = (options) => Effect.gen(function* () {
2280
+ if (options.manualApiKey) {
2281
+ yield* manualLogin;
2282
+ return;
2283
+ }
2284
+ yield* browserLogin;
2285
+ });
2286
+
2287
+ //#endregion
2288
+ //#region src/application/command-exit.ts
2289
+ const exitWith = (code, message) => Console.error(message).pipe(Effect.zipRight(Effect.gen(function* () {
2290
+ yield* (yield* CliRuntime).setExitCode(code);
2291
+ })));
2292
+
2293
+ //#endregion
2294
+ //#region src/lib/command-errors.ts
2295
+ const BASE_TAG_MAP = {
2296
+ AuthRequiredError: 3,
2297
+ ProjectNotLinkedError: 4,
2298
+ NotFound: 1,
2299
+ Conflict: 1,
2300
+ Forbidden: 1,
2301
+ BadRequest: 2,
2302
+ InvalidArgumentError: 2,
2303
+ InteractiveProhibitedError: 2
2304
+ };
2305
+ const SYSTEM_TAG_MESSAGE = {
2306
+ SystemError: (error) => `Filesystem error: ${error.message}`,
2307
+ BadArgument: (error) => `Invalid argument: ${error.message}`
2308
+ };
2309
+ const SYSTEM_TAG_CODE = {
2310
+ SystemError: 6,
2311
+ BadArgument: 6
2312
+ };
2313
+ const makeCommandErrorHandler = (extras = {}) => {
2314
+ const combined = {
2315
+ ...BASE_TAG_MAP,
2316
+ ...extras
2317
+ };
2318
+ const handlers = {};
2319
+ for (const [tag, code] of Object.entries(combined)) {
2320
+ const systemFormat = SYSTEM_TAG_MESSAGE[tag];
2321
+ const resolvedCode = SYSTEM_TAG_CODE[tag] ?? code;
2322
+ handlers[tag] = (error) => exitWith(resolvedCode, systemFormat ? systemFormat(error) : error.message);
2323
+ }
2324
+ return (effect) => {
2325
+ return effect.pipe(Effect.catchTags(handlers), Effect.catchAll((cause) => exitWith(1, formatCause(cause))));
2326
+ };
2327
+ };
2328
+
2329
+ //#endregion
2330
+ //#region src/lib/citty-effect.ts
2331
+ let activeCliLayer = CliLive;
1960
2332
  const setActiveCliLayer = (layer) => {
1961
2333
  activeCliLayer = layer;
1962
2334
  };
2335
+ const isAuthRequiredError = (error) => typeof error === "object" && error !== null && "_tag" in error && error._tag === "AuthRequiredError";
2336
+ const wrapWithAutoLogin = (effect) => {
2337
+ const attempt = (depth) => effect.pipe(Effect.catchAll((cause) => {
2338
+ if (depth >= 1 || !isAuthRequiredError(cause)) return Effect.fail(cause);
2339
+ return Effect.gen(function* () {
2340
+ if (!(yield* InteractiveMode).allow) return yield* Effect.fail(cause);
2341
+ yield* Console.log("");
2342
+ yield* Console.log("Authentication required.");
2343
+ yield* runLogin({ manualApiKey: false });
2344
+ yield* Console.log("");
2345
+ return yield* attempt(depth + 1);
2346
+ });
2347
+ }));
2348
+ return attempt(0);
2349
+ };
1963
2350
  const runEffect = async (effect, extras = {}) => {
1964
- const provided = makeCommandErrorHandler(extras)(effect).pipe(Effect.provide(activeCliLayer));
2351
+ const provided = makeCommandErrorHandler(extras)(wrapWithAutoLogin(effect)).pipe(Effect.provide(activeCliLayer));
1965
2352
  return Effect.runPromise(provided.pipe(Effect.asVoid));
1966
2353
  };
1967
2354
 
@@ -2007,7 +2394,7 @@ const getConfigFilePaths = (projectRoot) => Effect.try({
2007
2394
  });
2008
2395
  const extractProjectId = (config) => Effect.gen(function* () {
2009
2396
  const projectId = config.extra?.betterUpdate?.projectId;
2010
- if (typeof projectId !== "string") return yield* new ProjectNotLinkedError({ message: "Project not linked. Run `better-update link` to connect this project, or set extra.betterUpdate.projectId in your Expo config." });
2397
+ if (typeof projectId !== "string") return yield* new ProjectNotLinkedError({ message: "Project not linked. Run `better-update init` to connect this project, or set extra.betterUpdate.projectId in your Expo config." });
2011
2398
  return projectId;
2012
2399
  });
2013
2400
  const extractSlug = (config) => Effect.gen(function* () {
@@ -2320,6 +2707,73 @@ const analyticsCommand = defineCommand({
2320
2707
  }
2321
2708
  });
2322
2709
 
2710
+ //#endregion
2711
+ //#region src/commands/apple/login.ts
2712
+ const LOGIN_EXIT_EXTRAS = {
2713
+ AppleAuthError: 4,
2714
+ InteractiveProhibitedError: 4
2715
+ };
2716
+ const appleLoginCommand = defineCommand({
2717
+ meta: {
2718
+ name: "login",
2719
+ description: "Log in to your Apple Developer account (used to issue iOS certificates)"
2720
+ },
2721
+ args: { username: {
2722
+ type: "string",
2723
+ description: "Pre-fill the Apple ID prompt (defaults to last-used Apple ID)"
2724
+ } },
2725
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
2726
+ const session = yield* (yield* AppleAuth).ensureLoggedIn(args.username === void 0 ? {} : { username: args.username });
2727
+ yield* Console.log(`Logged in as ${session.username}. Team: ${session.teamName ?? session.teamId} (${session.teamId}).`);
2728
+ }), LOGIN_EXIT_EXTRAS)
2729
+ });
2730
+
2731
+ //#endregion
2732
+ //#region src/commands/apple/logout.ts
2733
+ const appleLogoutCommand = defineCommand({
2734
+ meta: {
2735
+ name: "logout",
2736
+ description: "Clear the cached Apple Developer session (cookies only; ASC API keys unaffected)"
2737
+ },
2738
+ run: async () => runEffect(Effect.gen(function* () {
2739
+ yield* (yield* AppleAuth).logout;
2740
+ yield* Console.log("Cleared Apple Developer session.");
2741
+ }))
2742
+ });
2743
+
2744
+ //#endregion
2745
+ //#region src/commands/apple/whoami.ts
2746
+ const appleWhoamiCommand = defineCommand({
2747
+ meta: {
2748
+ name: "whoami",
2749
+ description: "Show the currently-cached Apple Developer session"
2750
+ },
2751
+ run: async () => runEffect(Effect.gen(function* () {
2752
+ const session = yield* (yield* AppleAuth).whoami;
2753
+ if (session === null) {
2754
+ yield* Console.log("Not logged in to Apple. Run `better-update apple login` to start.");
2755
+ return;
2756
+ }
2757
+ yield* Console.log(`Apple ID: ${session.username}`);
2758
+ yield* Console.log(`Team: ${session.teamName ?? "(unknown)"} (${session.teamId})`);
2759
+ if (session.providerId !== void 0) yield* Console.log(`Provider: ${String(session.providerId)}`);
2760
+ }))
2761
+ });
2762
+
2763
+ //#endregion
2764
+ //#region src/commands/apple/index.ts
2765
+ const appleCommand = defineCommand({
2766
+ meta: {
2767
+ name: "apple",
2768
+ description: "Manage your Apple Developer session (used for issuing iOS credentials)"
2769
+ },
2770
+ subCommands: {
2771
+ login: appleLoginCommand,
2772
+ logout: appleLogoutCommand,
2773
+ whoami: appleWhoamiCommand
2774
+ }
2775
+ });
2776
+
2323
2777
  //#endregion
2324
2778
  //#region src/lib/cli-schemas.ts
2325
2779
  const RolloutPercentage = Schema.Number.pipe(Schema.int(), Schema.between(1, 100)).annotations({
@@ -3587,6 +4041,80 @@ const reserveAndUpload = (api, input) => Effect.gen(function* () {
3587
4041
  };
3588
4042
  });
3589
4043
 
4044
+ //#endregion
4045
+ //#region src/lib/auto-increment.ts
4046
+ const bumpBuildNumber = (current) => Effect.gen(function* () {
4047
+ const raw = current ?? "0";
4048
+ const parsed = Number.parseInt(raw, 10);
4049
+ if (Number.isNaN(parsed)) return yield* new BuildProfileError({ message: `Cannot autoIncrement ios.buildNumber: current value "${raw}" is not a base-10 integer.` });
4050
+ return String(parsed + 1);
4051
+ });
4052
+ const bumpVersionCode = (current) => Effect.gen(function* () {
4053
+ const value = current ?? 0;
4054
+ if (!Number.isInteger(value) || value < 0) return yield* new BuildProfileError({ message: `Cannot autoIncrement android.versionCode: current value ${String(value)} is not a non-negative integer.` });
4055
+ return value + 1;
4056
+ });
4057
+ const SEMVER_PATCH = /^(\d+)\.(\d+)\.(\d+)(.*)$/u;
4058
+ const bumpVersion = (current) => Effect.gen(function* () {
4059
+ if (current === void 0) return yield* new BuildProfileError({ message: "Cannot autoIncrement version: no `version` field set in Expo config." });
4060
+ const match = SEMVER_PATCH.exec(current);
4061
+ if (!match) return yield* new BuildProfileError({ message: `Cannot autoIncrement version: "${current}" is not a semver string like "1.2.3".` });
4062
+ const [, major, minor, patch, suffix] = match;
4063
+ const nextPatch = Number.parseInt(patch ?? "0", 10) + 1;
4064
+ return `${major ?? "0"}.${minor ?? "0"}.${String(nextPatch)}${suffix ?? ""}`;
4065
+ });
4066
+ const computeIosBumps = (config, mode) => Effect.gen(function* () {
4067
+ if (mode === "buildNumber") return { nextBuildNumber: yield* bumpBuildNumber(config.ios?.buildNumber) };
4068
+ return {
4069
+ nextVersion: yield* bumpVersion(config.version),
4070
+ nextBuildNumber: yield* bumpBuildNumber(config.ios?.buildNumber)
4071
+ };
4072
+ });
4073
+ const computeAndroidBumps = (config, mode) => Effect.gen(function* () {
4074
+ if (mode === "versionCode") return { nextVersionCode: yield* bumpVersionCode(config.android?.versionCode) };
4075
+ return {
4076
+ nextVersion: yield* bumpVersion(config.version),
4077
+ nextVersionCode: yield* bumpVersionCode(config.android?.versionCode)
4078
+ };
4079
+ });
4080
+ const buildPatch = (platform, bumps) => {
4081
+ const patch = {};
4082
+ if (bumps.nextVersion !== void 0) patch["version"] = bumps.nextVersion;
4083
+ if (platform === "ios" && bumps.nextBuildNumber !== void 0) patch["ios"] = { buildNumber: bumps.nextBuildNumber };
4084
+ if (platform === "android" && bumps.nextVersionCode !== void 0) patch["android"] = { versionCode: bumps.nextVersionCode };
4085
+ return patch;
4086
+ };
4087
+ const describeBumps = (platform, bumps) => {
4088
+ const parts = [];
4089
+ if (bumps.nextVersion !== void 0) parts.push(`version=${bumps.nextVersion}`);
4090
+ if (platform === "ios" && bumps.nextBuildNumber !== void 0) parts.push(`ios.buildNumber=${bumps.nextBuildNumber}`);
4091
+ if (platform === "android" && bumps.nextVersionCode !== void 0) parts.push(`android.versionCode=${String(bumps.nextVersionCode)}`);
4092
+ return parts.join(", ");
4093
+ };
4094
+ const computeBumps = (input) => {
4095
+ if (input.platform === "ios") return input.iosMode === void 0 ? Effect.succeed({}) : computeIosBumps(input.config, input.iosMode);
4096
+ return input.androidMode === void 0 ? Effect.succeed({}) : computeAndroidBumps(input.config, input.androidMode);
4097
+ };
4098
+ const hasAnyBump = (bumps) => bumps.nextVersion !== void 0 || bumps.nextBuildNumber !== void 0 || bumps.nextVersionCode !== void 0;
4099
+ /**
4100
+ * Bump `version` / `ios.buildNumber` / `android.versionCode` per the resolved
4101
+ * autoIncrement mode, persist via `@expo/config.modifyConfigAsync`, and log a
4102
+ * Human-readable summary. No-op when the mode is undefined. Returns the new
4103
+ * Bumped values so callers can refresh their in-memory ExpoConfig.
4104
+ */
4105
+ const applyAutoIncrement = (input) => Effect.gen(function* () {
4106
+ const bumps = yield* computeBumps(input);
4107
+ if (!hasAnyBump(bumps)) return bumps;
4108
+ const patch = buildPatch(input.platform, bumps);
4109
+ const result = yield* writeExpoConfigPatch(input.projectRoot, patch).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to persist autoIncrement: ${cause.message}` })));
4110
+ if (result.type === "warn" && result.configPath === null) {
4111
+ yield* Console.log(`autoIncrement: dynamic Expo config detected, cannot write back. Update manually: ${describeBumps(input.platform, bumps)}`);
4112
+ return bumps;
4113
+ }
4114
+ yield* Console.log(`autoIncrement: bumped ${describeBumps(input.platform, bumps)}`);
4115
+ return bumps;
4116
+ });
4117
+
3590
4118
  //#endregion
3591
4119
  //#region src/lib/eas-config.ts
3592
4120
  const MAX_EXTENDS_DEPTH = 10;
@@ -3619,6 +4147,21 @@ const asAndroidDistribution = (raw) => {
3619
4147
  const value = asStringValue(raw);
3620
4148
  return value === "play-store" || value === "direct" ? value : void 0;
3621
4149
  };
4150
+ const asIosAutoIncrement = (raw) => {
4151
+ if (typeof raw === "boolean") return raw;
4152
+ const value = asStringValue(raw);
4153
+ return value === "buildNumber" || value === "version" ? value : void 0;
4154
+ };
4155
+ const asAndroidAutoIncrement = (raw) => {
4156
+ if (typeof raw === "boolean") return raw;
4157
+ const value = asStringValue(raw);
4158
+ return value === "versionCode" || value === "version" ? value : void 0;
4159
+ };
4160
+ const asAutoIncrement = (raw) => {
4161
+ if (typeof raw === "boolean") return raw;
4162
+ const value = asStringValue(raw);
4163
+ return value === "buildNumber" || value === "versionCode" || value === "version" ? value : void 0;
4164
+ };
3622
4165
  const asEasDistribution = (raw) => {
3623
4166
  const value = asStringValue(raw);
3624
4167
  return value === "internal" || value === "store" ? value : void 0;
@@ -3635,12 +4178,14 @@ const parseIosProfile = (raw) => {
3635
4178
  const scheme = asStringValue(record["scheme"]);
3636
4179
  const simulator = asBooleanValue(record["simulator"]);
3637
4180
  const enterpriseProvisioning = asEnterpriseProvisioning(record["enterpriseProvisioning"]);
4181
+ const autoIncrement = asIosAutoIncrement(record["autoIncrement"]);
3638
4182
  return {
3639
4183
  ...distribution === void 0 ? {} : { distribution },
3640
4184
  ...buildConfiguration === void 0 ? {} : { buildConfiguration },
3641
4185
  ...scheme === void 0 ? {} : { scheme },
3642
4186
  ...simulator === void 0 ? {} : { simulator },
3643
- ...enterpriseProvisioning === void 0 ? {} : { enterpriseProvisioning }
4187
+ ...enterpriseProvisioning === void 0 ? {} : { enterpriseProvisioning },
4188
+ ...autoIncrement === void 0 ? {} : { autoIncrement }
3644
4189
  };
3645
4190
  };
3646
4191
  const parseAndroidProfile = (raw) => {
@@ -3651,12 +4196,14 @@ const parseAndroidProfile = (raw) => {
3651
4196
  const gradleCommand = asStringValue(record["gradleCommand"]);
3652
4197
  const format = asAndroidFormat(record["format"]);
3653
4198
  const distribution = asAndroidDistribution(record["distribution"]);
4199
+ const autoIncrement = asAndroidAutoIncrement(record["autoIncrement"]);
3654
4200
  return {
3655
4201
  ...buildType === void 0 ? {} : { buildType },
3656
4202
  ...flavor === void 0 ? {} : { flavor },
3657
4203
  ...gradleCommand === void 0 ? {} : { gradleCommand },
3658
4204
  ...format === void 0 ? {} : { format },
3659
- ...distribution === void 0 ? {} : { distribution }
4205
+ ...distribution === void 0 ? {} : { distribution },
4206
+ ...autoIncrement === void 0 ? {} : { autoIncrement }
3660
4207
  };
3661
4208
  };
3662
4209
  const parseBuildProfile = (raw) => {
@@ -3671,6 +4218,7 @@ const parseBuildProfile = (raw) => {
3671
4218
  const ios = parseIosProfile(record["ios"]);
3672
4219
  const android = parseAndroidProfile(record["android"]);
3673
4220
  const credentialsSource = asCredentialsSource(record["credentialsSource"]);
4221
+ const autoIncrement = asAutoIncrement(record["autoIncrement"]);
3674
4222
  return {
3675
4223
  ...extendsName === void 0 ? {} : { extends: extendsName },
3676
4224
  ...developmentClient === void 0 ? {} : { developmentClient },
@@ -3680,7 +4228,8 @@ const parseBuildProfile = (raw) => {
3680
4228
  ...env === void 0 ? {} : { env },
3681
4229
  ...ios === void 0 ? {} : { ios },
3682
4230
  ...android === void 0 ? {} : { android },
3683
- ...credentialsSource === void 0 ? {} : { credentialsSource }
4231
+ ...credentialsSource === void 0 ? {} : { credentialsSource },
4232
+ ...autoIncrement === void 0 ? {} : { autoIncrement }
3684
4233
  };
3685
4234
  };
3686
4235
  const parseEasConfig = (text) => Effect.gen(function* () {
@@ -3748,6 +4297,7 @@ const mergeProfile = (base, overlay) => {
3748
4297
  const channel = overlay.channel ?? base.channel;
3749
4298
  const environment = overlay.environment ?? base.environment;
3750
4299
  const credentialsSource = overlay.credentialsSource ?? base.credentialsSource;
4300
+ const autoIncrement = overlay.autoIncrement ?? base.autoIncrement;
3751
4301
  return {
3752
4302
  ...overlay.extends === void 0 ? {} : { extends: overlay.extends },
3753
4303
  ...developmentClient === void 0 ? {} : { developmentClient },
@@ -3757,7 +4307,8 @@ const mergeProfile = (base, overlay) => {
3757
4307
  ...env === void 0 ? {} : { env },
3758
4308
  ...ios === void 0 ? {} : { ios },
3759
4309
  ...android === void 0 ? {} : { android },
3760
- ...credentialsSource === void 0 ? {} : { credentialsSource }
4310
+ ...credentialsSource === void 0 ? {} : { credentialsSource },
4311
+ ...autoIncrement === void 0 ? {} : { autoIncrement }
3761
4312
  };
3762
4313
  };
3763
4314
  const collectExtendsChain = (profiles, profileName) => Effect.gen(function* () {
@@ -3811,16 +4362,36 @@ const deriveAndroidDistribution = (eas, format) => {
3811
4362
  };
3812
4363
  const hasIosIntent = (eas) => eas.ios !== void 0 || eas.distribution !== void 0 || eas.developmentClient === true;
3813
4364
  const hasAndroidIntent = (eas) => eas.android !== void 0 || eas.distribution !== void 0 || eas.developmentClient === true;
4365
+ const resolveIosAutoIncrement = (eas) => {
4366
+ const override = eas.ios?.autoIncrement;
4367
+ if (override === false) return;
4368
+ if (override === true) return "buildNumber";
4369
+ if (override === "buildNumber" || override === "version") return override;
4370
+ const top = eas.autoIncrement;
4371
+ if (top === true || top === "buildNumber") return "buildNumber";
4372
+ if (top === "version") return "version";
4373
+ };
4374
+ const resolveAndroidAutoIncrement = (eas) => {
4375
+ const override = eas.android?.autoIncrement;
4376
+ if (override === false) return;
4377
+ if (override === true) return "versionCode";
4378
+ if (override === "versionCode" || override === "version") return override;
4379
+ const top = eas.autoIncrement;
4380
+ if (top === true || top === "versionCode") return "versionCode";
4381
+ if (top === "version") return "version";
4382
+ };
3814
4383
  const toIosProfile = (eas) => {
3815
4384
  if (!hasIosIntent(eas)) return;
3816
4385
  const distribution = deriveIosDistribution(eas);
3817
4386
  if (!distribution) return;
3818
4387
  const ios = eas.ios ?? {};
4388
+ const autoIncrement = resolveIosAutoIncrement(eas);
3819
4389
  return {
3820
4390
  distribution,
3821
4391
  ...ios.buildConfiguration === void 0 ? {} : { buildConfiguration: ios.buildConfiguration },
3822
4392
  ...ios.scheme === void 0 ? {} : { scheme: ios.scheme },
3823
- ...ios.simulator === void 0 ? {} : { simulator: ios.simulator }
4393
+ ...ios.simulator === void 0 ? {} : { simulator: ios.simulator },
4394
+ ...autoIncrement === void 0 ? {} : { autoIncrement }
3824
4395
  };
3825
4396
  };
3826
4397
  const toAndroidProfile = (eas) => {
@@ -3828,12 +4399,15 @@ const toAndroidProfile = (eas) => {
3828
4399
  const format = deriveAndroidFormat(eas);
3829
4400
  if (!format) return;
3830
4401
  const android = eas.android ?? {};
4402
+ const distribution = deriveAndroidDistribution(eas, format);
4403
+ const autoIncrement = resolveAndroidAutoIncrement(eas);
3831
4404
  return {
3832
4405
  format,
3833
- distribution: deriveAndroidDistribution(eas, format),
4406
+ distribution,
3834
4407
  ...android.buildType === void 0 ? {} : { buildType: android.buildType },
3835
4408
  ...android.flavor === void 0 ? {} : { flavor: android.flavor },
3836
- ...android.gradleCommand === void 0 ? {} : { gradleCommand: android.gradleCommand }
4409
+ ...android.gradleCommand === void 0 ? {} : { gradleCommand: android.gradleCommand },
4410
+ ...autoIncrement === void 0 ? {} : { autoIncrement }
3837
4411
  };
3838
4412
  };
3839
4413
  const fromEasProfile = (eas, profileName) => {
@@ -3891,14 +4465,19 @@ const clearBuildCaches = (projectRoot) => Effect.gen(function* () {
3891
4465
 
3892
4466
  //#endregion
3893
4467
  //#region src/lib/env-exporter.ts
4468
+ const coerceEnvironment = (raw) => raw === "development" || raw === "preview" || raw === "production" ? raw : void 0;
3894
4469
  /**
3895
4470
  * Pull environment variables for a project + environment and flatten them into
3896
4471
  * a key/value map. Returns an empty map when the project has no variables.
3897
4472
  */
3898
- const pullEnvVars = (api, { projectId, environment }) => api["env-vars"].export({ urlParams: {
3899
- projectId,
3900
- environment
3901
- } }).pipe(Effect.map((result) => Object.fromEntries(result.items.map((item) => [item.key, item.value]))), Effect.mapError((cause) => new EnvExportError({ message: `Failed to export environment variables for "${environment}": ${String(cause)}` })));
4473
+ const pullEnvVars = (api, { projectId, environment }) => {
4474
+ const validated = coerceEnvironment(environment);
4475
+ if (!validated) return Effect.fail(new EnvExportError({ message: `Invalid environment "${environment}". Must be one of: development, preview, production.` }));
4476
+ return api["env-vars"].export({ urlParams: {
4477
+ projectId,
4478
+ environment: validated
4479
+ } }).pipe(Effect.map((result) => Object.fromEntries(result.items.map((item) => [item.key, item.value]))), Effect.mapError((cause) => new EnvExportError({ message: `Failed to export environment variables for "${environment}": ${String(cause)}` })));
4480
+ };
3902
4481
 
3903
4482
  //#endregion
3904
4483
  //#region src/lib/git-context.ts
@@ -3982,53 +4561,6 @@ const extractGradleConfig = (parsed) => {
3982
4561
  };
3983
4562
  const unquote = (input) => input.startsWith("\"") && input.endsWith("\"") ? input.slice(1, -1) : input;
3984
4563
 
3985
- //#endregion
3986
- //#region src/lib/prompts.ts
3987
- const ensureInteractive = (promptName) => Effect.gen(function* () {
3988
- if (!(yield* InteractiveMode).allow) return yield* new InteractiveProhibitedError({ message: `Interactive prompt "${promptName}" requested while running non-interactively. Provide the value via a flag, run with --interactive, or unset CI.` });
3989
- });
3990
- const handleCancel = (value) => {
3991
- if (isCancel(value)) {
3992
- cancel("Operation cancelled.");
3993
- process.exit(130);
3994
- }
3995
- return value;
3996
- };
3997
- const promptPassword = (message) => Effect.gen(function* () {
3998
- yield* ensureInteractive(message);
3999
- return handleCancel(yield* Effect.promise(async () => password({ message })));
4000
- });
4001
- const promptSelect = (message, options) => Effect.gen(function* () {
4002
- yield* ensureInteractive(message);
4003
- return handleCancel(yield* Effect.promise(async () => select({
4004
- message,
4005
- options: [...options]
4006
- })));
4007
- });
4008
- const promptMultiSelect = (message, options, config) => Effect.gen(function* () {
4009
- yield* ensureInteractive(message);
4010
- return handleCancel(yield* Effect.promise(async () => multiselect({
4011
- message,
4012
- options: [...options],
4013
- required: config?.required ?? false
4014
- })));
4015
- });
4016
- const promptText = (message, options) => Effect.gen(function* () {
4017
- yield* ensureInteractive(message);
4018
- return handleCancel(yield* Effect.promise(async () => text({
4019
- message,
4020
- ...options?.placeholder === void 0 ? {} : { placeholder: options.placeholder },
4021
- ...options?.defaultValue === void 0 ? {} : { defaultValue: options.defaultValue }
4022
- })));
4023
- });
4024
- const promptConfirm = (message, options) => Effect.gen(function* () {
4025
- yield* ensureInteractive(message);
4026
- return handleCancel(yield* Effect.promise(async () => confirm({
4027
- message,
4028
- ...options?.initialValue === void 0 ? {} : { initialValue: options.initialValue }
4029
- })));
4030
- });
4031
-
4032
4564
  //#endregion
4033
4565
  //#region src/lib/platform-detect.ts
4034
4566
  const PLATFORMS = ["ios", "android"];
@@ -4435,6 +4967,38 @@ const parseCert = (certDerBytes) => {
4435
4967
  return forge.pki.certificateFromAsn1(asn1);
4436
4968
  };
4437
4969
  const generatePassword = () => forge.util.encode64(forge.random.getBytesSync(16));
4970
+ const extractCertMetadata = (cert) => Effect.gen(function* () {
4971
+ const appleTeamId = extractTeamId$1(cert);
4972
+ if (appleTeamId === null) return yield* Effect.fail(new CertParseError({ message: "Could not extract Apple team identifier from certificate subject" }));
4973
+ return {
4974
+ serialNumber: cert.serialNumber.toUpperCase(),
4975
+ validFrom: cert.validity.notBefore.toISOString(),
4976
+ validUntil: cert.validity.notAfter.toISOString(),
4977
+ appleTeamId,
4978
+ appleTeamName: stringField(cert, "O"),
4979
+ developerIdIdentifier: stringField(cert, "UID"),
4980
+ commonName: stringField(cert, "CN")
4981
+ };
4982
+ });
4983
+ /**
4984
+ * Parse a PKCS#12 base64 bundle and extract certificate metadata. Used by the
4985
+ * Apple-ID flow which receives a P12 directly from `createCertificateAndP12Async`
4986
+ * and needs metadata before uploading to the better-update server.
4987
+ */
4988
+ const extractMetadataFromP12 = (params) => Effect.gen(function* () {
4989
+ const certBagOid = forge.pki.oids["certBag"];
4990
+ if (certBagOid === void 0) return yield* Effect.fail(new CertParseError({ message: "PKCS#12 OID lookup for certBag failed" }));
4991
+ const [first] = yield* Effect.try({
4992
+ try: () => {
4993
+ const p12Der = forge.util.decode64(params.p12Base64);
4994
+ const p12Asn1 = forge.asn1.fromDer(p12Der);
4995
+ return forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, params.password).getBags({ bagType: certBagOid })[certBagOid] ?? [];
4996
+ },
4997
+ catch: (error) => new CertParseError({ message: `Failed to parse PKCS#12 bundle: ${error instanceof Error ? error.message : String(error)}` })
4998
+ });
4999
+ if (first?.cert === void 0) return yield* Effect.fail(new CertParseError({ message: "PKCS#12 bundle does not contain a certificate" }));
5000
+ return yield* extractCertMetadata(first.cert);
5001
+ });
4438
5002
  const buildDistributionCertP12 = (params) => Effect.gen(function* () {
4439
5003
  const result = yield* Effect.try({
4440
5004
  try: () => {
@@ -4452,20 +5016,11 @@ const buildDistributionCertP12 = (params) => Effect.gen(function* () {
4452
5016
  },
4453
5017
  catch: (error) => new CertParseError({ message: `Failed to assemble .p12: ${error instanceof Error ? error.message : String(error)}` })
4454
5018
  });
4455
- const appleTeamId = extractTeamId$1(result.cert);
4456
- if (appleTeamId === null) return yield* Effect.fail(new CertParseError({ message: "Could not extract Apple team identifier from certificate subject" }));
5019
+ const metadata = yield* extractCertMetadata(result.cert);
4457
5020
  return {
4458
5021
  p12Base64: result.p12Base64,
4459
5022
  password: result.password,
4460
- metadata: {
4461
- serialNumber: result.cert.serialNumber.toUpperCase(),
4462
- validFrom: result.cert.validity.notBefore.toISOString(),
4463
- validUntil: result.cert.validity.notAfter.toISOString(),
4464
- appleTeamId,
4465
- appleTeamName: stringField(result.cert, "O"),
4466
- developerIdIdentifier: stringField(result.cert, "UID"),
4467
- commonName: stringField(result.cert, "CN")
4468
- }
5023
+ metadata
4469
5024
  };
4470
5025
  });
4471
5026
 
@@ -4498,7 +5053,7 @@ const generateCertificateSigningRequest = async () => {
4498
5053
 
4499
5054
  //#endregion
4500
5055
  //#region src/lib/credentials-generator.ts
4501
- const DISTRIBUTION_TO_PROFILE_TYPE = {
5056
+ const DISTRIBUTION_TO_PROFILE_TYPE$1 = {
4502
5057
  APP_STORE: "IOS_APP_STORE",
4503
5058
  AD_HOC: "IOS_APP_ADHOC",
4504
5059
  DEVELOPMENT: "IOS_APP_DEVELOPMENT",
@@ -4686,7 +5241,7 @@ const generateAndUploadProvisioningProfile = (api, input) => Effect.gen(function
4686
5241
  }));
4687
5242
  const profileBytes = fromBase64((yield* createProvisioningProfile(ascCreds, {
4688
5243
  profileName: `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`,
4689
- profileType: DISTRIBUTION_TO_PROFILE_TYPE[input.distributionType],
5244
+ profileType: DISTRIBUTION_TO_PROFILE_TYPE$1[input.distributionType],
4690
5245
  bundleIdAscId,
4691
5246
  certificateAscIds: [certAscId],
4692
5247
  deviceAscIds
@@ -4709,93 +5264,181 @@ const generateAndUploadProvisioningProfile = (api, input) => Effect.gen(function
4709
5264
  });
4710
5265
 
4711
5266
  //#endregion
4712
- //#region src/application/credentials-interactive.ts
4713
- const hasTag = (cause) => typeof cause === "object" && cause !== null && "_tag" in cause;
4714
- const isMissingResolveError = (cause) => hasTag(cause) && (cause._tag === "NotFound" || cause._tag === "BadRequest");
4715
- const generateKeystoreInteractive = (api) => Effect.gen(function* () {
4716
- const alias = yield* promptText("Key alias", { placeholder: "upload-key" });
4717
- const storePassword = yield* promptPassword("Keystore password");
4718
- const keyPassword = yield* promptPassword("Key password");
4719
- const commonName = yield* promptText("Common name (CN)", { placeholder: "Your App" });
4720
- const organization = yield* promptText("Organization (O)", { placeholder: "Your Company" });
4721
- yield* Console.log("Generating keystore with keytool...");
4722
- return (yield* generateAndUploadKeystore(api, {
4723
- keyAlias: alias,
4724
- storePassword,
4725
- keyPassword,
4726
- commonName,
4727
- organization
4728
- })).id;
5267
+ //#region src/lib/credentials-generator-apple-id.ts
5268
+ const DISTRIBUTION_TO_PROFILE_TYPE = {
5269
+ APP_STORE: AppleUtils.ProfileType.IOS_APP_STORE,
5270
+ AD_HOC: AppleUtils.ProfileType.IOS_APP_ADHOC,
5271
+ DEVELOPMENT: AppleUtils.ProfileType.IOS_APP_DEVELOPMENT,
5272
+ ENTERPRISE: AppleUtils.ProfileType.IOS_APP_INHOUSE
5273
+ };
5274
+ const DISTRIBUTION_TO_CERTIFICATE_TYPE = {
5275
+ APP_STORE: AppleUtils.CertificateType.IOS_DISTRIBUTION,
5276
+ AD_HOC: AppleUtils.CertificateType.IOS_DISTRIBUTION,
5277
+ ENTERPRISE: AppleUtils.CertificateType.IOS_DISTRIBUTION,
5278
+ DEVELOPMENT: AppleUtils.CertificateType.IOS_DEVELOPMENT
5279
+ };
5280
+ var AppleIdGenerateFailedError = class extends Data.TaggedError("AppleIdGenerateFailedError") {};
5281
+ const wrap = (step, run) => Effect.tryPromise({
5282
+ try: run,
5283
+ catch: (cause) => new AppleIdGenerateFailedError({
5284
+ step,
5285
+ message: cause instanceof Error ? cause.message : String(cause)
5286
+ })
4729
5287
  });
4730
- const pickExistingKeystore = (api) => Effect.gen(function* () {
4731
- const keystores = yield* api.androidUploadKeystores.list();
4732
- if (keystores.items.length === 0) return yield* Effect.fail(new MissingCredentialsError({
4733
- message: "No existing keystores in this organization.",
4734
- hint: "Re-run and choose 'Generate new keystore'."
4735
- }));
4736
- return yield* promptSelect("Select a keystore", keystores.items.map((item) => ({
4737
- value: item.id,
4738
- label: item.keyAlias
5288
+ const generateAndUploadDistributionCertificateViaAppleId = (api, input) => Effect.gen(function* () {
5289
+ const ctx = input.context;
5290
+ const certificateType = input.certificateType === "IOS_DEVELOPMENT" ? AppleUtils.CertificateType.IOS_DEVELOPMENT : AppleUtils.CertificateType.IOS_DISTRIBUTION;
5291
+ const result = yield* wrap("apple-create-certificate", async () => AppleUtils.createCertificateAndP12Async(ctx, { certificateType }));
5292
+ const metadata = yield* extractMetadataFromP12({
5293
+ p12Base64: result.certificateP12,
5294
+ password: result.password
5295
+ }).pipe(Effect.mapError((cause) => new AppleIdGenerateFailedError({
5296
+ step: "parse-p12",
5297
+ message: cause.message
4739
5298
  })));
5299
+ return {
5300
+ id: (yield* api.appleDistributionCertificates.upload({ payload: {
5301
+ p12Base64: result.certificateP12,
5302
+ p12Password: result.password,
5303
+ serialNumber: metadata.serialNumber,
5304
+ appleTeamIdentifier: metadata.appleTeamId,
5305
+ ...metadata.appleTeamName === null ? {} : { appleTeamName: metadata.appleTeamName },
5306
+ ...metadata.developerIdIdentifier === null ? {} : { developerIdIdentifier: metadata.developerIdIdentifier },
5307
+ validFrom: metadata.validFrom,
5308
+ validUntil: metadata.validUntil
5309
+ } })).id,
5310
+ serialNumber: metadata.serialNumber,
5311
+ appleTeamId: metadata.appleTeamId,
5312
+ developerPortalIdentifier: result.certificate.id
5313
+ };
4740
5314
  });
4741
- const resolveAndroidAppId = (api, input) => Effect.gen(function* () {
4742
- const existing = (yield* api.androidApplicationIdentifiers.list({ path: { projectId: input.projectId } })).items.find((item) => item.packageName === input.applicationIdentifier);
4743
- if (existing !== void 0) return existing.id;
4744
- return (yield* api.androidApplicationIdentifiers.create({
4745
- path: { projectId: input.projectId },
4746
- payload: { packageName: input.applicationIdentifier }
4747
- })).id;
5315
+ const findOrCreateBundleId = (ctx, bundleIdentifier) => Effect.gen(function* () {
5316
+ const existing = yield* wrap("apple-find-bundle-id", async () => AppleUtils.BundleId.findAsync(ctx, { identifier: bundleIdentifier }));
5317
+ if (existing !== null) return existing.id;
5318
+ return (yield* wrap("apple-create-bundle-id", async () => AppleUtils.BundleId.createAsync(ctx, {
5319
+ identifier: bundleIdentifier,
5320
+ name: bundleIdentifier,
5321
+ platform: AppleUtils.BundleIdPlatform.IOS
5322
+ }))).id;
5323
+ });
5324
+ const findAscCertificateId = (ctx, serialNumber, certificateType) => Effect.gen(function* () {
5325
+ const certs = yield* wrap("apple-list-certificates", async () => AppleUtils.Certificate.getAsync(ctx, { query: { filter: { certificateType } } }));
5326
+ const upper = serialNumber.toUpperCase();
5327
+ const match = certs.find((entry) => entry.attributes.serialNumber.toUpperCase() === upper);
5328
+ if (match === void 0) return yield* Effect.fail(new AppleIdGenerateFailedError({
5329
+ step: "match-apple-certificate",
5330
+ message: `Distribution certificate ${serialNumber} not present on Apple Developer Portal; upload or re-generate it`
5331
+ }));
5332
+ return match.id;
4748
5333
  });
4749
- const resolveAndroidKeystoreId = (api, choice) => choice === "generate" ? generateKeystoreInteractive(api) : pickExistingKeystore(api);
4750
- const setupAndroidInteractive = (api, input) => Effect.gen(function* () {
4751
- yield* Console.log("");
4752
- yield* Console.log(`No Android build credentials configured for ${input.applicationIdentifier}.`);
4753
- const appId = yield* resolveAndroidAppId(api, input);
4754
- const choice = yield* promptSelect("How would you like to provide a keystore?", [
4755
- {
4756
- value: "generate",
4757
- label: "Generate new keystore"
4758
- },
4759
- {
4760
- value: "existing",
4761
- label: "Pick an existing keystore"
4762
- },
4763
- {
4764
- value: "abort",
4765
- label: "Abort I'll configure it in the dashboard"
4766
- }
4767
- ]);
4768
- if (choice === "abort") return yield* Effect.fail(new MissingCredentialsError({
4769
- message: `Build aborted — no keystore bound to ${input.applicationIdentifier}.`,
4770
- hint: "Run `better-update credentials generate keystore` or upload via the dashboard."
5334
+ const collectIosDeviceIds = (ctx, deviceIds) => Effect.gen(function* () {
5335
+ const devices = yield* wrap("apple-list-devices", async () => AppleUtils.Device.getAllIOSProfileDevicesAsync(ctx));
5336
+ if (deviceIds === void 0) return devices.map((device) => device.id);
5337
+ const allowed = new Set(deviceIds);
5338
+ return devices.filter((device) => allowed.has(device.id)).map((device) => device.id);
5339
+ });
5340
+ const generateAndUploadProvisioningProfileViaAppleId = (api, input) => Effect.gen(function* () {
5341
+ const ctx = input.context;
5342
+ const cert = yield* api.appleDistributionCertificates.list().pipe(Effect.map(({ items }) => items.find((item) => item.id === input.distributionCertificateId)), Effect.flatMap((match) => match === void 0 ? Effect.fail(new AppleIdGenerateFailedError({
5343
+ step: "load-distribution-certificate",
5344
+ message: `Distribution certificate ${input.distributionCertificateId} not found`
5345
+ })) : Effect.succeed(match)));
5346
+ const certificateType = DISTRIBUTION_TO_CERTIFICATE_TYPE[input.distributionType];
5347
+ const [certAscId, bundleIdAscId] = yield* Effect.all([findAscCertificateId(ctx, cert.serialNumber, certificateType), findOrCreateBundleId(ctx, input.bundleIdentifier)], { concurrency: 2 });
5348
+ const useDevices = input.distributionType === "AD_HOC" || input.distributionType === "DEVELOPMENT";
5349
+ const deviceIds = useDevices ? yield* collectIosDeviceIds(ctx, input.deviceIds) : [];
5350
+ if (useDevices && deviceIds.length === 0) return yield* Effect.fail(new AppleIdGenerateFailedError({
5351
+ step: "collect-devices",
5352
+ message: "No registered devices to attach to the provisioning profile"
4771
5353
  }));
4772
- const keystoreId = yield* resolveAndroidKeystoreId(api, choice);
4773
- yield* api.androidBuildCredentials.create({
4774
- path: { applicationIdentifierId: appId },
5354
+ const profileName = `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`;
5355
+ const { profileContent } = (yield* wrap("apple-create-profile", async () => AppleUtils.Profile.createAsync(ctx, {
5356
+ bundleId: bundleIdAscId,
5357
+ certificates: [certAscId],
5358
+ devices: deviceIds,
5359
+ name: profileName,
5360
+ profileType: DISTRIBUTION_TO_PROFILE_TYPE[input.distributionType]
5361
+ }))).attributes;
5362
+ if (profileContent === null) return yield* Effect.fail(new AppleIdGenerateFailedError({
5363
+ step: "extract-profile-content",
5364
+ message: "Apple returned a profile with no content (likely expired/invalid)"
5365
+ }));
5366
+ const profileBytes = fromBase64(profileContent);
5367
+ const rosterHash = useDevices ? computeDeviceRosterHashHex(deviceIds) : void 0;
5368
+ const created = yield* api.appleProvisioningProfiles.upload({ payload: {
5369
+ profileBase64: toBase64(profileBytes),
5370
+ appleDistributionCertificateId: input.distributionCertificateId,
5371
+ isManaged: true,
5372
+ ...rosterHash === void 0 ? {} : { deviceRosterHash: rosterHash }
5373
+ } });
5374
+ return {
5375
+ id: created.id,
5376
+ bundleIdentifier: created.bundleIdentifier,
5377
+ distributionType: created.distributionType,
5378
+ profileName: created.profileName,
5379
+ validUntil: created.validUntil,
5380
+ developerPortalIdentifier: created.developerPortalIdentifier
5381
+ };
5382
+ });
5383
+
5384
+ //#endregion
5385
+ //#region src/application/credentials-interactive-apple-id.ts
5386
+ const chooseIosSetupPath = (api) => Effect.gen(function* () {
5387
+ if (!(yield* api.ascApiKeys.list()).items.some((key) => key.appleTeamId !== null)) return "apple-id";
5388
+ return yield* promptSelect("How would you like to provide your iOS credentials?", [{
5389
+ value: "apple-id",
5390
+ label: "Login with Apple ID (recommended for interactive use)"
5391
+ }, {
5392
+ value: "asc-key",
5393
+ label: "Use an App Store Connect API key"
5394
+ }]);
5395
+ });
5396
+ const setupIosViaAppleId = (api, input) => Effect.gen(function* () {
5397
+ const auth = yield* AppleAuth;
5398
+ const session = yield* auth.ensureLoggedIn();
5399
+ const ctx = auth.buildRequestContext(session);
5400
+ yield* Console.log(`Logged in as ${session.username}. Team: ${session.teamName ?? session.teamId} (${session.teamId}).`);
5401
+ yield* Console.log("Generating distribution certificate via Apple ID...");
5402
+ const cert = yield* generateAndUploadDistributionCertificateViaAppleId(api, { context: ctx });
5403
+ const distributionType = IOS_DISTRIBUTION_TO_TYPE[input.distribution];
5404
+ yield* Console.log("Generating provisioning profile via Apple ID...");
5405
+ const profile = yield* generateAndUploadProvisioningProfileViaAppleId(api, {
5406
+ context: ctx,
5407
+ distributionCertificateId: cert.id,
5408
+ bundleIdentifier: input.bundleIdentifier,
5409
+ distributionType
5410
+ });
5411
+ yield* api.iosBundleConfigurations.create({
5412
+ path: { projectId: input.projectId },
4775
5413
  payload: {
4776
- name: "Default",
4777
- isDefault: true,
4778
- androidUploadKeystoreId: keystoreId
5414
+ bundleIdentifier: input.bundleIdentifier,
5415
+ distributionType,
5416
+ appleTeamId: cert.appleTeamId,
5417
+ appleDistributionCertificateId: cert.id,
5418
+ appleProvisioningProfileId: profile.id
4779
5419
  }
4780
5420
  });
4781
- yield* Console.log("Android build credentials configured.");
5421
+ yield* Console.log("iOS bundle configuration saved.");
4782
5422
  });
4783
- const ensureAndroidCredentialsAvailable = (api, input) => api.buildCredentials.resolve({
4784
- path: { projectId: input.projectId },
4785
- payload: {
4786
- platform: "android",
4787
- applicationIdentifier: input.applicationIdentifier
4788
- }
4789
- }).pipe(Effect.asVoid);
4790
- const ensureAndroidCredentials = (api, input, options) => ensureAndroidCredentialsAvailable(api, input).pipe(Effect.catchIf(isMissingResolveError, () => Effect.gen(function* () {
4791
- const mode = yield* InteractiveMode;
4792
- if (options.freezeCredentials || !mode.allow) return yield* Effect.fail(new MissingCredentialsError({
4793
- message: `No Android build credentials for ${input.applicationIdentifier}.`,
4794
- 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."
4795
- }));
4796
- yield* setupAndroidInteractive(api, input);
4797
- return yield* ensureAndroidCredentialsAvailable(api, input);
4798
- })));
5423
+ const regenerateProvisioningProfileViaAppleId = (api, input) => Effect.gen(function* () {
5424
+ const auth = yield* AppleAuth;
5425
+ const session = yield* auth.ensureLoggedIn();
5426
+ yield* Console.log("Regenerating provisioning profile via Apple ID...");
5427
+ const created = yield* generateAndUploadProvisioningProfileViaAppleId(api, {
5428
+ context: auth.buildRequestContext(session),
5429
+ distributionCertificateId: input.distributionCertificateId,
5430
+ bundleIdentifier: input.bundleIdentifier,
5431
+ distributionType: input.distributionType
5432
+ });
5433
+ yield* api.iosBundleConfigurations.update({
5434
+ path: { id: input.bundleConfigurationId },
5435
+ payload: { appleProvisioningProfileId: created.id }
5436
+ });
5437
+ return created;
5438
+ });
5439
+
5440
+ //#endregion
5441
+ //#region src/application/credentials-interactive-ios-asc.ts
4799
5442
  const interactiveCertLimitRecover = (api, ascApiKeyId) => Effect.gen(function* () {
4800
5443
  yield* Console.log("");
4801
5444
  yield* Console.log("Apple reports the certificate limit was hit (max 3 distribution certs per team).");
@@ -4898,9 +5541,7 @@ const resolveIosProfileId = (api, input, ctx) => Effect.gen(function* () {
4898
5541
  label: profile.profileName ?? profile.developerPortalIdentifier ?? profile.id
4899
5542
  })));
4900
5543
  });
4901
- const setupIosInteractive = (api, input) => Effect.gen(function* () {
4902
- yield* Console.log("");
4903
- yield* Console.log(`No iOS bundle configuration for ${input.bundleIdentifier} (${input.distribution}).`);
5544
+ const setupIosViaAscKey = (api, input) => Effect.gen(function* () {
4904
5545
  const { certId, cert } = yield* pickIosCertificate(api);
4905
5546
  const ascKeyId = yield* pickIosAscKey(api, cert.appleTeamId);
4906
5547
  const distributionType = IOS_DISTRIBUTION_TO_TYPE[input.distribution];
@@ -4923,6 +5564,101 @@ const setupIosInteractive = (api, input) => Effect.gen(function* () {
4923
5564
  });
4924
5565
  yield* Console.log("iOS bundle configuration saved.");
4925
5566
  });
5567
+
5568
+ //#endregion
5569
+ //#region src/application/credentials-interactive.ts
5570
+ const hasTag = (cause) => typeof cause === "object" && cause !== null && "_tag" in cause;
5571
+ const isMissingResolveError = (cause) => hasTag(cause) && (cause._tag === "NotFound" || cause._tag === "BadRequest");
5572
+ const generateKeystoreInteractive = (api) => Effect.gen(function* () {
5573
+ const alias = yield* promptText("Key alias", { placeholder: "upload-key" });
5574
+ const storePassword = yield* promptPassword("Keystore password");
5575
+ const keyPassword = yield* promptPassword("Key password");
5576
+ const commonName = yield* promptText("Common name (CN)", { placeholder: "Your App" });
5577
+ const organization = yield* promptText("Organization (O)", { placeholder: "Your Company" });
5578
+ yield* Console.log("Generating keystore with keytool...");
5579
+ return (yield* generateAndUploadKeystore(api, {
5580
+ keyAlias: alias,
5581
+ storePassword,
5582
+ keyPassword,
5583
+ commonName,
5584
+ organization
5585
+ })).id;
5586
+ });
5587
+ const pickExistingKeystore = (api) => Effect.gen(function* () {
5588
+ const keystores = yield* api.androidUploadKeystores.list();
5589
+ if (keystores.items.length === 0) return yield* Effect.fail(new MissingCredentialsError({
5590
+ message: "No existing keystores in this organization.",
5591
+ hint: "Re-run and choose 'Generate new keystore'."
5592
+ }));
5593
+ return yield* promptSelect("Select a keystore", keystores.items.map((item) => ({
5594
+ value: item.id,
5595
+ label: item.keyAlias
5596
+ })));
5597
+ });
5598
+ const resolveAndroidAppId = (api, input) => Effect.gen(function* () {
5599
+ const existing = (yield* api.androidApplicationIdentifiers.list({ path: { projectId: input.projectId } })).items.find((item) => item.packageName === input.applicationIdentifier);
5600
+ if (existing !== void 0) return existing.id;
5601
+ return (yield* api.androidApplicationIdentifiers.create({
5602
+ path: { projectId: input.projectId },
5603
+ payload: { packageName: input.applicationIdentifier }
5604
+ })).id;
5605
+ });
5606
+ const resolveAndroidKeystoreId = (api, choice) => choice === "generate" ? generateKeystoreInteractive(api) : pickExistingKeystore(api);
5607
+ const setupAndroidInteractive = (api, input) => Effect.gen(function* () {
5608
+ yield* Console.log("");
5609
+ yield* Console.log(`No Android build credentials configured for ${input.applicationIdentifier}.`);
5610
+ const appId = yield* resolveAndroidAppId(api, input);
5611
+ const choice = yield* promptSelect("How would you like to provide a keystore?", [
5612
+ {
5613
+ value: "generate",
5614
+ label: "Generate new keystore"
5615
+ },
5616
+ {
5617
+ value: "existing",
5618
+ label: "Pick an existing keystore"
5619
+ },
5620
+ {
5621
+ value: "abort",
5622
+ label: "Abort — I'll configure it in the dashboard"
5623
+ }
5624
+ ]);
5625
+ if (choice === "abort") return yield* Effect.fail(new MissingCredentialsError({
5626
+ message: `Build aborted — no keystore bound to ${input.applicationIdentifier}.`,
5627
+ hint: "Run `better-update credentials generate keystore` or upload via the dashboard."
5628
+ }));
5629
+ const keystoreId = yield* resolveAndroidKeystoreId(api, choice);
5630
+ yield* api.androidBuildCredentials.create({
5631
+ path: { applicationIdentifierId: appId },
5632
+ payload: {
5633
+ name: "Default",
5634
+ isDefault: true,
5635
+ androidUploadKeystoreId: keystoreId
5636
+ }
5637
+ });
5638
+ yield* Console.log("Android build credentials configured.");
5639
+ });
5640
+ const ensureAndroidCredentialsAvailable = (api, input) => api.buildCredentials.resolve({
5641
+ path: { projectId: input.projectId },
5642
+ payload: {
5643
+ platform: "android",
5644
+ applicationIdentifier: input.applicationIdentifier
5645
+ }
5646
+ }).pipe(Effect.asVoid);
5647
+ const ensureAndroidCredentials = (api, input, options) => ensureAndroidCredentialsAvailable(api, input).pipe(Effect.catchIf(isMissingResolveError, () => Effect.gen(function* () {
5648
+ const mode = yield* InteractiveMode;
5649
+ if (options.freezeCredentials || !mode.allow) return yield* Effect.fail(new MissingCredentialsError({
5650
+ message: `No Android build credentials for ${input.applicationIdentifier}.`,
5651
+ 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."
5652
+ }));
5653
+ yield* setupAndroidInteractive(api, input);
5654
+ return yield* ensureAndroidCredentialsAvailable(api, input);
5655
+ })));
5656
+ const setupIosInteractive = (api, input) => Effect.gen(function* () {
5657
+ yield* Console.log("");
5658
+ yield* Console.log(`No iOS bundle configuration for ${input.bundleIdentifier} (${input.distribution}).`);
5659
+ if ((yield* chooseIosSetupPath(api)) === "apple-id") return yield* setupIosViaAppleId(api, input);
5660
+ return yield* setupIosViaAscKey(api, input);
5661
+ });
4926
5662
  const resolveIosBuildCredentials = (api, input) => api.buildCredentials.resolve({
4927
5663
  path: { projectId: input.projectId },
4928
5664
  payload: {
@@ -4942,16 +5678,23 @@ const findBoundIosConfig = (api, input) => Effect.gen(function* () {
4942
5678
  });
4943
5679
  const regenerateProvisioningProfile = (api, input) => Effect.gen(function* () {
4944
5680
  const config = yield* findBoundIosConfig(api, input);
4945
- if (config.ascApiKeyId === null || config.appleDistributionCertificateId === null) return yield* new MissingCredentialsError({
4946
- message: "Profile cannot be regenerated: bundle configuration is missing ASC key or distribution certificate",
5681
+ if (config.appleDistributionCertificateId === null) return yield* new MissingCredentialsError({
5682
+ message: "Profile cannot be regenerated: bundle configuration is missing the distribution certificate",
4947
5683
  hint: "Re-bind credentials via `better-update credentials generate` or the dashboard"
4948
5684
  });
5685
+ const distributionType = IOS_DISTRIBUTION_TO_TYPE[input.distribution];
5686
+ if (config.ascApiKeyId === null) return yield* regenerateProvisioningProfileViaAppleId(api, {
5687
+ bundleIdentifier: input.bundleIdentifier,
5688
+ distributionCertificateId: config.appleDistributionCertificateId,
5689
+ distributionType,
5690
+ bundleConfigurationId: config.id
5691
+ });
4949
5692
  yield* Console.log("Regenerating provisioning profile via App Store Connect API...");
4950
5693
  const created = yield* generateAndUploadProvisioningProfile(api, {
4951
5694
  ascApiKeyId: config.ascApiKeyId,
4952
5695
  distributionCertificateId: config.appleDistributionCertificateId,
4953
5696
  bundleIdentifier: input.bundleIdentifier,
4954
- distributionType: IOS_DISTRIBUTION_TO_TYPE[input.distribution]
5697
+ distributionType
4955
5698
  });
4956
5699
  yield* api.iosBundleConfigurations.update({
4957
5700
  path: { id: config.id },
@@ -5055,6 +5798,17 @@ const runAndroidPlatformBuild = (input) => Effect.gen(function* () {
5055
5798
  };
5056
5799
  });
5057
5800
  const runPlatformBuild = (input) => input.platform === "ios" ? runIosPlatformBuild(input) : runAndroidPlatformBuild(input);
5801
+ const resolveProfileName = (projectRoot, requested) => Effect.gen(function* () {
5802
+ const easConfig = yield* readEasJson(projectRoot);
5803
+ const available = Object.keys(easConfig.build ?? {});
5804
+ if (available.includes(requested)) return requested;
5805
+ if (!(yield* InteractiveMode).allow || available.length === 0) return requested;
5806
+ yield* Console.log(`Build profile "${requested}" not found in eas.json.`);
5807
+ return yield* promptSelect("Pick a build profile:", available.map((name) => ({
5808
+ value: name,
5809
+ label: name
5810
+ })));
5811
+ });
5058
5812
  const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
5059
5813
  const api = yield* apiClient;
5060
5814
  const projectRoot = yield* (yield* CliRuntime).cwd;
@@ -5066,11 +5820,18 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
5066
5820
  const baseConfig = yield* readExpoConfig(projectRoot);
5067
5821
  const projectId = yield* extractProjectId(baseConfig);
5068
5822
  const platform = yield* detectPlatform(options.platform, baseConfig);
5069
- const profile = yield* readBuildProfile(projectRoot, options.profileName);
5823
+ const profile = yield* readBuildProfile(projectRoot, yield* resolveProfileName(projectRoot, options.profileName));
5070
5824
  const envVars = yield* pullEnvVars(api, {
5071
5825
  projectId,
5072
5826
  environment: profile.environment
5073
5827
  });
5828
+ yield* applyAutoIncrement({
5829
+ projectRoot,
5830
+ platform,
5831
+ config: yield* readExpoConfig(projectRoot, envVars),
5832
+ ...platform === "ios" && profile.ios?.autoIncrement !== void 0 ? { iosMode: profile.ios.autoIncrement } : {},
5833
+ ...platform === "android" && profile.android?.autoIncrement !== void 0 ? { androidMode: profile.android.autoIncrement } : {}
5834
+ });
5074
5835
  const appMeta = yield* readAppMeta(yield* readExpoConfig(projectRoot, envVars), platform);
5075
5836
  const runtimeVersion = yield* resolveRuntimeVersion({
5076
5837
  raw: appMeta.rawRuntimeVersion,
@@ -10058,36 +10819,45 @@ const envErrorExtras = {
10058
10819
  SystemError: 6,
10059
10820
  BadArgument: 6
10060
10821
  };
10822
+ const isEnvironmentName = (value) => value === "development" || value === "preview" || value === "production";
10823
+ const parseEnvironmentsArg = (raw) => Effect.gen(function* () {
10824
+ const tokens = raw.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
10825
+ if (tokens.length === 0) return yield* new InvalidArgumentError({ message: "Provide at least one environment (development, preview, production)." });
10826
+ const seen = /* @__PURE__ */ new Set();
10827
+ yield* Effect.forEach(tokens, (token) => Effect.gen(function* () {
10828
+ if (!isEnvironmentName(token)) return yield* new InvalidArgumentError({ message: `Invalid environment "${token}". Must be one of: development, preview, production.` });
10829
+ seen.add(token);
10830
+ }), { discard: true });
10831
+ return [...seen];
10832
+ });
10833
+ const parseSingleEnvironmentArg = (raw) => Effect.gen(function* () {
10834
+ if (!isEnvironmentName(raw)) return yield* new InvalidArgumentError({ message: `Invalid environment "${raw}". Must be one of: development, preview, production.` });
10835
+ return raw;
10836
+ });
10837
+ const formatEnvironments = (environments) => [...environments].toSorted((left, right) => left.localeCompare(right)).join(",");
10061
10838
 
10062
10839
  //#endregion
10063
10840
  //#region src/commands/env/delete.ts
10064
10841
  const deleteCommand$2 = defineCommand({
10065
10842
  meta: {
10066
10843
  name: "delete",
10067
- description: "Delete an environment variable by key"
10068
- },
10069
- args: {
10070
- key: {
10071
- type: "positional",
10072
- required: true,
10073
- description: "Env var key"
10074
- },
10075
- environment: {
10076
- type: "string",
10077
- default: "production",
10078
- description: "Target environment"
10079
- }
10844
+ description: "Delete a project env var by key"
10080
10845
  },
10846
+ args: { key: {
10847
+ type: "positional",
10848
+ required: true,
10849
+ description: "Env var key"
10850
+ } },
10081
10851
  run: async ({ args }) => runEffect(Effect.gen(function* () {
10082
10852
  const projectId = yield* readProjectId;
10083
10853
  const api = yield* apiClient;
10084
10854
  const match = (yield* api["env-vars"].list({ urlParams: {
10085
10855
  projectId,
10086
- environment: args.environment
10856
+ scope: "project"
10087
10857
  } })).items.find((item) => item.key === args.key);
10088
- if (!match) return yield* new EnvResourceNotFoundError({ message: `Environment variable ${args.key} not found in ${args.environment}` });
10858
+ if (!match) return yield* new EnvResourceNotFoundError({ message: `Project env var "${args.key}" not found.` });
10089
10859
  yield* api["env-vars"].delete({ path: { id: match.id } });
10090
- yield* Console.log(`Deleted ${args.key} from ${args.environment}`);
10860
+ yield* Console.log(`Deleted ${args.key}`);
10091
10861
  }), envErrorExtras)
10092
10862
  });
10093
10863
 
@@ -10139,11 +10909,12 @@ const execCommand = defineCommand({
10139
10909
  } },
10140
10910
  run: async ({ args }) => runEffect(Effect.gen(function* () {
10141
10911
  const [bin, rest] = yield* splitTrailing(getExecTrailingArgv());
10912
+ const environment = yield* parseSingleEnvironmentArg(args.environment);
10142
10913
  const projectId = yield* readProjectId;
10143
10914
  const api = yield* apiClient;
10144
10915
  const runtime = yield* CliRuntime;
10145
10916
  const baseEnv = yield* runtime.commandEnvironment();
10146
- const pulled = yield* pullForExec(api, projectId, args.environment);
10917
+ const pulled = yield* pullForExec(api, projectId, environment);
10147
10918
  const cmd = Command.make(bin, ...rest).pipe(Command.env({
10148
10919
  ...baseEnv,
10149
10920
  ...pulled
@@ -10163,13 +10934,14 @@ const exportCommand = defineCommand({
10163
10934
  args: { environment: {
10164
10935
  type: "string",
10165
10936
  default: "production",
10166
- description: "Target environment"
10937
+ description: "Target environment (development, preview, production)"
10167
10938
  } },
10168
10939
  run: async ({ args }) => runEffect(Effect.gen(function* () {
10940
+ const environment = yield* parseSingleEnvironmentArg(args.environment);
10169
10941
  const projectId = yield* readProjectId;
10170
10942
  const result = yield* (yield* apiClient)["env-vars"].export({ urlParams: {
10171
10943
  projectId,
10172
- environment: args.environment
10944
+ environment
10173
10945
  } });
10174
10946
  for (const item of result.items) {
10175
10947
  const escaped = item.value.replaceAll("'", String.raw`'\''`);
@@ -10180,24 +10952,59 @@ const exportCommand = defineCommand({
10180
10952
 
10181
10953
  //#endregion
10182
10954
  //#region src/commands/env/get.ts
10955
+ const resolveByKey = (api, key, environment) => Effect.gen(function* () {
10956
+ const env = environment === void 0 ? void 0 : yield* parseSingleEnvironmentArg(environment);
10957
+ const urlParams = {
10958
+ projectId: yield* readProjectId,
10959
+ scope: "all",
10960
+ search: key,
10961
+ ...env === void 0 ? {} : { environments: env }
10962
+ };
10963
+ const { items } = yield* api["env-vars"].list({ urlParams });
10964
+ const matches = items.filter((item) => item.key === key);
10965
+ if (matches.length === 0) return yield* new EnvResourceNotFoundError({ message: `No env var with key "${key}" found${env === void 0 ? "" : ` for environment "${env}"`}.` });
10966
+ if (matches.length > 1) return yield* new EnvResourceNotFoundError({ message: `Multiple env vars match key "${key}". Disambiguate with --environment <${[...new Set(matches.flatMap((entry) => entry.environments))].join(", ")}>.` });
10967
+ return matches[0];
10968
+ });
10969
+ const renderValue$1 = (envVar, includeSensitive) => {
10970
+ if (envVar.visibility === "plaintext") return envVar.value ?? "";
10971
+ if (includeSensitive) return envVar.value ?? "";
10972
+ return "******";
10973
+ };
10183
10974
  const getCommand$1 = defineCommand({
10184
10975
  meta: {
10185
10976
  name: "get",
10186
- description: "Show an environment variable"
10977
+ description: "Show an environment variable by KEY (or --by-id)"
10978
+ },
10979
+ args: {
10980
+ key: {
10981
+ type: "positional",
10982
+ required: true,
10983
+ description: "Env var KEY (uppercase) — or ID when used with --by-id"
10984
+ },
10985
+ environment: {
10986
+ type: "string",
10987
+ description: "Filter by environment when looking up by KEY"
10988
+ },
10989
+ "by-id": {
10990
+ type: "boolean",
10991
+ description: "Treat the argument as an ID instead of KEY"
10992
+ },
10993
+ "include-sensitive": {
10994
+ type: "boolean",
10995
+ description: "Reveal masked sensitive values"
10996
+ }
10187
10997
  },
10188
- args: { id: {
10189
- type: "positional",
10190
- required: true,
10191
- description: "Env var ID"
10192
- } },
10193
10998
  run: async ({ args }) => runEffect(Effect.gen(function* () {
10194
- const envVar = yield* (yield* apiClient)["env-vars"].get({ path: { id: args.id } });
10999
+ const api = yield* apiClient;
11000
+ const envVar = args["by-id"] ? yield* api["env-vars"].get({ path: { id: args.key } }) : yield* resolveByKey(api, args.key, args.environment);
10195
11001
  yield* printKeyValue([
10196
11002
  ["ID", envVar.id],
10197
11003
  ["Key", envVar.key],
10198
- ["Environment", envVar.environment],
11004
+ ["Scope", envVar.scope],
11005
+ ["Environments", formatEnvironments(envVar.environments)],
10199
11006
  ["Visibility", envVar.visibility],
10200
- ["Value", envVar.visibility === "plaintext" ? envVar.value ?? "" : "******"],
11007
+ ["Value", renderValue$1(envVar, args["include-sensitive"] ?? false)],
10201
11008
  ["Created", envVar.createdAt],
10202
11009
  ["Updated", envVar.updatedAt]
10203
11010
  ]);
@@ -10220,25 +11027,23 @@ const importCommand = defineCommand({
10220
11027
  environment: {
10221
11028
  type: "string",
10222
11029
  default: "production",
10223
- description: "Target environment"
11030
+ description: "Target environments (comma-separated, e.g. development,production). Default: production"
10224
11031
  },
10225
11032
  visibility: {
10226
11033
  type: "enum",
10227
- options: [
10228
- "plaintext",
10229
- "sensitive",
10230
- "secret"
10231
- ],
11034
+ options: ["plaintext", "sensitive"],
10232
11035
  default: "plaintext",
10233
11036
  description: "Visibility applied to all imported values"
10234
11037
  }
10235
11038
  },
10236
11039
  run: async ({ args }) => runEffect(Effect.gen(function* () {
10237
11040
  const content = yield* (yield* FileSystem.FileSystem).readFileString(args.file);
11041
+ const environments = yield* parseEnvironmentsArg(args.environment);
10238
11042
  const projectId = yield* readProjectId;
10239
11043
  const result = yield* (yield* apiClient)["env-vars"].bulkImport({ payload: {
11044
+ scope: "project",
10240
11045
  projectId,
10241
- environment: args.environment,
11046
+ environments,
10242
11047
  content,
10243
11048
  visibility: args.visibility
10244
11049
  } });
@@ -10248,58 +11053,126 @@ const importCommand = defineCommand({
10248
11053
 
10249
11054
  //#endregion
10250
11055
  //#region src/commands/env/list.ts
11056
+ const renderValue = (item, includeSensitive) => {
11057
+ if (item.visibility === "plaintext") return item.value ?? "";
11058
+ if (includeSensitive) return item.value ?? "";
11059
+ return "••••••";
11060
+ };
10251
11061
  const listCommand$2 = defineCommand({
10252
11062
  meta: {
10253
11063
  name: "list",
10254
11064
  description: "List environment variables"
10255
11065
  },
10256
- args: { environment: {
10257
- type: "string",
10258
- description: "Filter by environment"
10259
- } },
10260
- run: async ({ args }) => runEffect(Effect.gen(function* () {
10261
- const projectId = yield* readProjectId;
10262
- const api = yield* apiClient;
10263
- const envFilter = args.environment ? { environment: args.environment } : {};
10264
- yield* printList([
11066
+ args: {
11067
+ environments: {
11068
+ type: "string",
11069
+ description: "Filter by environments (comma-separated, e.g. development,production). Default: all"
11070
+ },
11071
+ scope: {
11072
+ type: "enum",
11073
+ options: [
11074
+ "all",
11075
+ "project",
11076
+ "global"
11077
+ ],
11078
+ description: "Filter by scope (default: all — merged with global override resolution)"
11079
+ },
11080
+ search: {
11081
+ type: "string",
11082
+ description: "Filter by key substring (case-insensitive)"
11083
+ },
11084
+ "include-sensitive": {
11085
+ type: "boolean",
11086
+ description: "Reveal masked sensitive values (default: masked)"
11087
+ }
11088
+ },
11089
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
11090
+ const projectId = yield* readProjectId;
11091
+ const api = yield* apiClient;
11092
+ const urlParams = {
11093
+ projectId,
11094
+ ...args.scope ? { scope: args.scope } : {},
11095
+ ...args.environments ? { environments: args.environments } : {},
11096
+ ...args.search ? { search: args.search } : {}
11097
+ };
11098
+ const result = yield* api["env-vars"].list({ urlParams });
11099
+ const includeSensitive = args["include-sensitive"] ?? false;
11100
+ yield* printList([
10265
11101
  "Key",
10266
- "Environment",
11102
+ "Environments",
11103
+ "Scope",
10267
11104
  "Visibility",
10268
11105
  "Value"
10269
- ], (yield* api["env-vars"].list({ urlParams: {
10270
- projectId,
10271
- ...envFilter
10272
- } })).items.map((item) => [
11106
+ ], result.items.map((item) => [
10273
11107
  item.key,
10274
- item.environment,
11108
+ formatEnvironments(item.environments),
11109
+ item.overridesGlobal ? `${item.scope} (overrides global)` : item.scope,
10275
11110
  item.visibility,
10276
- item.visibility === "plaintext" ? item.value ?? "" : "••••••"
11111
+ renderValue(item, includeSensitive)
10277
11112
  ]), "No environment variables found.");
10278
11113
  }), envErrorExtras)
10279
11114
  });
10280
11115
 
10281
11116
  //#endregion
10282
11117
  //#region src/commands/env/pull.ts
11118
+ const DEFAULT_PATH = ".env.local";
11119
+ const escapeShellSingleQuoted = (value) => value.replaceAll("'", String.raw`'\''`);
11120
+ const escapeDotenvDoubleQuoted = (value) => `"${value.replaceAll("\\", String.raw`\\`).replaceAll("\"", String.raw`\"`).replaceAll("$", String.raw`\$`).replaceAll("\n", String.raw`\n`).replaceAll("\r", String.raw`\r`)}"`;
11121
+ const printStdout = (items) => Effect.forEach(items, (item) => Console.log(`export ${item.key}='${escapeShellSingleQuoted(item.value)}'`), { discard: true });
11122
+ const writeDotenvFile = (params) => Effect.gen(function* () {
11123
+ const fs = yield* FileSystem.FileSystem;
11124
+ if ((yield* fs.exists(params.targetPath).pipe(Effect.orElseSucceed(() => false))) && !params.force) {
11125
+ if (!(yield* InteractiveMode).allow) return yield* new InvalidArgumentError({ message: `${params.targetPath} already exists. Pass --force to overwrite, or --stdout to print instead.` });
11126
+ if (!(yield* promptConfirm(`Overwrite ${params.targetPath}?`, { initialValue: false }))) {
11127
+ yield* Console.log("Aborted.");
11128
+ return;
11129
+ }
11130
+ }
11131
+ const body = `${params.items.map((item) => `${item.key}=${escapeDotenvDoubleQuoted(item.value)}`).join("\n")}\n`;
11132
+ yield* fs.writeFileString(params.targetPath, body);
11133
+ yield* Console.log(`Wrote ${String(params.items.length)} env vars to ${params.targetPath}`);
11134
+ });
10283
11135
  const pullCommand = defineCommand({
10284
11136
  meta: {
10285
11137
  name: "pull",
10286
- description: "Print env vars in `export KEY='value'` format"
11138
+ description: `Write env vars to a dotenv file (default: ${DEFAULT_PATH}) — or pipe to stdout with --stdout`
11139
+ },
11140
+ args: {
11141
+ environment: {
11142
+ type: "string",
11143
+ default: "production",
11144
+ description: "Target environment (development, preview, production)"
11145
+ },
11146
+ path: {
11147
+ type: "string",
11148
+ description: `Output file path (default: ${DEFAULT_PATH})`
11149
+ },
11150
+ stdout: {
11151
+ type: "boolean",
11152
+ description: "Print `export KEY='value'` lines to stdout instead of writing a file"
11153
+ },
11154
+ force: {
11155
+ type: "boolean",
11156
+ description: "Overwrite the target file without prompting"
11157
+ }
10287
11158
  },
10288
- args: { environment: {
10289
- type: "string",
10290
- default: "production",
10291
- description: "Target environment"
10292
- } },
10293
11159
  run: async ({ args }) => runEffect(Effect.gen(function* () {
11160
+ const environment = yield* parseSingleEnvironmentArg(args.environment);
10294
11161
  const projectId = yield* readProjectId;
10295
11162
  const result = yield* (yield* apiClient)["env-vars"].export({ urlParams: {
10296
11163
  projectId,
10297
- environment: args.environment
11164
+ environment
10298
11165
  } });
10299
- for (const item of result.items) {
10300
- const escaped = item.value.replaceAll("'", String.raw`'\''`);
10301
- yield* Console.log(`export ${item.key}='${escaped}'`);
11166
+ if (args.stdout) {
11167
+ yield* printStdout(result.items);
11168
+ return;
10302
11169
  }
11170
+ const cwd = yield* (yield* CliRuntime).cwd;
11171
+ yield* writeDotenvFile({
11172
+ targetPath: path.resolve(cwd, args.path ?? DEFAULT_PATH),
11173
+ items: result.items,
11174
+ force: args.force ?? false
11175
+ });
10303
11176
  }), envErrorExtras)
10304
11177
  });
10305
11178
 
@@ -10342,7 +11215,7 @@ const pushCommand = defineCommand({
10342
11215
  environment: {
10343
11216
  type: "string",
10344
11217
  default: "production",
10345
- description: "Target environment"
11218
+ description: "Target environments (comma-separated, e.g. development,production). Default: production"
10346
11219
  },
10347
11220
  force: {
10348
11221
  type: "boolean",
@@ -10355,11 +11228,12 @@ const pushCommand = defineCommand({
10355
11228
  yield* printHuman(`No valid KEY=VALUE entries found in ${args.file}.`);
10356
11229
  return;
10357
11230
  }
11231
+ const environments = yield* parseEnvironmentsArg(args.environment);
10358
11232
  const projectId = yield* readProjectId;
10359
11233
  const api = yield* apiClient;
10360
11234
  const existingResp = yield* api["env-vars"].list({ urlParams: {
10361
11235
  projectId,
10362
- environment: args.environment
11236
+ scope: "project"
10363
11237
  } });
10364
11238
  const existingByKey = new Map(existingResp.items.map((item) => [item.key, item]));
10365
11239
  const conflicts = parsed.filter((entry) => existingByKey.has(entry.key));
@@ -10379,8 +11253,9 @@ const pushCommand = defineCommand({
10379
11253
  });
10380
11254
  const skipped = conflicts.length - entriesToOverwrite.length;
10381
11255
  yield* Effect.forEach(newEntries, (entry) => api["env-vars"].create({ payload: {
11256
+ scope: "project",
10382
11257
  projectId,
10383
- environment: args.environment,
11258
+ environments,
10384
11259
  key: entry.key,
10385
11260
  value: entry.value,
10386
11261
  visibility: entry.visibility
@@ -10392,11 +11267,12 @@ const pushCommand = defineCommand({
10392
11267
  path: { id: existing.id },
10393
11268
  payload: {
10394
11269
  value: entry.value,
10395
- visibility: entry.visibility
11270
+ visibility: entry.visibility,
11271
+ environments
10396
11272
  }
10397
11273
  });
10398
11274
  }, { concurrency: 4 });
10399
- yield* printHuman(`Pushed to ${args.environment}: ${String(newEntries.length)} created, ${String(entriesToOverwrite.length)} updated${skipped > 0 ? `, ${String(skipped)} skipped` : ""}.`);
11275
+ yield* printHuman(`Pushed to ${formatEnvironments(environments)}: ${String(newEntries.length)} created, ${String(entriesToOverwrite.length)} updated${skipped > 0 ? `, ${String(skipped)} skipped` : ""}.`);
10400
11276
  }), envErrorExtras)
10401
11277
  });
10402
11278
 
@@ -10405,7 +11281,7 @@ const pushCommand = defineCommand({
10405
11281
  const setCommand$1 = defineCommand({
10406
11282
  meta: {
10407
11283
  name: "set",
10408
- description: "Create or update an environment variable"
11284
+ description: "Create or update a project-scoped environment variable"
10409
11285
  },
10410
11286
  args: {
10411
11287
  keyValue: {
@@ -10416,47 +11292,46 @@ const setCommand$1 = defineCommand({
10416
11292
  environment: {
10417
11293
  type: "string",
10418
11294
  default: "production",
10419
- description: "Target environment"
11295
+ description: "Target environments (comma-separated, e.g. development,production). Default: production"
10420
11296
  },
10421
11297
  visibility: {
10422
11298
  type: "enum",
10423
- options: [
10424
- "plaintext",
10425
- "sensitive",
10426
- "secret"
10427
- ],
11299
+ options: ["plaintext", "sensitive"],
10428
11300
  default: "plaintext",
10429
11301
  description: "Value visibility"
10430
11302
  }
10431
11303
  },
10432
11304
  run: async ({ args }) => runEffect(Effect.gen(function* () {
10433
11305
  const { key, value } = yield* parseKeyValue(args.keyValue);
10434
- const { environment } = args;
11306
+ const environments = yield* parseEnvironmentsArg(args.environment);
10435
11307
  const { visibility } = args;
10436
11308
  const projectId = yield* readProjectId;
10437
11309
  const api = yield* apiClient;
10438
11310
  const match = (yield* api["env-vars"].list({ urlParams: {
10439
11311
  projectId,
10440
- environment
11312
+ scope: "project"
10441
11313
  } })).items.find((item) => item.key === key);
11314
+ const label = formatEnvironments(environments);
10442
11315
  if (match) {
10443
11316
  yield* api["env-vars"].update({
10444
11317
  path: { id: match.id },
10445
11318
  payload: {
10446
11319
  value,
10447
- visibility
11320
+ visibility,
11321
+ environments
10448
11322
  }
10449
11323
  });
10450
- yield* Console.log(`Updated ${key} in ${environment}`);
11324
+ yield* Console.log(`Updated ${key} (environments: ${label})`);
10451
11325
  } else {
10452
11326
  yield* api["env-vars"].create({ payload: {
11327
+ scope: "project",
10453
11328
  projectId,
10454
- environment,
11329
+ environments,
10455
11330
  key,
10456
11331
  value,
10457
11332
  visibility
10458
11333
  } });
10459
- yield* Console.log(`Created ${key} in ${environment}`);
11334
+ yield* Console.log(`Created ${key} (environments: ${label})`);
10460
11335
  }
10461
11336
  }), envErrorExtras)
10462
11337
  });
@@ -10466,7 +11341,7 @@ const setCommand$1 = defineCommand({
10466
11341
  const updateCommand$1 = defineCommand({
10467
11342
  meta: {
10468
11343
  name: "update",
10469
- description: "Update an env var's value or visibility"
11344
+ description: "Update a project env var's value, visibility, or environments"
10470
11345
  },
10471
11346
  args: {
10472
11347
  key: {
@@ -10480,32 +11355,29 @@ const updateCommand$1 = defineCommand({
10480
11355
  },
10481
11356
  visibility: {
10482
11357
  type: "enum",
10483
- options: [
10484
- "plaintext",
10485
- "sensitive",
10486
- "secret"
10487
- ],
11358
+ options: ["plaintext", "sensitive"],
10488
11359
  description: "New visibility (leave unset to keep current)"
10489
11360
  },
10490
- environment: {
11361
+ environments: {
10491
11362
  type: "string",
10492
- default: "production",
10493
- description: "Target environment"
11363
+ description: "New environments assignment (comma-separated, e.g. development,production). Leave unset to keep current."
10494
11364
  }
10495
11365
  },
10496
11366
  run: async ({ args }) => runEffect(Effect.gen(function* () {
10497
- const { key, value, visibility, environment } = args;
10498
- if (value === void 0 && visibility === void 0) return yield* new InvalidArgumentError({ message: "Pass --value, --visibility, or both. Nothing to update otherwise." });
11367
+ const { key, value, visibility, environments } = args;
11368
+ if (value === void 0 && visibility === void 0 && environments === void 0) return yield* new InvalidArgumentError({ message: "Pass --value, --visibility, --environments (or any combination). Nothing to update otherwise." });
10499
11369
  const projectId = yield* readProjectId;
10500
11370
  const api = yield* apiClient;
10501
11371
  const match = (yield* api["env-vars"].list({ urlParams: {
10502
11372
  projectId,
10503
- environment
11373
+ scope: "project"
10504
11374
  } })).items.find((item) => item.key === key);
10505
- if (!match) return yield* new EnvResourceNotFoundError({ message: `Env var "${key}" not found in environment "${environment}".` });
11375
+ if (!match) return yield* new EnvResourceNotFoundError({ message: `Env var "${key}" not found in project.` });
11376
+ const envList = environments ? yield* parseEnvironmentsArg(environments) : void 0;
10506
11377
  const payload = {
10507
11378
  ...value === void 0 ? {} : { value },
10508
- ...visibility === void 0 ? {} : { visibility }
11379
+ ...visibility === void 0 ? {} : { visibility },
11380
+ ...envList ? { environments: envList } : {}
10509
11381
  };
10510
11382
  yield* api["env-vars"].update({
10511
11383
  path: { id: match.id },
@@ -10514,7 +11386,8 @@ const updateCommand$1 = defineCommand({
10514
11386
  const changed = [];
10515
11387
  if (value !== void 0) changed.push("value");
10516
11388
  if (visibility !== void 0) changed.push("visibility");
10517
- yield* printHuman(`Updated ${changed.join(" + ")} for ${key} in ${environment}.`);
11389
+ if (envList) changed.push("environments");
11390
+ yield* printHuman(`Updated ${changed.join(" + ")} for ${key}.`);
10518
11391
  }), envErrorExtras)
10519
11392
  });
10520
11393
 
@@ -10612,18 +11485,34 @@ const checkExistingLink = (api, config, localSlug) => Effect.gen(function* () {
10612
11485
  }
10613
11486
  return (yield* promptConfirm("Overwrite local projectId with a fresh link by slug?", { initialValue: false })) ? "mismatch-overwrite" : "mismatch-abort";
10614
11487
  });
11488
+ const writeAndAnnounce = (projectRoot, projectId) => Effect.gen(function* () {
11489
+ const writeResult = yield* writeProjectId(projectRoot, projectId);
11490
+ const target = writeResult.configPath ? path.relative(projectRoot, writeResult.configPath) : "your Expo config";
11491
+ yield* Console.log(`Project linked successfully. ID saved to ${target}.`);
11492
+ if (writeResult.type === "warn" && writeResult.message) yield* Console.log(`Note: ${writeResult.message}`);
11493
+ });
10615
11494
  const initCommand = defineCommand({
10616
11495
  meta: {
10617
11496
  name: "init",
10618
11497
  description: "Link the local Expo project to a better-update project"
10619
11498
  },
10620
- run: async () => runEffect(Effect.gen(function* () {
11499
+ args: { id: {
11500
+ type: "string",
11501
+ description: "Link by explicit project ID (skips slug lookup / project creation)"
11502
+ } },
11503
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
10621
11504
  const projectRoot = yield* (yield* CliRuntime).cwd;
10622
11505
  const config = yield* readExpoConfig(projectRoot);
10623
11506
  const name = config.name ?? config.slug ?? "untitled";
11507
+ const api = yield* apiClient;
11508
+ if (args.id !== void 0 && args.id.length > 0) {
11509
+ const project = yield* api.projects.get({ path: { id: args.id } });
11510
+ yield* Console.log(`Linking project: ${project.name} (${project.id})`);
11511
+ yield* writeAndAnnounce(projectRoot, project.id);
11512
+ return;
11513
+ }
10624
11514
  const slug = yield* extractSlug(config);
10625
11515
  yield* Console.log(`Linking project: ${name} (${slug})`);
10626
- const api = yield* apiClient;
10627
11516
  const linkState = yield* checkExistingLink(api, config, slug);
10628
11517
  if (linkState === "matched" || linkState === "mismatch-abort") return;
10629
11518
  const { items } = yield* api.projects.list({ urlParams: {
@@ -10631,7 +11520,7 @@ const initCommand = defineCommand({
10631
11520
  limit: 100
10632
11521
  } });
10633
11522
  const existing = items.find((project) => project.slug === slug);
10634
- const writeResult = yield* writeProjectId(projectRoot, yield* Effect.gen(function* () {
11523
+ yield* writeAndAnnounce(projectRoot, yield* Effect.gen(function* () {
10635
11524
  if (existing) {
10636
11525
  yield* Console.log(`Found existing project: ${existing.name} (${existing.id})`);
10637
11526
  return existing.id;
@@ -10644,198 +11533,9 @@ const initCommand = defineCommand({
10644
11533
  yield* Console.log(`Created project: ${created.name} (${created.id})`);
10645
11534
  return created.id;
10646
11535
  }));
10647
- const target = writeResult.configPath ? path.relative(projectRoot, writeResult.configPath) : "your Expo config";
10648
- yield* Console.log(`Project linked successfully. ID saved to ${target}.`);
10649
- if (writeResult.type === "warn" && writeResult.message) yield* Console.log(`Note: ${writeResult.message}`);
10650
11536
  }))
10651
11537
  });
10652
11538
 
10653
- //#endregion
10654
- //#region src/lib/browser-login.ts
10655
- var BrowserLoginTimeoutError = class extends Data.TaggedError("BrowserLoginTimeoutError") {};
10656
- var BrowserLoginSessionClosedError = class extends Data.TaggedError("BrowserLoginSessionClosedError") {};
10657
- const CALLBACK_PAGE = `<!doctype html>
10658
- <html lang="en">
10659
- <head>
10660
- <meta charset="utf-8" />
10661
- <meta name="viewport" content="width=device-width, initial-scale=1" />
10662
- <title>better-update CLI Login</title>
10663
- <style>
10664
- :root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, sans-serif; }
10665
- body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px; }
10666
- main { max-width: 32rem; line-height: 1.5; }
10667
- code { font-family: ui-monospace, SFMono-Regular, monospace; }
10668
- </style>
10669
- </head>
10670
- <body>
10671
- <main>
10672
- <h1>Completing CLI login...</h1>
10673
- <p id="message">Finalizing the local session. You can keep this tab open.</p>
10674
- </main>
10675
- <script>
10676
- const message = document.getElementById("message");
10677
- const render = (text) => {
10678
- if (message) message.textContent = text;
10679
- };
10680
-
10681
- const params = new URLSearchParams(window.location.hash.slice(1));
10682
- const token = params.get("token");
10683
-
10684
- if (!token) {
10685
- render("Missing token. Return to the CLI and run login again.");
10686
- } else {
10687
- fetch("/callback/token", {
10688
- method: "POST",
10689
- headers: { "content-type": "application/json" },
10690
- body: JSON.stringify({ token }),
10691
- })
10692
- .then(async (response) => {
10693
- if (!response.ok) {
10694
- const body = await response.text();
10695
- throw new Error(body || "Callback failed");
10696
- }
10697
- window.history.replaceState({}, document.title, window.location.pathname);
10698
- render("CLI login complete. You can close this tab.");
10699
- setTimeout(() => window.close(), 300);
10700
- })
10701
- .catch((error) => {
10702
- render(error instanceof Error ? error.message : "Callback failed.");
10703
- });
10704
- }
10705
- <\/script>
10706
- </body>
10707
- </html>`;
10708
- const createBrowserLoginSession = (options = {}) => {
10709
- const tokenDeferred = Effect.runSync(Deferred.make());
10710
- const waitForToken = Deferred.await(tokenDeferred).pipe(Effect.timeoutFail({
10711
- duration: options.timeoutMs === void 0 ? Duration.minutes(5) : Duration.millis(options.timeoutMs),
10712
- onTimeout: () => new BrowserLoginTimeoutError({ message: "Timed out waiting for browser login to complete." })
10713
- }));
10714
- const dispose = () => {
10715
- Effect.runSync(Deferred.fail(tokenDeferred, new BrowserLoginSessionClosedError({ message: "Browser login session closed." })));
10716
- };
10717
- return {
10718
- callbackPath: "/callback",
10719
- waitForToken,
10720
- handleRequest: async (request) => {
10721
- const url = new URL(request.url);
10722
- if (request.method === "GET" && url.pathname === "/callback") return new Response(CALLBACK_PAGE, { headers: { "content-type": "text/html; charset=utf-8" } });
10723
- if (request.method === "POST" && url.pathname === "/callback/token") try {
10724
- const body = await request.json();
10725
- if (!isRecord(body)) return new Response("Invalid callback payload", { status: 400 });
10726
- const token = typeof body["token"] === "string" ? body["token"].trim() : "";
10727
- if (token.length === 0) return new Response("Missing token", { status: 400 });
10728
- Effect.runSync(Deferred.succeed(tokenDeferred, token));
10729
- return Response.json({ ok: true });
10730
- } catch {
10731
- return new Response("Invalid callback payload", { status: 400 });
10732
- }
10733
- return new Response("Not found", { status: 404 });
10734
- },
10735
- dispose
10736
- };
10737
- };
10738
- const readBody = async (req) => {
10739
- const chunks = [];
10740
- for await (const chunk of req) chunks.push(chunk);
10741
- return Buffer.concat(chunks);
10742
- };
10743
- const toFetchRequest = async (req, origin) => {
10744
- const url = new URL(req.url ?? "/", origin);
10745
- const method = req.method ?? "GET";
10746
- const headers = new Headers();
10747
- for (const [key, value] of Object.entries(req.headers)) {
10748
- if (value === void 0) continue;
10749
- if (Array.isArray(value)) for (const entry of value) headers.append(key, entry);
10750
- else headers.append(key, value);
10751
- }
10752
- const init = {
10753
- method,
10754
- headers
10755
- };
10756
- if (method !== "GET" && method !== "HEAD") {
10757
- const body = await readBody(req);
10758
- init.body = new Uint8Array(body);
10759
- }
10760
- return new Request(url, init);
10761
- };
10762
- const writeFetchResponse = async (res, response) => {
10763
- res.statusCode = response.status;
10764
- response.headers.forEach((value, key) => {
10765
- res.setHeader(key, value);
10766
- });
10767
- const body = await response.arrayBuffer();
10768
- res.end(Buffer.from(body));
10769
- };
10770
- const handleIncoming = async (req, res, session) => {
10771
- try {
10772
- const request = await toFetchRequest(req, "http://127.0.0.1");
10773
- await writeFetchResponse(res, await session.handleRequest(request));
10774
- } catch {
10775
- res.statusCode = 500;
10776
- res.end("Local callback failed");
10777
- }
10778
- };
10779
- const createBrowserLoginServer = async (options = {}) => {
10780
- const session = createBrowserLoginSession(options);
10781
- const server = createServer((req, res) => {
10782
- handleIncoming(req, res, session).catch(() => void 0);
10783
- });
10784
- server.listen(0, "127.0.0.1");
10785
- await once(server, "listening");
10786
- const address = server.address();
10787
- return {
10788
- callbackUrl: `http://127.0.0.1:${address !== null && typeof address === "object" ? address.port : 0}${session.callbackPath}`,
10789
- waitForToken: session.waitForToken,
10790
- stop: () => {
10791
- session.dispose();
10792
- server.close();
10793
- }
10794
- };
10795
- };
10796
-
10797
- //#endregion
10798
- //#region src/application/login.ts
10799
- const buildOpenBrowserCommand = (platform, url) => {
10800
- if (platform === "darwin") return Command.make("open", url);
10801
- if (platform === "win32") return Command.make("cmd", "/c", "start", "", url);
10802
- return Command.make("xdg-open", url);
10803
- };
10804
- const openBrowser = (url) => Effect.gen(function* () {
10805
- const command = buildOpenBrowserCommand((yield* CliRuntime).platform, url);
10806
- 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}`);
10807
- });
10808
- const browserLogin = Effect.scoped(Effect.gen(function* () {
10809
- const configStore = yield* ConfigStore;
10810
- const authStore = yield* AuthStore;
10811
- const webUrl = yield* configStore.getWebUrl;
10812
- const loginServer = yield* Effect.acquireRelease(Effect.promise(async () => createBrowserLoginServer()), (server) => Effect.sync(server.stop));
10813
- const loginUrl = `${webUrl}/auth/cli-login?callbackUrl=${encodeURIComponent(loginServer.callbackUrl)}`;
10814
- yield* Console.log("Opening browser for better-update login...");
10815
- yield* Console.log("");
10816
- yield* openBrowser(loginUrl);
10817
- const token = yield* loginServer.waitForToken;
10818
- yield* authStore.saveToken(token);
10819
- yield* Console.log("");
10820
- yield* Console.log("Logged in successfully. Token saved to ~/.better-update/auth.json");
10821
- }));
10822
- const manualLogin = Effect.gen(function* () {
10823
- yield* Console.log("Log in to better-update with an existing API key");
10824
- yield* Console.log("Get your API key from the dashboard > API Keys page");
10825
- yield* Console.log("");
10826
- const token = yield* promptPassword("Paste your API key (from dashboard > API Keys):");
10827
- yield* (yield* AuthStore).saveToken(token);
10828
- yield* Console.log("");
10829
- yield* Console.log("Logged in successfully. Token saved to ~/.better-update/auth.json");
10830
- });
10831
- const runLogin = (options) => Effect.gen(function* () {
10832
- if (options.manualApiKey) {
10833
- yield* manualLogin;
10834
- return;
10835
- }
10836
- yield* browserLogin;
10837
- });
10838
-
10839
11539
  //#endregion
10840
11540
  //#region src/commands/login.ts
10841
11541
  const loginCommand = defineCommand({
@@ -10857,9 +11557,17 @@ const logoutCommand = defineCommand({
10857
11557
  name: "logout",
10858
11558
  description: "Remove the stored auth token"
10859
11559
  },
10860
- run: async () => runEffect(Effect.gen(function* () {
11560
+ args: { all: {
11561
+ type: "boolean",
11562
+ description: "Also clear cached Apple Developer session (cookies)"
11563
+ } },
11564
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
10861
11565
  yield* (yield* AuthStore).clearToken;
10862
11566
  yield* Console.log("Logged out. Auth token removed.");
11567
+ if (args.all) {
11568
+ yield* (yield* AppleSessionStore).clearSession;
11569
+ yield* Console.log("Cleared Apple Developer session.");
11570
+ }
10863
11571
  }))
10864
11572
  });
10865
11573
 
@@ -11733,6 +12441,86 @@ const resolveChannelToBranch = (client, projectId, channelName) => Effect.gen(fu
11733
12441
  if (!branch) return yield* new UpdatePublishError({ message: `Channel "${channelName}" maps to a branch (${match.branchId}) not in the project's branch list.` });
11734
12442
  return branch.name;
11735
12443
  });
12444
+ const CREATE_NEW_BRANCH_SENTINEL = "__better_update_create_new_branch__";
12445
+ const promptBranchName$1 = Effect.gen(function* () {
12446
+ const trimmed = (yield* promptText("Branch name to create", { placeholder: "e.g. main, staging, release" })).trim();
12447
+ if (trimmed.length === 0) return yield* new UpdatePublishError({ message: "Branch name cannot be empty." });
12448
+ return trimmed;
12449
+ });
12450
+ /**
12451
+ * Interactive branch picker: lists existing branches with a "+ Create new..."
12452
+ * Sentinel option. When the user picks the sentinel (or there are no
12453
+ * Existing branches), prompts for a new branch name.
12454
+ */
12455
+ const promptForBranch = (client, projectId) => Effect.gen(function* () {
12456
+ const branches = yield* drainPages((page) => client.branches.list({ urlParams: {
12457
+ projectId,
12458
+ limit: 100,
12459
+ page
12460
+ } })).pipe(Effect.mapError((cause) => new UpdatePublishError({ message: `Failed to list branches: ${formatCause(cause)}` })));
12461
+ if (branches.length === 0) return yield* promptBranchName$1;
12462
+ const choice = yield* promptSelect("Which branch to publish to?", [...branches.map((branch) => ({
12463
+ value: branch.name,
12464
+ label: branch.name
12465
+ })), {
12466
+ value: CREATE_NEW_BRANCH_SENTINEL,
12467
+ label: "+ Create new branch..."
12468
+ }]);
12469
+ if (choice === CREATE_NEW_BRANCH_SENTINEL) return yield* promptBranchName$1;
12470
+ return choice;
12471
+ });
12472
+ /**
12473
+ * Interactive message prompt with the git commit subject as default.
12474
+ * Returns `undefined` if the user entered an empty value, so the caller
12475
+ * Can fall back to its own default.
12476
+ */
12477
+ const promptForMessage = (commitMessage) => Effect.gen(function* () {
12478
+ const fallback = commitMessage ?? "Publish via better-update CLI";
12479
+ const trimmed = (yield* promptText("Update message", {
12480
+ placeholder: fallback,
12481
+ defaultValue: fallback
12482
+ })).trim();
12483
+ return trimmed.length === 0 ? void 0 : trimmed;
12484
+ });
12485
+ /**
12486
+ * Apply the full branch/message resolution chain in priority order:
12487
+ * Explicit args → git context (when --auto) → channel lookup → env fallback →
12488
+ * Interactive picker. Returns the final values or fails with a helpful error
12489
+ * When everything is exhausted in non-interactive mode.
12490
+ */
12491
+ const resolveBranchAndMessage = (input) => Effect.gen(function* () {
12492
+ let branch = input.branchArg;
12493
+ let message = input.messageArg;
12494
+ if (input.auto) {
12495
+ if (branch === void 0 && input.gitCtx.ref !== void 0) branch = input.gitCtx.ref;
12496
+ if (message === void 0 && input.gitCtx.commitMessage !== void 0) message = input.gitCtx.commitMessage;
12497
+ }
12498
+ if (branch === void 0 && input.channelArg !== void 0) branch = yield* resolveChannelToBranch(input.client, input.projectId, input.channelArg);
12499
+ if (branch === void 0 && input.envBranch !== void 0 && input.envBranch.length > 0) branch = input.envBranch;
12500
+ const interactive = yield* InteractiveMode;
12501
+ if (branch === void 0) {
12502
+ if (!interactive.allow) return yield* new UpdatePublishError({ message: "Missing --branch or --channel. Provide one explicitly, set BETTER_UPDATE_BRANCH, use --auto to infer from git, or run interactively." });
12503
+ branch = yield* promptForBranch(input.client, input.projectId);
12504
+ }
12505
+ if (message === void 0 && interactive.allow && !input.auto) message = yield* promptForMessage(input.gitCtx.commitMessage);
12506
+ return {
12507
+ branch,
12508
+ message
12509
+ };
12510
+ });
12511
+ /**
12512
+ * Show a pre-publish preview and ask for confirmation. Returns `false`
12513
+ * If the user declines so the caller can abort gracefully.
12514
+ */
12515
+ const confirmPublishPreview = (preview) => Effect.gen(function* () {
12516
+ yield* Console.log("");
12517
+ yield* Console.log(`Branch: ${preview.branch}`);
12518
+ yield* Console.log(`Platforms: ${[...preview.platforms].join(", ")}`);
12519
+ yield* Console.log(`Environment: ${preview.environment}`);
12520
+ yield* Console.log(`Message: ${preview.message}`);
12521
+ yield* Console.log("");
12522
+ return yield* promptConfirm("Proceed with publish?", { initialValue: true });
12523
+ });
11736
12524
  const emitMetadataFile = (input) => Effect.gen(function* () {
11737
12525
  const fs = yield* FileSystem.FileSystem;
11738
12526
  yield* fs.makeDirectory(input.dir, { recursive: true }).pipe(Effect.mapError((cause) => new UpdatePublishError({ message: `Failed to prepare metadata directory: ${formatCause(cause)}` })));
@@ -11847,7 +12635,8 @@ const publishPlatform = (params) => Effect.gen(function* () {
11847
12635
  };
11848
12636
  });
11849
12637
  const runUpdatePublish = (options) => Effect.scoped(Effect.gen(function* () {
11850
- const projectRoot = yield* (yield* CliRuntime).cwd;
12638
+ const runtime = yield* CliRuntime;
12639
+ const projectRoot = yield* runtime.cwd;
11851
12640
  const api = yield* apiClient;
11852
12641
  yield* ensureRepoClean({
11853
12642
  projectRoot,
@@ -11868,23 +12657,30 @@ const runUpdatePublish = (options) => Effect.scoped(Effect.gen(function* () {
11868
12657
  envVars: environmentVars
11869
12658
  });
11870
12659
  const tempDir = yield* acquireBuildTempDir.pipe(Effect.mapError((cause) => new UpdatePublishError({ message: `Failed to create a temporary export directory: ${formatCause(cause)}` })));
11871
- let resolvedBranch = options.branch;
11872
- let resolvedMessage = options.message;
11873
- if (options.auto) {
11874
- const gitContext = yield* readGitContext(projectRoot);
11875
- if (!resolvedBranch) {
11876
- if (!gitContext.ref) return yield* new UpdatePublishError({ message: "Cannot infer branch from git. Ensure you are in a git repo with a checked-out branch, or provide --branch explicitly." });
11877
- resolvedBranch = gitContext.ref;
11878
- }
11879
- if (!resolvedMessage && gitContext.commitMessage) resolvedMessage = gitContext.commitMessage;
11880
- }
11881
- if (!resolvedBranch && options.channel !== void 0) resolvedBranch = yield* resolveChannelToBranch(api, projectId, options.channel);
11882
- if (!resolvedBranch) return yield* new UpdatePublishError({ message: "Missing --branch or --channel. Provide one explicitly or use --auto to infer from git." });
12660
+ const gitCtx = yield* readGitContext(projectRoot);
12661
+ const envBranch = (yield* runtime.getEnv("BETTER_UPDATE_BRANCH"))?.trim();
12662
+ const { branch, message: resolvedMessage } = yield* resolveBranchAndMessage({
12663
+ client: api,
12664
+ projectId,
12665
+ branchArg: options.branch,
12666
+ messageArg: options.message,
12667
+ channelArg: options.channel,
12668
+ auto: options.auto,
12669
+ gitCtx,
12670
+ envBranch
12671
+ });
11883
12672
  if (options.skipBundler && options.inputDir === void 0) return yield* new UpdatePublishError({ message: "--skip-bundler requires --input-dir <path> pointing to a pre-bundled export." });
11884
12673
  const sharedExportDir = options.inputDir === void 0 ? void 0 : path.resolve(projectRoot, options.inputDir);
11885
- const branch = resolvedBranch;
11886
12674
  const groupId = randomUUID();
11887
12675
  const message = resolvedMessage ?? "Publish via better-update CLI";
12676
+ if ((yield* InteractiveMode).allow && !options.auto) {
12677
+ if (!(yield* confirmPublishPreview({
12678
+ branch,
12679
+ platforms,
12680
+ message,
12681
+ environment: options.environment
12682
+ }))) return yield* new UpdatePublishError({ message: "Publish cancelled." });
12683
+ }
11888
12684
  const signedPayloads = yield* loadSignedPublishPayloads({
11889
12685
  platforms,
11890
12686
  globalFiles: {
@@ -13152,7 +13948,8 @@ await runMain(defineCommand({
13152
13948
  devices: devicesCommand,
13153
13949
  webhooks: webhooksCommand,
13154
13950
  autocomplete: autocompleteCommand,
13155
- "migrate-config": migrateConfigCommand
13951
+ "migrate-config": migrateConfigCommand,
13952
+ apple: appleCommand
13156
13953
  }
13157
13954
  }));
13158
13955