@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
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
import { toBase64 } from "@better-update/encoding";
|
|
2
|
-
import { FileSystem } from "@effect/platform";
|
|
3
|
-
import { Effect, Match } from "effect";
|
|
4
|
-
|
|
5
|
-
import { CredentialValidationError } from "./exit-codes";
|
|
6
|
-
import { inspectP12 } from "./pkcs12";
|
|
7
|
-
|
|
8
|
-
import type { ApiClient } from "../services/api-client";
|
|
9
|
-
|
|
10
|
-
export type CliCredentialType =
|
|
11
|
-
| "distribution-certificate"
|
|
12
|
-
| "push-key"
|
|
13
|
-
| "asc-api-key"
|
|
14
|
-
| "provisioning-profile"
|
|
15
|
-
| "keystore"
|
|
16
|
-
| "google-service-account-key";
|
|
17
|
-
|
|
18
|
-
export type CliCredentialPlatform = "ios" | "android";
|
|
19
|
-
|
|
20
|
-
export interface CliCredentialRow {
|
|
21
|
-
readonly id: string;
|
|
22
|
-
readonly name: string;
|
|
23
|
-
readonly platform: CliCredentialPlatform;
|
|
24
|
-
readonly type: CliCredentialType;
|
|
25
|
-
readonly distribution: string | null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const formatDistribution = (value: string): string => value.toLowerCase().replaceAll("_", "-");
|
|
29
|
-
|
|
30
|
-
export const listAllCredentials = (api: ApiClient) =>
|
|
31
|
-
Effect.gen(function* () {
|
|
32
|
-
const [certs, pushKeys, ascKeys, profiles, keystores, googleKeys] = yield* Effect.all(
|
|
33
|
-
[
|
|
34
|
-
api.appleDistributionCertificates.list(),
|
|
35
|
-
api.applePushKeys.list(),
|
|
36
|
-
api.ascApiKeys.list(),
|
|
37
|
-
api.appleProvisioningProfiles.list({ urlParams: {} }),
|
|
38
|
-
api.androidUploadKeystores.list(),
|
|
39
|
-
api.googleServiceAccountKeys.list(),
|
|
40
|
-
],
|
|
41
|
-
{ concurrency: "unbounded" },
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
const rows: CliCredentialRow[] = [
|
|
45
|
-
...certs.items.map(
|
|
46
|
-
(cert): CliCredentialRow => ({
|
|
47
|
-
id: cert.id,
|
|
48
|
-
name: cert.serialNumber,
|
|
49
|
-
platform: "ios",
|
|
50
|
-
type: "distribution-certificate",
|
|
51
|
-
distribution: null,
|
|
52
|
-
}),
|
|
53
|
-
),
|
|
54
|
-
...pushKeys.items.map(
|
|
55
|
-
(key): CliCredentialRow => ({
|
|
56
|
-
id: key.id,
|
|
57
|
-
name: key.keyId,
|
|
58
|
-
platform: "ios",
|
|
59
|
-
type: "push-key",
|
|
60
|
-
distribution: null,
|
|
61
|
-
}),
|
|
62
|
-
),
|
|
63
|
-
...ascKeys.items.map(
|
|
64
|
-
(key): CliCredentialRow => ({
|
|
65
|
-
id: key.id,
|
|
66
|
-
name: key.name,
|
|
67
|
-
platform: "ios",
|
|
68
|
-
type: "asc-api-key",
|
|
69
|
-
distribution: null,
|
|
70
|
-
}),
|
|
71
|
-
),
|
|
72
|
-
...profiles.items.map(
|
|
73
|
-
(profile): CliCredentialRow => ({
|
|
74
|
-
id: profile.id,
|
|
75
|
-
name: profile.profileName ?? profile.bundleIdentifier,
|
|
76
|
-
platform: "ios",
|
|
77
|
-
type: "provisioning-profile",
|
|
78
|
-
distribution: formatDistribution(profile.distributionType),
|
|
79
|
-
}),
|
|
80
|
-
),
|
|
81
|
-
...keystores.items.map(
|
|
82
|
-
(ks): CliCredentialRow => ({
|
|
83
|
-
id: ks.id,
|
|
84
|
-
name: ks.keyAlias,
|
|
85
|
-
platform: "android",
|
|
86
|
-
type: "keystore",
|
|
87
|
-
distribution: null,
|
|
88
|
-
}),
|
|
89
|
-
),
|
|
90
|
-
...googleKeys.items.map(
|
|
91
|
-
(key): CliCredentialRow => ({
|
|
92
|
-
id: key.id,
|
|
93
|
-
name: key.clientEmail,
|
|
94
|
-
platform: "android",
|
|
95
|
-
type: "google-service-account-key",
|
|
96
|
-
distribution: null,
|
|
97
|
-
}),
|
|
98
|
-
),
|
|
99
|
-
];
|
|
100
|
-
|
|
101
|
-
return rows;
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
export const filterCredentials = (
|
|
105
|
-
rows: readonly CliCredentialRow[],
|
|
106
|
-
filter: {
|
|
107
|
-
readonly platform?: CliCredentialPlatform;
|
|
108
|
-
readonly type?: CliCredentialType;
|
|
109
|
-
readonly distribution?: string;
|
|
110
|
-
},
|
|
111
|
-
): CliCredentialRow[] =>
|
|
112
|
-
rows.filter((row) => {
|
|
113
|
-
if (filter.platform && row.platform !== filter.platform) {
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
if (filter.type && row.type !== filter.type) {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
if (filter.distribution && row.distribution !== filter.distribution) {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
return true;
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
export interface UploadCredentialInput {
|
|
126
|
-
readonly platform: CliCredentialPlatform;
|
|
127
|
-
readonly type: CliCredentialType;
|
|
128
|
-
readonly name: string;
|
|
129
|
-
readonly filePath: string;
|
|
130
|
-
readonly password?: string;
|
|
131
|
-
readonly distribution?: string;
|
|
132
|
-
readonly keyAlias?: string;
|
|
133
|
-
readonly keyPassword?: string;
|
|
134
|
-
readonly keyId?: string;
|
|
135
|
-
readonly issuerId?: string;
|
|
136
|
-
readonly appleTeamIdentifier?: string;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const toUtf8 = (bytes: Uint8Array): string => new TextDecoder().decode(bytes);
|
|
140
|
-
|
|
141
|
-
const missing = (label: string) =>
|
|
142
|
-
new CredentialValidationError({
|
|
143
|
-
message: `Missing --${label} required for the selected credential type.`,
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const uploadIosDistributionCertificate = (
|
|
147
|
-
api: ApiClient,
|
|
148
|
-
input: UploadCredentialInput,
|
|
149
|
-
bytes: Uint8Array,
|
|
150
|
-
) =>
|
|
151
|
-
Effect.gen(function* () {
|
|
152
|
-
if (input.password === undefined) {
|
|
153
|
-
return yield* missing("password");
|
|
154
|
-
}
|
|
155
|
-
const info = yield* inspectP12({ data: Buffer.from(bytes), password: input.password });
|
|
156
|
-
if (!info.teamId) {
|
|
157
|
-
return yield* new CredentialValidationError({
|
|
158
|
-
message:
|
|
159
|
-
"Could not derive Apple Team ID from certificate subject (expected OU=TEAMID or CN with (TEAMID)).",
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
if (!info.validFrom || !info.expiresAt) {
|
|
163
|
-
return yield* new CredentialValidationError({
|
|
164
|
-
message: "Certificate is missing notBefore/notAfter dates.",
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
const created = yield* api.appleDistributionCertificates.upload({
|
|
168
|
-
payload: {
|
|
169
|
-
p12Base64: toBase64(bytes),
|
|
170
|
-
p12Password: input.password,
|
|
171
|
-
serialNumber: info.serialNumber,
|
|
172
|
-
appleTeamIdentifier: info.teamId,
|
|
173
|
-
validFrom: info.validFrom.toISOString(),
|
|
174
|
-
validUntil: info.expiresAt.toISOString(),
|
|
175
|
-
},
|
|
176
|
-
});
|
|
177
|
-
return {
|
|
178
|
-
id: created.id,
|
|
179
|
-
name: input.name,
|
|
180
|
-
platform: "ios" as const,
|
|
181
|
-
type: "distribution-certificate" as const,
|
|
182
|
-
};
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
const uploadIosPushKey = (api: ApiClient, input: UploadCredentialInput, bytes: Uint8Array) =>
|
|
186
|
-
Effect.gen(function* () {
|
|
187
|
-
if (!input.keyId) {
|
|
188
|
-
return yield* missing("key-id");
|
|
189
|
-
}
|
|
190
|
-
if (!input.appleTeamIdentifier) {
|
|
191
|
-
return yield* missing("apple-team-identifier");
|
|
192
|
-
}
|
|
193
|
-
const created = yield* api.applePushKeys.upload({
|
|
194
|
-
payload: {
|
|
195
|
-
keyId: input.keyId,
|
|
196
|
-
p8Pem: toUtf8(bytes),
|
|
197
|
-
appleTeamIdentifier: input.appleTeamIdentifier,
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
return {
|
|
201
|
-
id: created.id,
|
|
202
|
-
name: input.name,
|
|
203
|
-
platform: "ios" as const,
|
|
204
|
-
type: "push-key" as const,
|
|
205
|
-
};
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
const uploadIosAscApiKey = (api: ApiClient, input: UploadCredentialInput, bytes: Uint8Array) =>
|
|
209
|
-
Effect.gen(function* () {
|
|
210
|
-
if (!input.keyId) {
|
|
211
|
-
return yield* missing("key-id");
|
|
212
|
-
}
|
|
213
|
-
if (!input.issuerId) {
|
|
214
|
-
return yield* missing("issuer-id");
|
|
215
|
-
}
|
|
216
|
-
const created = yield* api.ascApiKeys.upload({
|
|
217
|
-
payload: {
|
|
218
|
-
name: input.name,
|
|
219
|
-
keyId: input.keyId,
|
|
220
|
-
issuerId: input.issuerId,
|
|
221
|
-
p8Pem: toUtf8(bytes),
|
|
222
|
-
...(input.appleTeamIdentifier === undefined
|
|
223
|
-
? {}
|
|
224
|
-
: { appleTeamIdentifier: input.appleTeamIdentifier }),
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
return {
|
|
228
|
-
id: created.id,
|
|
229
|
-
name: input.name,
|
|
230
|
-
platform: "ios" as const,
|
|
231
|
-
type: "asc-api-key" as const,
|
|
232
|
-
};
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
const uploadIosProvisioningProfile = (
|
|
236
|
-
api: ApiClient,
|
|
237
|
-
input: UploadCredentialInput,
|
|
238
|
-
bytes: Uint8Array,
|
|
239
|
-
) =>
|
|
240
|
-
Effect.gen(function* () {
|
|
241
|
-
const created = yield* api.appleProvisioningProfiles.upload({
|
|
242
|
-
payload: { profileBase64: toBase64(bytes) },
|
|
243
|
-
});
|
|
244
|
-
return {
|
|
245
|
-
id: created.id,
|
|
246
|
-
name: input.name,
|
|
247
|
-
platform: "ios" as const,
|
|
248
|
-
type: "provisioning-profile" as const,
|
|
249
|
-
};
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
const uploadAndroidKeystore = (api: ApiClient, input: UploadCredentialInput, bytes: Uint8Array) =>
|
|
253
|
-
Effect.gen(function* () {
|
|
254
|
-
if (input.password === undefined) {
|
|
255
|
-
return yield* missing("password");
|
|
256
|
-
}
|
|
257
|
-
if (!input.keyAlias) {
|
|
258
|
-
return yield* missing("key-alias");
|
|
259
|
-
}
|
|
260
|
-
if (!input.keyPassword) {
|
|
261
|
-
return yield* missing("key-password");
|
|
262
|
-
}
|
|
263
|
-
const created = yield* api.androidUploadKeystores.upload({
|
|
264
|
-
payload: {
|
|
265
|
-
keystoreBase64: toBase64(bytes),
|
|
266
|
-
keyAlias: input.keyAlias,
|
|
267
|
-
keystorePassword: input.password,
|
|
268
|
-
keyPassword: input.keyPassword,
|
|
269
|
-
},
|
|
270
|
-
});
|
|
271
|
-
return {
|
|
272
|
-
id: created.id,
|
|
273
|
-
name: input.name,
|
|
274
|
-
platform: "android" as const,
|
|
275
|
-
type: "keystore" as const,
|
|
276
|
-
};
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
const uploadAndroidGoogleServiceAccountKey = (
|
|
280
|
-
api: ApiClient,
|
|
281
|
-
input: UploadCredentialInput,
|
|
282
|
-
bytes: Uint8Array,
|
|
283
|
-
) =>
|
|
284
|
-
Effect.gen(function* () {
|
|
285
|
-
const created = yield* api.googleServiceAccountKeys.upload({
|
|
286
|
-
payload: { json: toUtf8(bytes) },
|
|
287
|
-
});
|
|
288
|
-
return {
|
|
289
|
-
id: created.id,
|
|
290
|
-
name: input.name,
|
|
291
|
-
platform: "android" as const,
|
|
292
|
-
type: "google-service-account-key" as const,
|
|
293
|
-
};
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
const uploadHandlers = {
|
|
297
|
-
"ios:distribution-certificate": uploadIosDistributionCertificate,
|
|
298
|
-
"ios:push-key": uploadIosPushKey,
|
|
299
|
-
"ios:asc-api-key": uploadIosAscApiKey,
|
|
300
|
-
"ios:provisioning-profile": uploadIosProvisioningProfile,
|
|
301
|
-
"android:keystore": uploadAndroidKeystore,
|
|
302
|
-
"android:google-service-account-key": uploadAndroidGoogleServiceAccountKey,
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
export const uploadCredential = (api: ApiClient, input: UploadCredentialInput) =>
|
|
306
|
-
Effect.gen(function* () {
|
|
307
|
-
const fs = yield* FileSystem.FileSystem;
|
|
308
|
-
const bytes = yield* fs.readFile(input.filePath);
|
|
309
|
-
const key = `${input.platform}:${input.type}`;
|
|
310
|
-
type HandlerKey = keyof typeof uploadHandlers;
|
|
311
|
-
const hasKey = (candidate: string): candidate is HandlerKey =>
|
|
312
|
-
Object.hasOwn(uploadHandlers, candidate);
|
|
313
|
-
const handler = hasKey(key) ? uploadHandlers[key] : undefined;
|
|
314
|
-
if (!handler) {
|
|
315
|
-
return yield* new CredentialValidationError({
|
|
316
|
-
message: `Unsupported credential combination: platform=${input.platform} type=${input.type}`,
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
return yield* handler(api, input, bytes);
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
export const deleteCredential = (
|
|
323
|
-
api: ApiClient,
|
|
324
|
-
input: {
|
|
325
|
-
readonly id: string;
|
|
326
|
-
readonly platform: CliCredentialPlatform;
|
|
327
|
-
readonly type: CliCredentialType;
|
|
328
|
-
},
|
|
329
|
-
) => {
|
|
330
|
-
const path = { id: input.id };
|
|
331
|
-
return Match.value({ platform: input.platform, type: input.type }).pipe(
|
|
332
|
-
Match.when({ platform: "ios", type: "distribution-certificate" }, () =>
|
|
333
|
-
api.appleDistributionCertificates.delete({ path }),
|
|
334
|
-
),
|
|
335
|
-
Match.when({ platform: "ios", type: "push-key" }, () => api.applePushKeys.delete({ path })),
|
|
336
|
-
Match.when({ platform: "ios", type: "asc-api-key" }, () => api.ascApiKeys.delete({ path })),
|
|
337
|
-
Match.when({ platform: "ios", type: "provisioning-profile" }, () =>
|
|
338
|
-
api.appleProvisioningProfiles.delete({ path }),
|
|
339
|
-
),
|
|
340
|
-
Match.when({ platform: "android", type: "keystore" }, () =>
|
|
341
|
-
api.androidUploadKeystores.delete({ path }),
|
|
342
|
-
),
|
|
343
|
-
Match.when({ platform: "android", type: "google-service-account-key" }, () =>
|
|
344
|
-
api.googleServiceAccountKeys.delete({ path }),
|
|
345
|
-
),
|
|
346
|
-
Match.orElse(() =>
|
|
347
|
-
Effect.fail(
|
|
348
|
-
new CredentialValidationError({
|
|
349
|
-
message: `Unsupported credential combination: platform=${input.platform} type=${input.type}`,
|
|
350
|
-
}),
|
|
351
|
-
),
|
|
352
|
-
),
|
|
353
|
-
);
|
|
354
|
-
};
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { it } from "@effect/vitest";
|
|
2
|
-
import { Effect, Exit } from "effect";
|
|
3
|
-
|
|
4
|
-
import { pullEnvVars } from "./env-exporter";
|
|
5
|
-
import { EnvExportError } from "./exit-codes";
|
|
6
|
-
import { failureError } from "./test-utils";
|
|
7
|
-
|
|
8
|
-
import type { ApiClient } from "../services/api-client";
|
|
9
|
-
|
|
10
|
-
// ── helpers ───────────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
interface ExportResult {
|
|
13
|
-
readonly environment: string;
|
|
14
|
-
readonly items: readonly {
|
|
15
|
-
readonly key: string;
|
|
16
|
-
readonly value: string;
|
|
17
|
-
readonly visibility: "plaintext" | "sensitive" | "secret";
|
|
18
|
-
}[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const makeApiStub = (
|
|
22
|
-
exportFn: (args: {
|
|
23
|
-
urlParams: { projectId: string; environment: string };
|
|
24
|
-
}) => Effect.Effect<ExportResult, unknown>,
|
|
25
|
-
): ApiClient =>
|
|
26
|
-
({
|
|
27
|
-
"env-vars": {
|
|
28
|
-
export: exportFn,
|
|
29
|
-
},
|
|
30
|
-
}) as unknown as ApiClient;
|
|
31
|
-
|
|
32
|
-
// ── tests ─────────────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
describe(pullEnvVars, () => {
|
|
35
|
-
it.effect("flattens items into a Record<string,string>", () =>
|
|
36
|
-
Effect.gen(function* () {
|
|
37
|
-
const api = makeApiStub(() =>
|
|
38
|
-
Effect.succeed({
|
|
39
|
-
environment: "production",
|
|
40
|
-
items: [
|
|
41
|
-
{ key: "API_URL", value: "https://api.example.com", visibility: "plaintext" as const },
|
|
42
|
-
{ key: "SECRET", value: "xyz", visibility: "secret" as const },
|
|
43
|
-
],
|
|
44
|
-
}),
|
|
45
|
-
);
|
|
46
|
-
const result = yield* pullEnvVars(api, {
|
|
47
|
-
projectId: "proj_123",
|
|
48
|
-
environment: "production",
|
|
49
|
-
});
|
|
50
|
-
expect(result).toStrictEqual({
|
|
51
|
-
API_URL: "https://api.example.com",
|
|
52
|
-
SECRET: "xyz",
|
|
53
|
-
});
|
|
54
|
-
}),
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
it.effect("returns empty object when no items", () =>
|
|
58
|
-
Effect.gen(function* () {
|
|
59
|
-
const api = makeApiStub(() => Effect.succeed({ environment: "development", items: [] }));
|
|
60
|
-
const result = yield* pullEnvVars(api, {
|
|
61
|
-
projectId: "proj_123",
|
|
62
|
-
environment: "development",
|
|
63
|
-
});
|
|
64
|
-
expect(result).toStrictEqual({});
|
|
65
|
-
}),
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
it.effect("wraps API errors as EnvExportError", () =>
|
|
69
|
-
Effect.gen(function* () {
|
|
70
|
-
const api = makeApiStub(() => Effect.fail(new Error("boom")));
|
|
71
|
-
const exit = yield* pullEnvVars(api, {
|
|
72
|
-
projectId: "proj_123",
|
|
73
|
-
environment: "production",
|
|
74
|
-
}).pipe(Effect.exit);
|
|
75
|
-
expect(Exit.isFailure(exit)).toBe(true);
|
|
76
|
-
if (Exit.isFailure(exit)) {
|
|
77
|
-
const error = failureError(exit);
|
|
78
|
-
expect(error).toBeInstanceOf(EnvExportError);
|
|
79
|
-
}
|
|
80
|
-
}),
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
it.effect("forwards projectId and environment via urlParams", () =>
|
|
84
|
-
Effect.gen(function* () {
|
|
85
|
-
let receivedArgs: { urlParams: { projectId: string; environment: string } } | undefined;
|
|
86
|
-
const api = makeApiStub((args) => {
|
|
87
|
-
receivedArgs = args;
|
|
88
|
-
return Effect.succeed({ environment: args.urlParams.environment, items: [] });
|
|
89
|
-
});
|
|
90
|
-
yield* pullEnvVars(api, { projectId: "p_1", environment: "staging" });
|
|
91
|
-
expect(receivedArgs).toStrictEqual({
|
|
92
|
-
urlParams: { projectId: "p_1", environment: "staging" },
|
|
93
|
-
});
|
|
94
|
-
}),
|
|
95
|
-
);
|
|
96
|
-
});
|
package/src/lib/env-exporter.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
|
|
3
|
-
import { EnvExportError } from "./exit-codes";
|
|
4
|
-
|
|
5
|
-
import type { ApiClient } from "../services/api-client";
|
|
6
|
-
|
|
7
|
-
export interface PullEnvVarsOptions {
|
|
8
|
-
readonly projectId: string;
|
|
9
|
-
readonly environment: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Pull environment variables for a project + environment and flatten them into
|
|
14
|
-
* a key/value map. Returns an empty map when the project has no variables.
|
|
15
|
-
*/
|
|
16
|
-
export const pullEnvVars = (
|
|
17
|
-
api: ApiClient,
|
|
18
|
-
{ projectId, environment }: PullEnvVarsOptions,
|
|
19
|
-
): Effect.Effect<Record<string, string>, EnvExportError> =>
|
|
20
|
-
api["env-vars"].export({ urlParams: { projectId, environment } }).pipe(
|
|
21
|
-
Effect.map((result) => Object.fromEntries(result.items.map((item) => [item.key, item.value]))),
|
|
22
|
-
Effect.mapError(
|
|
23
|
-
(cause) =>
|
|
24
|
-
new EnvExportError({
|
|
25
|
-
message: `Failed to export environment variables for "${environment}": ${String(cause)}`,
|
|
26
|
-
}),
|
|
27
|
-
),
|
|
28
|
-
);
|
package/src/lib/exit-codes.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/* eslint-disable eslint/max-classes-per-file -- dedicated error-taxonomy module: each error class is a small, purpose-built tag used across the CLI for Effect.catchTag */
|
|
2
|
-
|
|
3
|
-
import { Data } from "effect";
|
|
4
|
-
|
|
5
|
-
export class AuthRequiredError extends Data.TaggedError("AuthRequiredError")<{
|
|
6
|
-
readonly message: string;
|
|
7
|
-
}> {}
|
|
8
|
-
|
|
9
|
-
export class ProjectNotLinkedError extends Data.TaggedError("ProjectNotLinkedError")<{
|
|
10
|
-
readonly message: string;
|
|
11
|
-
}> {}
|
|
12
|
-
|
|
13
|
-
export class UploadFailedError extends Data.TaggedError("UploadFailedError")<{
|
|
14
|
-
readonly message: string;
|
|
15
|
-
}> {}
|
|
16
|
-
|
|
17
|
-
export class BuildProfileError extends Data.TaggedError("BuildProfileError")<{
|
|
18
|
-
readonly message: string;
|
|
19
|
-
}> {}
|
|
20
|
-
|
|
21
|
-
export class RuntimeVersionError extends Data.TaggedError("RuntimeVersionError")<{
|
|
22
|
-
readonly message: string;
|
|
23
|
-
}> {}
|
|
24
|
-
|
|
25
|
-
export class MissingCredentialsError extends Data.TaggedError("MissingCredentialsError")<{
|
|
26
|
-
readonly message: string;
|
|
27
|
-
readonly hint: string;
|
|
28
|
-
}> {}
|
|
29
|
-
|
|
30
|
-
export class BuildFailedError extends Data.TaggedError("BuildFailedError")<{
|
|
31
|
-
readonly step: string;
|
|
32
|
-
readonly exitCode: number;
|
|
33
|
-
readonly message: string;
|
|
34
|
-
}> {}
|
|
35
|
-
|
|
36
|
-
export class ReserveError extends Data.TaggedError("ReserveError")<{
|
|
37
|
-
readonly message: string;
|
|
38
|
-
}> {}
|
|
39
|
-
|
|
40
|
-
export class CompleteError extends Data.TaggedError("CompleteError")<{
|
|
41
|
-
readonly message: string;
|
|
42
|
-
}> {}
|
|
43
|
-
|
|
44
|
-
export class PresignedUrlExpiredError extends Data.TaggedError("PresignedUrlExpiredError")<{
|
|
45
|
-
readonly message: string;
|
|
46
|
-
}> {}
|
|
47
|
-
|
|
48
|
-
export class ArtifactNotFoundError extends Data.TaggedError("ArtifactNotFoundError")<{
|
|
49
|
-
readonly message: string;
|
|
50
|
-
}> {}
|
|
51
|
-
|
|
52
|
-
export class KeychainError extends Data.TaggedError("KeychainError")<{
|
|
53
|
-
readonly message: string;
|
|
54
|
-
}> {}
|
|
55
|
-
|
|
56
|
-
export class ProvisioningError extends Data.TaggedError("ProvisioningError")<{
|
|
57
|
-
readonly message: string;
|
|
58
|
-
}> {}
|
|
59
|
-
|
|
60
|
-
export class EnvExportError extends Data.TaggedError("EnvExportError")<{
|
|
61
|
-
readonly message: string;
|
|
62
|
-
}> {}
|
|
63
|
-
|
|
64
|
-
export class UpdatePublishError extends Data.TaggedError("UpdatePublishError")<{
|
|
65
|
-
readonly message: string;
|
|
66
|
-
}> {}
|
|
67
|
-
|
|
68
|
-
export class UpdateRollbackError extends Data.TaggedError("UpdateRollbackError")<{
|
|
69
|
-
readonly message: string;
|
|
70
|
-
}> {}
|
|
71
|
-
|
|
72
|
-
export class UpdatePromoteError extends Data.TaggedError("UpdatePromoteError")<{
|
|
73
|
-
readonly message: string;
|
|
74
|
-
}> {}
|
|
75
|
-
|
|
76
|
-
export class CredentialValidationError extends Data.TaggedError("CredentialValidationError")<{
|
|
77
|
-
readonly message: string;
|
|
78
|
-
}> {}
|
|
79
|
-
|
|
80
|
-
export class AppleAuthError extends Data.TaggedError("AppleAuthError")<{
|
|
81
|
-
readonly message: string;
|
|
82
|
-
}> {}
|
package/src/lib/expo-config.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import process from "node:process";
|
|
2
|
-
|
|
3
|
-
import { Effect } from "effect";
|
|
4
|
-
|
|
5
|
-
import { BuildProfileError } from "./exit-codes";
|
|
6
|
-
|
|
7
|
-
import type { AppMeta, Platform, RawRuntimeVersion } from "./build-profile";
|
|
8
|
-
|
|
9
|
-
interface ExpoConfig {
|
|
10
|
-
readonly name?: string;
|
|
11
|
-
readonly slug?: string;
|
|
12
|
-
readonly version?: string;
|
|
13
|
-
readonly runtimeVersion?: string | { readonly policy: string };
|
|
14
|
-
readonly ios?: {
|
|
15
|
-
readonly bundleIdentifier?: string;
|
|
16
|
-
readonly buildNumber?: string;
|
|
17
|
-
};
|
|
18
|
-
readonly android?: {
|
|
19
|
-
readonly package?: string;
|
|
20
|
-
readonly versionCode?: number;
|
|
21
|
-
};
|
|
22
|
-
readonly [key: string]: unknown;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Resolve the full Expo config using `@expo/config`, which handles
|
|
27
|
-
* `app.json`, `app.config.js`, and `app.config.ts` with plugin evaluation.
|
|
28
|
-
*
|
|
29
|
-
* `envVars` are applied as a scoped overlay on `process.env` for the duration
|
|
30
|
-
* of the call (restored afterwards) so dynamic configs (`app.config.js`)
|
|
31
|
-
* can read them without leaking server-side secrets to child processes.
|
|
32
|
-
*
|
|
33
|
-
* Falls back to undefined if `@expo/config` is not available or fails.
|
|
34
|
-
*/
|
|
35
|
-
export const readExpoConfig = (
|
|
36
|
-
projectRoot: string,
|
|
37
|
-
envVars: Record<string, string> = {},
|
|
38
|
-
): Effect.Effect<ExpoConfig | undefined> =>
|
|
39
|
-
Effect.acquireUseRelease(
|
|
40
|
-
Effect.sync(() => {
|
|
41
|
-
const previous: Record<string, string | undefined> = {};
|
|
42
|
-
for (const [key, value] of Object.entries(envVars)) {
|
|
43
|
-
previous[key] = process.env[key];
|
|
44
|
-
process.env[key] = value;
|
|
45
|
-
}
|
|
46
|
-
return previous;
|
|
47
|
-
}),
|
|
48
|
-
() =>
|
|
49
|
-
Effect.try({
|
|
50
|
-
try: () => {
|
|
51
|
-
const expoConfigCjs =
|
|
52
|
-
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- CJS require returns `any`; narrow to @expo/config's shape at this boundary
|
|
53
|
-
require("@expo/config") as {
|
|
54
|
-
getConfig: (
|
|
55
|
-
projectRoot: string,
|
|
56
|
-
options?: { skipSDKVersionRequirement?: boolean },
|
|
57
|
-
) => { exp: ExpoConfig };
|
|
58
|
-
};
|
|
59
|
-
const { getConfig } = expoConfigCjs;
|
|
60
|
-
|
|
61
|
-
const { exp } = getConfig(projectRoot, {
|
|
62
|
-
skipSDKVersionRequirement: true,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
return exp;
|
|
66
|
-
},
|
|
67
|
-
catch: () => undefined,
|
|
68
|
-
}).pipe(Effect.catchAll(() => Effect.succeed<ExpoConfig | undefined>(undefined))),
|
|
69
|
-
(previous) =>
|
|
70
|
-
Effect.sync(() => {
|
|
71
|
-
for (const [key, value] of Object.entries(previous)) {
|
|
72
|
-
if (value === undefined) {
|
|
73
|
-
// eslint-disable-next-line typescript/no-dynamic-delete -- restoring previous process.env snapshot; keys are arbitrary env var names we captured earlier
|
|
74
|
-
delete process.env[key];
|
|
75
|
-
} else {
|
|
76
|
-
process.env[key] = value;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}),
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const extractBuildNumber = (config: ExpoConfig, platform: Platform): string | undefined => {
|
|
83
|
-
if (platform === "ios") {
|
|
84
|
-
return config.ios?.buildNumber;
|
|
85
|
-
}
|
|
86
|
-
if (config.android?.versionCode === undefined) {
|
|
87
|
-
return undefined;
|
|
88
|
-
}
|
|
89
|
-
return String(config.android.versionCode);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const extractRawRuntimeVersion = (config: ExpoConfig): RawRuntimeVersion | undefined => {
|
|
93
|
-
if (typeof config.runtimeVersion === "string") {
|
|
94
|
-
return config.runtimeVersion;
|
|
95
|
-
}
|
|
96
|
-
if (typeof config.runtimeVersion === "object") {
|
|
97
|
-
return config.runtimeVersion;
|
|
98
|
-
}
|
|
99
|
-
return undefined;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Extract AppMeta from a resolved ExpoConfig (from `@expo/config`).
|
|
104
|
-
* Mirrors `readAppMeta` from build-profile.ts but uses the resolved config
|
|
105
|
-
* which handles dynamic configs (`app.config.js`, `app.config.ts`).
|
|
106
|
-
*/
|
|
107
|
-
export const readAppMetaFromConfig = (
|
|
108
|
-
config: ExpoConfig,
|
|
109
|
-
platform: Platform,
|
|
110
|
-
): Effect.Effect<AppMeta, BuildProfileError> =>
|
|
111
|
-
Effect.gen(function* () {
|
|
112
|
-
if (platform === "ios" && !config.ios) {
|
|
113
|
-
return yield* new BuildProfileError({
|
|
114
|
-
message: "Missing expo.ios section in config. Required for iOS builds (bundleIdentifier).",
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
if (platform === "android" && !config.android) {
|
|
118
|
-
return yield* new BuildProfileError({
|
|
119
|
-
message: "Missing expo.android section in config. Required for Android builds (package).",
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
bundleId: config.ios?.bundleIdentifier,
|
|
125
|
-
androidPackage: config.android?.package,
|
|
126
|
-
appVersion: config.version,
|
|
127
|
-
buildNumber: extractBuildNumber(config, platform),
|
|
128
|
-
rawRuntimeVersion: extractRawRuntimeVersion(config),
|
|
129
|
-
};
|
|
130
|
-
});
|