@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.
Files changed (152) hide show
  1. package/dist/index.mjs +5190 -0
  2. package/dist/index.mjs.map +1 -0
  3. package/package.json +12 -9
  4. package/CHANGELOG.md +0 -58
  5. package/oxlint.config.ts +0 -6
  6. package/src/app-layer.ts +0 -29
  7. package/src/application/build-workflow.ts +0 -222
  8. package/src/application/command-exit.ts +0 -13
  9. package/src/application/login.ts +0 -87
  10. package/src/application/update-promote.ts +0 -88
  11. package/src/application/update-publish.ts +0 -402
  12. package/src/application/update-rollback.ts +0 -275
  13. package/src/commands/analytics/adoption.ts +0 -40
  14. package/src/commands/analytics/channels.ts +0 -35
  15. package/src/commands/analytics/helpers.ts +0 -3
  16. package/src/commands/analytics/index.ts +0 -13
  17. package/src/commands/analytics/platforms.ts +0 -39
  18. package/src/commands/analytics/updates.ts +0 -35
  19. package/src/commands/audit-logs/helpers.ts +0 -3
  20. package/src/commands/audit-logs/index.ts +0 -8
  21. package/src/commands/audit-logs/list.ts +0 -66
  22. package/src/commands/branches.ts +0 -70
  23. package/src/commands/build/android.ts +0 -129
  24. package/src/commands/build/index.ts +0 -63
  25. package/src/commands/build/ios.ts +0 -199
  26. package/src/commands/build/reserve-and-upload.test.ts +0 -263
  27. package/src/commands/build/reserve-and-upload.ts +0 -160
  28. package/src/commands/build/run-step.ts +0 -131
  29. package/src/commands/builds/compatibility-matrix.ts +0 -48
  30. package/src/commands/builds/delete.ts +0 -15
  31. package/src/commands/builds/get.ts +0 -34
  32. package/src/commands/builds/helpers.ts +0 -3
  33. package/src/commands/builds/index.ts +0 -20
  34. package/src/commands/builds/install-link.ts +0 -20
  35. package/src/commands/builds/list.ts +0 -38
  36. package/src/commands/channels/create.ts +0 -37
  37. package/src/commands/channels/delete.ts +0 -15
  38. package/src/commands/channels/helpers.ts +0 -18
  39. package/src/commands/channels/index.ts +0 -24
  40. package/src/commands/channels/list.ts +0 -38
  41. package/src/commands/channels/pause.ts +0 -15
  42. package/src/commands/channels/resume.ts +0 -15
  43. package/src/commands/channels/rollout/complete.ts +0 -17
  44. package/src/commands/channels/rollout/create.ts +0 -36
  45. package/src/commands/channels/rollout/index.ts +0 -11
  46. package/src/commands/channels/rollout/revert.ts +0 -17
  47. package/src/commands/channels/rollout/update.ts +0 -23
  48. package/src/commands/channels/update.ts +0 -32
  49. package/src/commands/credentials/delete.ts +0 -24
  50. package/src/commands/credentials/index.ts +0 -10
  51. package/src/commands/credentials/list.ts +0 -33
  52. package/src/commands/credentials/upload.ts +0 -91
  53. package/src/commands/env/delete.ts +0 -35
  54. package/src/commands/env/export.ts +0 -27
  55. package/src/commands/env/get.ts +0 -25
  56. package/src/commands/env/helpers.ts +0 -13
  57. package/src/commands/env/import.ts +0 -31
  58. package/src/commands/env/index.ts +0 -24
  59. package/src/commands/env/list.ts +0 -44
  60. package/src/commands/env/pull.ts +0 -27
  61. package/src/commands/env/set.ts +0 -42
  62. package/src/commands/fingerprint/compare.ts +0 -25
  63. package/src/commands/fingerprint/generate.ts +0 -18
  64. package/src/commands/fingerprint/index.ts +0 -9
  65. package/src/commands/init.ts +0 -35
  66. package/src/commands/login.ts +0 -13
  67. package/src/commands/logout.ts +0 -12
  68. package/src/commands/projects.ts +0 -84
  69. package/src/commands/status.ts +0 -48
  70. package/src/commands/update/delete.ts +0 -15
  71. package/src/commands/update/helpers.ts +0 -22
  72. package/src/commands/update/index.ts +0 -22
  73. package/src/commands/update/list.ts +0 -60
  74. package/src/commands/update/promote.ts +0 -30
  75. package/src/commands/update/publish.ts +0 -94
  76. package/src/commands/update/rollback.ts +0 -42
  77. package/src/commands/update/rollout/complete.ts +0 -17
  78. package/src/commands/update/rollout/index.ts +0 -10
  79. package/src/commands/update/rollout/revert.ts +0 -17
  80. package/src/commands/update/rollout/set.ts +0 -23
  81. package/src/index.ts +0 -53
  82. package/src/lib/android-keystore.test.ts +0 -114
  83. package/src/lib/android-keystore.ts +0 -76
  84. package/src/lib/android-signing-gradle.test.ts +0 -95
  85. package/src/lib/android-signing-gradle.ts +0 -52
  86. package/src/lib/app-json.ts +0 -81
  87. package/src/lib/apple-auth.test.ts +0 -402
  88. package/src/lib/apple-auth.ts +0 -132
  89. package/src/lib/artifact-finder.test.ts +0 -195
  90. package/src/lib/artifact-finder.ts +0 -122
  91. package/src/lib/browser-login.test.ts +0 -88
  92. package/src/lib/browser-login.ts +0 -193
  93. package/src/lib/build-profile.test.ts +0 -290
  94. package/src/lib/build-profile.ts +0 -234
  95. package/src/lib/cli-schemas.ts +0 -39
  96. package/src/lib/command-errors.ts +0 -60
  97. package/src/lib/credentials-downloader.ts +0 -181
  98. package/src/lib/credentials-manager.ts +0 -354
  99. package/src/lib/env-exporter.test.ts +0 -96
  100. package/src/lib/env-exporter.ts +0 -28
  101. package/src/lib/exit-codes.ts +0 -82
  102. package/src/lib/expo-config.ts +0 -130
  103. package/src/lib/expo-export.test.ts +0 -94
  104. package/src/lib/expo-export.ts +0 -281
  105. package/src/lib/fingerprint.ts +0 -67
  106. package/src/lib/format-error.ts +0 -22
  107. package/src/lib/git-context.ts +0 -56
  108. package/src/lib/gradle-config.ts +0 -126
  109. package/src/lib/ios-export-options.test.ts +0 -98
  110. package/src/lib/ios-export-options.ts +0 -62
  111. package/src/lib/ios-keychain.ts +0 -181
  112. package/src/lib/ios-provisioning.test.ts +0 -115
  113. package/src/lib/ios-provisioning.ts +0 -179
  114. package/src/lib/output.ts +0 -32
  115. package/src/lib/pkcs12.ts +0 -73
  116. package/src/lib/plist.ts +0 -39
  117. package/src/lib/post-build-validation.ts +0 -146
  118. package/src/lib/presigned-upload.test.ts +0 -140
  119. package/src/lib/presigned-upload.ts +0 -35
  120. package/src/lib/record.ts +0 -5
  121. package/src/lib/resolve-named-resource.ts +0 -24
  122. package/src/lib/runtime-version.test.ts +0 -119
  123. package/src/lib/runtime-version.ts +0 -62
  124. package/src/lib/sha256.test.ts +0 -108
  125. package/src/lib/sha256.ts +0 -80
  126. package/src/lib/signed-payloads.test.ts +0 -181
  127. package/src/lib/signed-payloads.ts +0 -164
  128. package/src/lib/string-utils.ts +0 -4
  129. package/src/lib/temp-dir.ts +0 -14
  130. package/src/lib/test-utils.ts +0 -13
  131. package/src/lib/update-platforms.test.ts +0 -45
  132. package/src/lib/update-platforms.ts +0 -19
  133. package/src/lib/xcpretty-formatter.ts +0 -21
  134. package/src/services/api-client.ts +0 -42
  135. package/src/services/apple-session-store.ts +0 -100
  136. package/src/services/auth-store.ts +0 -85
  137. package/src/services/cli-runtime.ts +0 -46
  138. package/src/services/config-store.ts +0 -108
  139. package/src/services/presigned-upload.ts +0 -84
  140. package/src/services/update-asset-uploader.ts +0 -72
  141. package/src/types/keychain.d.ts +0 -22
  142. package/tests/e2e/build.test.ts +0 -270
  143. package/tests/e2e/commands.test.ts +0 -694
  144. package/tests/e2e/ota-lifecycle.test.ts +0 -275
  145. package/tests/e2e/publish.test.ts +0 -150
  146. package/tests/helpers/cli-e2e.ts +0 -426
  147. package/tests/helpers/pty-driver.ts +0 -142
  148. package/tests/interactive/harness/provider-prompt.ts +0 -54
  149. package/tests/interactive/login.test.ts +0 -47
  150. package/tests/interactive/provider-select.test.ts +0 -59
  151. package/tsconfig.json +0 -7
  152. package/vitest.config.ts +0 -38
@@ -1,132 +0,0 @@
1
- import { Prompt } from "@effect/cli";
2
- import { Effect } from "effect";
3
-
4
- import type { QuitException, Terminal } from "@effect/platform/Terminal";
5
- import type { Session } from "@expo/apple-utils";
6
- // eslint-disable-next-line import-plugin/no-namespace -- the `appleUtils` injected dependency is typed as `typeof AppleUtils` (the whole module shape); no equivalent named type exists
7
- import type * as AppleUtils from "@expo/apple-utils";
8
-
9
- import { CliRuntime } from "../services/cli-runtime";
10
- import { AppleAuthError } from "./exit-codes";
11
-
12
- type SessionProvider = Session.SessionProvider;
13
-
14
- interface ProviderResolution {
15
- readonly providerId: number | undefined;
16
- readonly switched: boolean;
17
- }
18
-
19
- const APPLE_PROVIDER_ID_ENV = "APPLE_PROVIDER_ID";
20
-
21
- const readEnv = (name: string) =>
22
- Effect.gen(function* () {
23
- const runtime = yield* CliRuntime;
24
- return yield* runtime.getEnv(name);
25
- });
26
-
27
- export const parseProviderId = (raw: string): Effect.Effect<number, AppleAuthError> => {
28
- const id = Number(raw);
29
- return Number.isInteger(id)
30
- ? Effect.succeed(id)
31
- : Effect.fail(
32
- new AppleAuthError({
33
- message: `${APPLE_PROVIDER_ID_ENV} must be a numeric provider ID, got "${raw}".`,
34
- }),
35
- );
36
- };
37
-
38
- const readEnvProviderId: Effect.Effect<number | undefined, AppleAuthError, CliRuntime> = Effect.gen(
39
- function* () {
40
- const raw = yield* readEnv(APPLE_PROVIDER_ID_ENV);
41
- if (!raw) {
42
- return undefined;
43
- }
44
- return yield* parseProviderId(raw);
45
- },
46
- );
47
-
48
- const switchSessionProvider = (
49
- appleUtils: typeof AppleUtils,
50
- providerId: number,
51
- ): Effect.Effect<void, AppleAuthError> =>
52
- Effect.tryPromise({
53
- try: async () => appleUtils.Session.setSessionProviderIdAsync(providerId),
54
- catch: (error) =>
55
- new AppleAuthError({
56
- message: `Failed to switch App Store Connect provider (${providerId}): ${String(error)}`,
57
- }),
58
- }).pipe(Effect.asVoid);
59
-
60
- const isProviderAvailable = (providers: readonly SessionProvider[], providerId: number): boolean =>
61
- providers.some((provider) => provider.providerId === providerId);
62
-
63
- /**
64
- * Resolve App Store Connect provider for an interactive session.
65
- *
66
- * Selection order: APPLE_PROVIDER_ID env → valid cached pick → single available
67
- * → preserve apple-utils' auto-resolved provider → prompt.
68
- *
69
- * `switched` flags that the apple-utils cookie jar was mutated; previously-captured
70
- * cookies are stale and callers should re-extract.
71
- *
72
- * Headless-safe: prompt only fires when no env, no valid cache, multiple providers,
73
- * AND apple-utils returned no auto-resolved provider.
74
- */
75
- export const resolveProvider = (
76
- appleUtils: typeof AppleUtils,
77
- availableProviders: readonly SessionProvider[],
78
- currentProviderId: number | undefined,
79
- cachedProviderId: number | undefined,
80
- ): Effect.Effect<ProviderResolution, AppleAuthError | QuitException, CliRuntime | Terminal> =>
81
- Effect.gen(function* () {
82
- let switched = false;
83
-
84
- const applyChoice = (picked: number) =>
85
- Effect.gen(function* () {
86
- if (currentProviderId !== picked) {
87
- yield* switchSessionProvider(appleUtils, picked);
88
- switched = true;
89
- }
90
- return picked;
91
- });
92
-
93
- const envId = yield* readEnvProviderId;
94
- if (envId !== undefined) {
95
- const id = yield* applyChoice(envId);
96
- return { providerId: id, switched };
97
- }
98
-
99
- if (
100
- cachedProviderId !== undefined &&
101
- isProviderAvailable(availableProviders, cachedProviderId)
102
- ) {
103
- const id = yield* applyChoice(cachedProviderId);
104
- return { providerId: id, switched };
105
- }
106
-
107
- if (availableProviders.length === 0) {
108
- return { providerId: currentProviderId, switched };
109
- }
110
- const [firstProvider] = availableProviders;
111
- if (availableProviders.length === 1 && firstProvider) {
112
- const id = yield* applyChoice(firstProvider.providerId);
113
- return { providerId: id, switched };
114
- }
115
-
116
- // Multi-provider, no explicit signal: respect apple-utils auto-resolution
117
- // (CI-safe). Only fall through to prompt when apple-utils returned nothing.
118
- if (currentProviderId !== undefined) {
119
- return { providerId: currentProviderId, switched };
120
- }
121
-
122
- const picked = yield* Prompt.select({
123
- message: "Select App Store Connect provider:",
124
- choices: availableProviders.map((provider) => ({
125
- title: `${provider.name} [${provider.subType}] (${provider.providerId})`,
126
- value: provider.providerId,
127
- })),
128
- });
129
-
130
- const id = yield* applyChoice(picked);
131
- return { providerId: id, switched };
132
- });
@@ -1,195 +0,0 @@
1
- import { FileSystem } from "@effect/platform";
2
- import { it } from "@effect/vitest";
3
- import { Effect, Exit, Option } from "effect";
4
-
5
- import { findAndroidArtifact, findIosArtifact } from "./artifact-finder";
6
- import { ArtifactNotFoundError } from "./exit-codes";
7
- import { failureError } from "./test-utils";
8
-
9
- // ── fake FS ───────────────────────────────────────────────────────
10
-
11
- interface FakeFile {
12
- readonly type: "File";
13
- readonly mtimeMs: number;
14
- }
15
- interface FakeDir {
16
- readonly type: "Directory";
17
- }
18
- type FakeEntry = FakeFile | FakeDir;
19
-
20
- const mkInfo = (entry: FakeEntry) =>
21
- ({
22
- type: entry.type,
23
- mtime: entry.type === "File" ? Option.some(new Date(entry.mtimeMs)) : Option.none<Date>(),
24
- atime: Option.none<Date>(),
25
- birthtime: Option.none<Date>(),
26
- dev: 0,
27
- ino: Option.none<number>(),
28
- mode: 0o644,
29
- nlink: Option.none<number>(),
30
- uid: Option.none<number>(),
31
- gid: Option.none<number>(),
32
- rdev: Option.none<number>(),
33
- size: 0n,
34
- blksize: Option.none<bigint>(),
35
- blocks: Option.none<number>(),
36
- }) as unknown as FileSystem.File.Info;
37
-
38
- const makeFakeFs = (entries: Record<string, FakeEntry>) => {
39
- const paths = new Set(Object.keys(entries));
40
- return FileSystem.layerNoop({
41
- exists: (targetPath: string) => Effect.succeed(paths.has(targetPath)),
42
- readDirectory: (targetPath: string) =>
43
- Effect.sync(() => {
44
- const prefix = `${targetPath}/`;
45
- const children = new Set<string>();
46
- for (const full of paths) {
47
- if (full.startsWith(prefix)) {
48
- const rest = full.slice(prefix.length);
49
- const [head] = rest.split("/");
50
- if (head) {
51
- children.add(head);
52
- }
53
- }
54
- }
55
- return [...children];
56
- }),
57
- stat: (targetPath: string) => {
58
- const entry = entries[targetPath];
59
- if (!entry) {
60
- return Effect.die(new Error(`ENOENT: ${targetPath}`));
61
- }
62
- return Effect.succeed(mkInfo(entry));
63
- },
64
- });
65
- };
66
-
67
- // ── iOS tests ─────────────────────────────────────────────────────
68
-
69
- describe(findIosArtifact, () => {
70
- it.effect("returns the newest .ipa under exportPath", () =>
71
- Effect.gen(function* () {
72
- const fs = makeFakeFs({
73
- "/export": { type: "Directory" },
74
- "/export/OldBuild.ipa": { type: "File", mtimeMs: 1000 },
75
- "/export/NewBuild.ipa": { type: "File", mtimeMs: 5000 },
76
- "/export/notes.txt": { type: "File", mtimeMs: 6000 },
77
- });
78
- const result = yield* findIosArtifact({ exportPath: "/export" }).pipe(Effect.provide(fs));
79
- expect(result).toBe("/export/NewBuild.ipa");
80
- }),
81
- );
82
-
83
- it.effect("fails when no .ipa found", () =>
84
- Effect.gen(function* () {
85
- const fs = makeFakeFs({
86
- "/export": { type: "Directory" },
87
- "/export/readme.md": { type: "File", mtimeMs: 1 },
88
- });
89
- const exit = yield* findIosArtifact({ exportPath: "/export" }).pipe(
90
- Effect.provide(fs),
91
- Effect.exit,
92
- );
93
- expect(Exit.isFailure(exit)).toBe(true);
94
- if (Exit.isFailure(exit)) {
95
- const error = failureError(exit);
96
- expect(error).toBeInstanceOf(ArtifactNotFoundError);
97
- }
98
- }),
99
- );
100
-
101
- it.effect("returns nested .ipa via walk", () =>
102
- Effect.gen(function* () {
103
- const fs = makeFakeFs({
104
- "/export": { type: "Directory" },
105
- "/export/nested": { type: "Directory" },
106
- "/export/nested/inner.ipa": { type: "File", mtimeMs: 9000 },
107
- });
108
- const result = yield* findIosArtifact({ exportPath: "/export" }).pipe(Effect.provide(fs));
109
- expect(result).toBe("/export/nested/inner.ipa");
110
- }),
111
- );
112
- });
113
-
114
- // ── Android tests ─────────────────────────────────────────────────
115
-
116
- describe(findAndroidArtifact, () => {
117
- it.effect("finds .aab at expected Gradle output path (no flavor)", () =>
118
- Effect.gen(function* () {
119
- const fs = makeFakeFs({
120
- "/project/android/app/build/outputs": { type: "Directory" },
121
- "/project/android/app/build/outputs/bundle": { type: "Directory" },
122
- "/project/android/app/build/outputs/bundle/release": { type: "Directory" },
123
- "/project/android/app/build/outputs/bundle/release/app.aab": {
124
- type: "File",
125
- mtimeMs: 1234,
126
- },
127
- });
128
- const result = yield* findAndroidArtifact({
129
- projectRoot: "/project",
130
- format: "aab",
131
- buildType: "release",
132
- }).pipe(Effect.provide(fs));
133
- expect(result).toBe("/project/android/app/build/outputs/bundle/release/app.aab");
134
- }),
135
- );
136
-
137
- it.effect("finds .apk at expected Gradle output path with flavor", () =>
138
- Effect.gen(function* () {
139
- const fs = makeFakeFs({
140
- "/p/android/app/build/outputs": { type: "Directory" },
141
- "/p/android/app/build/outputs/apk": { type: "Directory" },
142
- "/p/android/app/build/outputs/apk/prodRelease": { type: "Directory" },
143
- "/p/android/app/build/outputs/apk/prodRelease/app-prod-release.apk": {
144
- type: "File",
145
- mtimeMs: 100,
146
- },
147
- });
148
- const result = yield* findAndroidArtifact({
149
- projectRoot: "/p",
150
- format: "apk",
151
- flavor: "prod",
152
- buildType: "release",
153
- }).pipe(Effect.provide(fs));
154
- expect(result).toBe("/p/android/app/build/outputs/apk/prodRelease/app-prod-release.apk");
155
- }),
156
- );
157
-
158
- it.effect("falls back to walking outputs tree when expected dir missing", () =>
159
- Effect.gen(function* () {
160
- const fs = makeFakeFs({
161
- "/p/android/app/build/outputs": { type: "Directory" },
162
- "/p/android/app/build/outputs/bundle": { type: "Directory" },
163
- "/p/android/app/build/outputs/bundle/somewhere-else": { type: "Directory" },
164
- "/p/android/app/build/outputs/bundle/somewhere-else/fallback.aab": {
165
- type: "File",
166
- mtimeMs: 500,
167
- },
168
- });
169
- const result = yield* findAndroidArtifact({
170
- projectRoot: "/p",
171
- format: "aab",
172
- buildType: "release",
173
- }).pipe(Effect.provide(fs));
174
- expect(result).toBe("/p/android/app/build/outputs/bundle/somewhere-else/fallback.aab");
175
- }),
176
- );
177
-
178
- it.effect("fails with ArtifactNotFoundError when nothing matches", () =>
179
- Effect.gen(function* () {
180
- const fs = makeFakeFs({
181
- "/p/android/app/build/outputs": { type: "Directory" },
182
- });
183
- const exit = yield* findAndroidArtifact({
184
- projectRoot: "/p",
185
- format: "aab",
186
- buildType: "release",
187
- }).pipe(Effect.provide(fs), Effect.exit);
188
- expect(Exit.isFailure(exit)).toBe(true);
189
- if (Exit.isFailure(exit)) {
190
- const error = failureError(exit);
191
- expect(error).toBeInstanceOf(ArtifactNotFoundError);
192
- }
193
- }),
194
- );
195
- });
@@ -1,122 +0,0 @@
1
- import path from "node:path";
2
-
3
- import { FileSystem } from "@effect/platform";
4
- import { Effect, Option } from "effect";
5
- import { maxBy } from "es-toolkit";
6
-
7
- import type { PlatformError } from "@effect/platform/Error";
8
-
9
- import { ArtifactNotFoundError } from "./exit-codes";
10
- import { capitalize } from "./string-utils";
11
-
12
- export interface FindIosArtifactOptions {
13
- readonly exportPath: string;
14
- }
15
-
16
- export interface FindAndroidArtifactOptions {
17
- readonly projectRoot: string;
18
- readonly format: "apk" | "aab";
19
- readonly flavor?: string;
20
- readonly buildType: "debug" | "release";
21
- /**
22
- * If provided, only artifacts with mtimeMs >= this value are considered.
23
- * Used to exclude stale artifacts from previous builds when the current
24
- * build failed to write an expected output.
25
- */
26
- readonly minMtimeMs?: number;
27
- }
28
-
29
- interface FoundFile {
30
- readonly path: string;
31
- readonly mtimeMs: number;
32
- }
33
-
34
- const walkAndFind = (
35
- root: string,
36
- extension: string,
37
- ): Effect.Effect<readonly FoundFile[], PlatformError, FileSystem.FileSystem> =>
38
- Effect.gen(function* () {
39
- const fs = yield* FileSystem.FileSystem;
40
- // No fs.exists pre-check: readDirectory on a missing/non-dir path fails
41
- // With a PlatformError that we catch into an empty list below.
42
- const entries = yield* fs.readDirectory(root).pipe(Effect.orElseSucceed(() => []));
43
-
44
- const results: FoundFile[] = [];
45
- for (const entry of entries) {
46
- const fullPath = path.join(root, entry);
47
- const stat = yield* fs.stat(fullPath).pipe(Effect.option);
48
- if (Option.isSome(stat)) {
49
- const info = stat.value;
50
- if (info.type === "Directory") {
51
- const nested = yield* walkAndFind(fullPath, extension);
52
- results.push(...nested);
53
- } else if (info.type === "File" && entry.toLowerCase().endsWith(extension)) {
54
- results.push({
55
- path: fullPath,
56
- mtimeMs: Option.match(info.mtime, {
57
- onNone: () => 0,
58
- onSome: (date) => date.getTime(),
59
- }),
60
- });
61
- }
62
- }
63
- }
64
- return results;
65
- });
66
-
67
- const newest = (files: readonly FoundFile[], minMtimeMs?: number): FoundFile | undefined => {
68
- const eligible =
69
- minMtimeMs === undefined ? files : files.filter((file) => file.mtimeMs >= minMtimeMs);
70
- return maxBy(eligible, (file) => file.mtimeMs);
71
- };
72
-
73
- export const findIosArtifact = ({
74
- exportPath,
75
- }: FindIosArtifactOptions): Effect.Effect<
76
- string,
77
- ArtifactNotFoundError | PlatformError,
78
- FileSystem.FileSystem
79
- > =>
80
- Effect.gen(function* () {
81
- const files = yield* walkAndFind(exportPath, ".ipa");
82
- const picked = newest(files);
83
- if (!picked) {
84
- return yield* new ArtifactNotFoundError({
85
- message: `No .ipa file found under "${exportPath}".`,
86
- });
87
- }
88
- return picked.path;
89
- });
90
-
91
- export const findAndroidArtifact = ({
92
- projectRoot,
93
- format,
94
- flavor,
95
- buildType,
96
- minMtimeMs,
97
- }: FindAndroidArtifactOptions): Effect.Effect<
98
- string,
99
- ArtifactNotFoundError | PlatformError,
100
- FileSystem.FileSystem
101
- > =>
102
- Effect.gen(function* () {
103
- const outputsRoot = path.join(projectRoot, "android", "app", "build", "outputs");
104
- const subdir = format === "aab" ? "bundle" : "apk";
105
- const variantDir = flavor ? `${flavor}${capitalize(buildType)}` : buildType;
106
- const expectedDir = path.join(outputsRoot, subdir, variantDir);
107
-
108
- const direct = yield* walkAndFind(expectedDir, `.${format}`);
109
- const pickedDirect = newest(direct, minMtimeMs);
110
- if (pickedDirect) {
111
- return pickedDirect.path;
112
- }
113
-
114
- const fallback = yield* walkAndFind(outputsRoot, `.${format}`);
115
- const pickedFallback = newest(fallback, minMtimeMs);
116
- if (!pickedFallback) {
117
- return yield* new ArtifactNotFoundError({
118
- message: `No .${format} artifact found under "${outputsRoot}"${minMtimeMs === undefined ? "" : " (newer than build start)"}.`,
119
- });
120
- }
121
- return pickedFallback.path;
122
- });
@@ -1,88 +0,0 @@
1
- import { Effect } from "effect";
2
-
3
- import {
4
- BrowserLoginSessionClosedError,
5
- BrowserLoginTimeoutError,
6
- CALLBACK_PAGE,
7
- createBrowserLoginSession,
8
- } from "./browser-login";
9
-
10
- describe(createBrowserLoginSession, () => {
11
- it("serves the callback page HTML", async () => {
12
- const session = createBrowserLoginSession({ timeoutMs: 1000 });
13
-
14
- try {
15
- const response = await session.handleRequest(new Request("http://127.0.0.1/callback"));
16
- const html = await response.text();
17
-
18
- expect(response.status).toBe(200);
19
- expect(response.headers.get("content-type")).toContain("text/html");
20
- expect(html).toContain("Completing CLI login");
21
- expect(html).toContain(CALLBACK_PAGE.slice(0, 15));
22
- } finally {
23
- session.dispose();
24
- }
25
- });
26
-
27
- it("resolves the submitted token", async () => {
28
- const session = createBrowserLoginSession({ timeoutMs: 1000 });
29
-
30
- try {
31
- const response = await session.handleRequest(
32
- new Request("http://127.0.0.1/callback/token", {
33
- method: "POST",
34
- headers: { "content-type": "application/json" },
35
- body: JSON.stringify({ token: "bu_secret_123" }),
36
- }),
37
- );
38
-
39
- expect(response.status).toBe(200);
40
- await expect(Effect.runPromise(session.waitForToken)).resolves.toBe("bu_secret_123");
41
- } finally {
42
- session.dispose();
43
- }
44
- });
45
-
46
- it("rejects invalid callback payloads", async () => {
47
- const session = createBrowserLoginSession({ timeoutMs: 50 });
48
-
49
- try {
50
- const response = await session.handleRequest(
51
- new Request("http://127.0.0.1/callback/token", {
52
- method: "POST",
53
- headers: { "content-type": "application/json" },
54
- body: JSON.stringify({ token: "" }),
55
- }),
56
- );
57
-
58
- expect(response.status).toBe(400);
59
- const error = await Effect.runPromise(Effect.flip(session.waitForToken));
60
- expect(error).toBeInstanceOf(BrowserLoginTimeoutError);
61
- expect(error._tag).toBe("BrowserLoginTimeoutError");
62
- } finally {
63
- session.dispose();
64
- }
65
- });
66
-
67
- it("times out when no token arrives", async () => {
68
- const session = createBrowserLoginSession({ timeoutMs: 20 });
69
-
70
- try {
71
- const error = await Effect.runPromise(Effect.flip(session.waitForToken));
72
- expect(error).toBeInstanceOf(BrowserLoginTimeoutError);
73
- expect(error._tag).toBe("BrowserLoginTimeoutError");
74
- } finally {
75
- session.dispose();
76
- }
77
- });
78
-
79
- it("fails with a tagged error when the session is disposed", async () => {
80
- const session = createBrowserLoginSession({ timeoutMs: 1000 });
81
-
82
- session.dispose();
83
-
84
- const error = await Effect.runPromise(Effect.flip(session.waitForToken));
85
- expect(error).toBeInstanceOf(BrowserLoginSessionClosedError);
86
- expect(error._tag).toBe("BrowserLoginSessionClosedError");
87
- });
88
- });