@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/build-profile.ts
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
|
|
3
|
-
import { BuildProfileError } from "./exit-codes";
|
|
4
|
-
import { asRecord } from "./record";
|
|
5
|
-
|
|
6
|
-
export type Platform = "ios" | "android";
|
|
7
|
-
|
|
8
|
-
export type IosDistribution = "app-store" | "ad-hoc" | "development" | "enterprise";
|
|
9
|
-
|
|
10
|
-
export interface IosProfile {
|
|
11
|
-
readonly buildConfiguration?: string;
|
|
12
|
-
readonly distribution: IosDistribution;
|
|
13
|
-
readonly scheme?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type AndroidDistribution = "play-store" | "direct";
|
|
17
|
-
|
|
18
|
-
export interface AndroidProfile {
|
|
19
|
-
readonly buildType?: "debug" | "release";
|
|
20
|
-
readonly format: "apk" | "aab";
|
|
21
|
-
readonly flavor?: string;
|
|
22
|
-
readonly distribution: AndroidDistribution;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface BuildProfile {
|
|
26
|
-
readonly name: string;
|
|
27
|
-
readonly environment: string;
|
|
28
|
-
readonly ios?: IosProfile;
|
|
29
|
-
readonly android?: AndroidProfile;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type RawRuntimeVersion = string | { readonly policy: string };
|
|
33
|
-
|
|
34
|
-
export interface AppMeta {
|
|
35
|
-
readonly bundleId: string | undefined;
|
|
36
|
-
readonly androidPackage: string | undefined;
|
|
37
|
-
readonly appVersion: string | undefined;
|
|
38
|
-
readonly buildNumber: string | undefined;
|
|
39
|
-
readonly rawRuntimeVersion: RawRuntimeVersion | undefined;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface RuntimeVersionMeta {
|
|
43
|
-
readonly appVersion: string | undefined;
|
|
44
|
-
readonly rawRuntimeVersion: RawRuntimeVersion | undefined;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export const asString = (value: unknown): string | undefined =>
|
|
48
|
-
typeof value === "string" ? value : undefined;
|
|
49
|
-
|
|
50
|
-
const getBetterUpdateExtra = (
|
|
51
|
-
appJson: Record<string, unknown>,
|
|
52
|
-
): Record<string, unknown> | undefined => {
|
|
53
|
-
const expo = asRecord(appJson["expo"]);
|
|
54
|
-
const extra = asRecord(expo?.["extra"]);
|
|
55
|
-
return asRecord(extra?.["betterUpdate"]);
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const VALID_IOS_DISTRIBUTIONS: readonly IosDistribution[] = [
|
|
59
|
-
"app-store",
|
|
60
|
-
"ad-hoc",
|
|
61
|
-
"development",
|
|
62
|
-
"enterprise",
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
const isIosDistribution = (value: string): value is IosDistribution =>
|
|
66
|
-
(VALID_IOS_DISTRIBUTIONS as readonly string[]).includes(value);
|
|
67
|
-
|
|
68
|
-
const readIosProfile = (raw: unknown): IosProfile | undefined => {
|
|
69
|
-
const iosRaw = asRecord(raw);
|
|
70
|
-
if (!iosRaw) {
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
73
|
-
const distributionRaw = asString(iosRaw["distribution"]);
|
|
74
|
-
if (!distributionRaw) {
|
|
75
|
-
return undefined;
|
|
76
|
-
}
|
|
77
|
-
if (!isIosDistribution(distributionRaw)) {
|
|
78
|
-
return undefined;
|
|
79
|
-
}
|
|
80
|
-
const distribution: IosDistribution = distributionRaw;
|
|
81
|
-
const buildConfiguration = asString(iosRaw["buildConfiguration"]);
|
|
82
|
-
const scheme = asString(iosRaw["scheme"]);
|
|
83
|
-
return {
|
|
84
|
-
distribution,
|
|
85
|
-
...(buildConfiguration === undefined ? {} : { buildConfiguration }),
|
|
86
|
-
...(scheme === undefined ? {} : { scheme }),
|
|
87
|
-
};
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const resolveAndroidDistribution = (
|
|
91
|
-
raw: string | undefined,
|
|
92
|
-
format: "apk" | "aab",
|
|
93
|
-
): AndroidDistribution => {
|
|
94
|
-
if (raw === "play-store" || raw === "direct") {
|
|
95
|
-
return raw;
|
|
96
|
-
}
|
|
97
|
-
return format === "aab" ? "play-store" : "direct";
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const readAndroidProfile = (raw: unknown): AndroidProfile | undefined => {
|
|
101
|
-
const androidRaw = asRecord(raw);
|
|
102
|
-
if (!androidRaw) {
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
const formatValue = asString(androidRaw["format"]);
|
|
106
|
-
const format = formatValue === "apk" || formatValue === "aab" ? formatValue : undefined;
|
|
107
|
-
if (!format) {
|
|
108
|
-
return undefined;
|
|
109
|
-
}
|
|
110
|
-
const buildTypeValue = asString(androidRaw["buildType"]);
|
|
111
|
-
const buildType =
|
|
112
|
-
buildTypeValue === "debug" || buildTypeValue === "release" ? buildTypeValue : undefined;
|
|
113
|
-
const flavor = asString(androidRaw["flavor"]);
|
|
114
|
-
const distribution = resolveAndroidDistribution(asString(androidRaw["distribution"]), format);
|
|
115
|
-
return {
|
|
116
|
-
format,
|
|
117
|
-
distribution,
|
|
118
|
-
...(buildType === undefined ? {} : { buildType }),
|
|
119
|
-
...(flavor === undefined ? {} : { flavor }),
|
|
120
|
-
};
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
export const readBuildProfile = (
|
|
124
|
-
appJson: Record<string, unknown>,
|
|
125
|
-
profileName: string,
|
|
126
|
-
): Effect.Effect<BuildProfile, BuildProfileError> =>
|
|
127
|
-
Effect.gen(function* () {
|
|
128
|
-
const betterUpdate = getBetterUpdateExtra(appJson);
|
|
129
|
-
const profiles = asRecord(betterUpdate?.["profiles"]);
|
|
130
|
-
if (!profiles) {
|
|
131
|
-
return yield* new BuildProfileError({
|
|
132
|
-
message: "No build profiles defined. Add expo.extra.betterUpdate.profiles to app.json.",
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
const profileRaw = asRecord(profiles[profileName]);
|
|
136
|
-
if (!profileRaw) {
|
|
137
|
-
return yield* new BuildProfileError({
|
|
138
|
-
message: `Build profile "${profileName}" not found in app.json.`,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
const environment = asString(profileRaw["environment"]) ?? "production";
|
|
142
|
-
const ios = readIosProfile(profileRaw["ios"]);
|
|
143
|
-
const android = readAndroidProfile(profileRaw["android"]);
|
|
144
|
-
return {
|
|
145
|
-
name: profileName,
|
|
146
|
-
environment,
|
|
147
|
-
...(ios === undefined ? {} : { ios }),
|
|
148
|
-
...(android === undefined ? {} : { android }),
|
|
149
|
-
};
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
export const readRuntimeVersionMeta = (
|
|
153
|
-
appJson: Record<string, unknown>,
|
|
154
|
-
): Effect.Effect<RuntimeVersionMeta, BuildProfileError> =>
|
|
155
|
-
Effect.gen(function* () {
|
|
156
|
-
const expo = asRecord(appJson["expo"]);
|
|
157
|
-
if (!expo) {
|
|
158
|
-
return yield* new BuildProfileError({
|
|
159
|
-
message: "Missing expo section in app.json.",
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
appVersion: asString(expo["version"]),
|
|
165
|
-
rawRuntimeVersion: readRawRuntimeVersion(expo["runtimeVersion"]),
|
|
166
|
-
};
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
export const readAppMeta = (
|
|
170
|
-
appJson: Record<string, unknown>,
|
|
171
|
-
platform: Platform,
|
|
172
|
-
): Effect.Effect<AppMeta, BuildProfileError> =>
|
|
173
|
-
Effect.gen(function* () {
|
|
174
|
-
const expo = asRecord(appJson["expo"]);
|
|
175
|
-
if (!expo) {
|
|
176
|
-
return yield* new BuildProfileError({
|
|
177
|
-
message: "Missing expo section in app.json.",
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (platform === "ios") {
|
|
182
|
-
const ios = asRecord(expo["ios"]);
|
|
183
|
-
if (!ios) {
|
|
184
|
-
return yield* new BuildProfileError({
|
|
185
|
-
message:
|
|
186
|
-
"Missing expo.ios section in app.json. Required for iOS builds (bundleIdentifier).",
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
} else {
|
|
190
|
-
const android = asRecord(expo["android"]);
|
|
191
|
-
if (!android) {
|
|
192
|
-
return yield* new BuildProfileError({
|
|
193
|
-
message:
|
|
194
|
-
"Missing expo.android section in app.json. Required for Android builds (package).",
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const iosSection = asRecord(expo["ios"]);
|
|
200
|
-
const androidSection = asRecord(expo["android"]);
|
|
201
|
-
const buildNumber =
|
|
202
|
-
platform === "ios"
|
|
203
|
-
? asString(iosSection?.["buildNumber"])
|
|
204
|
-
: asStringOrNumber(androidSection?.["versionCode"]);
|
|
205
|
-
return {
|
|
206
|
-
bundleId: asString(iosSection?.["bundleIdentifier"]),
|
|
207
|
-
androidPackage: asString(androidSection?.["package"]),
|
|
208
|
-
appVersion: asString(expo["version"]),
|
|
209
|
-
buildNumber,
|
|
210
|
-
rawRuntimeVersion: readRawRuntimeVersion(expo["runtimeVersion"]),
|
|
211
|
-
};
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
const asStringOrNumber = (value: unknown): string | undefined => {
|
|
215
|
-
if (typeof value === "string") {
|
|
216
|
-
return value;
|
|
217
|
-
}
|
|
218
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
219
|
-
return String(value);
|
|
220
|
-
}
|
|
221
|
-
return undefined;
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
const readRawRuntimeVersion = (value: unknown): RawRuntimeVersion | undefined => {
|
|
225
|
-
if (typeof value === "string") {
|
|
226
|
-
return value;
|
|
227
|
-
}
|
|
228
|
-
const record = asRecord(value);
|
|
229
|
-
const policy = asString(record?.["policy"]);
|
|
230
|
-
if (policy) {
|
|
231
|
-
return { policy };
|
|
232
|
-
}
|
|
233
|
-
return undefined;
|
|
234
|
-
};
|
package/src/lib/cli-schemas.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Args, Options } from "@effect/cli";
|
|
2
|
-
import { ParseResult, Schema } from "effect";
|
|
3
|
-
|
|
4
|
-
export const RolloutPercentage = Schema.Number.pipe(
|
|
5
|
-
Schema.int(),
|
|
6
|
-
Schema.between(1, 100),
|
|
7
|
-
).annotations({
|
|
8
|
-
message: () => "Rollout percentage must be between 1 and 100.",
|
|
9
|
-
identifier: "RolloutPercentage",
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
export const rolloutPercentageOption = (name: string): Options.Options<number> =>
|
|
13
|
-
Options.integer(name).pipe(Options.withSchema(RolloutPercentage));
|
|
14
|
-
|
|
15
|
-
export const KeyValuePair = Schema.Struct({
|
|
16
|
-
key: Schema.String,
|
|
17
|
-
value: Schema.String,
|
|
18
|
-
});
|
|
19
|
-
export type KeyValuePair = Schema.Schema.Type<typeof KeyValuePair>;
|
|
20
|
-
|
|
21
|
-
export const KeyValueFromString = Schema.transformOrFail(Schema.String, KeyValuePair, {
|
|
22
|
-
strict: true,
|
|
23
|
-
decode: (input, _options, ast) => {
|
|
24
|
-
const eqIndex = input.indexOf("=");
|
|
25
|
-
if (eqIndex <= 0) {
|
|
26
|
-
return ParseResult.fail(
|
|
27
|
-
new ParseResult.Type(ast, input, "Invalid format. Use KEY=VALUE (e.g. API_KEY=abc123)"),
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
return ParseResult.succeed({
|
|
31
|
-
key: input.slice(0, eqIndex),
|
|
32
|
-
value: input.slice(eqIndex + 1),
|
|
33
|
-
});
|
|
34
|
-
},
|
|
35
|
-
encode: ({ key, value }) => ParseResult.succeed(`${key}=${value}`),
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
export const keyValueArg = (name: string): Args.Args<KeyValuePair> =>
|
|
39
|
-
Args.text({ name }).pipe(Args.withSchema(KeyValueFromString));
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
|
|
3
|
-
import { exitWith } from "../application/command-exit";
|
|
4
|
-
import { formatCause } from "./format-error";
|
|
5
|
-
|
|
6
|
-
import type { CliRuntime } from "../services/cli-runtime";
|
|
7
|
-
|
|
8
|
-
type ExitCode = 1 | 2 | 3 | 4 | 5 | 6;
|
|
9
|
-
|
|
10
|
-
interface TaggedError {
|
|
11
|
-
readonly message: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
type Handler = (error: TaggedError) => Effect.Effect<void, never, CliRuntime>;
|
|
15
|
-
|
|
16
|
-
const BASE_TAG_MAP: Record<string, ExitCode> = {
|
|
17
|
-
AuthRequiredError: 3,
|
|
18
|
-
ProjectNotLinkedError: 4,
|
|
19
|
-
NotFound: 1,
|
|
20
|
-
Conflict: 1,
|
|
21
|
-
Forbidden: 1,
|
|
22
|
-
BadRequest: 2,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const SYSTEM_TAG_MESSAGE: Record<string, (error: TaggedError) => string> = {
|
|
26
|
-
SystemError: (error) => `Filesystem error: ${error.message}`,
|
|
27
|
-
BadArgument: (error) => `Invalid argument: ${error.message}`,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const SYSTEM_TAG_CODE: Record<string, ExitCode> = {
|
|
31
|
-
SystemError: 6,
|
|
32
|
-
BadArgument: 6,
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export const makeCommandErrorHandler = (
|
|
36
|
-
extras: Record<string, ExitCode> = {},
|
|
37
|
-
): (<Success, Requirements>(
|
|
38
|
-
effect: Effect.Effect<Success, unknown, Requirements>,
|
|
39
|
-
) => Effect.Effect<Success, never, Requirements | CliRuntime>) => {
|
|
40
|
-
const combined = { ...BASE_TAG_MAP, ...extras };
|
|
41
|
-
const handlers: Record<string, Handler> = {};
|
|
42
|
-
for (const [tag, code] of Object.entries(combined)) {
|
|
43
|
-
const systemFormat = SYSTEM_TAG_MESSAGE[tag];
|
|
44
|
-
const resolvedCode = SYSTEM_TAG_CODE[tag] ?? code;
|
|
45
|
-
handlers[tag] = (error) =>
|
|
46
|
-
exitWith(resolvedCode, systemFormat ? systemFormat(error) : error.message);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return <Success, Requirements>(
|
|
50
|
-
effect: Effect.Effect<Success, unknown, Requirements>,
|
|
51
|
-
): Effect.Effect<Success, never, Requirements | CliRuntime> => {
|
|
52
|
-
const piped = effect.pipe(
|
|
53
|
-
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- Effect.catchTags tag-inference requires a literal object; we accept a dynamic handler map so tags are chosen at runtime
|
|
54
|
-
Effect.catchTags(handlers as never),
|
|
55
|
-
Effect.catchAll((cause) => exitWith(1, formatCause(cause))),
|
|
56
|
-
);
|
|
57
|
-
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- catchTags narrowing lost when handlers is dynamic; re-narrow at the boundary
|
|
58
|
-
return piped as Effect.Effect<Success, never, Requirements | CliRuntime>;
|
|
59
|
-
};
|
|
60
|
-
};
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { fromBase64 } from "@better-update/encoding";
|
|
2
|
-
import { FileSystem } from "@effect/platform";
|
|
3
|
-
import { Effect } from "effect";
|
|
4
|
-
|
|
5
|
-
import type { PlatformError } from "@effect/platform/Error";
|
|
6
|
-
|
|
7
|
-
import { MissingCredentialsError } from "./exit-codes";
|
|
8
|
-
|
|
9
|
-
import type { ApiClient } from "../services/api-client";
|
|
10
|
-
import type { IosDistribution } from "./build-profile";
|
|
11
|
-
|
|
12
|
-
export interface DownloadIosCredentialsOptions {
|
|
13
|
-
readonly projectId: string;
|
|
14
|
-
readonly bundleIdentifier: string;
|
|
15
|
-
readonly distribution: IosDistribution;
|
|
16
|
-
readonly tempDir: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface IosCredentials {
|
|
20
|
-
readonly p12Path: string;
|
|
21
|
-
readonly p12Password: string;
|
|
22
|
-
readonly profilePath: string;
|
|
23
|
-
readonly profileFilename: string;
|
|
24
|
-
readonly teamId: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const IOS_DISTRIBUTION_TO_TYPE = {
|
|
28
|
-
"app-store": "APP_STORE",
|
|
29
|
-
"ad-hoc": "AD_HOC",
|
|
30
|
-
development: "DEVELOPMENT",
|
|
31
|
-
enterprise: "ENTERPRISE",
|
|
32
|
-
} as const satisfies Record<IosDistribution, "APP_STORE" | "AD_HOC" | "DEVELOPMENT" | "ENTERPRISE">;
|
|
33
|
-
|
|
34
|
-
const bindHint =
|
|
35
|
-
"Bind the bundle via the dashboard (Credentials → iOS Bundle Configurations) and make sure a distribution certificate, provisioning profile, and ASC API key are attached.";
|
|
36
|
-
|
|
37
|
-
const permissionHint = "Ask an org admin to grant the build-credentials download permission.";
|
|
38
|
-
|
|
39
|
-
const androidBindHint =
|
|
40
|
-
"Register the package in the dashboard (Credentials → Android Build Credentials) and bind a keystore to the default group.";
|
|
41
|
-
|
|
42
|
-
interface TaggedCause {
|
|
43
|
-
readonly _tag: string;
|
|
44
|
-
readonly message?: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const hasTag = (cause: unknown): cause is TaggedCause =>
|
|
48
|
-
typeof cause === "object" && cause !== null && "_tag" in cause;
|
|
49
|
-
|
|
50
|
-
const resolveErrorToMissingCredentials = (
|
|
51
|
-
cause: unknown,
|
|
52
|
-
platform: "ios" | "android",
|
|
53
|
-
): MissingCredentialsError => {
|
|
54
|
-
const tag = hasTag(cause) ? cause._tag : null;
|
|
55
|
-
const message = hasTag(cause) && typeof cause.message === "string" ? cause.message : null;
|
|
56
|
-
const platformLabel = platform === "ios" ? "iOS" : "Android";
|
|
57
|
-
const bind = platform === "ios" ? bindHint : androidBindHint;
|
|
58
|
-
|
|
59
|
-
if (tag === "Forbidden") {
|
|
60
|
-
return new MissingCredentialsError({
|
|
61
|
-
message: message ?? `Permission denied when resolving ${platformLabel} build credentials`,
|
|
62
|
-
hint: permissionHint,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
if (tag === "NotFound") {
|
|
66
|
-
return new MissingCredentialsError({
|
|
67
|
-
message: message ?? `No ${platformLabel} build credentials configured`,
|
|
68
|
-
hint: bind,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
if (tag === "BadRequest") {
|
|
72
|
-
return new MissingCredentialsError({
|
|
73
|
-
message: message ?? `${platformLabel} build credentials are misconfigured`,
|
|
74
|
-
hint: bind,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
return new MissingCredentialsError({
|
|
78
|
-
message: message ?? `Failed to resolve ${platformLabel} build credentials`,
|
|
79
|
-
hint: bind,
|
|
80
|
-
});
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
export const downloadIosCredentials = (
|
|
84
|
-
api: ApiClient,
|
|
85
|
-
options: DownloadIosCredentialsOptions,
|
|
86
|
-
): Effect.Effect<IosCredentials, MissingCredentialsError | PlatformError, FileSystem.FileSystem> =>
|
|
87
|
-
Effect.gen(function* () {
|
|
88
|
-
const fs = yield* FileSystem.FileSystem;
|
|
89
|
-
|
|
90
|
-
const resolved = yield* api.buildCredentials
|
|
91
|
-
.resolve({
|
|
92
|
-
path: { projectId: options.projectId },
|
|
93
|
-
payload: {
|
|
94
|
-
platform: "ios" as const,
|
|
95
|
-
bundleIdentifier: options.bundleIdentifier,
|
|
96
|
-
distributionType: IOS_DISTRIBUTION_TO_TYPE[options.distribution],
|
|
97
|
-
},
|
|
98
|
-
})
|
|
99
|
-
.pipe(Effect.mapError((cause) => resolveErrorToMissingCredentials(cause, "ios")));
|
|
100
|
-
|
|
101
|
-
if (resolved.platform !== "ios") {
|
|
102
|
-
return yield* Effect.fail(
|
|
103
|
-
new MissingCredentialsError({
|
|
104
|
-
message: "Server returned non-iOS credentials for an iOS build request",
|
|
105
|
-
hint: bindHint,
|
|
106
|
-
}),
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const p12Path = `${options.tempDir}/signing.p12`;
|
|
111
|
-
const profileFilename = `${resolved.provisioningProfile.uuid ?? "profile"}.mobileprovision`;
|
|
112
|
-
const profilePath = `${options.tempDir}/${profileFilename}`;
|
|
113
|
-
|
|
114
|
-
yield* fs.writeFile(p12Path, fromBase64(resolved.distributionCertificate.p12Base64));
|
|
115
|
-
yield* fs.writeFile(
|
|
116
|
-
profilePath,
|
|
117
|
-
fromBase64(resolved.provisioningProfile.mobileprovisionBase64),
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
p12Path,
|
|
122
|
-
p12Password: resolved.distributionCertificate.p12Password,
|
|
123
|
-
profilePath,
|
|
124
|
-
profileFilename,
|
|
125
|
-
teamId: resolved.provisioningProfile.teamId,
|
|
126
|
-
};
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
export interface DownloadAndroidCredentialsOptions {
|
|
130
|
-
readonly projectId: string;
|
|
131
|
-
readonly applicationIdentifier: string;
|
|
132
|
-
readonly tempDir: string;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export interface AndroidCredentials {
|
|
136
|
-
readonly keystorePath: string;
|
|
137
|
-
readonly storePassword: string;
|
|
138
|
-
readonly keyAlias: string;
|
|
139
|
-
readonly keyPassword: string;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export const downloadAndroidCredentials = (
|
|
143
|
-
api: ApiClient,
|
|
144
|
-
options: DownloadAndroidCredentialsOptions,
|
|
145
|
-
): Effect.Effect<
|
|
146
|
-
AndroidCredentials,
|
|
147
|
-
MissingCredentialsError | PlatformError,
|
|
148
|
-
FileSystem.FileSystem
|
|
149
|
-
> =>
|
|
150
|
-
Effect.gen(function* () {
|
|
151
|
-
const fs = yield* FileSystem.FileSystem;
|
|
152
|
-
|
|
153
|
-
const resolved = yield* api.buildCredentials
|
|
154
|
-
.resolve({
|
|
155
|
-
path: { projectId: options.projectId },
|
|
156
|
-
payload: {
|
|
157
|
-
platform: "android" as const,
|
|
158
|
-
applicationIdentifier: options.applicationIdentifier,
|
|
159
|
-
},
|
|
160
|
-
})
|
|
161
|
-
.pipe(Effect.mapError((cause) => resolveErrorToMissingCredentials(cause, "android")));
|
|
162
|
-
|
|
163
|
-
if (resolved.platform !== "android") {
|
|
164
|
-
return yield* Effect.fail(
|
|
165
|
-
new MissingCredentialsError({
|
|
166
|
-
message: "Server returned non-Android credentials for an Android build request",
|
|
167
|
-
hint: androidBindHint,
|
|
168
|
-
}),
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const keystorePath = `${options.tempDir}/upload.keystore`;
|
|
173
|
-
yield* fs.writeFile(keystorePath, fromBase64(resolved.keystore.keystoreBase64));
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
keystorePath,
|
|
177
|
-
storePassword: resolved.keystore.storePassword,
|
|
178
|
-
keyAlias: resolved.keystore.keyAlias,
|
|
179
|
-
keyPassword: resolved.keystore.keyPassword,
|
|
180
|
-
};
|
|
181
|
-
});
|