@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
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { renderSigningGradle } from "./android-signing-gradle";
|
|
2
|
-
|
|
3
|
-
describe(renderSigningGradle, () => {
|
|
4
|
-
it("renders standard release signing config", () => {
|
|
5
|
-
const script = renderSigningGradle({
|
|
6
|
-
keystorePath: "/tmp/release.keystore",
|
|
7
|
-
storePassword: "store-pass",
|
|
8
|
-
keyAlias: "my-key",
|
|
9
|
-
keyPassword: "key-pass",
|
|
10
|
-
});
|
|
11
|
-
expect(script).toMatchInlineSnapshot(`
|
|
12
|
-
"allprojects {
|
|
13
|
-
afterEvaluate { project ->
|
|
14
|
-
if (project.plugins.hasPlugin('com.android.application')) {
|
|
15
|
-
project.android {
|
|
16
|
-
signingConfigs {
|
|
17
|
-
release {
|
|
18
|
-
storeFile file('/tmp/release.keystore')
|
|
19
|
-
storePassword 'store-pass'
|
|
20
|
-
keyAlias 'my-key'
|
|
21
|
-
keyPassword 'key-pass'
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
buildTypes {
|
|
25
|
-
release {
|
|
26
|
-
signingConfig signingConfigs.release
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
"
|
|
34
|
-
`);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("references com.android.application plugin check + afterEvaluate", () => {
|
|
38
|
-
const script = renderSigningGradle({
|
|
39
|
-
keystorePath: "/k",
|
|
40
|
-
storePassword: "s",
|
|
41
|
-
keyAlias: "a",
|
|
42
|
-
keyPassword: "k",
|
|
43
|
-
});
|
|
44
|
-
expect(script).toContain("allprojects {");
|
|
45
|
-
expect(script).toContain("afterEvaluate { project ->");
|
|
46
|
-
expect(script).toContain("project.plugins.hasPlugin('com.android.application')");
|
|
47
|
-
expect(script).toContain("signingConfigs {");
|
|
48
|
-
expect(script).toContain("buildTypes {");
|
|
49
|
-
expect(script).toContain("signingConfig signingConfigs.release");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("escapes single quotes in passwords", () => {
|
|
53
|
-
const script = renderSigningGradle({
|
|
54
|
-
keystorePath: "/k",
|
|
55
|
-
storePassword: "can't",
|
|
56
|
-
keyAlias: "won't",
|
|
57
|
-
keyPassword: "pass'word",
|
|
58
|
-
});
|
|
59
|
-
expect(script).toContain(String.raw`storePassword 'can\'t'`);
|
|
60
|
-
expect(script).toContain(String.raw`keyAlias 'won\'t'`);
|
|
61
|
-
expect(script).toContain(String.raw`keyPassword 'pass\'word'`);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("escapes backslashes in path", () => {
|
|
65
|
-
const script = renderSigningGradle({
|
|
66
|
-
keystorePath: "C:\\keys\\release.keystore",
|
|
67
|
-
storePassword: "s",
|
|
68
|
-
keyAlias: "a",
|
|
69
|
-
keyPassword: "k",
|
|
70
|
-
});
|
|
71
|
-
expect(script).toContain(String.raw`storeFile file('C:\\keys\\release.keystore')`);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("escapes combined backslash and quote", () => {
|
|
75
|
-
const script = renderSigningGradle({
|
|
76
|
-
keystorePath: "/k",
|
|
77
|
-
storePassword: "a\\'b",
|
|
78
|
-
keyAlias: "x",
|
|
79
|
-
keyPassword: "y",
|
|
80
|
-
});
|
|
81
|
-
// \\ → \\\\ and ' → \\'
|
|
82
|
-
expect(script).toContain(String.raw`storePassword 'a\\\'b'`);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("escapes `$` in passwords to prevent Groovy interpolation", () => {
|
|
86
|
-
const script = renderSigningGradle({
|
|
87
|
-
keystorePath: "/k",
|
|
88
|
-
storePassword: "p@ss$word",
|
|
89
|
-
keyAlias: "a",
|
|
90
|
-
keyPassword: "$secret",
|
|
91
|
-
});
|
|
92
|
-
expect(script).toContain(String.raw`storePassword 'p@ss\$word'`);
|
|
93
|
-
expect(script).toContain(String.raw`keyPassword '\$secret'`);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
export interface RenderSigningGradleInput {
|
|
2
|
-
readonly keystorePath: string;
|
|
3
|
-
readonly storePassword: string;
|
|
4
|
-
readonly keyAlias: string;
|
|
5
|
-
readonly keyPassword: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Escape a Groovy single-quoted string literal: backslashes, single quotes,
|
|
10
|
-
* and `$` (to prevent string interpolation on Groovy double-quoted strings,
|
|
11
|
-
* though we use single quotes everywhere for safety).
|
|
12
|
-
*/
|
|
13
|
-
const escapeGroovySingleQuoted = (value: string): string =>
|
|
14
|
-
value
|
|
15
|
-
.replaceAll("\\", String.raw`\\`)
|
|
16
|
-
.replaceAll("'", String.raw`\'`)
|
|
17
|
-
.replaceAll("$", String.raw`\$`);
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Render a Gradle init script that injects a `release` signing config into
|
|
21
|
-
* every Android application module after evaluation. This is passed to
|
|
22
|
-
* `./gradlew --init-script <path>` so the keystore never has to live in the
|
|
23
|
-
* project tree.
|
|
24
|
-
*/
|
|
25
|
-
export const renderSigningGradle = ({
|
|
26
|
-
keystorePath,
|
|
27
|
-
storePassword,
|
|
28
|
-
keyAlias,
|
|
29
|
-
keyPassword,
|
|
30
|
-
}: RenderSigningGradleInput): string =>
|
|
31
|
-
`allprojects {
|
|
32
|
-
afterEvaluate { project ->
|
|
33
|
-
if (project.plugins.hasPlugin('com.android.application')) {
|
|
34
|
-
project.android {
|
|
35
|
-
signingConfigs {
|
|
36
|
-
release {
|
|
37
|
-
storeFile file('${escapeGroovySingleQuoted(keystorePath)}')
|
|
38
|
-
storePassword '${escapeGroovySingleQuoted(storePassword)}'
|
|
39
|
-
keyAlias '${escapeGroovySingleQuoted(keyAlias)}'
|
|
40
|
-
keyPassword '${escapeGroovySingleQuoted(keyPassword)}'
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
buildTypes {
|
|
44
|
-
release {
|
|
45
|
-
signingConfig signingConfigs.release
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
`;
|
package/src/lib/app-json.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { FileSystem } from "@effect/platform";
|
|
2
|
-
import { Effect } from "effect";
|
|
3
|
-
|
|
4
|
-
import { ProjectNotLinkedError } from "./exit-codes";
|
|
5
|
-
import { formatCause } from "./format-error";
|
|
6
|
-
import { asRecord, isRecord } from "./record";
|
|
7
|
-
|
|
8
|
-
export const readAppJson = Effect.gen(function* () {
|
|
9
|
-
const fs = yield* FileSystem.FileSystem;
|
|
10
|
-
const content = yield* fs
|
|
11
|
-
.readFileString("./app.json")
|
|
12
|
-
.pipe(
|
|
13
|
-
Effect.mapError(
|
|
14
|
-
() => new ProjectNotLinkedError({ message: "app.json not found in current directory" }),
|
|
15
|
-
),
|
|
16
|
-
);
|
|
17
|
-
const parsed = yield* Effect.try({
|
|
18
|
-
try: (): unknown => JSON.parse(content),
|
|
19
|
-
catch: () => new ProjectNotLinkedError({ message: "app.json contains malformed JSON" }),
|
|
20
|
-
});
|
|
21
|
-
if (!isRecord(parsed)) {
|
|
22
|
-
return yield* new ProjectNotLinkedError({ message: "app.json must be a JSON object" });
|
|
23
|
-
}
|
|
24
|
-
return parsed;
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
export const readProjectId = Effect.gen(function* () {
|
|
28
|
-
const appJson = yield* readAppJson;
|
|
29
|
-
const expo = asRecord(appJson["expo"]);
|
|
30
|
-
const extra = asRecord(expo?.["extra"]);
|
|
31
|
-
const betterUpdate = asRecord(extra?.["betterUpdate"]);
|
|
32
|
-
const projectId = betterUpdate?.["projectId"];
|
|
33
|
-
|
|
34
|
-
if (typeof projectId !== "string") {
|
|
35
|
-
return yield* new ProjectNotLinkedError({
|
|
36
|
-
message:
|
|
37
|
-
"Project not linked. Run `better-update link` to connect this project, or set expo.extra.betterUpdate.projectId in app.json.",
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return projectId;
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
export const readSlug = Effect.gen(function* () {
|
|
45
|
-
const appJson = yield* readAppJson;
|
|
46
|
-
const expo = asRecord(appJson["expo"]);
|
|
47
|
-
const slug = expo?.["slug"];
|
|
48
|
-
|
|
49
|
-
if (typeof slug !== "string") {
|
|
50
|
-
return yield* new ProjectNotLinkedError({
|
|
51
|
-
message: "Missing expo.slug in app.json. Required to identify the project.",
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return slug;
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
export const writeProjectId = (id: string) =>
|
|
59
|
-
Effect.gen(function* () {
|
|
60
|
-
const fs = yield* FileSystem.FileSystem;
|
|
61
|
-
const appJson = yield* readAppJson;
|
|
62
|
-
|
|
63
|
-
const expo = asRecord(appJson["expo"]) ?? {};
|
|
64
|
-
const extra = asRecord(expo["extra"]) ?? {};
|
|
65
|
-
const betterUpdate = asRecord(extra["betterUpdate"]) ?? {};
|
|
66
|
-
|
|
67
|
-
betterUpdate["projectId"] = id;
|
|
68
|
-
extra["betterUpdate"] = betterUpdate;
|
|
69
|
-
expo["extra"] = extra;
|
|
70
|
-
appJson["expo"] = expo;
|
|
71
|
-
|
|
72
|
-
yield* fs.writeFileString("./app.json", `${JSON.stringify(appJson, null, 2)}\n`);
|
|
73
|
-
}).pipe(
|
|
74
|
-
Effect.mapError((cause) =>
|
|
75
|
-
cause instanceof ProjectNotLinkedError
|
|
76
|
-
? cause
|
|
77
|
-
: new ProjectNotLinkedError({
|
|
78
|
-
message: `Failed to write project ID to app.json: ${formatCause(cause)}`,
|
|
79
|
-
}),
|
|
80
|
-
),
|
|
81
|
-
);
|
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
import { Terminal } from "@effect/platform";
|
|
2
|
-
import { it } from "@effect/vitest";
|
|
3
|
-
import { Effect, Exit, Layer, Mailbox, Option } from "effect";
|
|
4
|
-
|
|
5
|
-
import type { Session } from "@expo/apple-utils";
|
|
6
|
-
// eslint-disable-next-line import-plugin/no-namespace -- stub factory typed as `typeof AppleUtils` (whole module shape); no named type covers the full module
|
|
7
|
-
import type * as AppleUtils from "@expo/apple-utils";
|
|
8
|
-
|
|
9
|
-
import { CliRuntime } from "../services/cli-runtime";
|
|
10
|
-
import { parseProviderId, resolveProvider } from "./apple-auth";
|
|
11
|
-
import { AppleAuthError } from "./exit-codes";
|
|
12
|
-
|
|
13
|
-
// ── helpers ──────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
const provider = (
|
|
16
|
-
providerId: number,
|
|
17
|
-
name = `Provider ${providerId}`,
|
|
18
|
-
subType = "ORGANIZATION",
|
|
19
|
-
): Session.SessionProvider => ({
|
|
20
|
-
providerId,
|
|
21
|
-
publicProviderId: `pub-${providerId}`,
|
|
22
|
-
name,
|
|
23
|
-
contentTypes: ["SOFTWARE"],
|
|
24
|
-
subType,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const makeAppleUtilsStub = (setProviderSpy?: (id: number) => Promise<unknown>) =>
|
|
28
|
-
({
|
|
29
|
-
Session: {
|
|
30
|
-
setSessionProviderIdAsync: async (id: number) =>
|
|
31
|
-
setProviderSpy?.(id) ?? Promise.resolve(null),
|
|
32
|
-
},
|
|
33
|
-
}) as unknown as typeof AppleUtils;
|
|
34
|
-
|
|
35
|
-
const makeCliRuntimeLayer = (env: Readonly<Record<string, string | undefined>> = {}) =>
|
|
36
|
-
Layer.succeed(CliRuntime, {
|
|
37
|
-
argv: [],
|
|
38
|
-
platform: "linux" as NodeJS.Platform,
|
|
39
|
-
cwd: Effect.succeed("/"),
|
|
40
|
-
getEnv: (name: string) => Effect.succeed(env[name]),
|
|
41
|
-
homeDirectory: Effect.succeed("/"),
|
|
42
|
-
userName: Effect.succeed("test"),
|
|
43
|
-
commandEnvironment: () => Effect.succeed({}),
|
|
44
|
-
setExitCode: () => Effect.void,
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// Stub Terminal — none of the non-prompt branches read from it.
|
|
48
|
-
const terminalStubLayer = Layer.succeed(Terminal.Terminal, {
|
|
49
|
-
columns: Effect.succeed(80),
|
|
50
|
-
rows: Effect.succeed(24),
|
|
51
|
-
isTTY: Effect.succeed(false),
|
|
52
|
-
readInput: Effect.dieMessage("readInput not used in tests") as never,
|
|
53
|
-
readLine: Effect.dieMessage("readLine not used in tests") as never,
|
|
54
|
-
display: () => Effect.void,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const provideTestServices = (env: Readonly<Record<string, string | undefined>> = {}) =>
|
|
58
|
-
Layer.mergeAll(makeCliRuntimeLayer(env), terminalStubLayer);
|
|
59
|
-
|
|
60
|
-
// ── parseProviderId ──────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
describe(parseProviderId, () => {
|
|
63
|
-
it.effect("accepts a positive integer string", () =>
|
|
64
|
-
Effect.gen(function* () {
|
|
65
|
-
const result = yield* parseProviderId("118573544");
|
|
66
|
-
expect(result).toBe(118_573_544);
|
|
67
|
-
}),
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
it.effect("accepts zero", () =>
|
|
71
|
-
Effect.gen(function* () {
|
|
72
|
-
const result = yield* parseProviderId("0");
|
|
73
|
-
expect(result).toBe(0);
|
|
74
|
-
}),
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
it.effect("rejects a non-numeric string with AppleAuthError", () =>
|
|
78
|
-
Effect.gen(function* () {
|
|
79
|
-
const exit = yield* Effect.exit(parseProviderId("abc"));
|
|
80
|
-
expect(Exit.isFailure(exit)).toBe(true);
|
|
81
|
-
if (Exit.isFailure(exit)) {
|
|
82
|
-
const err = exit.cause._tag === "Fail" ? exit.cause.error : null;
|
|
83
|
-
expect(err).toBeInstanceOf(AppleAuthError);
|
|
84
|
-
expect(err!.message).toContain("APPLE_PROVIDER_ID");
|
|
85
|
-
}
|
|
86
|
-
}),
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
it.effect("rejects a decimal value", () =>
|
|
90
|
-
Effect.gen(function* () {
|
|
91
|
-
const exit = yield* Effect.exit(parseProviderId("1.5"));
|
|
92
|
-
expect(Exit.isFailure(exit)).toBe(true);
|
|
93
|
-
}),
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
it.effect("rejects an empty string", () =>
|
|
97
|
-
Effect.gen(function* () {
|
|
98
|
-
// Number("") === 0, which IS an integer — guard at call site (readEnvProviderId)
|
|
99
|
-
// Skips empty strings. Document that parseProviderId treats "" as 0.
|
|
100
|
-
const result = yield* parseProviderId("");
|
|
101
|
-
expect(result).toBe(0);
|
|
102
|
-
}),
|
|
103
|
-
);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// ── resolveProvider ──────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
describe(resolveProvider, () => {
|
|
109
|
-
it.effect("uses APPLE_PROVIDER_ID env when set, switching when it differs from current", () =>
|
|
110
|
-
Effect.gen(function* () {
|
|
111
|
-
const calls: number[] = [];
|
|
112
|
-
const appleUtils = makeAppleUtilsStub(async (id) => {
|
|
113
|
-
calls.push(id);
|
|
114
|
-
return null;
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const result = yield* resolveProvider(appleUtils, [provider(1), provider(2)], 1, undefined);
|
|
118
|
-
|
|
119
|
-
expect(result).toStrictEqual({ providerId: 2, switched: true });
|
|
120
|
-
expect(calls).toStrictEqual([2]);
|
|
121
|
-
}).pipe(Effect.provide(provideTestServices({ APPLE_PROVIDER_ID: "2" }))),
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
it.effect("env match against current provider does not trigger switch", () =>
|
|
125
|
-
Effect.gen(function* () {
|
|
126
|
-
const calls: number[] = [];
|
|
127
|
-
const appleUtils = makeAppleUtilsStub(async (id) => {
|
|
128
|
-
calls.push(id);
|
|
129
|
-
return null;
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const result = yield* resolveProvider(appleUtils, [provider(1), provider(2)], 1, undefined);
|
|
133
|
-
|
|
134
|
-
expect(result).toStrictEqual({ providerId: 1, switched: false });
|
|
135
|
-
expect(calls).toStrictEqual([]);
|
|
136
|
-
}).pipe(Effect.provide(provideTestServices({ APPLE_PROVIDER_ID: "1" }))),
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
it.effect("invalid env value fails with AppleAuthError", () =>
|
|
140
|
-
Effect.gen(function* () {
|
|
141
|
-
const appleUtils = makeAppleUtilsStub();
|
|
142
|
-
const exit = yield* Effect.exit(resolveProvider(appleUtils, [provider(1)], 1, undefined));
|
|
143
|
-
expect(Exit.isFailure(exit)).toBe(true);
|
|
144
|
-
}).pipe(Effect.provide(provideTestServices({ APPLE_PROVIDER_ID: "not-a-number" }))),
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
it.effect("uses cached provider when still available", () =>
|
|
148
|
-
Effect.gen(function* () {
|
|
149
|
-
const calls: number[] = [];
|
|
150
|
-
const appleUtils = makeAppleUtilsStub(async (id) => {
|
|
151
|
-
calls.push(id);
|
|
152
|
-
return null;
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
const result = yield* resolveProvider(
|
|
156
|
-
appleUtils,
|
|
157
|
-
[provider(1), provider(2), provider(3)],
|
|
158
|
-
1,
|
|
159
|
-
3,
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
expect(result).toStrictEqual({ providerId: 3, switched: true });
|
|
163
|
-
expect(calls).toStrictEqual([3]);
|
|
164
|
-
}).pipe(Effect.provide(provideTestServices())),
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
it.effect("ignores stale cached provider and falls through to single available", () =>
|
|
168
|
-
Effect.gen(function* () {
|
|
169
|
-
const calls: number[] = [];
|
|
170
|
-
const appleUtils = makeAppleUtilsStub(async (id) => {
|
|
171
|
-
calls.push(id);
|
|
172
|
-
return null;
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// Cached provider 99 no longer in availableProviders → fall through.
|
|
176
|
-
const result = yield* resolveProvider(appleUtils, [provider(7)], 7, 99);
|
|
177
|
-
|
|
178
|
-
expect(result).toStrictEqual({ providerId: 7, switched: false });
|
|
179
|
-
expect(calls).toStrictEqual([]);
|
|
180
|
-
}).pipe(Effect.provide(provideTestServices())),
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
it.effect("returns currentProviderId when availableProviders is empty", () =>
|
|
184
|
-
Effect.gen(function* () {
|
|
185
|
-
const appleUtils = makeAppleUtilsStub();
|
|
186
|
-
|
|
187
|
-
const result = yield* resolveProvider(appleUtils, [], 5, undefined);
|
|
188
|
-
|
|
189
|
-
expect(result).toStrictEqual({ providerId: 5, switched: false });
|
|
190
|
-
}).pipe(Effect.provide(provideTestServices())),
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
it.effect("returns undefined when no providers and no current id", () =>
|
|
194
|
-
Effect.gen(function* () {
|
|
195
|
-
const appleUtils = makeAppleUtilsStub();
|
|
196
|
-
|
|
197
|
-
const result = yield* resolveProvider(appleUtils, [], undefined, undefined);
|
|
198
|
-
|
|
199
|
-
expect(result).toStrictEqual({ providerId: undefined, switched: false });
|
|
200
|
-
}).pipe(Effect.provide(provideTestServices())),
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
it.effect("single available provider applies through applyChoice", () =>
|
|
204
|
-
Effect.gen(function* () {
|
|
205
|
-
const calls: number[] = [];
|
|
206
|
-
const appleUtils = makeAppleUtilsStub(async (id) => {
|
|
207
|
-
calls.push(id);
|
|
208
|
-
return null;
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
const result = yield* resolveProvider(appleUtils, [provider(42)], undefined, undefined);
|
|
212
|
-
|
|
213
|
-
expect(result).toStrictEqual({ providerId: 42, switched: true });
|
|
214
|
-
expect(calls).toStrictEqual([42]);
|
|
215
|
-
}).pipe(Effect.provide(provideTestServices())),
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
it.effect(
|
|
219
|
-
"multi-provider with no env, no cache, autoresolved current → preserves without prompt",
|
|
220
|
-
() =>
|
|
221
|
-
Effect.gen(function* () {
|
|
222
|
-
const calls: number[] = [];
|
|
223
|
-
const appleUtils = makeAppleUtilsStub(async (id) => {
|
|
224
|
-
calls.push(id);
|
|
225
|
-
return null;
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// CurrentProviderId is set (apple-utils auto-resolved). No prompt — CI-safe.
|
|
229
|
-
const result = yield* resolveProvider(
|
|
230
|
-
appleUtils,
|
|
231
|
-
[provider(1), provider(2), provider(3)],
|
|
232
|
-
2,
|
|
233
|
-
undefined,
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
expect(result).toStrictEqual({ providerId: 2, switched: false });
|
|
237
|
-
expect(calls).toStrictEqual([]);
|
|
238
|
-
}).pipe(Effect.provide(provideTestServices())),
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
it.effect("env value takes precedence over cached pick", () =>
|
|
242
|
-
Effect.gen(function* () {
|
|
243
|
-
const calls: number[] = [];
|
|
244
|
-
const appleUtils = makeAppleUtilsStub(async (id) => {
|
|
245
|
-
calls.push(id);
|
|
246
|
-
return null;
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
const result = yield* resolveProvider(
|
|
250
|
-
appleUtils,
|
|
251
|
-
[provider(1), provider(2), provider(3)],
|
|
252
|
-
1,
|
|
253
|
-
3,
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
expect(result).toStrictEqual({ providerId: 2, switched: true });
|
|
257
|
-
expect(calls).toStrictEqual([2]);
|
|
258
|
-
}).pipe(Effect.provide(provideTestServices({ APPLE_PROVIDER_ID: "2" }))),
|
|
259
|
-
);
|
|
260
|
-
|
|
261
|
-
it.effect("propagates AppleAuthError when setSessionProviderIdAsync rejects", () =>
|
|
262
|
-
Effect.gen(function* () {
|
|
263
|
-
const appleUtils = makeAppleUtilsStub(async () => {
|
|
264
|
-
throw new Error("provider not accessible");
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
const exit = yield* Effect.exit(
|
|
268
|
-
resolveProvider(appleUtils, [provider(1), provider(2)], 1, undefined),
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
expect(Exit.isFailure(exit)).toBe(true);
|
|
272
|
-
if (Exit.isFailure(exit)) {
|
|
273
|
-
const err = exit.cause._tag === "Fail" ? exit.cause.error : null;
|
|
274
|
-
expect(err).toBeInstanceOf(AppleAuthError);
|
|
275
|
-
expect((err as AppleAuthError).message).toContain("Failed to switch");
|
|
276
|
-
}
|
|
277
|
-
}).pipe(Effect.provide(provideTestServices({ APPLE_PROVIDER_ID: "2" }))),
|
|
278
|
-
);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
// ── resolveProvider (prompt branch via scripted Terminal) ────────
|
|
282
|
-
|
|
283
|
-
interface KeyEvent {
|
|
284
|
-
readonly name: string;
|
|
285
|
-
readonly input?: string;
|
|
286
|
-
readonly ctrl?: boolean;
|
|
287
|
-
readonly meta?: boolean;
|
|
288
|
-
readonly shift?: boolean;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const toUserInput = (event: KeyEvent): Terminal.UserInput => ({
|
|
292
|
-
input: event.input ? Option.some(event.input) : Option.none(),
|
|
293
|
-
key: {
|
|
294
|
-
name: event.name,
|
|
295
|
-
ctrl: event.ctrl ?? false,
|
|
296
|
-
meta: event.meta ?? false,
|
|
297
|
-
shift: event.shift ?? false,
|
|
298
|
-
},
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Build a Terminal layer backed by a pre-filled Mailbox of scripted keystrokes.
|
|
303
|
-
* `display` output is captured into `displayed` for optional assertions.
|
|
304
|
-
*/
|
|
305
|
-
const makeScriptedTerminalLayer = (events: readonly KeyEvent[], displayed: string[]) =>
|
|
306
|
-
Layer.effect(
|
|
307
|
-
Terminal.Terminal,
|
|
308
|
-
Effect.gen(function* () {
|
|
309
|
-
const mailbox = yield* Mailbox.make<Terminal.UserInput>();
|
|
310
|
-
yield* mailbox.offerAll(events.map(toUserInput));
|
|
311
|
-
return {
|
|
312
|
-
columns: Effect.succeed(80),
|
|
313
|
-
rows: Effect.succeed(24),
|
|
314
|
-
isTTY: Effect.succeed(true),
|
|
315
|
-
readInput: Effect.succeed(mailbox),
|
|
316
|
-
readLine: Effect.dieMessage("readLine not used in prompt tests") as never,
|
|
317
|
-
display: (text: string) =>
|
|
318
|
-
Effect.sync(() => {
|
|
319
|
-
displayed.push(text);
|
|
320
|
-
}),
|
|
321
|
-
};
|
|
322
|
-
}),
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
const provideScriptedPrompt = (
|
|
326
|
-
events: readonly KeyEvent[],
|
|
327
|
-
displayed: string[],
|
|
328
|
-
env: Readonly<Record<string, string | undefined>> = {},
|
|
329
|
-
) => Layer.mergeAll(makeCliRuntimeLayer(env), makeScriptedTerminalLayer(events, displayed));
|
|
330
|
-
|
|
331
|
-
describe("resolveProvider (prompt branch)", () => {
|
|
332
|
-
it.effect("prompts when multi-provider + no env + no cache + no auto-current", () =>
|
|
333
|
-
Effect.gen(function* () {
|
|
334
|
-
const displayed: string[] = [];
|
|
335
|
-
const calls: number[] = [];
|
|
336
|
-
const appleUtils = makeAppleUtilsStub(async (id) => {
|
|
337
|
-
calls.push(id);
|
|
338
|
-
return null;
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
const result = yield* resolveProvider(
|
|
342
|
-
appleUtils,
|
|
343
|
-
[provider(10, "Org A"), provider(20, "Org B"), provider(30, "Org C")],
|
|
344
|
-
undefined,
|
|
345
|
-
undefined,
|
|
346
|
-
).pipe(
|
|
347
|
-
Effect.provide(
|
|
348
|
-
provideScriptedPrompt(
|
|
349
|
-
[{ name: "down" }, { name: "down" }, { name: "return" }],
|
|
350
|
-
displayed,
|
|
351
|
-
),
|
|
352
|
-
),
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
expect(result).toStrictEqual({ providerId: 30, switched: true });
|
|
356
|
-
expect(calls).toStrictEqual([30]);
|
|
357
|
-
const allDisplay = displayed.join("");
|
|
358
|
-
expect(allDisplay).toContain("Select App Store Connect provider");
|
|
359
|
-
expect(allDisplay).toContain("Org C");
|
|
360
|
-
}),
|
|
361
|
-
);
|
|
362
|
-
|
|
363
|
-
it.effect("enter on first item picks it without arrow keys", () =>
|
|
364
|
-
Effect.gen(function* () {
|
|
365
|
-
const displayed: string[] = [];
|
|
366
|
-
const calls: number[] = [];
|
|
367
|
-
const appleUtils = makeAppleUtilsStub(async (id) => {
|
|
368
|
-
calls.push(id);
|
|
369
|
-
return null;
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
const result = yield* resolveProvider(
|
|
373
|
-
appleUtils,
|
|
374
|
-
[provider(1), provider(2)],
|
|
375
|
-
undefined,
|
|
376
|
-
undefined,
|
|
377
|
-
).pipe(Effect.provide(provideScriptedPrompt([{ name: "return" }], displayed)));
|
|
378
|
-
|
|
379
|
-
expect(result).toStrictEqual({ providerId: 1, switched: true });
|
|
380
|
-
expect(calls).toStrictEqual([1]);
|
|
381
|
-
}),
|
|
382
|
-
);
|
|
383
|
-
|
|
384
|
-
it.effect("up-arrow wraps around from top to bottom", () =>
|
|
385
|
-
Effect.gen(function* () {
|
|
386
|
-
const displayed: string[] = [];
|
|
387
|
-
const appleUtils = makeAppleUtilsStub(async () => null);
|
|
388
|
-
|
|
389
|
-
const result = yield* resolveProvider(
|
|
390
|
-
appleUtils,
|
|
391
|
-
[provider(1), provider(2), provider(3)],
|
|
392
|
-
undefined,
|
|
393
|
-
undefined,
|
|
394
|
-
).pipe(
|
|
395
|
-
Effect.provide(provideScriptedPrompt([{ name: "up" }, { name: "return" }], displayed)),
|
|
396
|
-
);
|
|
397
|
-
|
|
398
|
-
expect(result.providerId).toBe(3);
|
|
399
|
-
expect(result.switched).toBe(true);
|
|
400
|
-
}),
|
|
401
|
-
);
|
|
402
|
-
});
|