@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 +1318 -521
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
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.
|
|
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"
|
|
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
|
-
|
|
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
|
|
1150
|
+
const EnvVarEnvironmentArray = Schema.Array(EnvVarEnvironment).pipe(Schema.minItems(1));
|
|
1145
1151
|
const CreateEnvVarBody = Schema.Struct({
|
|
1146
|
-
|
|
1147
|
-
|
|
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
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
1186
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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/
|
|
1917
|
-
|
|
1918
|
-
|
|
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
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
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
|
|
1938
|
-
|
|
1939
|
-
|
|
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
|
|
1942
|
-
const
|
|
1943
|
-
|
|
1944
|
-
|
|
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
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
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 (
|
|
1953
|
-
|
|
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/
|
|
1959
|
-
|
|
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
|
|
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
|
|
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 }) =>
|
|
3899
|
-
|
|
3900
|
-
environment
|
|
3901
|
-
|
|
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
|
|
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/
|
|
4713
|
-
const
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
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
|
|
4731
|
-
const
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
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
|
|
4742
|
-
const existing =
|
|
4743
|
-
if (existing !==
|
|
4744
|
-
return (yield*
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
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
|
|
4750
|
-
const
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
{
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
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
|
|
4773
|
-
yield*
|
|
4774
|
-
|
|
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
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
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("
|
|
5421
|
+
yield* Console.log("iOS bundle configuration saved.");
|
|
4782
5422
|
});
|
|
4783
|
-
const
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
return
|
|
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
|
|
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.
|
|
4946
|
-
message: "Profile cannot be regenerated: bundle configuration is missing
|
|
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
|
|
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
|
|
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
|
-
|
|
10856
|
+
scope: "project"
|
|
10087
10857
|
} })).items.find((item) => item.key === args.key);
|
|
10088
|
-
if (!match) return yield* new EnvResourceNotFoundError({ message: `
|
|
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}
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
["
|
|
11004
|
+
["Scope", envVar.scope],
|
|
11005
|
+
["Environments", formatEnvironments(envVar.environments)],
|
|
10199
11006
|
["Visibility", envVar.visibility],
|
|
10200
|
-
["Value", envVar
|
|
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
|
|
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
|
-
|
|
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: {
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
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
|
-
"
|
|
11102
|
+
"Environments",
|
|
11103
|
+
"Scope",
|
|
10267
11104
|
"Visibility",
|
|
10268
11105
|
"Value"
|
|
10269
|
-
],
|
|
10270
|
-
projectId,
|
|
10271
|
-
...envFilter
|
|
10272
|
-
} })).items.map((item) => [
|
|
11106
|
+
], result.items.map((item) => [
|
|
10273
11107
|
item.key,
|
|
10274
|
-
item.
|
|
11108
|
+
formatEnvironments(item.environments),
|
|
11109
|
+
item.overridesGlobal ? `${item.scope} (overrides global)` : item.scope,
|
|
10275
11110
|
item.visibility,
|
|
10276
|
-
item
|
|
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:
|
|
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
|
|
11164
|
+
environment
|
|
10298
11165
|
} });
|
|
10299
|
-
|
|
10300
|
-
|
|
10301
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
11329
|
+
environments,
|
|
10455
11330
|
key,
|
|
10456
11331
|
value,
|
|
10457
11332
|
visibility
|
|
10458
11333
|
} });
|
|
10459
|
-
yield* Console.log(`Created ${key}
|
|
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
|
|
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
|
-
|
|
11361
|
+
environments: {
|
|
10491
11362
|
type: "string",
|
|
10492
|
-
|
|
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,
|
|
10498
|
-
if (value === void 0 && visibility === void 0) return yield* new InvalidArgumentError({ message: "Pass --value, --visibility, or
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
11872
|
-
|
|
11873
|
-
|
|
11874
|
-
|
|
11875
|
-
|
|
11876
|
-
|
|
11877
|
-
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
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
|
|