@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.
Files changed (152) hide show
  1. package/dist/index.js +5319 -0
  2. package/dist/index.js.map +1 -0
  3. package/package.json +12 -9
  4. package/CHANGELOG.md +0 -58
  5. package/oxlint.config.ts +0 -6
  6. package/src/app-layer.ts +0 -29
  7. package/src/application/build-workflow.ts +0 -222
  8. package/src/application/command-exit.ts +0 -13
  9. package/src/application/login.ts +0 -87
  10. package/src/application/update-promote.ts +0 -88
  11. package/src/application/update-publish.ts +0 -402
  12. package/src/application/update-rollback.ts +0 -275
  13. package/src/commands/analytics/adoption.ts +0 -40
  14. package/src/commands/analytics/channels.ts +0 -35
  15. package/src/commands/analytics/helpers.ts +0 -3
  16. package/src/commands/analytics/index.ts +0 -13
  17. package/src/commands/analytics/platforms.ts +0 -39
  18. package/src/commands/analytics/updates.ts +0 -35
  19. package/src/commands/audit-logs/helpers.ts +0 -3
  20. package/src/commands/audit-logs/index.ts +0 -8
  21. package/src/commands/audit-logs/list.ts +0 -66
  22. package/src/commands/branches.ts +0 -70
  23. package/src/commands/build/android.ts +0 -129
  24. package/src/commands/build/index.ts +0 -63
  25. package/src/commands/build/ios.ts +0 -199
  26. package/src/commands/build/reserve-and-upload.test.ts +0 -263
  27. package/src/commands/build/reserve-and-upload.ts +0 -160
  28. package/src/commands/build/run-step.ts +0 -131
  29. package/src/commands/builds/compatibility-matrix.ts +0 -48
  30. package/src/commands/builds/delete.ts +0 -15
  31. package/src/commands/builds/get.ts +0 -34
  32. package/src/commands/builds/helpers.ts +0 -3
  33. package/src/commands/builds/index.ts +0 -20
  34. package/src/commands/builds/install-link.ts +0 -20
  35. package/src/commands/builds/list.ts +0 -38
  36. package/src/commands/channels/create.ts +0 -37
  37. package/src/commands/channels/delete.ts +0 -15
  38. package/src/commands/channels/helpers.ts +0 -18
  39. package/src/commands/channels/index.ts +0 -24
  40. package/src/commands/channels/list.ts +0 -38
  41. package/src/commands/channels/pause.ts +0 -15
  42. package/src/commands/channels/resume.ts +0 -15
  43. package/src/commands/channels/rollout/complete.ts +0 -17
  44. package/src/commands/channels/rollout/create.ts +0 -36
  45. package/src/commands/channels/rollout/index.ts +0 -11
  46. package/src/commands/channels/rollout/revert.ts +0 -17
  47. package/src/commands/channels/rollout/update.ts +0 -23
  48. package/src/commands/channels/update.ts +0 -32
  49. package/src/commands/credentials/delete.ts +0 -24
  50. package/src/commands/credentials/index.ts +0 -10
  51. package/src/commands/credentials/list.ts +0 -33
  52. package/src/commands/credentials/upload.ts +0 -91
  53. package/src/commands/env/delete.ts +0 -35
  54. package/src/commands/env/export.ts +0 -27
  55. package/src/commands/env/get.ts +0 -25
  56. package/src/commands/env/helpers.ts +0 -13
  57. package/src/commands/env/import.ts +0 -31
  58. package/src/commands/env/index.ts +0 -24
  59. package/src/commands/env/list.ts +0 -44
  60. package/src/commands/env/pull.ts +0 -27
  61. package/src/commands/env/set.ts +0 -42
  62. package/src/commands/fingerprint/compare.ts +0 -25
  63. package/src/commands/fingerprint/generate.ts +0 -18
  64. package/src/commands/fingerprint/index.ts +0 -9
  65. package/src/commands/init.ts +0 -35
  66. package/src/commands/login.ts +0 -13
  67. package/src/commands/logout.ts +0 -12
  68. package/src/commands/projects.ts +0 -84
  69. package/src/commands/status.ts +0 -48
  70. package/src/commands/update/delete.ts +0 -15
  71. package/src/commands/update/helpers.ts +0 -22
  72. package/src/commands/update/index.ts +0 -22
  73. package/src/commands/update/list.ts +0 -60
  74. package/src/commands/update/promote.ts +0 -30
  75. package/src/commands/update/publish.ts +0 -94
  76. package/src/commands/update/rollback.ts +0 -42
  77. package/src/commands/update/rollout/complete.ts +0 -17
  78. package/src/commands/update/rollout/index.ts +0 -10
  79. package/src/commands/update/rollout/revert.ts +0 -17
  80. package/src/commands/update/rollout/set.ts +0 -23
  81. package/src/index.ts +0 -53
  82. package/src/lib/android-keystore.test.ts +0 -114
  83. package/src/lib/android-keystore.ts +0 -76
  84. package/src/lib/android-signing-gradle.test.ts +0 -95
  85. package/src/lib/android-signing-gradle.ts +0 -52
  86. package/src/lib/app-json.ts +0 -81
  87. package/src/lib/apple-auth.test.ts +0 -402
  88. package/src/lib/apple-auth.ts +0 -132
  89. package/src/lib/artifact-finder.test.ts +0 -195
  90. package/src/lib/artifact-finder.ts +0 -122
  91. package/src/lib/browser-login.test.ts +0 -88
  92. package/src/lib/browser-login.ts +0 -193
  93. package/src/lib/build-profile.test.ts +0 -290
  94. package/src/lib/build-profile.ts +0 -234
  95. package/src/lib/cli-schemas.ts +0 -39
  96. package/src/lib/command-errors.ts +0 -60
  97. package/src/lib/credentials-downloader.ts +0 -181
  98. package/src/lib/credentials-manager.ts +0 -354
  99. package/src/lib/env-exporter.test.ts +0 -96
  100. package/src/lib/env-exporter.ts +0 -28
  101. package/src/lib/exit-codes.ts +0 -82
  102. package/src/lib/expo-config.ts +0 -130
  103. package/src/lib/expo-export.test.ts +0 -94
  104. package/src/lib/expo-export.ts +0 -281
  105. package/src/lib/fingerprint.ts +0 -67
  106. package/src/lib/format-error.ts +0 -22
  107. package/src/lib/git-context.ts +0 -56
  108. package/src/lib/gradle-config.ts +0 -126
  109. package/src/lib/ios-export-options.test.ts +0 -98
  110. package/src/lib/ios-export-options.ts +0 -62
  111. package/src/lib/ios-keychain.ts +0 -181
  112. package/src/lib/ios-provisioning.test.ts +0 -115
  113. package/src/lib/ios-provisioning.ts +0 -179
  114. package/src/lib/output.ts +0 -32
  115. package/src/lib/pkcs12.ts +0 -73
  116. package/src/lib/plist.ts +0 -39
  117. package/src/lib/post-build-validation.ts +0 -146
  118. package/src/lib/presigned-upload.test.ts +0 -140
  119. package/src/lib/presigned-upload.ts +0 -35
  120. package/src/lib/record.ts +0 -5
  121. package/src/lib/resolve-named-resource.ts +0 -24
  122. package/src/lib/runtime-version.test.ts +0 -119
  123. package/src/lib/runtime-version.ts +0 -62
  124. package/src/lib/sha256.test.ts +0 -108
  125. package/src/lib/sha256.ts +0 -80
  126. package/src/lib/signed-payloads.test.ts +0 -181
  127. package/src/lib/signed-payloads.ts +0 -164
  128. package/src/lib/string-utils.ts +0 -4
  129. package/src/lib/temp-dir.ts +0 -14
  130. package/src/lib/test-utils.ts +0 -13
  131. package/src/lib/update-platforms.test.ts +0 -45
  132. package/src/lib/update-platforms.ts +0 -19
  133. package/src/lib/xcpretty-formatter.ts +0 -21
  134. package/src/services/api-client.ts +0 -42
  135. package/src/services/apple-session-store.ts +0 -100
  136. package/src/services/auth-store.ts +0 -85
  137. package/src/services/cli-runtime.ts +0 -46
  138. package/src/services/config-store.ts +0 -108
  139. package/src/services/presigned-upload.ts +0 -84
  140. package/src/services/update-asset-uploader.ts +0 -72
  141. package/src/types/keychain.d.ts +0 -22
  142. package/tests/e2e/build.test.ts +0 -270
  143. package/tests/e2e/commands.test.ts +0 -694
  144. package/tests/e2e/ota-lifecycle.test.ts +0 -275
  145. package/tests/e2e/publish.test.ts +0 -150
  146. package/tests/helpers/cli-e2e.ts +0 -426
  147. package/tests/helpers/pty-driver.ts +0 -142
  148. package/tests/interactive/harness/provider-prompt.ts +0 -54
  149. package/tests/interactive/login.test.ts +0 -47
  150. package/tests/interactive/provider-select.test.ts +0 -59
  151. package/tsconfig.json +0 -7
  152. package/vitest.config.ts +0 -38
@@ -1,108 +0,0 @@
1
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
-
5
- import { it } from "@effect/vitest";
6
- import { Effect, Exit } from "effect";
7
-
8
- import { BuildFailedError } from "./exit-codes";
9
- import { sha256File, sha256FileBase64Url, sha256Namespaced } from "./sha256";
10
- import { failureError } from "./test-utils";
11
-
12
- // ── fixtures ──────────────────────────────────────────────────────
13
-
14
- const HELLO_WORLD = "hello world";
15
- const HELLO_WORLD_SHA256 = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
16
-
17
- // ── helpers ───────────────────────────────────────────────────────
18
-
19
- const withTempFile = <Result>(
20
- content: Buffer,
21
- run: (path: string) => Effect.Effect<Result, BuildFailedError>,
22
- ) =>
23
- Effect.gen(function* () {
24
- const dir = mkdtempSync(join(tmpdir(), "sha256-test-"));
25
- const filePath = join(dir, "fixture.bin");
26
- writeFileSync(filePath, content);
27
- const result = yield* run(filePath).pipe(
28
- Effect.ensuring(Effect.sync(() => rmSync(dir, { recursive: true, force: true }))),
29
- );
30
- return result;
31
- });
32
-
33
- // ── tests ─────────────────────────────────────────────────────────
34
-
35
- describe(sha256File, () => {
36
- it.effect('computes known SHA-256 for "hello world"', () =>
37
- Effect.gen(function* () {
38
- const result = yield* withTempFile(Buffer.from(HELLO_WORLD, "utf8"), (path) =>
39
- sha256File(path),
40
- );
41
- expect(result.sha256).toBe(HELLO_WORLD_SHA256);
42
- expect(result.byteSize).toBe(HELLO_WORLD.length);
43
- }),
44
- );
45
-
46
- it.effect("handles an empty file", () =>
47
- Effect.gen(function* () {
48
- const result = yield* withTempFile(Buffer.alloc(0), (path) => sha256File(path));
49
- // SHA-256 of empty input
50
- expect(result.sha256).toBe(
51
- "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
52
- );
53
- expect(result.byteSize).toBe(0);
54
- }),
55
- );
56
-
57
- it.effect("streams larger content without loading all into memory", () =>
58
- Effect.gen(function* () {
59
- // 1 MiB of zeros
60
- const buf = Buffer.alloc(1_048_576, 0);
61
- const result = yield* withTempFile(buf, (path) => sha256File(path));
62
- expect(result.byteSize).toBe(1_048_576);
63
- expect(result.sha256).toHaveLength(64);
64
- }),
65
- );
66
-
67
- it.effect("fails with BuildFailedError on non-existent path", () =>
68
- Effect.gen(function* () {
69
- const exit = yield* sha256File("/nonexistent-path-for-test-xyz").pipe(Effect.exit);
70
- expect(Exit.isFailure(exit)).toBe(true);
71
- expect(failureError(exit)).toBeInstanceOf(BuildFailedError);
72
- }),
73
- );
74
- });
75
-
76
- describe(sha256FileBase64Url, () => {
77
- it.effect('computes a base64url SHA-256 digest for "hello world"', () =>
78
- Effect.gen(function* () {
79
- const result = yield* withTempFile(Buffer.from(HELLO_WORLD, "utf8"), (path) =>
80
- sha256FileBase64Url(path),
81
- );
82
- expect(result.sha256Base64Url).toBe("uU0nuZNNPgilLlLX2n2r-sSE7-N6U4DukIj3rOLvzek");
83
- expect(result.byteSize).toBe(HELLO_WORLD.length);
84
- }),
85
- );
86
- });
87
-
88
- describe(sha256Namespaced, () => {
89
- it("produces different hashes for same content with different content types", () => {
90
- const jsHash = sha256Namespaced("application/javascript", HELLO_WORLD_SHA256);
91
- const textHash = sha256Namespaced("text/plain", HELLO_WORLD_SHA256);
92
-
93
- expect(jsHash).not.toBe(textHash);
94
- expect(jsHash).not.toBe(HELLO_WORLD_SHA256);
95
- });
96
-
97
- it("produces same hash for same content type + content hash", () => {
98
- const first = sha256Namespaced("application/javascript", HELLO_WORLD_SHA256);
99
- const second = sha256Namespaced("application/javascript", HELLO_WORLD_SHA256);
100
- expect(first).toBe(second);
101
- });
102
-
103
- it("returns base64url-encoded string", () => {
104
- const result = sha256Namespaced("application/javascript", HELLO_WORLD_SHA256);
105
- // Base64url: no +, /, or =
106
- expect(result).toMatch(/^[A-Za-z0-9_-]+$/u);
107
- });
108
- });
package/src/lib/sha256.ts DELETED
@@ -1,80 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { createReadStream } from "node:fs";
3
-
4
- import { toBase64Url } from "@better-update/encoding";
5
- import { Effect } from "effect";
6
-
7
- import { BuildFailedError } from "./exit-codes";
8
-
9
- export interface Sha256FileResult {
10
- readonly sha256: string;
11
- readonly byteSize: number;
12
- }
13
-
14
- export interface Sha256FileBase64UrlResult {
15
- readonly sha256Base64Url: string;
16
- readonly byteSize: number;
17
- }
18
-
19
- const hashReadError = (message: string) =>
20
- new BuildFailedError({
21
- step: "sha256",
22
- exitCode: 1,
23
- message,
24
- });
25
-
26
- const hashFile = <TDigest>(
27
- path: string,
28
- formatDigest: (digest: Buffer) => TDigest,
29
- ): Effect.Effect<{ digest: TDigest; byteSize: number }, BuildFailedError> =>
30
- Effect.async<{ digest: TDigest; byteSize: number }, BuildFailedError>((resume) => {
31
- const hash = createHash("sha256");
32
- const stream = createReadStream(path);
33
- let byteSize = 0;
34
-
35
- stream.on("data", (chunk) => {
36
- const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
37
- byteSize += buffer.byteLength;
38
- hash.update(buffer);
39
- });
40
- stream.on("error", (error) => {
41
- resume(Effect.fail(hashReadError(`Failed to read file for SHA-256: ${error.message}`)));
42
- });
43
- stream.on("end", () => {
44
- resume(
45
- Effect.succeed({
46
- digest: formatDigest(hash.digest()),
47
- byteSize,
48
- }),
49
- );
50
- });
51
- });
52
-
53
- /**
54
- * Compute the SHA-256 digest and byte size of a file using Node's streaming
55
- * hash API. The file is never fully loaded into memory — chunks flow through
56
- * `createReadStream` into `crypto.createHash("sha256")`.
57
- */
58
- export const sha256File = (path: string): Effect.Effect<Sha256FileResult, BuildFailedError> =>
59
- hashFile(path, (digest) => digest.toString("hex")).pipe(
60
- Effect.map(({ digest, byteSize }) => ({ sha256: digest, byteSize })),
61
- );
62
-
63
- export const sha256FileBase64Url = (
64
- path: string,
65
- ): Effect.Effect<Sha256FileBase64UrlResult, BuildFailedError> =>
66
- hashFile(path, toBase64Url).pipe(
67
- Effect.map(({ digest, byteSize }) => ({ sha256Base64Url: digest, byteSize })),
68
- );
69
-
70
- /**
71
- * Compute a content-type-namespaced hash: `SHA-256(contentType + '\0' + SHA-256_hex(fileBytes))`.
72
- *
73
- * This prevents hash collisions when identical bytes are served with different
74
- * MIME types (e.g. same file used as both `application/javascript` and `text/plain`).
75
- * The raw content hash is still needed separately for R2 upload verification.
76
- */
77
- export const sha256Namespaced = (contentType: string, contentSha256Hex: string): string => {
78
- const input = `${contentType}\0${contentSha256Hex}`;
79
- return toBase64Url(createHash("sha256").update(input).digest());
80
- };
@@ -1,181 +0,0 @@
1
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
-
5
- import { BunFileSystem } from "@effect/platform-bun";
6
- import { it } from "@effect/vitest";
7
- import { Effect, Exit } from "effect";
8
-
9
- import { loadOptionalSignedPayload, loadSignedPublishPayloads } from "./signed-payloads";
10
- import { failureError } from "./test-utils";
11
-
12
- const withSignedFiles = () => {
13
- const dir = mkdtempSync(join(tmpdir(), "signed-payloads-"));
14
- const manifestPath = join(dir, "manifest.json");
15
- const signaturePath = join(dir, "manifest.sig");
16
- const certificatePath = join(dir, "manifest.pem");
17
-
18
- writeFileSync(manifestPath, '{"runtimeVersion":"1.0.0"}\n');
19
- writeFileSync(signaturePath, 'sig="test-signature"\n');
20
- writeFileSync(certificatePath, "-----BEGIN CERTIFICATE-----\nTEST\n-----END CERTIFICATE-----\n");
21
-
22
- return {
23
- manifestPath,
24
- signaturePath,
25
- certificatePath,
26
- dispose: () => rmSync(dir, { recursive: true, force: true }),
27
- };
28
- };
29
-
30
- describe(loadOptionalSignedPayload, () => {
31
- it.effect("loads a complete signed payload triplet", () =>
32
- Effect.gen(function* () {
33
- const files = withSignedFiles();
34
- const payload = yield* loadOptionalSignedPayload({
35
- files: {
36
- manifestBodyFile: files.manifestPath,
37
- signatureFile: files.signaturePath,
38
- certificateChainFile: files.certificatePath,
39
- },
40
- label: "Signed promote",
41
- makeError: (message) => new Error(message),
42
- }).pipe(Effect.provide(BunFileSystem.layer), Effect.ensuring(Effect.sync(files.dispose)));
43
-
44
- expect(payload).toStrictEqual({
45
- manifestBody: '{"runtimeVersion":"1.0.0"}\n',
46
- signature: 'sig="test-signature"',
47
- certificateChain: "-----BEGIN CERTIFICATE-----\nTEST\n-----END CERTIFICATE-----",
48
- });
49
- }),
50
- );
51
- });
52
-
53
- describe(loadSignedPublishPayloads, () => {
54
- it.effect("loads platform-specific signed payloads for a single-platform publish", () =>
55
- Effect.gen(function* () {
56
- const iosFiles = withSignedFiles();
57
- const payloads = yield* loadSignedPublishPayloads({
58
- platforms: ["ios"],
59
- globalFiles: {
60
- manifestBodyFile: undefined,
61
- signatureFile: undefined,
62
- certificateChainFile: undefined,
63
- },
64
- platformFiles: {
65
- ios: {
66
- manifestBodyFile: iosFiles.manifestPath,
67
- signatureFile: iosFiles.signaturePath,
68
- certificateChainFile: iosFiles.certificatePath,
69
- },
70
- },
71
- makeError: (message) => new Error(message),
72
- }).pipe(Effect.provide(BunFileSystem.layer), Effect.ensuring(Effect.sync(iosFiles.dispose)));
73
-
74
- expect(payloads.ios?.manifestBody).toBe('{"runtimeVersion":"1.0.0"}\n');
75
- expect(payloads.android).toBeUndefined();
76
- }),
77
- );
78
-
79
- it.effect("loads per-platform signed payloads for a multi-platform publish", () =>
80
- Effect.gen(function* () {
81
- const iosFiles = withSignedFiles();
82
- const androidFiles = withSignedFiles();
83
- const payloads = yield* loadSignedPublishPayloads({
84
- platforms: ["ios", "android"],
85
- globalFiles: {
86
- manifestBodyFile: undefined,
87
- signatureFile: undefined,
88
- certificateChainFile: undefined,
89
- },
90
- platformFiles: {
91
- ios: {
92
- manifestBodyFile: iosFiles.manifestPath,
93
- signatureFile: iosFiles.signaturePath,
94
- certificateChainFile: iosFiles.certificatePath,
95
- },
96
- android: {
97
- manifestBodyFile: androidFiles.manifestPath,
98
- signatureFile: androidFiles.signaturePath,
99
- certificateChainFile: androidFiles.certificatePath,
100
- },
101
- },
102
- makeError: (message) => new Error(message),
103
- }).pipe(
104
- Effect.provide(BunFileSystem.layer),
105
- Effect.ensuring(
106
- Effect.sync(() => {
107
- iosFiles.dispose();
108
- androidFiles.dispose();
109
- }),
110
- ),
111
- );
112
-
113
- expect(payloads.ios?.signature).toBe('sig="test-signature"');
114
- expect(payloads.android?.certificateChain).toContain("BEGIN CERTIFICATE");
115
- }),
116
- );
117
-
118
- it.effect("rejects generic signed files for multi-platform publish", () =>
119
- Effect.gen(function* () {
120
- const files = withSignedFiles();
121
- const exit = yield* loadSignedPublishPayloads({
122
- platforms: ["ios", "android"],
123
- globalFiles: {
124
- manifestBodyFile: files.manifestPath,
125
- signatureFile: files.signaturePath,
126
- certificateChainFile: files.certificatePath,
127
- },
128
- platformFiles: {},
129
- makeError: (message) => new Error(message),
130
- }).pipe(
131
- Effect.provide(BunFileSystem.layer),
132
- Effect.ensuring(Effect.sync(files.dispose)),
133
- Effect.exit,
134
- );
135
-
136
- expect(Exit.isFailure(exit)).toBe(true);
137
- if (Exit.isFailure(exit)) {
138
- expect(failureError(exit)).toStrictEqual(
139
- new Error(
140
- "Signed multi-platform publish requires per-platform file sets. Use the --*-ios and --*-android options.",
141
- ),
142
- );
143
- }
144
- }),
145
- );
146
-
147
- it.effect("rejects ambiguous generic and platform-specific files for the same platform", () =>
148
- Effect.gen(function* () {
149
- const files = withSignedFiles();
150
- const exit = yield* loadSignedPublishPayloads({
151
- platforms: ["ios"],
152
- globalFiles: {
153
- manifestBodyFile: files.manifestPath,
154
- signatureFile: files.signaturePath,
155
- certificateChainFile: files.certificatePath,
156
- },
157
- platformFiles: {
158
- ios: {
159
- manifestBodyFile: files.manifestPath,
160
- signatureFile: files.signaturePath,
161
- certificateChainFile: files.certificatePath,
162
- },
163
- },
164
- makeError: (message) => new Error(message),
165
- }).pipe(
166
- Effect.provide(BunFileSystem.layer),
167
- Effect.ensuring(Effect.sync(files.dispose)),
168
- Effect.exit,
169
- );
170
-
171
- expect(Exit.isFailure(exit)).toBe(true);
172
- if (Exit.isFailure(exit)) {
173
- expect(failureError(exit)).toStrictEqual(
174
- new Error(
175
- "Signed publish for ios is ambiguous. Use either the generic file options or the ios-specific file options, not both.",
176
- ),
177
- );
178
- }
179
- }),
180
- );
181
- });
@@ -1,164 +0,0 @@
1
- import { FileSystem } from "@effect/platform";
2
- import { Effect } from "effect";
3
-
4
- import { formatCause } from "./format-error";
5
-
6
- import type { Platform } from "./build-profile";
7
-
8
- export interface SignedPayload {
9
- readonly manifestBody: string;
10
- readonly signature: string;
11
- readonly certificateChain: string;
12
- }
13
-
14
- export interface SignedPayloadFileSet {
15
- readonly manifestBodyFile: string | undefined;
16
- readonly signatureFile: string | undefined;
17
- readonly certificateChainFile: string | undefined;
18
- }
19
-
20
- const emptySignedPayloadFileSet = {
21
- manifestBodyFile: undefined,
22
- signatureFile: undefined,
23
- certificateChainFile: undefined,
24
- } as const satisfies SignedPayloadFileSet;
25
-
26
- const hasAnySignedPayloadFile = (files: SignedPayloadFileSet) =>
27
- files.manifestBodyFile !== undefined ||
28
- files.signatureFile !== undefined ||
29
- files.certificateChainFile !== undefined;
30
-
31
- const loadSignedPayloadFromFiles = <Err>(params: {
32
- readonly files: SignedPayloadFileSet;
33
- readonly label: string;
34
- readonly makeError: (message: string) => Err;
35
- }): Effect.Effect<SignedPayload | null, Err, FileSystem.FileSystem> =>
36
- Effect.gen(function* () {
37
- const fileSystem = yield* FileSystem.FileSystem;
38
- if (!hasAnySignedPayloadFile(params.files)) {
39
- return null;
40
- }
41
-
42
- if (
43
- !params.files.manifestBodyFile ||
44
- !params.files.signatureFile ||
45
- !params.files.certificateChainFile
46
- ) {
47
- return yield* Effect.fail(
48
- params.makeError(
49
- `${params.label} requires ${[
50
- params.files.manifestBodyFile ? null : "manifest body",
51
- params.files.signatureFile ? null : "signature",
52
- params.files.certificateChainFile ? null : "certificate chain",
53
- ]
54
- .filter(Boolean)
55
- .join(", ")} file inputs to be provided as a complete triplet.`,
56
- ),
57
- );
58
- }
59
-
60
- const [manifestBody, signature, certificateChain] = yield* Effect.all(
61
- [
62
- fileSystem.readFileString(params.files.manifestBodyFile),
63
- fileSystem.readFileString(params.files.signatureFile),
64
- fileSystem.readFileString(params.files.certificateChainFile),
65
- ],
66
- { concurrency: "unbounded" },
67
- ).pipe(
68
- Effect.mapError((cause) =>
69
- params.makeError(`${params.label} failed to read signed inputs: ${formatCause(cause)}`),
70
- ),
71
- );
72
-
73
- return {
74
- manifestBody,
75
- signature: signature.trim(),
76
- certificateChain: certificateChain.trimEnd(),
77
- } as const satisfies SignedPayload;
78
- });
79
-
80
- export const loadOptionalSignedPayload = loadSignedPayloadFromFiles;
81
-
82
- export const loadSignedPublishPayloads = <Err>(params: {
83
- readonly platforms: readonly Platform[];
84
- readonly globalFiles: SignedPayloadFileSet;
85
- readonly platformFiles: Partial<Record<Platform, SignedPayloadFileSet>>;
86
- readonly makeError: (message: string) => Err;
87
- }): Effect.Effect<Partial<Record<Platform, SignedPayload>>, Err, FileSystem.FileSystem> =>
88
- Effect.gen(function* () {
89
- const targetedPlatforms = new Set(params.platforms);
90
- const nonTargetedPlatforms = (["ios", "android"] as const).filter(
91
- (platform) =>
92
- !targetedPlatforms.has(platform) &&
93
- hasAnySignedPayloadFile(params.platformFiles[platform] ?? emptySignedPayloadFileSet),
94
- );
95
- if (nonTargetedPlatforms.length > 0) {
96
- return yield* Effect.fail(
97
- params.makeError(
98
- `Signed publish inputs were provided for non-targeted platform(s): ${nonTargetedPlatforms.join(", ")}.`,
99
- ),
100
- );
101
- }
102
-
103
- const hasGlobalFiles = hasAnySignedPayloadFile(params.globalFiles);
104
- if (
105
- !hasGlobalFiles &&
106
- Object.values(params.platformFiles).every((files) => !hasAnySignedPayloadFile(files))
107
- ) {
108
- return {};
109
- }
110
-
111
- if (params.platforms.length > 1 && hasGlobalFiles) {
112
- return yield* Effect.fail(
113
- params.makeError(
114
- "Signed multi-platform publish requires per-platform file sets. Use the --*-ios and --*-android options.",
115
- ),
116
- );
117
- }
118
-
119
- if (params.platforms.length === 1 && hasGlobalFiles) {
120
- const [platform] = params.platforms;
121
- if (!platform) {
122
- return {};
123
- }
124
- if (hasAnySignedPayloadFile(params.platformFiles[platform] ?? emptySignedPayloadFileSet)) {
125
- return yield* Effect.fail(
126
- params.makeError(
127
- `Signed publish for ${platform} is ambiguous. Use either the generic file options or the ${platform}-specific file options, not both.`,
128
- ),
129
- );
130
- }
131
-
132
- const globalPayload = yield* loadSignedPayloadFromFiles({
133
- files: params.globalFiles,
134
- label: "Signed publish",
135
- makeError: params.makeError,
136
- });
137
- return globalPayload === null ? {} : { [platform]: globalPayload };
138
- }
139
-
140
- const platformPayloadEntries = yield* Effect.forEach(
141
- params.platforms,
142
- (platform) =>
143
- Effect.gen(function* () {
144
- const payload = yield* loadSignedPayloadFromFiles({
145
- files: params.platformFiles[platform] ?? emptySignedPayloadFileSet,
146
- label: `Signed publish for ${platform}`,
147
- makeError: params.makeError,
148
- });
149
-
150
- if (payload === null) {
151
- return yield* Effect.fail(
152
- params.makeError(
153
- `Signed multi-platform publish requires a signed payload for ${platform}.`,
154
- ),
155
- );
156
- }
157
-
158
- return [platform, payload] as const;
159
- }),
160
- { concurrency: 1 },
161
- );
162
-
163
- return Object.fromEntries(platformPayloadEntries) as Partial<Record<Platform, SignedPayload>>;
164
- });
@@ -1,4 +0,0 @@
1
- export const capitalize = (value: string): string => {
2
- const [first] = value;
3
- return first === undefined ? value : `${first.toUpperCase()}${value.slice(1)}`;
4
- };
@@ -1,14 +0,0 @@
1
- import { FileSystem } from "@effect/platform";
2
- import { Effect } from "effect";
3
-
4
- /**
5
- * Create a scoped temp directory prefixed with "better-update-" and `chmod 0o700`
6
- * it so only the current user can read its contents. The directory and all files
7
- * inside it are removed when the enclosing scope closes.
8
- */
9
- export const acquireBuildTempDir = Effect.gen(function* () {
10
- const fs = yield* FileSystem.FileSystem;
11
- const dir = yield* fs.makeTempDirectoryScoped({ prefix: "better-update-" });
12
- yield* fs.chmod(dir, 0o700);
13
- return dir;
14
- });
@@ -1,13 +0,0 @@
1
- import { Cause, Exit, Option } from "effect";
2
-
3
- /**
4
- * Extract the tagged error from a failed `Effect.exit` result. Returns the
5
- * first `Fail` value in the cause, or `undefined` for interrupts / dies /
6
- * success. Use in tests after `Effect.exit` to assert a specific error class.
7
- */
8
- export const failureError = <Err, Value>(exit: Exit.Exit<Value, Err>): Err | undefined => {
9
- if (!Exit.isFailure(exit)) {
10
- return undefined;
11
- }
12
- return Option.getOrUndefined(Cause.failureOption(exit.cause));
13
- };
@@ -1,45 +0,0 @@
1
- import { resolveUpdatePlatforms } from "./update-platforms";
2
-
3
- describe(resolveUpdatePlatforms, () => {
4
- const fullConfig = {
5
- expo: {
6
- ios: { bundleIdentifier: "com.example.app" },
7
- android: { package: "com.example.app" },
8
- },
9
- } satisfies Record<string, unknown>;
10
-
11
- it('returns both platforms when "all" is requested', () => {
12
- expect(resolveUpdatePlatforms(fullConfig, "all")).toStrictEqual(["ios", "android"]);
13
- });
14
-
15
- it("returns only the requested platform when it exists", () => {
16
- expect(resolveUpdatePlatforms(fullConfig, "ios")).toStrictEqual(["ios"]);
17
- expect(resolveUpdatePlatforms(fullConfig, "android")).toStrictEqual(["android"]);
18
- });
19
-
20
- it('returns configured platforms when "all" is requested against a partial config', () => {
21
- expect(
22
- resolveUpdatePlatforms(
23
- {
24
- expo: {
25
- ios: { bundleIdentifier: "com.example.app" },
26
- },
27
- },
28
- "all",
29
- ),
30
- ).toStrictEqual(["ios"]);
31
- });
32
-
33
- it("returns the explicitly requested platform even when app.json omits that section", () => {
34
- expect(
35
- resolveUpdatePlatforms(
36
- {
37
- expo: {
38
- ios: { bundleIdentifier: "com.example.app" },
39
- },
40
- },
41
- "android",
42
- ),
43
- ).toStrictEqual(["android"]);
44
- });
45
- });
@@ -1,19 +0,0 @@
1
- import { asRecord } from "./record";
2
-
3
- import type { Platform } from "./build-profile";
4
-
5
- export type UpdatePlatformOption = Platform | "all";
6
-
7
- export const resolveUpdatePlatforms = (
8
- appJson: Record<string, unknown>,
9
- requestedPlatform: UpdatePlatformOption,
10
- ): readonly Platform[] => {
11
- if (requestedPlatform !== "all") {
12
- return [requestedPlatform] as const;
13
- }
14
-
15
- const expo = asRecord(appJson["expo"]);
16
- return (["ios", "android"] as const).filter(
17
- (platform) => asRecord(expo?.[platform]) !== undefined,
18
- );
19
- };
@@ -1,21 +0,0 @@
1
- import { ExpoRunFormatter } from "@expo/xcpretty";
2
-
3
- export interface XcodebuildFormatter {
4
- /** Feed a line of raw xcodebuild output. Returns formatted lines (0 or more). */
5
- readonly pipe: (line: string) => readonly string[];
6
- /** Get a build summary after the process completes. */
7
- readonly getBuildSummary: () => string;
8
- }
9
-
10
- /**
11
- * Create a stateful xcodebuild output formatter backed by `@expo/xcpretty`.
12
- * Each `pipe(line)` call may return zero or more formatted lines — zero means
13
- * the line was suppressed (e.g., intermediate compiler invocations).
14
- */
15
- export const createXcodebuildFormatter = (projectRoot: string): XcodebuildFormatter => {
16
- const formatter = ExpoRunFormatter.create(projectRoot);
17
- return {
18
- pipe: (line: string) => formatter.pipe(line),
19
- getBuildSummary: () => formatter.getBuildSummary(),
20
- };
21
- };