@better-update/cli 0.11.0 → 0.12.1
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 +1180 -434
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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 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.1";
|
|
31
32
|
|
|
32
33
|
//#endregion
|
|
33
34
|
//#region src/lib/interactive-mode.ts
|
|
@@ -1772,6 +1773,53 @@ const ApiClientLive = Layer.effect(ApiClientService, Effect.gen(function* () {
|
|
|
1772
1773
|
}) };
|
|
1773
1774
|
}));
|
|
1774
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
|
+
|
|
1775
1823
|
//#endregion
|
|
1776
1824
|
//#region ../../packages/safe-json/src/index.ts
|
|
1777
1825
|
const parseJsonResult = (text) => {
|
|
@@ -1797,6 +1845,7 @@ const AppleSessionStoreLive = Layer.effect(AppleSessionStore, Effect.gen(functio
|
|
|
1797
1845
|
const homeDirectory = yield* (yield* CliRuntime).homeDirectory;
|
|
1798
1846
|
const sessionDir = path.join(homeDirectory, ".better-update");
|
|
1799
1847
|
const sessionFile = path.join(sessionDir, "apple-session.json");
|
|
1848
|
+
const usernameFile = path.join(sessionDir, "apple-username.json");
|
|
1800
1849
|
return {
|
|
1801
1850
|
loadSession: Effect.gen(function* () {
|
|
1802
1851
|
const content = yield* fs.readFileString(sessionFile).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
@@ -1819,9 +1868,129 @@ const AppleSessionStoreLive = Layer.effect(AppleSessionStore, Effect.gen(functio
|
|
|
1819
1868
|
yield* fs.writeFileString(sessionFile, `${JSON.stringify(session, null, 2)}\n`);
|
|
1820
1869
|
yield* fs.chmod(sessionFile, 384);
|
|
1821
1870
|
}).pipe(Effect.mapError((cause) => new AppleAuthError$1({ message: `Failed to save Apple session: ${formatCause(cause)}` }))),
|
|
1822
|
-
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
|
+
})
|
|
1823
1991
|
};
|
|
1824
1992
|
}));
|
|
1993
|
+
const AppleAuthLive = makeAppleAuthLive();
|
|
1825
1994
|
|
|
1826
1995
|
//#endregion
|
|
1827
1996
|
//#region src/services/presigned-upload.ts
|
|
@@ -1918,10 +2087,11 @@ const CliPlatformLayer = Layer.mergeAll(CliRuntimeLive, NodeContext.layer, Fetch
|
|
|
1918
2087
|
const CliStoreLayer = Layer.mergeAll(AuthStoreLive, ConfigStoreLive, AppleSessionStoreLive).pipe(Layer.provide(CliPlatformLayer));
|
|
1919
2088
|
const CliAdapterDependencies = Layer.mergeAll(CliPlatformLayer, CliStoreLayer);
|
|
1920
2089
|
const ApiClientLayer = ApiClientLive.pipe(Layer.provide(CliAdapterDependencies));
|
|
2090
|
+
const AppleAuthLayer = AppleAuthLive.pipe(Layer.provide(CliAdapterDependencies));
|
|
1921
2091
|
const PresignedUploadLayer = PresignedUploadClientLive.pipe(Layer.provide(CliPlatformLayer));
|
|
1922
2092
|
const UpdateAssetUploaderLayer = UpdateAssetUploaderLive.pipe(Layer.provide(Layer.mergeAll(ApiClientLayer, PresignedUploadLayer)));
|
|
1923
2093
|
const VersionCheckLayer = VersionCheckLive.pipe(Layer.provide(CliPlatformLayer));
|
|
1924
|
-
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));
|
|
1925
2095
|
/** Default CLI layer: human-readable, interactive. Override via flags at the entrypoint. */
|
|
1926
2096
|
const CliLive = makeCliLive({
|
|
1927
2097
|
json: false,
|
|
@@ -1929,63 +2099,264 @@ const CliLive = makeCliLive({
|
|
|
1929
2099
|
});
|
|
1930
2100
|
|
|
1931
2101
|
//#endregion
|
|
1932
|
-
//#region src/
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
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
|
+
};
|
|
1936
2128
|
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
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
|
+
};
|
|
1952
2185
|
};
|
|
1953
|
-
const
|
|
1954
|
-
|
|
1955
|
-
|
|
2186
|
+
const readBody = async (req) => {
|
|
2187
|
+
const chunks = [];
|
|
2188
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
2189
|
+
return Buffer.concat(chunks);
|
|
1956
2190
|
};
|
|
1957
|
-
const
|
|
1958
|
-
const
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
const resolvedCode = SYSTEM_TAG_CODE[tag] ?? code;
|
|
1966
|
-
handlers[tag] = (error) => exitWith(resolvedCode, systemFormat ? systemFormat(error) : error.message);
|
|
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);
|
|
1967
2199
|
}
|
|
1968
|
-
|
|
1969
|
-
|
|
2200
|
+
const init = {
|
|
2201
|
+
method,
|
|
2202
|
+
headers
|
|
1970
2203
|
};
|
|
2204
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
2205
|
+
const body = await readBody(req);
|
|
2206
|
+
init.body = new Uint8Array(body);
|
|
2207
|
+
}
|
|
2208
|
+
return new Request(url, init);
|
|
1971
2209
|
};
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
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));
|
|
1978
2217
|
};
|
|
1979
|
-
const
|
|
1980
|
-
|
|
1981
|
-
|
|
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
|
+
}
|
|
2242
|
+
};
|
|
1982
2243
|
};
|
|
1983
2244
|
|
|
1984
2245
|
//#endregion
|
|
1985
|
-
//#region src/
|
|
1986
|
-
const
|
|
1987
|
-
|
|
1988
|
-
|
|
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;
|
|
2332
|
+
const setActiveCliLayer = (layer) => {
|
|
2333
|
+
activeCliLayer = layer;
|
|
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
|
+
};
|
|
2350
|
+
const runEffect = async (effect, extras = {}) => {
|
|
2351
|
+
const provided = makeCommandErrorHandler(extras)(wrapWithAutoLogin(effect)).pipe(Effect.provide(activeCliLayer));
|
|
2352
|
+
return Effect.runPromise(provided.pipe(Effect.asVoid));
|
|
2353
|
+
};
|
|
2354
|
+
|
|
2355
|
+
//#endregion
|
|
2356
|
+
//#region src/lib/expo-config.ts
|
|
2357
|
+
const loadExpoConfigModule = () => __require("@expo/config");
|
|
2358
|
+
const clearDynamicConfigCache = (projectRoot) => {
|
|
2359
|
+
const { dynamicConfigPath } = loadExpoConfigModule().getConfigFilePaths(projectRoot);
|
|
1989
2360
|
if (dynamicConfigPath) delete __require.cache[dynamicConfigPath];
|
|
1990
2361
|
};
|
|
1991
2362
|
const applyEnvOverlay = (envVars) => {
|
|
@@ -2023,7 +2394,7 @@ const getConfigFilePaths = (projectRoot) => Effect.try({
|
|
|
2023
2394
|
});
|
|
2024
2395
|
const extractProjectId = (config) => Effect.gen(function* () {
|
|
2025
2396
|
const projectId = config.extra?.betterUpdate?.projectId;
|
|
2026
|
-
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." });
|
|
2027
2398
|
return projectId;
|
|
2028
2399
|
});
|
|
2029
2400
|
const extractSlug = (config) => Effect.gen(function* () {
|
|
@@ -2336,6 +2707,73 @@ const analyticsCommand = defineCommand({
|
|
|
2336
2707
|
}
|
|
2337
2708
|
});
|
|
2338
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
|
+
|
|
2339
2777
|
//#endregion
|
|
2340
2778
|
//#region src/lib/cli-schemas.ts
|
|
2341
2779
|
const RolloutPercentage = Schema.Number.pipe(Schema.int(), Schema.between(1, 100)).annotations({
|
|
@@ -3603,6 +4041,80 @@ const reserveAndUpload = (api, input) => Effect.gen(function* () {
|
|
|
3603
4041
|
};
|
|
3604
4042
|
});
|
|
3605
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
|
+
|
|
3606
4118
|
//#endregion
|
|
3607
4119
|
//#region src/lib/eas-config.ts
|
|
3608
4120
|
const MAX_EXTENDS_DEPTH = 10;
|
|
@@ -3635,6 +4147,21 @@ const asAndroidDistribution = (raw) => {
|
|
|
3635
4147
|
const value = asStringValue(raw);
|
|
3636
4148
|
return value === "play-store" || value === "direct" ? value : void 0;
|
|
3637
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
|
+
};
|
|
3638
4165
|
const asEasDistribution = (raw) => {
|
|
3639
4166
|
const value = asStringValue(raw);
|
|
3640
4167
|
return value === "internal" || value === "store" ? value : void 0;
|
|
@@ -3651,12 +4178,14 @@ const parseIosProfile = (raw) => {
|
|
|
3651
4178
|
const scheme = asStringValue(record["scheme"]);
|
|
3652
4179
|
const simulator = asBooleanValue(record["simulator"]);
|
|
3653
4180
|
const enterpriseProvisioning = asEnterpriseProvisioning(record["enterpriseProvisioning"]);
|
|
4181
|
+
const autoIncrement = asIosAutoIncrement(record["autoIncrement"]);
|
|
3654
4182
|
return {
|
|
3655
4183
|
...distribution === void 0 ? {} : { distribution },
|
|
3656
4184
|
...buildConfiguration === void 0 ? {} : { buildConfiguration },
|
|
3657
4185
|
...scheme === void 0 ? {} : { scheme },
|
|
3658
4186
|
...simulator === void 0 ? {} : { simulator },
|
|
3659
|
-
...enterpriseProvisioning === void 0 ? {} : { enterpriseProvisioning }
|
|
4187
|
+
...enterpriseProvisioning === void 0 ? {} : { enterpriseProvisioning },
|
|
4188
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
3660
4189
|
};
|
|
3661
4190
|
};
|
|
3662
4191
|
const parseAndroidProfile = (raw) => {
|
|
@@ -3667,12 +4196,14 @@ const parseAndroidProfile = (raw) => {
|
|
|
3667
4196
|
const gradleCommand = asStringValue(record["gradleCommand"]);
|
|
3668
4197
|
const format = asAndroidFormat(record["format"]);
|
|
3669
4198
|
const distribution = asAndroidDistribution(record["distribution"]);
|
|
4199
|
+
const autoIncrement = asAndroidAutoIncrement(record["autoIncrement"]);
|
|
3670
4200
|
return {
|
|
3671
4201
|
...buildType === void 0 ? {} : { buildType },
|
|
3672
4202
|
...flavor === void 0 ? {} : { flavor },
|
|
3673
4203
|
...gradleCommand === void 0 ? {} : { gradleCommand },
|
|
3674
4204
|
...format === void 0 ? {} : { format },
|
|
3675
|
-
...distribution === void 0 ? {} : { distribution }
|
|
4205
|
+
...distribution === void 0 ? {} : { distribution },
|
|
4206
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
3676
4207
|
};
|
|
3677
4208
|
};
|
|
3678
4209
|
const parseBuildProfile = (raw) => {
|
|
@@ -3687,6 +4218,7 @@ const parseBuildProfile = (raw) => {
|
|
|
3687
4218
|
const ios = parseIosProfile(record["ios"]);
|
|
3688
4219
|
const android = parseAndroidProfile(record["android"]);
|
|
3689
4220
|
const credentialsSource = asCredentialsSource(record["credentialsSource"]);
|
|
4221
|
+
const autoIncrement = asAutoIncrement(record["autoIncrement"]);
|
|
3690
4222
|
return {
|
|
3691
4223
|
...extendsName === void 0 ? {} : { extends: extendsName },
|
|
3692
4224
|
...developmentClient === void 0 ? {} : { developmentClient },
|
|
@@ -3696,7 +4228,8 @@ const parseBuildProfile = (raw) => {
|
|
|
3696
4228
|
...env === void 0 ? {} : { env },
|
|
3697
4229
|
...ios === void 0 ? {} : { ios },
|
|
3698
4230
|
...android === void 0 ? {} : { android },
|
|
3699
|
-
...credentialsSource === void 0 ? {} : { credentialsSource }
|
|
4231
|
+
...credentialsSource === void 0 ? {} : { credentialsSource },
|
|
4232
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
3700
4233
|
};
|
|
3701
4234
|
};
|
|
3702
4235
|
const parseEasConfig = (text) => Effect.gen(function* () {
|
|
@@ -3764,6 +4297,7 @@ const mergeProfile = (base, overlay) => {
|
|
|
3764
4297
|
const channel = overlay.channel ?? base.channel;
|
|
3765
4298
|
const environment = overlay.environment ?? base.environment;
|
|
3766
4299
|
const credentialsSource = overlay.credentialsSource ?? base.credentialsSource;
|
|
4300
|
+
const autoIncrement = overlay.autoIncrement ?? base.autoIncrement;
|
|
3767
4301
|
return {
|
|
3768
4302
|
...overlay.extends === void 0 ? {} : { extends: overlay.extends },
|
|
3769
4303
|
...developmentClient === void 0 ? {} : { developmentClient },
|
|
@@ -3773,7 +4307,8 @@ const mergeProfile = (base, overlay) => {
|
|
|
3773
4307
|
...env === void 0 ? {} : { env },
|
|
3774
4308
|
...ios === void 0 ? {} : { ios },
|
|
3775
4309
|
...android === void 0 ? {} : { android },
|
|
3776
|
-
...credentialsSource === void 0 ? {} : { credentialsSource }
|
|
4310
|
+
...credentialsSource === void 0 ? {} : { credentialsSource },
|
|
4311
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
3777
4312
|
};
|
|
3778
4313
|
};
|
|
3779
4314
|
const collectExtendsChain = (profiles, profileName) => Effect.gen(function* () {
|
|
@@ -3827,16 +4362,36 @@ const deriveAndroidDistribution = (eas, format) => {
|
|
|
3827
4362
|
};
|
|
3828
4363
|
const hasIosIntent = (eas) => eas.ios !== void 0 || eas.distribution !== void 0 || eas.developmentClient === true;
|
|
3829
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
|
+
};
|
|
3830
4383
|
const toIosProfile = (eas) => {
|
|
3831
4384
|
if (!hasIosIntent(eas)) return;
|
|
3832
4385
|
const distribution = deriveIosDistribution(eas);
|
|
3833
4386
|
if (!distribution) return;
|
|
3834
4387
|
const ios = eas.ios ?? {};
|
|
4388
|
+
const autoIncrement = resolveIosAutoIncrement(eas);
|
|
3835
4389
|
return {
|
|
3836
4390
|
distribution,
|
|
3837
4391
|
...ios.buildConfiguration === void 0 ? {} : { buildConfiguration: ios.buildConfiguration },
|
|
3838
4392
|
...ios.scheme === void 0 ? {} : { scheme: ios.scheme },
|
|
3839
|
-
...ios.simulator === void 0 ? {} : { simulator: ios.simulator }
|
|
4393
|
+
...ios.simulator === void 0 ? {} : { simulator: ios.simulator },
|
|
4394
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
3840
4395
|
};
|
|
3841
4396
|
};
|
|
3842
4397
|
const toAndroidProfile = (eas) => {
|
|
@@ -3844,12 +4399,15 @@ const toAndroidProfile = (eas) => {
|
|
|
3844
4399
|
const format = deriveAndroidFormat(eas);
|
|
3845
4400
|
if (!format) return;
|
|
3846
4401
|
const android = eas.android ?? {};
|
|
4402
|
+
const distribution = deriveAndroidDistribution(eas, format);
|
|
4403
|
+
const autoIncrement = resolveAndroidAutoIncrement(eas);
|
|
3847
4404
|
return {
|
|
3848
4405
|
format,
|
|
3849
|
-
distribution
|
|
4406
|
+
distribution,
|
|
3850
4407
|
...android.buildType === void 0 ? {} : { buildType: android.buildType },
|
|
3851
4408
|
...android.flavor === void 0 ? {} : { flavor: android.flavor },
|
|
3852
|
-
...android.gradleCommand === void 0 ? {} : { gradleCommand: android.gradleCommand }
|
|
4409
|
+
...android.gradleCommand === void 0 ? {} : { gradleCommand: android.gradleCommand },
|
|
4410
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
3853
4411
|
};
|
|
3854
4412
|
};
|
|
3855
4413
|
const fromEasProfile = (eas, profileName) => {
|
|
@@ -4003,53 +4561,6 @@ const extractGradleConfig = (parsed) => {
|
|
|
4003
4561
|
};
|
|
4004
4562
|
const unquote = (input) => input.startsWith("\"") && input.endsWith("\"") ? input.slice(1, -1) : input;
|
|
4005
4563
|
|
|
4006
|
-
//#endregion
|
|
4007
|
-
//#region src/lib/prompts.ts
|
|
4008
|
-
const ensureInteractive = (promptName) => Effect.gen(function* () {
|
|
4009
|
-
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.` });
|
|
4010
|
-
});
|
|
4011
|
-
const handleCancel = (value) => {
|
|
4012
|
-
if (isCancel(value)) {
|
|
4013
|
-
cancel("Operation cancelled.");
|
|
4014
|
-
process.exit(130);
|
|
4015
|
-
}
|
|
4016
|
-
return value;
|
|
4017
|
-
};
|
|
4018
|
-
const promptPassword = (message) => Effect.gen(function* () {
|
|
4019
|
-
yield* ensureInteractive(message);
|
|
4020
|
-
return handleCancel(yield* Effect.promise(async () => password({ message })));
|
|
4021
|
-
});
|
|
4022
|
-
const promptSelect = (message, options) => Effect.gen(function* () {
|
|
4023
|
-
yield* ensureInteractive(message);
|
|
4024
|
-
return handleCancel(yield* Effect.promise(async () => select({
|
|
4025
|
-
message,
|
|
4026
|
-
options: [...options]
|
|
4027
|
-
})));
|
|
4028
|
-
});
|
|
4029
|
-
const promptMultiSelect = (message, options, config) => Effect.gen(function* () {
|
|
4030
|
-
yield* ensureInteractive(message);
|
|
4031
|
-
return handleCancel(yield* Effect.promise(async () => multiselect({
|
|
4032
|
-
message,
|
|
4033
|
-
options: [...options],
|
|
4034
|
-
required: config?.required ?? false
|
|
4035
|
-
})));
|
|
4036
|
-
});
|
|
4037
|
-
const promptText = (message, options) => Effect.gen(function* () {
|
|
4038
|
-
yield* ensureInteractive(message);
|
|
4039
|
-
return handleCancel(yield* Effect.promise(async () => text({
|
|
4040
|
-
message,
|
|
4041
|
-
...options?.placeholder === void 0 ? {} : { placeholder: options.placeholder },
|
|
4042
|
-
...options?.defaultValue === void 0 ? {} : { defaultValue: options.defaultValue }
|
|
4043
|
-
})));
|
|
4044
|
-
});
|
|
4045
|
-
const promptConfirm = (message, options) => Effect.gen(function* () {
|
|
4046
|
-
yield* ensureInteractive(message);
|
|
4047
|
-
return handleCancel(yield* Effect.promise(async () => confirm({
|
|
4048
|
-
message,
|
|
4049
|
-
...options?.initialValue === void 0 ? {} : { initialValue: options.initialValue }
|
|
4050
|
-
})));
|
|
4051
|
-
});
|
|
4052
|
-
|
|
4053
4564
|
//#endregion
|
|
4054
4565
|
//#region src/lib/platform-detect.ts
|
|
4055
4566
|
const PLATFORMS = ["ios", "android"];
|
|
@@ -4456,6 +4967,38 @@ const parseCert = (certDerBytes) => {
|
|
|
4456
4967
|
return forge.pki.certificateFromAsn1(asn1);
|
|
4457
4968
|
};
|
|
4458
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
|
+
});
|
|
4459
5002
|
const buildDistributionCertP12 = (params) => Effect.gen(function* () {
|
|
4460
5003
|
const result = yield* Effect.try({
|
|
4461
5004
|
try: () => {
|
|
@@ -4473,20 +5016,11 @@ const buildDistributionCertP12 = (params) => Effect.gen(function* () {
|
|
|
4473
5016
|
},
|
|
4474
5017
|
catch: (error) => new CertParseError({ message: `Failed to assemble .p12: ${error instanceof Error ? error.message : String(error)}` })
|
|
4475
5018
|
});
|
|
4476
|
-
const
|
|
4477
|
-
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);
|
|
4478
5020
|
return {
|
|
4479
5021
|
p12Base64: result.p12Base64,
|
|
4480
5022
|
password: result.password,
|
|
4481
|
-
metadata
|
|
4482
|
-
serialNumber: result.cert.serialNumber.toUpperCase(),
|
|
4483
|
-
validFrom: result.cert.validity.notBefore.toISOString(),
|
|
4484
|
-
validUntil: result.cert.validity.notAfter.toISOString(),
|
|
4485
|
-
appleTeamId,
|
|
4486
|
-
appleTeamName: stringField(result.cert, "O"),
|
|
4487
|
-
developerIdIdentifier: stringField(result.cert, "UID"),
|
|
4488
|
-
commonName: stringField(result.cert, "CN")
|
|
4489
|
-
}
|
|
5023
|
+
metadata
|
|
4490
5024
|
};
|
|
4491
5025
|
});
|
|
4492
5026
|
|
|
@@ -4519,7 +5053,7 @@ const generateCertificateSigningRequest = async () => {
|
|
|
4519
5053
|
|
|
4520
5054
|
//#endregion
|
|
4521
5055
|
//#region src/lib/credentials-generator.ts
|
|
4522
|
-
const DISTRIBUTION_TO_PROFILE_TYPE = {
|
|
5056
|
+
const DISTRIBUTION_TO_PROFILE_TYPE$1 = {
|
|
4523
5057
|
APP_STORE: "IOS_APP_STORE",
|
|
4524
5058
|
AD_HOC: "IOS_APP_ADHOC",
|
|
4525
5059
|
DEVELOPMENT: "IOS_APP_DEVELOPMENT",
|
|
@@ -4707,7 +5241,7 @@ const generateAndUploadProvisioningProfile = (api, input) => Effect.gen(function
|
|
|
4707
5241
|
}));
|
|
4708
5242
|
const profileBytes = fromBase64((yield* createProvisioningProfile(ascCreds, {
|
|
4709
5243
|
profileName: `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`,
|
|
4710
|
-
profileType: DISTRIBUTION_TO_PROFILE_TYPE[input.distributionType],
|
|
5244
|
+
profileType: DISTRIBUTION_TO_PROFILE_TYPE$1[input.distributionType],
|
|
4711
5245
|
bundleIdAscId,
|
|
4712
5246
|
certificateAscIds: [certAscId],
|
|
4713
5247
|
deviceAscIds
|
|
@@ -4730,93 +5264,181 @@ const generateAndUploadProvisioningProfile = (api, input) => Effect.gen(function
|
|
|
4730
5264
|
});
|
|
4731
5265
|
|
|
4732
5266
|
//#endregion
|
|
4733
|
-
//#region src/
|
|
4734
|
-
const
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
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
|
+
})
|
|
4750
5287
|
});
|
|
4751
|
-
const
|
|
4752
|
-
const
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
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
|
|
4760
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
|
+
};
|
|
4761
5314
|
});
|
|
4762
|
-
const
|
|
4763
|
-
const existing =
|
|
4764
|
-
if (existing !==
|
|
4765
|
-
return (yield*
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
});
|
|
4770
|
-
|
|
4771
|
-
const
|
|
4772
|
-
yield*
|
|
4773
|
-
|
|
4774
|
-
const
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
label: "Generate new keystore"
|
|
4779
|
-
},
|
|
4780
|
-
{
|
|
4781
|
-
value: "existing",
|
|
4782
|
-
label: "Pick an existing keystore"
|
|
4783
|
-
},
|
|
4784
|
-
{
|
|
4785
|
-
value: "abort",
|
|
4786
|
-
label: "Abort — I'll configure it in the dashboard"
|
|
4787
|
-
}
|
|
4788
|
-
]);
|
|
4789
|
-
if (choice === "abort") return yield* Effect.fail(new MissingCredentialsError({
|
|
4790
|
-
message: `Build aborted — no keystore bound to ${input.applicationIdentifier}.`,
|
|
4791
|
-
hint: "Run `better-update credentials generate keystore` or upload via the dashboard."
|
|
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`
|
|
4792
5331
|
}));
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
5332
|
+
return match.id;
|
|
5333
|
+
});
|
|
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"
|
|
5353
|
+
}));
|
|
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 },
|
|
4796
5413
|
payload: {
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
5414
|
+
bundleIdentifier: input.bundleIdentifier,
|
|
5415
|
+
distributionType,
|
|
5416
|
+
appleTeamId: cert.appleTeamId,
|
|
5417
|
+
appleDistributionCertificateId: cert.id,
|
|
5418
|
+
appleProvisioningProfileId: profile.id
|
|
4800
5419
|
}
|
|
4801
5420
|
});
|
|
4802
|
-
yield* Console.log("
|
|
5421
|
+
yield* Console.log("iOS bundle configuration saved.");
|
|
4803
5422
|
});
|
|
4804
|
-
const
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
return
|
|
4819
|
-
})
|
|
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
|
|
4820
5442
|
const interactiveCertLimitRecover = (api, ascApiKeyId) => Effect.gen(function* () {
|
|
4821
5443
|
yield* Console.log("");
|
|
4822
5444
|
yield* Console.log("Apple reports the certificate limit was hit (max 3 distribution certs per team).");
|
|
@@ -4919,9 +5541,7 @@ const resolveIosProfileId = (api, input, ctx) => Effect.gen(function* () {
|
|
|
4919
5541
|
label: profile.profileName ?? profile.developerPortalIdentifier ?? profile.id
|
|
4920
5542
|
})));
|
|
4921
5543
|
});
|
|
4922
|
-
const
|
|
4923
|
-
yield* Console.log("");
|
|
4924
|
-
yield* Console.log(`No iOS bundle configuration for ${input.bundleIdentifier} (${input.distribution}).`);
|
|
5544
|
+
const setupIosViaAscKey = (api, input) => Effect.gen(function* () {
|
|
4925
5545
|
const { certId, cert } = yield* pickIosCertificate(api);
|
|
4926
5546
|
const ascKeyId = yield* pickIosAscKey(api, cert.appleTeamId);
|
|
4927
5547
|
const distributionType = IOS_DISTRIBUTION_TO_TYPE[input.distribution];
|
|
@@ -4944,6 +5564,101 @@ const setupIosInteractive = (api, input) => Effect.gen(function* () {
|
|
|
4944
5564
|
});
|
|
4945
5565
|
yield* Console.log("iOS bundle configuration saved.");
|
|
4946
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
|
+
});
|
|
4947
5662
|
const resolveIosBuildCredentials = (api, input) => api.buildCredentials.resolve({
|
|
4948
5663
|
path: { projectId: input.projectId },
|
|
4949
5664
|
payload: {
|
|
@@ -4963,16 +5678,23 @@ const findBoundIosConfig = (api, input) => Effect.gen(function* () {
|
|
|
4963
5678
|
});
|
|
4964
5679
|
const regenerateProvisioningProfile = (api, input) => Effect.gen(function* () {
|
|
4965
5680
|
const config = yield* findBoundIosConfig(api, input);
|
|
4966
|
-
if (config.
|
|
4967
|
-
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",
|
|
4968
5683
|
hint: "Re-bind credentials via `better-update credentials generate` or the dashboard"
|
|
4969
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
|
+
});
|
|
4970
5692
|
yield* Console.log("Regenerating provisioning profile via App Store Connect API...");
|
|
4971
5693
|
const created = yield* generateAndUploadProvisioningProfile(api, {
|
|
4972
5694
|
ascApiKeyId: config.ascApiKeyId,
|
|
4973
5695
|
distributionCertificateId: config.appleDistributionCertificateId,
|
|
4974
5696
|
bundleIdentifier: input.bundleIdentifier,
|
|
4975
|
-
distributionType
|
|
5697
|
+
distributionType
|
|
4976
5698
|
});
|
|
4977
5699
|
yield* api.iosBundleConfigurations.update({
|
|
4978
5700
|
path: { id: config.id },
|
|
@@ -5076,6 +5798,17 @@ const runAndroidPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
5076
5798
|
};
|
|
5077
5799
|
});
|
|
5078
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
|
+
});
|
|
5079
5812
|
const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
5080
5813
|
const api = yield* apiClient;
|
|
5081
5814
|
const projectRoot = yield* (yield* CliRuntime).cwd;
|
|
@@ -5087,11 +5820,18 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
5087
5820
|
const baseConfig = yield* readExpoConfig(projectRoot);
|
|
5088
5821
|
const projectId = yield* extractProjectId(baseConfig);
|
|
5089
5822
|
const platform = yield* detectPlatform(options.platform, baseConfig);
|
|
5090
|
-
const profile = yield* readBuildProfile(projectRoot, options.profileName);
|
|
5823
|
+
const profile = yield* readBuildProfile(projectRoot, yield* resolveProfileName(projectRoot, options.profileName));
|
|
5091
5824
|
const envVars = yield* pullEnvVars(api, {
|
|
5092
5825
|
projectId,
|
|
5093
5826
|
environment: profile.environment
|
|
5094
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
|
+
});
|
|
5095
5835
|
const appMeta = yield* readAppMeta(yield* readExpoConfig(projectRoot, envVars), platform);
|
|
5096
5836
|
const runtimeVersion = yield* resolveRuntimeVersion({
|
|
5097
5837
|
raw: appMeta.rawRuntimeVersion,
|
|
@@ -10212,25 +10952,59 @@ const exportCommand = defineCommand({
|
|
|
10212
10952
|
|
|
10213
10953
|
//#endregion
|
|
10214
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
|
+
};
|
|
10215
10974
|
const getCommand$1 = defineCommand({
|
|
10216
10975
|
meta: {
|
|
10217
10976
|
name: "get",
|
|
10218
|
-
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
|
+
}
|
|
10219
10997
|
},
|
|
10220
|
-
args: { id: {
|
|
10221
|
-
type: "positional",
|
|
10222
|
-
required: true,
|
|
10223
|
-
description: "Env var ID"
|
|
10224
|
-
} },
|
|
10225
10998
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
10226
|
-
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);
|
|
10227
11001
|
yield* printKeyValue([
|
|
10228
11002
|
["ID", envVar.id],
|
|
10229
11003
|
["Key", envVar.key],
|
|
10230
11004
|
["Scope", envVar.scope],
|
|
10231
11005
|
["Environments", formatEnvironments(envVar.environments)],
|
|
10232
11006
|
["Visibility", envVar.visibility],
|
|
10233
|
-
["Value", envVar
|
|
11007
|
+
["Value", renderValue$1(envVar, args["include-sensitive"] ?? false)],
|
|
10234
11008
|
["Created", envVar.createdAt],
|
|
10235
11009
|
["Updated", envVar.updatedAt]
|
|
10236
11010
|
]);
|
|
@@ -10279,6 +11053,11 @@ const importCommand = defineCommand({
|
|
|
10279
11053
|
|
|
10280
11054
|
//#endregion
|
|
10281
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
|
+
};
|
|
10282
11061
|
const listCommand$2 = defineCommand({
|
|
10283
11062
|
meta: {
|
|
10284
11063
|
name: "list",
|
|
@@ -10301,6 +11080,10 @@ const listCommand$2 = defineCommand({
|
|
|
10301
11080
|
search: {
|
|
10302
11081
|
type: "string",
|
|
10303
11082
|
description: "Filter by key substring (case-insensitive)"
|
|
11083
|
+
},
|
|
11084
|
+
"include-sensitive": {
|
|
11085
|
+
type: "boolean",
|
|
11086
|
+
description: "Reveal masked sensitive values (default: masked)"
|
|
10304
11087
|
}
|
|
10305
11088
|
},
|
|
10306
11089
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
@@ -10312,34 +11095,67 @@ const listCommand$2 = defineCommand({
|
|
|
10312
11095
|
...args.environments ? { environments: args.environments } : {},
|
|
10313
11096
|
...args.search ? { search: args.search } : {}
|
|
10314
11097
|
};
|
|
11098
|
+
const result = yield* api["env-vars"].list({ urlParams });
|
|
11099
|
+
const includeSensitive = args["include-sensitive"] ?? false;
|
|
10315
11100
|
yield* printList([
|
|
10316
11101
|
"Key",
|
|
10317
11102
|
"Environments",
|
|
10318
11103
|
"Scope",
|
|
10319
11104
|
"Visibility",
|
|
10320
11105
|
"Value"
|
|
10321
|
-
],
|
|
11106
|
+
], result.items.map((item) => [
|
|
10322
11107
|
item.key,
|
|
10323
11108
|
formatEnvironments(item.environments),
|
|
10324
11109
|
item.overridesGlobal ? `${item.scope} (overrides global)` : item.scope,
|
|
10325
11110
|
item.visibility,
|
|
10326
|
-
item
|
|
11111
|
+
renderValue(item, includeSensitive)
|
|
10327
11112
|
]), "No environment variables found.");
|
|
10328
11113
|
}), envErrorExtras)
|
|
10329
11114
|
});
|
|
10330
11115
|
|
|
10331
11116
|
//#endregion
|
|
10332
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
|
+
});
|
|
10333
11135
|
const pullCommand = defineCommand({
|
|
10334
11136
|
meta: {
|
|
10335
11137
|
name: "pull",
|
|
10336
|
-
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
|
+
}
|
|
10337
11158
|
},
|
|
10338
|
-
args: { environment: {
|
|
10339
|
-
type: "string",
|
|
10340
|
-
default: "production",
|
|
10341
|
-
description: "Target environment (development, preview, production)"
|
|
10342
|
-
} },
|
|
10343
11159
|
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
10344
11160
|
const environment = yield* parseSingleEnvironmentArg(args.environment);
|
|
10345
11161
|
const projectId = yield* readProjectId;
|
|
@@ -10347,10 +11163,16 @@ const pullCommand = defineCommand({
|
|
|
10347
11163
|
projectId,
|
|
10348
11164
|
environment
|
|
10349
11165
|
} });
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
|
|
11166
|
+
if (args.stdout) {
|
|
11167
|
+
yield* printStdout(result.items);
|
|
11168
|
+
return;
|
|
10353
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
|
+
});
|
|
10354
11176
|
}), envErrorExtras)
|
|
10355
11177
|
});
|
|
10356
11178
|
|
|
@@ -10663,18 +11485,34 @@ const checkExistingLink = (api, config, localSlug) => Effect.gen(function* () {
|
|
|
10663
11485
|
}
|
|
10664
11486
|
return (yield* promptConfirm("Overwrite local projectId with a fresh link by slug?", { initialValue: false })) ? "mismatch-overwrite" : "mismatch-abort";
|
|
10665
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
|
+
});
|
|
10666
11494
|
const initCommand = defineCommand({
|
|
10667
11495
|
meta: {
|
|
10668
11496
|
name: "init",
|
|
10669
11497
|
description: "Link the local Expo project to a better-update project"
|
|
10670
11498
|
},
|
|
10671
|
-
|
|
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* () {
|
|
10672
11504
|
const projectRoot = yield* (yield* CliRuntime).cwd;
|
|
10673
11505
|
const config = yield* readExpoConfig(projectRoot);
|
|
10674
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
|
+
}
|
|
10675
11514
|
const slug = yield* extractSlug(config);
|
|
10676
11515
|
yield* Console.log(`Linking project: ${name} (${slug})`);
|
|
10677
|
-
const api = yield* apiClient;
|
|
10678
11516
|
const linkState = yield* checkExistingLink(api, config, slug);
|
|
10679
11517
|
if (linkState === "matched" || linkState === "mismatch-abort") return;
|
|
10680
11518
|
const { items } = yield* api.projects.list({ urlParams: {
|
|
@@ -10682,7 +11520,7 @@ const initCommand = defineCommand({
|
|
|
10682
11520
|
limit: 100
|
|
10683
11521
|
} });
|
|
10684
11522
|
const existing = items.find((project) => project.slug === slug);
|
|
10685
|
-
|
|
11523
|
+
yield* writeAndAnnounce(projectRoot, yield* Effect.gen(function* () {
|
|
10686
11524
|
if (existing) {
|
|
10687
11525
|
yield* Console.log(`Found existing project: ${existing.name} (${existing.id})`);
|
|
10688
11526
|
return existing.id;
|
|
@@ -10695,198 +11533,9 @@ const initCommand = defineCommand({
|
|
|
10695
11533
|
yield* Console.log(`Created project: ${created.name} (${created.id})`);
|
|
10696
11534
|
return created.id;
|
|
10697
11535
|
}));
|
|
10698
|
-
const target = writeResult.configPath ? path.relative(projectRoot, writeResult.configPath) : "your Expo config";
|
|
10699
|
-
yield* Console.log(`Project linked successfully. ID saved to ${target}.`);
|
|
10700
|
-
if (writeResult.type === "warn" && writeResult.message) yield* Console.log(`Note: ${writeResult.message}`);
|
|
10701
11536
|
}))
|
|
10702
11537
|
});
|
|
10703
11538
|
|
|
10704
|
-
//#endregion
|
|
10705
|
-
//#region src/lib/browser-login.ts
|
|
10706
|
-
var BrowserLoginTimeoutError = class extends Data.TaggedError("BrowserLoginTimeoutError") {};
|
|
10707
|
-
var BrowserLoginSessionClosedError = class extends Data.TaggedError("BrowserLoginSessionClosedError") {};
|
|
10708
|
-
const CALLBACK_PAGE = `<!doctype html>
|
|
10709
|
-
<html lang="en">
|
|
10710
|
-
<head>
|
|
10711
|
-
<meta charset="utf-8" />
|
|
10712
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
10713
|
-
<title>better-update CLI Login</title>
|
|
10714
|
-
<style>
|
|
10715
|
-
:root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, sans-serif; }
|
|
10716
|
-
body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px; }
|
|
10717
|
-
main { max-width: 32rem; line-height: 1.5; }
|
|
10718
|
-
code { font-family: ui-monospace, SFMono-Regular, monospace; }
|
|
10719
|
-
</style>
|
|
10720
|
-
</head>
|
|
10721
|
-
<body>
|
|
10722
|
-
<main>
|
|
10723
|
-
<h1>Completing CLI login...</h1>
|
|
10724
|
-
<p id="message">Finalizing the local session. You can keep this tab open.</p>
|
|
10725
|
-
</main>
|
|
10726
|
-
<script>
|
|
10727
|
-
const message = document.getElementById("message");
|
|
10728
|
-
const render = (text) => {
|
|
10729
|
-
if (message) message.textContent = text;
|
|
10730
|
-
};
|
|
10731
|
-
|
|
10732
|
-
const params = new URLSearchParams(window.location.hash.slice(1));
|
|
10733
|
-
const token = params.get("token");
|
|
10734
|
-
|
|
10735
|
-
if (!token) {
|
|
10736
|
-
render("Missing token. Return to the CLI and run login again.");
|
|
10737
|
-
} else {
|
|
10738
|
-
fetch("/callback/token", {
|
|
10739
|
-
method: "POST",
|
|
10740
|
-
headers: { "content-type": "application/json" },
|
|
10741
|
-
body: JSON.stringify({ token }),
|
|
10742
|
-
})
|
|
10743
|
-
.then(async (response) => {
|
|
10744
|
-
if (!response.ok) {
|
|
10745
|
-
const body = await response.text();
|
|
10746
|
-
throw new Error(body || "Callback failed");
|
|
10747
|
-
}
|
|
10748
|
-
window.history.replaceState({}, document.title, window.location.pathname);
|
|
10749
|
-
render("CLI login complete. You can close this tab.");
|
|
10750
|
-
setTimeout(() => window.close(), 300);
|
|
10751
|
-
})
|
|
10752
|
-
.catch((error) => {
|
|
10753
|
-
render(error instanceof Error ? error.message : "Callback failed.");
|
|
10754
|
-
});
|
|
10755
|
-
}
|
|
10756
|
-
<\/script>
|
|
10757
|
-
</body>
|
|
10758
|
-
</html>`;
|
|
10759
|
-
const createBrowserLoginSession = (options = {}) => {
|
|
10760
|
-
const tokenDeferred = Effect.runSync(Deferred.make());
|
|
10761
|
-
const waitForToken = Deferred.await(tokenDeferred).pipe(Effect.timeoutFail({
|
|
10762
|
-
duration: options.timeoutMs === void 0 ? Duration.minutes(5) : Duration.millis(options.timeoutMs),
|
|
10763
|
-
onTimeout: () => new BrowserLoginTimeoutError({ message: "Timed out waiting for browser login to complete." })
|
|
10764
|
-
}));
|
|
10765
|
-
const dispose = () => {
|
|
10766
|
-
Effect.runSync(Deferred.fail(tokenDeferred, new BrowserLoginSessionClosedError({ message: "Browser login session closed." })));
|
|
10767
|
-
};
|
|
10768
|
-
return {
|
|
10769
|
-
callbackPath: "/callback",
|
|
10770
|
-
waitForToken,
|
|
10771
|
-
handleRequest: async (request) => {
|
|
10772
|
-
const url = new URL(request.url);
|
|
10773
|
-
if (request.method === "GET" && url.pathname === "/callback") return new Response(CALLBACK_PAGE, { headers: { "content-type": "text/html; charset=utf-8" } });
|
|
10774
|
-
if (request.method === "POST" && url.pathname === "/callback/token") try {
|
|
10775
|
-
const body = await request.json();
|
|
10776
|
-
if (!isRecord(body)) return new Response("Invalid callback payload", { status: 400 });
|
|
10777
|
-
const token = typeof body["token"] === "string" ? body["token"].trim() : "";
|
|
10778
|
-
if (token.length === 0) return new Response("Missing token", { status: 400 });
|
|
10779
|
-
Effect.runSync(Deferred.succeed(tokenDeferred, token));
|
|
10780
|
-
return Response.json({ ok: true });
|
|
10781
|
-
} catch {
|
|
10782
|
-
return new Response("Invalid callback payload", { status: 400 });
|
|
10783
|
-
}
|
|
10784
|
-
return new Response("Not found", { status: 404 });
|
|
10785
|
-
},
|
|
10786
|
-
dispose
|
|
10787
|
-
};
|
|
10788
|
-
};
|
|
10789
|
-
const readBody = async (req) => {
|
|
10790
|
-
const chunks = [];
|
|
10791
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
10792
|
-
return Buffer.concat(chunks);
|
|
10793
|
-
};
|
|
10794
|
-
const toFetchRequest = async (req, origin) => {
|
|
10795
|
-
const url = new URL(req.url ?? "/", origin);
|
|
10796
|
-
const method = req.method ?? "GET";
|
|
10797
|
-
const headers = new Headers();
|
|
10798
|
-
for (const [key, value] of Object.entries(req.headers)) {
|
|
10799
|
-
if (value === void 0) continue;
|
|
10800
|
-
if (Array.isArray(value)) for (const entry of value) headers.append(key, entry);
|
|
10801
|
-
else headers.append(key, value);
|
|
10802
|
-
}
|
|
10803
|
-
const init = {
|
|
10804
|
-
method,
|
|
10805
|
-
headers
|
|
10806
|
-
};
|
|
10807
|
-
if (method !== "GET" && method !== "HEAD") {
|
|
10808
|
-
const body = await readBody(req);
|
|
10809
|
-
init.body = new Uint8Array(body);
|
|
10810
|
-
}
|
|
10811
|
-
return new Request(url, init);
|
|
10812
|
-
};
|
|
10813
|
-
const writeFetchResponse = async (res, response) => {
|
|
10814
|
-
res.statusCode = response.status;
|
|
10815
|
-
response.headers.forEach((value, key) => {
|
|
10816
|
-
res.setHeader(key, value);
|
|
10817
|
-
});
|
|
10818
|
-
const body = await response.arrayBuffer();
|
|
10819
|
-
res.end(Buffer.from(body));
|
|
10820
|
-
};
|
|
10821
|
-
const handleIncoming = async (req, res, session) => {
|
|
10822
|
-
try {
|
|
10823
|
-
const request = await toFetchRequest(req, "http://127.0.0.1");
|
|
10824
|
-
await writeFetchResponse(res, await session.handleRequest(request));
|
|
10825
|
-
} catch {
|
|
10826
|
-
res.statusCode = 500;
|
|
10827
|
-
res.end("Local callback failed");
|
|
10828
|
-
}
|
|
10829
|
-
};
|
|
10830
|
-
const createBrowserLoginServer = async (options = {}) => {
|
|
10831
|
-
const session = createBrowserLoginSession(options);
|
|
10832
|
-
const server = createServer((req, res) => {
|
|
10833
|
-
handleIncoming(req, res, session).catch(() => void 0);
|
|
10834
|
-
});
|
|
10835
|
-
server.listen(0, "127.0.0.1");
|
|
10836
|
-
await once(server, "listening");
|
|
10837
|
-
const address = server.address();
|
|
10838
|
-
return {
|
|
10839
|
-
callbackUrl: `http://127.0.0.1:${address !== null && typeof address === "object" ? address.port : 0}${session.callbackPath}`,
|
|
10840
|
-
waitForToken: session.waitForToken,
|
|
10841
|
-
stop: () => {
|
|
10842
|
-
session.dispose();
|
|
10843
|
-
server.close();
|
|
10844
|
-
}
|
|
10845
|
-
};
|
|
10846
|
-
};
|
|
10847
|
-
|
|
10848
|
-
//#endregion
|
|
10849
|
-
//#region src/application/login.ts
|
|
10850
|
-
const buildOpenBrowserCommand = (platform, url) => {
|
|
10851
|
-
if (platform === "darwin") return Command.make("open", url);
|
|
10852
|
-
if (platform === "win32") return Command.make("cmd", "/c", "start", "", url);
|
|
10853
|
-
return Command.make("xdg-open", url);
|
|
10854
|
-
};
|
|
10855
|
-
const openBrowser = (url) => Effect.gen(function* () {
|
|
10856
|
-
const command = buildOpenBrowserCommand((yield* CliRuntime).platform, url);
|
|
10857
|
-
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}`);
|
|
10858
|
-
});
|
|
10859
|
-
const browserLogin = Effect.scoped(Effect.gen(function* () {
|
|
10860
|
-
const configStore = yield* ConfigStore;
|
|
10861
|
-
const authStore = yield* AuthStore;
|
|
10862
|
-
const webUrl = yield* configStore.getWebUrl;
|
|
10863
|
-
const loginServer = yield* Effect.acquireRelease(Effect.promise(async () => createBrowserLoginServer()), (server) => Effect.sync(server.stop));
|
|
10864
|
-
const loginUrl = `${webUrl}/auth/cli-login?callbackUrl=${encodeURIComponent(loginServer.callbackUrl)}`;
|
|
10865
|
-
yield* Console.log("Opening browser for better-update login...");
|
|
10866
|
-
yield* Console.log("");
|
|
10867
|
-
yield* openBrowser(loginUrl);
|
|
10868
|
-
const token = yield* loginServer.waitForToken;
|
|
10869
|
-
yield* authStore.saveToken(token);
|
|
10870
|
-
yield* Console.log("");
|
|
10871
|
-
yield* Console.log("Logged in successfully. Token saved to ~/.better-update/auth.json");
|
|
10872
|
-
}));
|
|
10873
|
-
const manualLogin = Effect.gen(function* () {
|
|
10874
|
-
yield* Console.log("Log in to better-update with an existing API key");
|
|
10875
|
-
yield* Console.log("Get your API key from the dashboard > API Keys page");
|
|
10876
|
-
yield* Console.log("");
|
|
10877
|
-
const token = yield* promptPassword("Paste your API key (from dashboard > API Keys):");
|
|
10878
|
-
yield* (yield* AuthStore).saveToken(token);
|
|
10879
|
-
yield* Console.log("");
|
|
10880
|
-
yield* Console.log("Logged in successfully. Token saved to ~/.better-update/auth.json");
|
|
10881
|
-
});
|
|
10882
|
-
const runLogin = (options) => Effect.gen(function* () {
|
|
10883
|
-
if (options.manualApiKey) {
|
|
10884
|
-
yield* manualLogin;
|
|
10885
|
-
return;
|
|
10886
|
-
}
|
|
10887
|
-
yield* browserLogin;
|
|
10888
|
-
});
|
|
10889
|
-
|
|
10890
11539
|
//#endregion
|
|
10891
11540
|
//#region src/commands/login.ts
|
|
10892
11541
|
const loginCommand = defineCommand({
|
|
@@ -10908,9 +11557,17 @@ const logoutCommand = defineCommand({
|
|
|
10908
11557
|
name: "logout",
|
|
10909
11558
|
description: "Remove the stored auth token"
|
|
10910
11559
|
},
|
|
10911
|
-
|
|
11560
|
+
args: { all: {
|
|
11561
|
+
type: "boolean",
|
|
11562
|
+
description: "Also clear cached Apple Developer session (cookies)"
|
|
11563
|
+
} },
|
|
11564
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
10912
11565
|
yield* (yield* AuthStore).clearToken;
|
|
10913
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
|
+
}
|
|
10914
11571
|
}))
|
|
10915
11572
|
});
|
|
10916
11573
|
|
|
@@ -11784,6 +12441,86 @@ const resolveChannelToBranch = (client, projectId, channelName) => Effect.gen(fu
|
|
|
11784
12441
|
if (!branch) return yield* new UpdatePublishError({ message: `Channel "${channelName}" maps to a branch (${match.branchId}) not in the project's branch list.` });
|
|
11785
12442
|
return branch.name;
|
|
11786
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
|
+
});
|
|
11787
12524
|
const emitMetadataFile = (input) => Effect.gen(function* () {
|
|
11788
12525
|
const fs = yield* FileSystem.FileSystem;
|
|
11789
12526
|
yield* fs.makeDirectory(input.dir, { recursive: true }).pipe(Effect.mapError((cause) => new UpdatePublishError({ message: `Failed to prepare metadata directory: ${formatCause(cause)}` })));
|
|
@@ -11898,7 +12635,8 @@ const publishPlatform = (params) => Effect.gen(function* () {
|
|
|
11898
12635
|
};
|
|
11899
12636
|
});
|
|
11900
12637
|
const runUpdatePublish = (options) => Effect.scoped(Effect.gen(function* () {
|
|
11901
|
-
const
|
|
12638
|
+
const runtime = yield* CliRuntime;
|
|
12639
|
+
const projectRoot = yield* runtime.cwd;
|
|
11902
12640
|
const api = yield* apiClient;
|
|
11903
12641
|
yield* ensureRepoClean({
|
|
11904
12642
|
projectRoot,
|
|
@@ -11919,23 +12657,30 @@ const runUpdatePublish = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
11919
12657
|
envVars: environmentVars
|
|
11920
12658
|
});
|
|
11921
12659
|
const tempDir = yield* acquireBuildTempDir.pipe(Effect.mapError((cause) => new UpdatePublishError({ message: `Failed to create a temporary export directory: ${formatCause(cause)}` })));
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
|
|
11928
|
-
|
|
11929
|
-
|
|
11930
|
-
|
|
11931
|
-
|
|
11932
|
-
|
|
11933
|
-
|
|
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
|
+
});
|
|
11934
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." });
|
|
11935
12673
|
const sharedExportDir = options.inputDir === void 0 ? void 0 : path.resolve(projectRoot, options.inputDir);
|
|
11936
|
-
const branch = resolvedBranch;
|
|
11937
12674
|
const groupId = randomUUID();
|
|
11938
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
|
+
}
|
|
11939
12684
|
const signedPayloads = yield* loadSignedPublishPayloads({
|
|
11940
12685
|
platforms,
|
|
11941
12686
|
globalFiles: {
|
|
@@ -13203,7 +13948,8 @@ await runMain(defineCommand({
|
|
|
13203
13948
|
devices: devicesCommand,
|
|
13204
13949
|
webhooks: webhooksCommand,
|
|
13205
13950
|
autocomplete: autocompleteCommand,
|
|
13206
|
-
"migrate-config": migrateConfigCommand
|
|
13951
|
+
"migrate-config": migrateConfigCommand,
|
|
13952
|
+
apple: appleCommand
|
|
13207
13953
|
}
|
|
13208
13954
|
}));
|
|
13209
13955
|
|