@better-update/cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +5190 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +12 -9
- package/CHANGELOG.md +0 -58
- package/oxlint.config.ts +0 -6
- package/src/app-layer.ts +0 -29
- package/src/application/build-workflow.ts +0 -222
- package/src/application/command-exit.ts +0 -13
- package/src/application/login.ts +0 -87
- package/src/application/update-promote.ts +0 -88
- package/src/application/update-publish.ts +0 -402
- package/src/application/update-rollback.ts +0 -275
- package/src/commands/analytics/adoption.ts +0 -40
- package/src/commands/analytics/channels.ts +0 -35
- package/src/commands/analytics/helpers.ts +0 -3
- package/src/commands/analytics/index.ts +0 -13
- package/src/commands/analytics/platforms.ts +0 -39
- package/src/commands/analytics/updates.ts +0 -35
- package/src/commands/audit-logs/helpers.ts +0 -3
- package/src/commands/audit-logs/index.ts +0 -8
- package/src/commands/audit-logs/list.ts +0 -66
- package/src/commands/branches.ts +0 -70
- package/src/commands/build/android.ts +0 -129
- package/src/commands/build/index.ts +0 -63
- package/src/commands/build/ios.ts +0 -199
- package/src/commands/build/reserve-and-upload.test.ts +0 -263
- package/src/commands/build/reserve-and-upload.ts +0 -160
- package/src/commands/build/run-step.ts +0 -131
- package/src/commands/builds/compatibility-matrix.ts +0 -48
- package/src/commands/builds/delete.ts +0 -15
- package/src/commands/builds/get.ts +0 -34
- package/src/commands/builds/helpers.ts +0 -3
- package/src/commands/builds/index.ts +0 -20
- package/src/commands/builds/install-link.ts +0 -20
- package/src/commands/builds/list.ts +0 -38
- package/src/commands/channels/create.ts +0 -37
- package/src/commands/channels/delete.ts +0 -15
- package/src/commands/channels/helpers.ts +0 -18
- package/src/commands/channels/index.ts +0 -24
- package/src/commands/channels/list.ts +0 -38
- package/src/commands/channels/pause.ts +0 -15
- package/src/commands/channels/resume.ts +0 -15
- package/src/commands/channels/rollout/complete.ts +0 -17
- package/src/commands/channels/rollout/create.ts +0 -36
- package/src/commands/channels/rollout/index.ts +0 -11
- package/src/commands/channels/rollout/revert.ts +0 -17
- package/src/commands/channels/rollout/update.ts +0 -23
- package/src/commands/channels/update.ts +0 -32
- package/src/commands/credentials/delete.ts +0 -24
- package/src/commands/credentials/index.ts +0 -10
- package/src/commands/credentials/list.ts +0 -33
- package/src/commands/credentials/upload.ts +0 -91
- package/src/commands/env/delete.ts +0 -35
- package/src/commands/env/export.ts +0 -27
- package/src/commands/env/get.ts +0 -25
- package/src/commands/env/helpers.ts +0 -13
- package/src/commands/env/import.ts +0 -31
- package/src/commands/env/index.ts +0 -24
- package/src/commands/env/list.ts +0 -44
- package/src/commands/env/pull.ts +0 -27
- package/src/commands/env/set.ts +0 -42
- package/src/commands/fingerprint/compare.ts +0 -25
- package/src/commands/fingerprint/generate.ts +0 -18
- package/src/commands/fingerprint/index.ts +0 -9
- package/src/commands/init.ts +0 -35
- package/src/commands/login.ts +0 -13
- package/src/commands/logout.ts +0 -12
- package/src/commands/projects.ts +0 -84
- package/src/commands/status.ts +0 -48
- package/src/commands/update/delete.ts +0 -15
- package/src/commands/update/helpers.ts +0 -22
- package/src/commands/update/index.ts +0 -22
- package/src/commands/update/list.ts +0 -60
- package/src/commands/update/promote.ts +0 -30
- package/src/commands/update/publish.ts +0 -94
- package/src/commands/update/rollback.ts +0 -42
- package/src/commands/update/rollout/complete.ts +0 -17
- package/src/commands/update/rollout/index.ts +0 -10
- package/src/commands/update/rollout/revert.ts +0 -17
- package/src/commands/update/rollout/set.ts +0 -23
- package/src/index.ts +0 -53
- package/src/lib/android-keystore.test.ts +0 -114
- package/src/lib/android-keystore.ts +0 -76
- package/src/lib/android-signing-gradle.test.ts +0 -95
- package/src/lib/android-signing-gradle.ts +0 -52
- package/src/lib/app-json.ts +0 -81
- package/src/lib/apple-auth.test.ts +0 -402
- package/src/lib/apple-auth.ts +0 -132
- package/src/lib/artifact-finder.test.ts +0 -195
- package/src/lib/artifact-finder.ts +0 -122
- package/src/lib/browser-login.test.ts +0 -88
- package/src/lib/browser-login.ts +0 -193
- package/src/lib/build-profile.test.ts +0 -290
- package/src/lib/build-profile.ts +0 -234
- package/src/lib/cli-schemas.ts +0 -39
- package/src/lib/command-errors.ts +0 -60
- package/src/lib/credentials-downloader.ts +0 -181
- package/src/lib/credentials-manager.ts +0 -354
- package/src/lib/env-exporter.test.ts +0 -96
- package/src/lib/env-exporter.ts +0 -28
- package/src/lib/exit-codes.ts +0 -82
- package/src/lib/expo-config.ts +0 -130
- package/src/lib/expo-export.test.ts +0 -94
- package/src/lib/expo-export.ts +0 -281
- package/src/lib/fingerprint.ts +0 -67
- package/src/lib/format-error.ts +0 -22
- package/src/lib/git-context.ts +0 -56
- package/src/lib/gradle-config.ts +0 -126
- package/src/lib/ios-export-options.test.ts +0 -98
- package/src/lib/ios-export-options.ts +0 -62
- package/src/lib/ios-keychain.ts +0 -181
- package/src/lib/ios-provisioning.test.ts +0 -115
- package/src/lib/ios-provisioning.ts +0 -179
- package/src/lib/output.ts +0 -32
- package/src/lib/pkcs12.ts +0 -73
- package/src/lib/plist.ts +0 -39
- package/src/lib/post-build-validation.ts +0 -146
- package/src/lib/presigned-upload.test.ts +0 -140
- package/src/lib/presigned-upload.ts +0 -35
- package/src/lib/record.ts +0 -5
- package/src/lib/resolve-named-resource.ts +0 -24
- package/src/lib/runtime-version.test.ts +0 -119
- package/src/lib/runtime-version.ts +0 -62
- package/src/lib/sha256.test.ts +0 -108
- package/src/lib/sha256.ts +0 -80
- package/src/lib/signed-payloads.test.ts +0 -181
- package/src/lib/signed-payloads.ts +0 -164
- package/src/lib/string-utils.ts +0 -4
- package/src/lib/temp-dir.ts +0 -14
- package/src/lib/test-utils.ts +0 -13
- package/src/lib/update-platforms.test.ts +0 -45
- package/src/lib/update-platforms.ts +0 -19
- package/src/lib/xcpretty-formatter.ts +0 -21
- package/src/services/api-client.ts +0 -42
- package/src/services/apple-session-store.ts +0 -100
- package/src/services/auth-store.ts +0 -85
- package/src/services/cli-runtime.ts +0 -46
- package/src/services/config-store.ts +0 -108
- package/src/services/presigned-upload.ts +0 -84
- package/src/services/update-asset-uploader.ts +0 -72
- package/src/types/keychain.d.ts +0 -22
- package/tests/e2e/build.test.ts +0 -270
- package/tests/e2e/commands.test.ts +0 -694
- package/tests/e2e/ota-lifecycle.test.ts +0 -275
- package/tests/e2e/publish.test.ts +0 -150
- package/tests/helpers/cli-e2e.ts +0 -426
- package/tests/helpers/pty-driver.ts +0 -142
- package/tests/interactive/harness/provider-prompt.ts +0 -54
- package/tests/interactive/login.test.ts +0 -47
- package/tests/interactive/provider-select.test.ts +0 -59
- package/tsconfig.json +0 -7
- package/vitest.config.ts +0 -38
package/tests/helpers/cli-e2e.ts
DELETED
|
@@ -1,426 +0,0 @@
|
|
|
1
|
-
import { execSync, spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
6
|
-
|
|
7
|
-
import { applyProcessEnv, createServerE2EEnvironment } from "../../../server/tests/helpers/e2e-env";
|
|
8
|
-
|
|
9
|
-
import type { unstable_startWorker } from "../../../server/node_modules/wrangler";
|
|
10
|
-
|
|
11
|
-
const CLI_DIR = path.resolve(import.meta.dirname, "../..");
|
|
12
|
-
const SERVER_DIR = path.resolve(import.meta.dirname, "../../../server");
|
|
13
|
-
|
|
14
|
-
export interface SetupCliE2EOptions {
|
|
15
|
-
/** Use an existing directory as the CLI project root instead of creating a temp dir. */
|
|
16
|
-
readonly projectDir?: string;
|
|
17
|
-
/** Custom app.json template. ScopeKey and project name are derived from expo.owner/slug/name. */
|
|
18
|
-
readonly appJsonTemplate?: Record<string, unknown>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const defaultAppJsonTemplate = {
|
|
22
|
-
expo: {
|
|
23
|
-
name: "CLI E2E App",
|
|
24
|
-
slug: "cli-e2e-app",
|
|
25
|
-
owner: "cli-e2e",
|
|
26
|
-
version: "1.0.0",
|
|
27
|
-
runtimeVersion: "1.0.0",
|
|
28
|
-
ios: {
|
|
29
|
-
bundleIdentifier: "com.example.cli",
|
|
30
|
-
buildNumber: "1",
|
|
31
|
-
},
|
|
32
|
-
android: {
|
|
33
|
-
package: "com.example.cli",
|
|
34
|
-
versionCode: 1,
|
|
35
|
-
},
|
|
36
|
-
extra: {
|
|
37
|
-
betterUpdate: {
|
|
38
|
-
profiles: {
|
|
39
|
-
production: {
|
|
40
|
-
environment: "production",
|
|
41
|
-
ios: { distribution: "ad-hoc" },
|
|
42
|
-
android: { distribution: "direct", format: "apk" },
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const parseCookies = (response: Response): string => {
|
|
51
|
-
const setCookie = response.headers.getSetCookie();
|
|
52
|
-
return setCookie
|
|
53
|
-
.map((cookie) => cookie.split(";")[0])
|
|
54
|
-
.filter(Boolean)
|
|
55
|
-
.join("; ");
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const sqlString = (value: string) => `'${value.replaceAll("'", "''")}'`;
|
|
59
|
-
|
|
60
|
-
const getNodeErrorCode = (error: unknown): string | undefined => {
|
|
61
|
-
if (!(error instanceof Error)) {
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const directCode = (error as NodeJS.ErrnoException).code;
|
|
66
|
-
if (typeof directCode === "string") {
|
|
67
|
-
return directCode;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const { cause } = error as Error & { readonly cause?: unknown };
|
|
71
|
-
if (typeof cause !== "object" || cause === null) {
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const nestedCode = (cause as NodeJS.ErrnoException).code;
|
|
76
|
-
return typeof nestedCode === "string" ? nestedCode : undefined;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const isRetryableFetchError = (error: unknown) => {
|
|
80
|
-
const code = getNodeErrorCode(error);
|
|
81
|
-
return code !== undefined && ["ECONNRESET", "EPIPE", "UND_ERR_SOCKET"].includes(code);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
export interface CliCommandResult {
|
|
85
|
-
readonly stdout: string;
|
|
86
|
-
readonly stderr: string;
|
|
87
|
-
readonly exitCode: number;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export interface CliE2EContext {
|
|
91
|
-
readonly getBaseUrl: () => string;
|
|
92
|
-
readonly getProjectDir: () => string;
|
|
93
|
-
readonly getProjectId: () => string;
|
|
94
|
-
readonly readAppJson: () => Record<string, unknown>;
|
|
95
|
-
readonly runCli: (...args: readonly string[]) => CliCommandResult;
|
|
96
|
-
readonly seedSql: (sql: string) => void;
|
|
97
|
-
readonly post: (
|
|
98
|
-
path: string,
|
|
99
|
-
body: unknown,
|
|
100
|
-
headers?: Record<string, string>,
|
|
101
|
-
) => Promise<Response>;
|
|
102
|
-
readonly get: (path: string, headers?: Record<string, string>) => Promise<Response>;
|
|
103
|
-
readonly getAuthorized: (path: string, headers?: Record<string, string>) => Promise<Response>;
|
|
104
|
-
readonly postAuthorized: (
|
|
105
|
-
path: string,
|
|
106
|
-
body: unknown,
|
|
107
|
-
headers?: Record<string, string>,
|
|
108
|
-
) => Promise<Response>;
|
|
109
|
-
readonly patchAuthorized: (
|
|
110
|
-
path: string,
|
|
111
|
-
body: unknown,
|
|
112
|
-
headers?: Record<string, string>,
|
|
113
|
-
) => Promise<Response>;
|
|
114
|
-
readonly deleteAuthorized: (
|
|
115
|
-
path: string,
|
|
116
|
-
body: unknown,
|
|
117
|
-
headers?: Record<string, string>,
|
|
118
|
-
) => Promise<Response>;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export const setupCliE2E = (persistDir: string, options?: SetupCliE2EOptions): CliE2EContext => {
|
|
122
|
-
const template = options?.appJsonTemplate ?? defaultAppJsonTemplate;
|
|
123
|
-
const expoConfig = (template as { expo?: Record<string, unknown> }).expo ?? {};
|
|
124
|
-
const slugRaw = expoConfig["slug"];
|
|
125
|
-
const slug = typeof slugRaw === "string" ? slugRaw : "cli-e2e-app";
|
|
126
|
-
const nameRaw = expoConfig["name"];
|
|
127
|
-
const projectName = `${typeof nameRaw === "string" ? nameRaw : "E2E"} Project`;
|
|
128
|
-
const useExternalProjectDir = options?.projectDir !== undefined;
|
|
129
|
-
|
|
130
|
-
const state = {
|
|
131
|
-
worker: null as Awaited<ReturnType<typeof unstable_startWorker>> | null,
|
|
132
|
-
baseUrl: "",
|
|
133
|
-
cookies: "",
|
|
134
|
-
organizationId: "",
|
|
135
|
-
projectId: "",
|
|
136
|
-
apiKey: "",
|
|
137
|
-
projectDir: "",
|
|
138
|
-
homeDir: "",
|
|
139
|
-
restoreProcessEnv: undefined as (() => void) | undefined,
|
|
140
|
-
originalAppJson: undefined as string | undefined,
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const persistPath = path.resolve(SERVER_DIR, persistDir);
|
|
144
|
-
const persistArg = path.relative(SERVER_DIR, persistPath) || ".";
|
|
145
|
-
const seedFileId = persistDir.replaceAll(/[^a-zA-Z0-9]+/gu, "-");
|
|
146
|
-
const seedFile = path.resolve(SERVER_DIR, `.wrangler/seed-${seedFileId}.sql`);
|
|
147
|
-
|
|
148
|
-
const post = async (requestPath: string, body: unknown, headers?: Record<string, string>) =>
|
|
149
|
-
requestWithRetry(async () =>
|
|
150
|
-
fetch(`${state.baseUrl}${requestPath}`, {
|
|
151
|
-
method: "POST",
|
|
152
|
-
headers: { "content-type": "application/json", ...headers },
|
|
153
|
-
body: JSON.stringify(body),
|
|
154
|
-
}),
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
const get = async (requestPath: string, headers?: Record<string, string>) =>
|
|
158
|
-
requestWithRetry(async () =>
|
|
159
|
-
fetch(`${state.baseUrl}${requestPath}`, headers ? { headers } : {}),
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
const patch = async (requestPath: string, body: unknown, headers?: Record<string, string>) =>
|
|
163
|
-
requestWithRetry(async () =>
|
|
164
|
-
fetch(`${state.baseUrl}${requestPath}`, {
|
|
165
|
-
method: "PATCH",
|
|
166
|
-
headers: { "content-type": "application/json", ...headers },
|
|
167
|
-
body: JSON.stringify(body),
|
|
168
|
-
}),
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
const del = async (requestPath: string, body: unknown, headers?: Record<string, string>) =>
|
|
172
|
-
requestWithRetry(async () =>
|
|
173
|
-
fetch(`${state.baseUrl}${requestPath}`, {
|
|
174
|
-
method: "DELETE",
|
|
175
|
-
headers: { "content-type": "application/json", ...headers },
|
|
176
|
-
body: JSON.stringify(body),
|
|
177
|
-
}),
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
const seedSql = (sql: string) => {
|
|
181
|
-
writeFileSync(seedFile, sql);
|
|
182
|
-
try {
|
|
183
|
-
execSync(
|
|
184
|
-
`bunx wrangler d1 execute DB --local --persist-to ${persistArg} --file ${seedFile}`,
|
|
185
|
-
{
|
|
186
|
-
cwd: SERVER_DIR,
|
|
187
|
-
stdio: "pipe",
|
|
188
|
-
},
|
|
189
|
-
);
|
|
190
|
-
} finally {
|
|
191
|
-
rmSync(seedFile, { force: true });
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const requestWithRetry = async (run: () => Promise<Response>): Promise<Response> => {
|
|
196
|
-
const maxAttempts = 4;
|
|
197
|
-
|
|
198
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
199
|
-
try {
|
|
200
|
-
return await run();
|
|
201
|
-
} catch (error) {
|
|
202
|
-
if (!isRetryableFetchError(error) || attempt === maxAttempts) {
|
|
203
|
-
throw error;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
await sleep(attempt * 100);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
throw new Error("requestWithRetry exhausted unexpectedly");
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
const writeAppJson = () => {
|
|
214
|
-
writeFileSync(
|
|
215
|
-
path.join(state.projectDir, "app.json"),
|
|
216
|
-
`${JSON.stringify(template, null, 2)}\n`,
|
|
217
|
-
);
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
const runCli = (...args: readonly string[]): CliCommandResult => {
|
|
221
|
-
const result = spawnSync("bun", [path.resolve(CLI_DIR, "src/index.ts"), ...args], {
|
|
222
|
-
cwd: state.projectDir,
|
|
223
|
-
env: {
|
|
224
|
-
...process.env,
|
|
225
|
-
HOME: state.homeDir,
|
|
226
|
-
BETTER_UPDATE_URL: state.baseUrl,
|
|
227
|
-
BETTER_UPDATE_TOKEN: state.apiKey,
|
|
228
|
-
FORCE_COLOR: "0",
|
|
229
|
-
NO_COLOR: "1",
|
|
230
|
-
},
|
|
231
|
-
encoding: "utf8",
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
return {
|
|
235
|
-
stdout: result.stdout,
|
|
236
|
-
stderr: result.stderr,
|
|
237
|
-
exitCode: result.status ?? 1,
|
|
238
|
-
};
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
beforeAll(async () => {
|
|
242
|
-
rmSync(persistPath, { recursive: true, force: true });
|
|
243
|
-
const e2eEnv = createServerE2EEnvironment({ projectRoot: SERVER_DIR });
|
|
244
|
-
state.restoreProcessEnv = applyProcessEnv(e2eEnv.processOverrides);
|
|
245
|
-
|
|
246
|
-
execSync(`bunx wrangler d1 migrations apply DB --local --persist-to ${persistArg}`, {
|
|
247
|
-
cwd: SERVER_DIR,
|
|
248
|
-
env: e2eEnv.wranglerEnv,
|
|
249
|
-
stdio: "pipe",
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
const originalCwd = process.cwd();
|
|
253
|
-
process.chdir(SERVER_DIR);
|
|
254
|
-
try {
|
|
255
|
-
const { unstable_startWorker } = await import("../../../server/node_modules/wrangler");
|
|
256
|
-
state.worker = await unstable_startWorker({
|
|
257
|
-
config: path.resolve(SERVER_DIR, "wrangler.jsonc"),
|
|
258
|
-
envFiles: [],
|
|
259
|
-
bindings: e2eEnv.workerBindings,
|
|
260
|
-
build: { nodejsCompatMode: "v2" },
|
|
261
|
-
dev: {
|
|
262
|
-
server: { port: 0 },
|
|
263
|
-
inspector: false,
|
|
264
|
-
logLevel: "error",
|
|
265
|
-
persist: persistPath,
|
|
266
|
-
},
|
|
267
|
-
});
|
|
268
|
-
} finally {
|
|
269
|
-
process.chdir(originalCwd);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const url = await state.worker.url;
|
|
273
|
-
state.baseUrl = url.href.replace(/\/$/, "");
|
|
274
|
-
|
|
275
|
-
state.homeDir = mkdtempSync(path.join(os.tmpdir(), "better-update-cli-home-"));
|
|
276
|
-
|
|
277
|
-
if (useExternalProjectDir) {
|
|
278
|
-
state.projectDir = options.projectDir!;
|
|
279
|
-
const appJsonPath = path.join(state.projectDir, "app.json");
|
|
280
|
-
if (existsSync(appJsonPath)) {
|
|
281
|
-
state.originalAppJson = readFileSync(appJsonPath, "utf8");
|
|
282
|
-
}
|
|
283
|
-
} else {
|
|
284
|
-
state.projectDir = mkdtempSync(path.join(os.tmpdir(), "better-update-cli-project-"));
|
|
285
|
-
}
|
|
286
|
-
writeAppJson();
|
|
287
|
-
|
|
288
|
-
const signUpResponse = await post("/api/auth/sign-up/email", {
|
|
289
|
-
name: "CLI E2E User",
|
|
290
|
-
email: "cli-e2e@example.com",
|
|
291
|
-
password: "SecureP@ss123",
|
|
292
|
-
});
|
|
293
|
-
expect(signUpResponse.status).toBe(200);
|
|
294
|
-
state.cookies = parseCookies(signUpResponse);
|
|
295
|
-
|
|
296
|
-
const createOrgResponse = await post(
|
|
297
|
-
"/api/auth/organization/create",
|
|
298
|
-
{ name: "CLI Org", slug: "cli-org" },
|
|
299
|
-
{ cookie: state.cookies },
|
|
300
|
-
);
|
|
301
|
-
expect(createOrgResponse.status).toBe(200);
|
|
302
|
-
const createOrgBody = await createOrgResponse.json();
|
|
303
|
-
state.organizationId = createOrgBody.id;
|
|
304
|
-
state.cookies = parseCookies(createOrgResponse) || state.cookies;
|
|
305
|
-
|
|
306
|
-
const setActiveResponse = await post(
|
|
307
|
-
"/api/auth/organization/set-active",
|
|
308
|
-
{ organizationId: state.organizationId },
|
|
309
|
-
{ cookie: state.cookies },
|
|
310
|
-
);
|
|
311
|
-
expect(setActiveResponse.status).toBe(200);
|
|
312
|
-
state.cookies = parseCookies(setActiveResponse) || state.cookies;
|
|
313
|
-
|
|
314
|
-
const createProjectResponse = await post(
|
|
315
|
-
"/api/projects",
|
|
316
|
-
{ name: projectName, slug },
|
|
317
|
-
{ cookie: state.cookies },
|
|
318
|
-
);
|
|
319
|
-
expect(createProjectResponse.status).toBe(201);
|
|
320
|
-
const createProjectBody = await createProjectResponse.json();
|
|
321
|
-
state.projectId = createProjectBody.id;
|
|
322
|
-
|
|
323
|
-
const createKeyResponse = await post(
|
|
324
|
-
"/api/auth/api-key/create",
|
|
325
|
-
{ name: "cli-e2e-key", organizationId: state.organizationId },
|
|
326
|
-
{ cookie: state.cookies },
|
|
327
|
-
);
|
|
328
|
-
expect(createKeyResponse.status).toBe(200);
|
|
329
|
-
const createKeyBody = await createKeyResponse.json();
|
|
330
|
-
state.apiKey = createKeyBody.key;
|
|
331
|
-
|
|
332
|
-
const createBranchResponse = await post(
|
|
333
|
-
"/api/branches",
|
|
334
|
-
{ projectId: state.projectId, name: "main" },
|
|
335
|
-
{ cookie: state.cookies },
|
|
336
|
-
);
|
|
337
|
-
expect(createBranchResponse.status).toBe(201);
|
|
338
|
-
|
|
339
|
-
const createEnvVarResponse = await post(
|
|
340
|
-
"/api/env-vars",
|
|
341
|
-
{
|
|
342
|
-
projectId: state.projectId,
|
|
343
|
-
environment: "production",
|
|
344
|
-
key: "APP_SECRET",
|
|
345
|
-
value: "super-secret",
|
|
346
|
-
visibility: "secret",
|
|
347
|
-
},
|
|
348
|
-
{ cookie: state.cookies },
|
|
349
|
-
);
|
|
350
|
-
expect(createEnvVarResponse.status).toBe(201);
|
|
351
|
-
|
|
352
|
-
seedSql(`
|
|
353
|
-
INSERT INTO "builds" (
|
|
354
|
-
"id", "project_id", "platform", "profile", "distribution", "runtime_version",
|
|
355
|
-
"app_version", "build_number", "bundle_id", "git_ref", "git_commit",
|
|
356
|
-
"message", "metadata_json", "created_at"
|
|
357
|
-
)
|
|
358
|
-
VALUES (
|
|
359
|
-
'cli-build-1',
|
|
360
|
-
${sqlString(state.projectId)},
|
|
361
|
-
'ios',
|
|
362
|
-
'production',
|
|
363
|
-
'ad-hoc',
|
|
364
|
-
'1.0.0',
|
|
365
|
-
'1.0.0',
|
|
366
|
-
'1',
|
|
367
|
-
'com.example.cli',
|
|
368
|
-
'main',
|
|
369
|
-
'abcdef1',
|
|
370
|
-
'CLI seeded build',
|
|
371
|
-
'{}',
|
|
372
|
-
'2024-04-01T00:00:00Z'
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
INSERT INTO "build_artifacts" (
|
|
376
|
-
"build_id", "r2_key", "format", "content_type", "byte_size", "sha256", "created_at"
|
|
377
|
-
)
|
|
378
|
-
VALUES (
|
|
379
|
-
'cli-build-1',
|
|
380
|
-
'builds/${state.organizationId}/${state.projectId}/cli-build-1.ipa',
|
|
381
|
-
'ipa',
|
|
382
|
-
'application/octet-stream',
|
|
383
|
-
1024,
|
|
384
|
-
'cli-build-sha',
|
|
385
|
-
'2024-04-01T00:00:00Z'
|
|
386
|
-
);
|
|
387
|
-
`);
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
afterAll(async () => {
|
|
391
|
-
await state.worker?.dispose();
|
|
392
|
-
state.restoreProcessEnv?.();
|
|
393
|
-
rmSync(persistPath, { recursive: true, force: true });
|
|
394
|
-
if (useExternalProjectDir) {
|
|
395
|
-
if (state.originalAppJson !== undefined) {
|
|
396
|
-
writeFileSync(path.join(state.projectDir, "app.json"), state.originalAppJson);
|
|
397
|
-
}
|
|
398
|
-
} else {
|
|
399
|
-
rmSync(state.projectDir, { recursive: true, force: true });
|
|
400
|
-
}
|
|
401
|
-
rmSync(state.homeDir, { recursive: true, force: true });
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
return {
|
|
405
|
-
getBaseUrl: () => state.baseUrl,
|
|
406
|
-
getProjectDir: () => state.projectDir,
|
|
407
|
-
getProjectId: () => state.projectId,
|
|
408
|
-
readAppJson: () =>
|
|
409
|
-
JSON.parse(readFileSync(path.join(state.projectDir, "app.json"), "utf8")) as Record<
|
|
410
|
-
string,
|
|
411
|
-
unknown
|
|
412
|
-
>,
|
|
413
|
-
runCli,
|
|
414
|
-
seedSql,
|
|
415
|
-
post,
|
|
416
|
-
get,
|
|
417
|
-
getAuthorized: async (requestPath, headers) =>
|
|
418
|
-
get(requestPath, { authorization: `Bearer ${state.apiKey}`, ...headers }),
|
|
419
|
-
postAuthorized: async (requestPath, body, headers) =>
|
|
420
|
-
post(requestPath, body, { authorization: `Bearer ${state.apiKey}`, ...headers }),
|
|
421
|
-
patchAuthorized: async (requestPath, body, headers) =>
|
|
422
|
-
patch(requestPath, body, { authorization: `Bearer ${state.apiKey}`, ...headers }),
|
|
423
|
-
deleteAuthorized: async (requestPath, body, headers) =>
|
|
424
|
-
del(requestPath, body, { authorization: `Bearer ${state.apiKey}`, ...headers }),
|
|
425
|
-
};
|
|
426
|
-
};
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
-
|
|
3
|
-
import { spawn } from "node-pty";
|
|
4
|
-
|
|
5
|
-
import type { IPty } from "node-pty";
|
|
6
|
-
|
|
7
|
-
export interface PtySpawnOptions {
|
|
8
|
-
readonly cwd?: string;
|
|
9
|
-
readonly env?: NodeJS.ProcessEnv;
|
|
10
|
-
readonly cols?: number;
|
|
11
|
-
readonly rows?: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface PtyDriver {
|
|
15
|
-
readonly output: () => string;
|
|
16
|
-
readonly stripped: () => string;
|
|
17
|
-
readonly expect: (
|
|
18
|
-
pattern: string | RegExp,
|
|
19
|
-
options?: { readonly timeoutMs?: number },
|
|
20
|
-
) => Promise<void>;
|
|
21
|
-
readonly send: (text: string) => void;
|
|
22
|
-
readonly enter: () => void;
|
|
23
|
-
readonly down: (count?: number) => void;
|
|
24
|
-
readonly up: (count?: number) => void;
|
|
25
|
-
readonly waitExit: (options?: { readonly timeoutMs?: number }) => Promise<number>;
|
|
26
|
-
readonly kill: () => void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const ANSI_REGEX =
|
|
30
|
-
// eslint-disable-next-line no-control-regex -- stripping ANSI escapes
|
|
31
|
-
/[\u001B\u009B][[()#;?]*(?:\d{1,4}(?:;\d{0,4})*)?[\dA-ORZcf-ntqry=><]/g;
|
|
32
|
-
|
|
33
|
-
export const stripAnsi = (input: string) => input.replace(ANSI_REGEX, "");
|
|
34
|
-
|
|
35
|
-
export const spawnPty = (
|
|
36
|
-
command: string,
|
|
37
|
-
args: readonly string[],
|
|
38
|
-
options?: PtySpawnOptions,
|
|
39
|
-
): PtyDriver => {
|
|
40
|
-
const ptyProcess: IPty = spawn(command, [...args], {
|
|
41
|
-
cols: options?.cols ?? 120,
|
|
42
|
-
rows: options?.rows ?? 40,
|
|
43
|
-
cwd: options?.cwd ?? process.cwd(),
|
|
44
|
-
env: { ...process.env, ...options?.env } as Record<string, string>,
|
|
45
|
-
name: "xterm-256color",
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
let buffer = "";
|
|
49
|
-
let exitCode: number | null = null;
|
|
50
|
-
const waiters: (() => void)[] = [];
|
|
51
|
-
|
|
52
|
-
const drainWaiters = () => {
|
|
53
|
-
const pending = waiters.splice(0);
|
|
54
|
-
for (const waiter of pending) {
|
|
55
|
-
waiter();
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
ptyProcess.onData((chunk) => {
|
|
60
|
-
buffer += chunk;
|
|
61
|
-
drainWaiters();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
ptyProcess.onExit(({ exitCode: code }) => {
|
|
65
|
-
exitCode = code;
|
|
66
|
-
drainWaiters();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const waitFor = async (timeoutMs: number, predicate: () => boolean) =>
|
|
70
|
-
new Promise<void>((resolve, reject) => {
|
|
71
|
-
if (predicate()) {
|
|
72
|
-
resolve();
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
let settled = false;
|
|
76
|
-
const tick = () => {
|
|
77
|
-
if (settled) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
if (predicate()) {
|
|
81
|
-
settled = true;
|
|
82
|
-
clearTimeout(timer);
|
|
83
|
-
resolve();
|
|
84
|
-
} else if (exitCode === null) {
|
|
85
|
-
waiters.push(tick);
|
|
86
|
-
} else {
|
|
87
|
-
settled = true;
|
|
88
|
-
clearTimeout(timer);
|
|
89
|
-
reject(
|
|
90
|
-
new Error(
|
|
91
|
-
`pty exited (code=${exitCode}) before predicate matched. Buffer:\n${stripAnsi(buffer)}`,
|
|
92
|
-
),
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
const timer = setTimeout(() => {
|
|
97
|
-
if (settled) {
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
settled = true;
|
|
101
|
-
reject(new Error(`pty wait timed out after ${timeoutMs}ms. Buffer:\n${stripAnsi(buffer)}`));
|
|
102
|
-
}, timeoutMs);
|
|
103
|
-
waiters.push(tick);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
output: () => buffer,
|
|
108
|
-
stripped: () => stripAnsi(buffer),
|
|
109
|
-
expect: async (pattern, { timeoutMs = 5000 } = {}) => {
|
|
110
|
-
const matcher =
|
|
111
|
-
typeof pattern === "string"
|
|
112
|
-
? (text: string) => text.includes(pattern)
|
|
113
|
-
: (text: string) => pattern.test(text);
|
|
114
|
-
await waitFor(timeoutMs, () => matcher(stripAnsi(buffer)));
|
|
115
|
-
},
|
|
116
|
-
send: (text) => {
|
|
117
|
-
ptyProcess.write(text);
|
|
118
|
-
},
|
|
119
|
-
enter: () => {
|
|
120
|
-
ptyProcess.write("\r");
|
|
121
|
-
},
|
|
122
|
-
down: (count = 1) => {
|
|
123
|
-
for (let index = 0; index < count; index += 1) {
|
|
124
|
-
ptyProcess.write("\u001B[B");
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
up: (count = 1) => {
|
|
128
|
-
for (let index = 0; index < count; index += 1) {
|
|
129
|
-
ptyProcess.write("\u001B[A");
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
waitExit: async ({ timeoutMs = 10_000 } = {}) => {
|
|
133
|
-
await waitFor(timeoutMs, () => exitCode !== null);
|
|
134
|
-
// Give the pty a beat to flush any trailing bytes.
|
|
135
|
-
await sleep(10);
|
|
136
|
-
return exitCode ?? -1;
|
|
137
|
-
},
|
|
138
|
-
kill: () => {
|
|
139
|
-
ptyProcess.kill();
|
|
140
|
-
},
|
|
141
|
-
};
|
|
142
|
-
};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import process from "node:process";
|
|
3
|
-
|
|
4
|
-
import { BunContext, BunRuntime } from "@effect/platform-bun";
|
|
5
|
-
import { Effect, Layer } from "effect";
|
|
6
|
-
|
|
7
|
-
import type { Session } from "@expo/apple-utils";
|
|
8
|
-
// eslint-disable-next-line import-plugin/no-namespace -- harness casts a stub as `typeof AppleUtils` (whole module shape) to satisfy resolveProvider's injected module param
|
|
9
|
-
import type * as AppleUtils from "@expo/apple-utils";
|
|
10
|
-
|
|
11
|
-
import { resolveProvider } from "../../../src/lib/apple-auth";
|
|
12
|
-
import { CliRuntimeLive } from "../../../src/services/cli-runtime";
|
|
13
|
-
|
|
14
|
-
// Force the prompt branch: ignore any APPLE_PROVIDER_ID from the host shell.
|
|
15
|
-
delete process.env["APPLE_PROVIDER_ID"];
|
|
16
|
-
|
|
17
|
-
const fakeAppleUtils = {
|
|
18
|
-
Session: {
|
|
19
|
-
setSessionProviderIdAsync: async (_id: number) => null,
|
|
20
|
-
},
|
|
21
|
-
} as unknown as typeof AppleUtils;
|
|
22
|
-
|
|
23
|
-
const providers: readonly Session.SessionProvider[] = [
|
|
24
|
-
{
|
|
25
|
-
providerId: 10,
|
|
26
|
-
publicProviderId: "pub-10",
|
|
27
|
-
name: "Org Alpha",
|
|
28
|
-
contentTypes: ["SOFTWARE"],
|
|
29
|
-
subType: "ORGANIZATION",
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
providerId: 20,
|
|
33
|
-
publicProviderId: "pub-20",
|
|
34
|
-
name: "Org Beta",
|
|
35
|
-
contentTypes: ["SOFTWARE"],
|
|
36
|
-
subType: "ORGANIZATION",
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
providerId: 30,
|
|
40
|
-
publicProviderId: "pub-30",
|
|
41
|
-
name: "Org Gamma",
|
|
42
|
-
contentTypes: ["SOFTWARE"],
|
|
43
|
-
subType: "ORGANIZATION",
|
|
44
|
-
},
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
const program = Effect.gen(function* () {
|
|
48
|
-
const result = yield* resolveProvider(fakeAppleUtils, providers, undefined, undefined);
|
|
49
|
-
// Distinctive marker so the PTY test can extract the JSON past any rendered prompt text.
|
|
50
|
-
// eslint-disable-next-line eslint/no-console -- interactive PTY harness prints a parseable marker to stdout; Effect.Console adds formatting that breaks the parser
|
|
51
|
-
console.log(`RESULT=${JSON.stringify(result)}`);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
program.pipe(Effect.provide(Layer.mergeAll(BunContext.layer, CliRuntimeLive)), BunRuntime.runMain);
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
import { spawnPty } from "../helpers/pty-driver";
|
|
6
|
-
|
|
7
|
-
const CLI_ENTRY = path.resolve(import.meta.dirname, "../../src/index.ts");
|
|
8
|
-
|
|
9
|
-
describe("login --api-key (interactive PoC)", () => {
|
|
10
|
-
let homeDir: string;
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
homeDir = mkdtempSync(path.join(os.tmpdir(), "better-update-pty-home-"));
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
afterEach(() => {
|
|
17
|
-
rmSync(homeDir, { recursive: true, force: true });
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("prompts for API key, stores token on enter", async () => {
|
|
21
|
-
const driver = spawnPty("bun", [CLI_ENTRY, "login", "--api-key"], {
|
|
22
|
-
env: {
|
|
23
|
-
HOME: homeDir,
|
|
24
|
-
FORCE_COLOR: "0",
|
|
25
|
-
NO_COLOR: "1",
|
|
26
|
-
// Layer construction currently yields apiClient eagerly via UpdateAssetUploaderLive,
|
|
27
|
-
// So every CLI invocation resolves a token. Pass a placeholder — login still writes
|
|
28
|
-
// The prompted token to auth.json, which is what we assert.
|
|
29
|
-
BETTER_UPDATE_TOKEN: "startup-placeholder",
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
await driver.expect(/Paste your API key/, { timeoutMs: 15_000 });
|
|
34
|
-
|
|
35
|
-
driver.send("pk_test_abc123");
|
|
36
|
-
driver.enter();
|
|
37
|
-
|
|
38
|
-
await driver.expect("Logged in successfully", { timeoutMs: 10_000 });
|
|
39
|
-
const code = await driver.waitExit({ timeoutMs: 5000 });
|
|
40
|
-
expect(code).toBe(0);
|
|
41
|
-
|
|
42
|
-
const authJson = JSON.parse(
|
|
43
|
-
readFileSync(path.join(homeDir, ".better-update/auth.json"), "utf8"),
|
|
44
|
-
) as { token: string };
|
|
45
|
-
expect(authJson.token).toBe("pk_test_abc123");
|
|
46
|
-
});
|
|
47
|
-
});
|