@better-update/cli 0.3.0 → 0.3.2
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.js +5319 -0
- package/dist/index.js.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/src/lib/apple-auth.ts
DELETED
|
@@ -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
|
-
});
|