@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
package/src/lib/pkcs12.ts DELETED
@@ -1,73 +0,0 @@
1
- /* eslint-disable typescript/no-unsafe-assignment, typescript/no-unsafe-member-access, typescript/no-unsafe-call -- @expo/pkcs12 exports declare node-forge cert shapes as `any`; this file is the narrowing boundary that produces the typed P12Info for the rest of the CLI */
2
-
3
- import { getFormattedSerialNumber, getX509Certificate, parsePKCS12 } from "@expo/pkcs12";
4
- import { Effect } from "effect";
5
-
6
- import { CredentialValidationError } from "./exit-codes";
7
-
8
- export interface P12Info {
9
- readonly serialNumber: string;
10
- readonly validFrom: Date | undefined;
11
- readonly expiresAt: Date | undefined;
12
- readonly subject: string;
13
- readonly issuerCN: string | undefined;
14
- readonly signingIdentity: string;
15
- readonly teamId: string | undefined;
16
- }
17
-
18
- const APPLE_TEAM_ID_RE = /^[A-Z0-9]{10}$/u;
19
-
20
- const extractTeamId = (params: {
21
- readonly signingIdentity: string;
22
- readonly orgUnit: string | undefined;
23
- }): string | undefined => {
24
- if (params.orgUnit && APPLE_TEAM_ID_RE.test(params.orgUnit)) {
25
- return params.orgUnit;
26
- }
27
- const parenMatch = /\(([A-Z0-9]{10})\)\s*$/u.exec(params.signingIdentity);
28
- return parenMatch?.[1];
29
- };
30
-
31
- /**
32
- * Parse a PKCS#12 (.p12) buffer and extract certificate metadata.
33
- */
34
- export const inspectP12 = (params: {
35
- readonly data: Buffer;
36
- readonly password: string;
37
- }): Effect.Effect<P12Info, CredentialValidationError> =>
38
- Effect.try({
39
- try: () => {
40
- const p12 = parsePKCS12(params.data, params.password);
41
- const cert = getX509Certificate(p12);
42
-
43
- const serialNumber = getFormattedSerialNumber(cert) ?? "unknown";
44
-
45
- const validFrom =
46
- cert.validity.notBefore instanceof Date ? cert.validity.notBefore : undefined;
47
- const expiresAt = cert.validity.notAfter instanceof Date ? cert.validity.notAfter : undefined;
48
-
49
- const subjectParts = cert.subject.attributes.map(
50
- (attr: { shortName?: string; name: string; value: unknown }) =>
51
- `${attr.shortName ?? attr.name}=${String(attr.value)}`,
52
- );
53
- const subject = subjectParts.join(", ");
54
-
55
- const issuerCNValue = cert.issuer.getField("CN")?.value;
56
- const issuerCN = typeof issuerCNValue === "string" ? issuerCNValue : undefined;
57
-
58
- // Signing identity = Common Name from subject, e.g. "Apple Distribution: Name (TEAMID)"
59
- const cnValue = cert.subject.getField("CN")?.value;
60
- const cn = typeof cnValue === "string" ? cnValue : undefined;
61
- const signingIdentity = cn ?? subject;
62
- const orgUnitValue = cert.subject.getField("OU")?.value;
63
- const orgUnit = typeof orgUnitValue === "string" ? orgUnitValue : undefined;
64
-
65
- const teamId = extractTeamId({ signingIdentity, orgUnit });
66
-
67
- return { serialNumber, validFrom, expiresAt, subject, issuerCN, signingIdentity, teamId };
68
- },
69
- catch: (error) =>
70
- new CredentialValidationError({
71
- message: `Failed to parse P12 certificate: ${error instanceof Error ? error.message : String(error)}`,
72
- }),
73
- });
package/src/lib/plist.ts DELETED
@@ -1,39 +0,0 @@
1
- import plist from "@expo/plist";
2
-
3
- import type { PlistObject } from "@expo/plist";
4
- // eslint-disable-next-line import-plugin/no-namespace -- bplist-parser typings have no named export; used only as `typeof BplistParser` for the CJS require result
5
- import type * as BplistParser from "bplist-parser";
6
-
7
- export type { PlistObject, PlistValue } from "@expo/plist";
8
-
9
- /**
10
- * Parse an XML plist string into a typed object.
11
- * Throws on malformed XML — callers should wrap in Effect.try.
12
- */
13
- export const parsePlistXml = (xml: string): PlistObject =>
14
- // eslint-disable-next-line typescript/no-unsafe-type-assertion -- @expo/plist.parse returns `any`; PlistObject is the library's declared shape for XML plists
15
- plist.parse(xml) as PlistObject;
16
-
17
- /**
18
- * Parse a binary plist buffer into a typed object.
19
- * Uses bplist-parser for Apple's binary plist format.
20
- */
21
- export const parsePlistBinary = (buffer: Buffer): PlistObject => {
22
- const bplistParser =
23
- // eslint-disable-next-line typescript/no-unsafe-type-assertion -- CJS require returns `any`; narrow to the package's own typings at the boundary
24
- require("bplist-parser") as typeof BplistParser;
25
- // eslint-disable-next-line typescript/no-unsafe-assignment -- bplist-parser typings declare parseBuffer<T>(): T[] with T=any in the shipped .d.ts
26
- const [result] = bplistParser.parseBuffer(buffer);
27
- // eslint-disable-next-line typescript/no-unsafe-type-assertion -- bplist-parser typings return `any[]`; PlistObject is the superset shape we consume
28
- return result as PlistObject;
29
- };
30
-
31
- const BPLIST_MAGIC = Buffer.from("bplist00");
32
-
33
- /**
34
- * Auto-detect plist format (binary vs XML) and parse accordingly.
35
- */
36
- export const parsePlist = (data: Buffer): PlistObject =>
37
- data.subarray(0, 8).equals(BPLIST_MAGIC)
38
- ? parsePlistBinary(data)
39
- : parsePlistXml(data.toString("utf8"));
@@ -1,146 +0,0 @@
1
- import path from "node:path";
2
-
3
- import { Command, FileSystem } from "@effect/platform";
4
- import { Console, Effect } from "effect";
5
-
6
- import type { CommandExecutor } from "@effect/platform";
7
-
8
- import { parsePlist, parsePlistXml } from "./plist";
9
-
10
- export interface IosValidationParams {
11
- readonly archivePath: string;
12
- readonly expectedBundleId: string;
13
- readonly expectedTeamId: string;
14
- readonly expectedProfileUuid: string;
15
- }
16
-
17
- export interface ValidationResult {
18
- readonly passed: boolean;
19
- readonly warnings: readonly string[];
20
- }
21
-
22
- /**
23
- * Validate an iOS build after xcodebuild completes. Checks:
24
- * 1. Bundle ID matches expected value
25
- * 2. Provisioning profile UUID matches
26
- * 3. Team ID matches
27
- *
28
- * All checks are non-blocking — returns warnings, never fails the build.
29
- */
30
- export const validateIosBuild = (
31
- params: IosValidationParams,
32
- ): Effect.Effect<
33
- ValidationResult,
34
- never,
35
- CommandExecutor.CommandExecutor | FileSystem.FileSystem
36
- > =>
37
- Effect.gen(function* () {
38
- const appDir = yield* findAppDirectory(params.archivePath).pipe(
39
- Effect.catchAll(() => Effect.succeed(undefined)),
40
- );
41
-
42
- if (!appDir) {
43
- const warnings = ["Could not locate .app bundle in archive — skipping post-build validation"];
44
- return { passed: false, warnings };
45
- }
46
-
47
- const bundleWarning = yield* checkBundleId(appDir, params.expectedBundleId).pipe(
48
- Effect.catchAll(() => Effect.succeed(undefined)),
49
- );
50
-
51
- const profileWarnings = yield* checkEmbeddedProfile(
52
- appDir,
53
- params.expectedProfileUuid,
54
- params.expectedTeamId,
55
- ).pipe(Effect.catchAll(() => Effect.succeed([] as readonly string[])));
56
-
57
- const warnings: readonly string[] = [
58
- ...(bundleWarning ? [bundleWarning] : []),
59
- ...profileWarnings,
60
- ];
61
-
62
- if (warnings.length > 0) {
63
- yield* Console.warn("Post-build validation warnings:");
64
- for (const warning of warnings) {
65
- yield* Console.warn(` - ${warning}`);
66
- }
67
- }
68
-
69
- return { passed: warnings.length === 0, warnings };
70
- });
71
-
72
- // ── helpers ──────────────────────────────────────────────────────
73
-
74
- const findAppDirectory = (
75
- archivePath: string,
76
- ): Effect.Effect<string, unknown, FileSystem.FileSystem> =>
77
- Effect.gen(function* () {
78
- const fs = yield* FileSystem.FileSystem;
79
- const productsDir = path.join(archivePath, "Products", "Applications");
80
- const entries = yield* fs.readDirectory(productsDir);
81
- const appEntry = entries.find((entry) => entry.endsWith(".app"));
82
- if (!appEntry) {
83
- return yield* Effect.fail("No .app found");
84
- }
85
- return path.join(productsDir, appEntry);
86
- });
87
-
88
- const checkBundleId = (
89
- appDir: string,
90
- expectedBundleId: string,
91
- ): Effect.Effect<string | undefined, unknown, FileSystem.FileSystem> =>
92
- Effect.gen(function* () {
93
- const fs = yield* FileSystem.FileSystem;
94
- const plistPath = path.join(appDir, "Info.plist");
95
- const data = yield* fs.readFile(plistPath);
96
- const parsed = parsePlist(Buffer.from(data));
97
- const actualBundleId = parsed["CFBundleIdentifier"];
98
-
99
- if (typeof actualBundleId === "string" && actualBundleId !== expectedBundleId) {
100
- return `Bundle ID mismatch: expected "${expectedBundleId}", got "${actualBundleId}"`;
101
- }
102
- return undefined;
103
- });
104
-
105
- const checkEmbeddedProfile = (
106
- appDir: string,
107
- expectedUuid: string,
108
- expectedTeamId: string,
109
- ): Effect.Effect<
110
- readonly string[],
111
- unknown,
112
- CommandExecutor.CommandExecutor | FileSystem.FileSystem
113
- > =>
114
- Effect.gen(function* () {
115
- const warnings: string[] = [];
116
- const profilePath = path.join(appDir, "embedded.mobileprovision");
117
-
118
- // Use security cms to decrypt the profile (it's CMS-signed)
119
- const plistXml = yield* Command.string(
120
- Command.make("security", "cms", "-D", "-i", profilePath),
121
- );
122
-
123
- const parsed = parsePlistXml(plistXml);
124
-
125
- const actualUuid = parsed["UUID"];
126
- if (typeof actualUuid === "string" && actualUuid !== expectedUuid) {
127
- warnings.push(`Profile UUID mismatch: expected "${expectedUuid}", got "${actualUuid}"`);
128
- }
129
-
130
- const teamIdentifiers = parsed["TeamIdentifier"];
131
- if (Array.isArray(teamIdentifiers)) {
132
- // eslint-disable-next-line typescript/no-unsafe-assignment -- @expo/plist types array entries as any; narrowed via typeof check below
133
- const [actualTeamId] = teamIdentifiers;
134
- if (typeof actualTeamId === "string" && actualTeamId !== expectedTeamId) {
135
- warnings.push(`Team ID mismatch: expected "${expectedTeamId}", got "${actualTeamId}"`);
136
- }
137
- }
138
-
139
- // Check expiration
140
- const expirationDate = parsed["ExpirationDate"];
141
- if (expirationDate instanceof Date && expirationDate.getTime() < Date.now()) {
142
- warnings.push(`Embedded provisioning profile expired on ${expirationDate.toISOString()}`);
143
- }
144
-
145
- return warnings;
146
- });
@@ -1,140 +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 { HttpClient, HttpClientResponse, FileSystem } from "@effect/platform";
6
- import { BunFileSystem } from "@effect/platform-bun";
7
- import { it } from "@effect/vitest";
8
- import { Effect, Exit, Layer } from "effect";
9
-
10
- import { PresignedUploadClientLive } from "../services/presigned-upload";
11
- import { PresignedUrlExpiredError, UploadFailedError } from "./exit-codes";
12
- import { putToPresignedUrl } from "./presigned-upload";
13
- import { failureError } from "./test-utils";
14
-
15
- // ── helpers ───────────────────────────────────────────────────────
16
-
17
- const makeHttpClientLayer = (
18
- respond: () => globalThis.Response,
19
- ): Layer.Layer<HttpClient.HttpClient> =>
20
- Layer.succeed(
21
- HttpClient.HttpClient,
22
- HttpClient.make((request) => Effect.sync(() => HttpClientResponse.fromWeb(request, respond()))),
23
- );
24
-
25
- // In-memory noop filesystem for the "expired" branch where the file is never opened.
26
- const noopFsLayer: Layer.Layer<FileSystem.FileSystem> = FileSystem.layerNoop({});
27
-
28
- const makePresignedUploadLayer = (
29
- fileSystemLayer: Layer.Layer<FileSystem.FileSystem>,
30
- respond: () => globalThis.Response,
31
- ) =>
32
- PresignedUploadClientLive.pipe(
33
- Layer.provide(Layer.mergeAll(fileSystemLayer, makeHttpClientLayer(respond))),
34
- );
35
-
36
- const withTempFile = (bytes: Buffer): { path: string; dispose: () => void } => {
37
- const dir = mkdtempSync(join(tmpdir(), "presigned-test-"));
38
- const filePath = join(dir, "artifact.bin");
39
- writeFileSync(filePath, bytes);
40
- return {
41
- path: filePath,
42
- dispose: () => rmSync(dir, { recursive: true, force: true }),
43
- };
44
- };
45
-
46
- const futureExpiry = () => new Date(Date.now() + 60 * 60 * 1000).toISOString();
47
- const pastExpiry = () => new Date(Date.now() - 1000).toISOString();
48
-
49
- // ── tests ─────────────────────────────────────────────────────────
50
-
51
- describe(putToPresignedUrl, () => {
52
- it.effect("fails with PresignedUrlExpiredError when expiry is in the past", () =>
53
- Effect.gen(function* () {
54
- const exit = yield* putToPresignedUrl({
55
- url: "https://example.com/upload",
56
- filePath: "/dev/null",
57
- byteSize: 0,
58
- expiresAt: pastExpiry(),
59
- }).pipe(
60
- Effect.provide(
61
- makePresignedUploadLayer(noopFsLayer, () => new Response(null, { status: 200 })),
62
- ),
63
- Effect.exit,
64
- );
65
- expect(Exit.isFailure(exit)).toBe(true);
66
- if (Exit.isFailure(exit)) {
67
- const error = failureError(exit);
68
- expect(error).toBeInstanceOf(PresignedUrlExpiredError);
69
- }
70
- }),
71
- );
72
-
73
- it.effect("fails with PresignedUrlExpiredError within 30s safety margin", () =>
74
- Effect.gen(function* () {
75
- const inTenSeconds = new Date(Date.now() + 10_000).toISOString();
76
- const exit = yield* putToPresignedUrl({
77
- url: "https://example.com/upload",
78
- filePath: "/dev/null",
79
- byteSize: 0,
80
- expiresAt: inTenSeconds,
81
- }).pipe(
82
- Effect.provide(
83
- makePresignedUploadLayer(noopFsLayer, () => new Response(null, { status: 200 })),
84
- ),
85
- Effect.exit,
86
- );
87
- expect(Exit.isFailure(exit)).toBe(true);
88
- if (Exit.isFailure(exit)) {
89
- const error = failureError(exit);
90
- expect(error).toBeInstanceOf(PresignedUrlExpiredError);
91
- }
92
- }),
93
- );
94
-
95
- it.effect("succeeds on 2xx response", () =>
96
- Effect.gen(function* () {
97
- const file = withTempFile(Buffer.from("hello world"));
98
- const exit = yield* putToPresignedUrl({
99
- url: "https://example.com/upload",
100
- filePath: file.path,
101
- byteSize: 11,
102
- expiresAt: futureExpiry(),
103
- }).pipe(
104
- Effect.provide(
105
- makePresignedUploadLayer(BunFileSystem.layer, () => new Response(null, { status: 200 })),
106
- ),
107
- Effect.ensuring(Effect.sync(file.dispose)),
108
- Effect.exit,
109
- );
110
- expect(Exit.isSuccess(exit)).toBe(true);
111
- }),
112
- );
113
-
114
- it.effect("fails with UploadFailedError on 403 response", () =>
115
- Effect.gen(function* () {
116
- const file = withTempFile(Buffer.from("hello world"));
117
- const exit = yield* putToPresignedUrl({
118
- url: "https://example.com/upload",
119
- filePath: file.path,
120
- byteSize: 11,
121
- expiresAt: futureExpiry(),
122
- }).pipe(
123
- Effect.provide(
124
- makePresignedUploadLayer(
125
- BunFileSystem.layer,
126
- () => new Response("AccessDenied", { status: 403, statusText: "Forbidden" }),
127
- ),
128
- ),
129
- Effect.ensuring(Effect.sync(file.dispose)),
130
- Effect.exit,
131
- );
132
- expect(Exit.isFailure(exit)).toBe(true);
133
- if (Exit.isFailure(exit)) {
134
- const error = failureError(exit);
135
- expect(error).toBeInstanceOf(UploadFailedError);
136
- expect((error as UploadFailedError).message).toContain("403");
137
- }
138
- }),
139
- );
140
- });
@@ -1,35 +0,0 @@
1
- import { Effect } from "effect";
2
-
3
- import { PresignedUploadClient } from "../services/presigned-upload";
4
-
5
- import type { PresignedUrlExpiredError, UploadFailedError } from "./exit-codes";
6
-
7
- export interface PutToPresignedUrlOptions {
8
- readonly url: string;
9
- readonly filePath: string;
10
- readonly byteSize: number;
11
- readonly expiresAt: string;
12
- readonly headers?: Record<string, string>;
13
- }
14
-
15
- export const putToPresignedUrl = ({
16
- url,
17
- filePath,
18
- byteSize,
19
- expiresAt,
20
- headers,
21
- }: PutToPresignedUrlOptions): Effect.Effect<
22
- void,
23
- PresignedUrlExpiredError | UploadFailedError,
24
- PresignedUploadClient
25
- > =>
26
- Effect.gen(function* () {
27
- const presignedUploadClient = yield* PresignedUploadClient;
28
- yield* presignedUploadClient.putToPresignedUrl({
29
- url,
30
- filePath,
31
- byteSize,
32
- expiresAt,
33
- ...(headers === undefined ? {} : { headers }),
34
- });
35
- });
package/src/lib/record.ts DELETED
@@ -1,5 +0,0 @@
1
- export const isRecord = (value: unknown): value is Record<string, unknown> =>
2
- value !== null && typeof value === "object" && !Array.isArray(value);
3
-
4
- export const asRecord = (value: unknown): Record<string, unknown> | undefined =>
5
- isRecord(value) ? value : undefined;
@@ -1,24 +0,0 @@
1
- import { Effect } from "effect";
2
-
3
- interface NamedResource {
4
- readonly id: string;
5
- readonly name: string;
6
- }
7
-
8
- export const resolveNamedResourceId = <Err>(
9
- params: {
10
- readonly items: readonly NamedResource[];
11
- readonly kind: string;
12
- readonly name: string;
13
- },
14
- makeError: (message: string) => Err,
15
- ): Effect.Effect<string, Err> =>
16
- Effect.gen(function* () {
17
- const match = params.items.find((item) => item.name === params.name);
18
- if (match === undefined) {
19
- return yield* Effect.fail(
20
- makeError(`${params.kind} "${params.name}" not found in the linked project.`),
21
- );
22
- }
23
- return match.id;
24
- });
@@ -1,119 +0,0 @@
1
- import { CommandExecutor } from "@effect/platform";
2
- import { it } from "@effect/vitest";
3
- import { Effect, Exit } from "effect";
4
-
5
- import { RuntimeVersionError } from "./exit-codes";
6
- import { resolveRuntimeVersion } from "./runtime-version";
7
- import { failureError } from "./test-utils";
8
-
9
- // ── helpers ───────────────────────────────────────────────────────
10
-
11
- const makeStubExecutor = (stdout: string): CommandExecutor.CommandExecutor =>
12
- ({
13
- [CommandExecutor.TypeId]: CommandExecutor.TypeId,
14
- string: () => Effect.succeed(stdout),
15
- }) as unknown as CommandExecutor.CommandExecutor;
16
-
17
- const provideStubExecutor = (stdout: string) =>
18
- Effect.provideService(CommandExecutor.CommandExecutor, makeStubExecutor(stdout));
19
-
20
- // ── tests ─────────────────────────────────────────────────────────
21
-
22
- describe(resolveRuntimeVersion, () => {
23
- it.effect("returns literal string as-is", () =>
24
- Effect.gen(function* () {
25
- const result = yield* resolveRuntimeVersion({
26
- raw: "1.2.3",
27
- appVersion: "9.9.9",
28
- projectRoot: ".",
29
- }).pipe(provideStubExecutor(""));
30
- expect(result).toBe("1.2.3");
31
- }),
32
- );
33
-
34
- it.effect('resolves {policy:"appVersion"} to appVersion', () =>
35
- Effect.gen(function* () {
36
- const result = yield* resolveRuntimeVersion({
37
- raw: { policy: "appVersion" },
38
- appVersion: "2.5.0",
39
- projectRoot: ".",
40
- }).pipe(provideStubExecutor(""));
41
- expect(result).toBe("2.5.0");
42
- }),
43
- );
44
-
45
- it.effect('fails when policy "appVersion" has no expo.version', () =>
46
- Effect.gen(function* () {
47
- const exit = yield* resolveRuntimeVersion({
48
- raw: { policy: "appVersion" },
49
- appVersion: undefined,
50
- projectRoot: ".",
51
- }).pipe(provideStubExecutor(""), Effect.exit);
52
- expect(Exit.isFailure(exit)).toBe(true);
53
- }),
54
- );
55
-
56
- it.effect('resolves {policy:"fingerprint"} via CommandExecutor JSON hash', () =>
57
- Effect.gen(function* () {
58
- const result = yield* resolveRuntimeVersion({
59
- raw: { policy: "fingerprint" },
60
- appVersion: undefined,
61
- projectRoot: ".",
62
- }).pipe(provideStubExecutor('{"hash":"abc123","sources":[]}'));
63
- expect(result).toBe("abc123");
64
- }),
65
- );
66
-
67
- it.effect("fails when fingerprint stdout is not JSON", () =>
68
- Effect.gen(function* () {
69
- const exit = yield* resolveRuntimeVersion({
70
- raw: { policy: "fingerprint" },
71
- appVersion: undefined,
72
- projectRoot: ".",
73
- }).pipe(provideStubExecutor("not-json"), Effect.exit);
74
- expect(Exit.isFailure(exit)).toBe(true);
75
- if (Exit.isFailure(exit)) {
76
- const error = failureError(exit);
77
- expect(error).toBeInstanceOf(RuntimeVersionError);
78
- }
79
- }),
80
- );
81
-
82
- it.effect("fails when fingerprint JSON has no hash field", () =>
83
- Effect.gen(function* () {
84
- const exit = yield* resolveRuntimeVersion({
85
- raw: { policy: "fingerprint" },
86
- appVersion: undefined,
87
- projectRoot: ".",
88
- }).pipe(provideStubExecutor('{"sources":[]}'), Effect.exit);
89
- expect(Exit.isFailure(exit)).toBe(true);
90
- }),
91
- );
92
-
93
- it.effect('fails with guidance on policy "nativeVersion"', () =>
94
- Effect.gen(function* () {
95
- const exit = yield* resolveRuntimeVersion({
96
- raw: { policy: "nativeVersion" },
97
- appVersion: undefined,
98
- projectRoot: ".",
99
- }).pipe(provideStubExecutor(""), Effect.exit);
100
- expect(Exit.isFailure(exit)).toBe(true);
101
- if (Exit.isFailure(exit)) {
102
- const error = failureError(exit);
103
- expect(error).toBeInstanceOf(RuntimeVersionError);
104
- expect(error!.message).toContain("nativeVersion");
105
- }
106
- }),
107
- );
108
-
109
- it.effect("fails when runtimeVersion is missing entirely", () =>
110
- Effect.gen(function* () {
111
- const exit = yield* resolveRuntimeVersion({
112
- raw: undefined,
113
- appVersion: "1.0.0",
114
- projectRoot: ".",
115
- }).pipe(provideStubExecutor(""), Effect.exit);
116
- expect(Exit.isFailure(exit)).toBe(true);
117
- }),
118
- );
119
- });
@@ -1,62 +0,0 @@
1
- import { Effect } from "effect";
2
-
3
- import type { CommandExecutor } from "@effect/platform";
4
-
5
- import { RuntimeVersionError } from "./exit-codes";
6
- import { runFingerprintFull } from "./fingerprint";
7
-
8
- import type { RawRuntimeVersion } from "./build-profile";
9
-
10
- export interface ResolveRuntimeVersionOptions {
11
- readonly raw: RawRuntimeVersion | undefined;
12
- readonly appVersion: string | undefined;
13
- readonly projectRoot: string;
14
- }
15
-
16
- export const resolveRuntimeVersion = ({
17
- raw,
18
- appVersion,
19
- projectRoot,
20
- }: ResolveRuntimeVersionOptions): Effect.Effect<
21
- string,
22
- RuntimeVersionError,
23
- CommandExecutor.CommandExecutor
24
- > =>
25
- Effect.gen(function* () {
26
- if (typeof raw === "string") {
27
- return raw;
28
- }
29
- if (raw === undefined) {
30
- return yield* new RuntimeVersionError({
31
- message: "No runtimeVersion configured in expo section of app.json.",
32
- });
33
- }
34
-
35
- const { policy } = raw;
36
- if (policy === "appVersion") {
37
- if (appVersion === undefined) {
38
- return yield* new RuntimeVersionError({
39
- message: 'runtimeVersion policy is "appVersion" but expo.version is missing in app.json.',
40
- });
41
- }
42
- return appVersion;
43
- }
44
-
45
- if (policy === "fingerprint") {
46
- return yield* runFingerprintFull(projectRoot).pipe(
47
- Effect.map((result) => result.hash),
48
- Effect.mapError((cause) => new RuntimeVersionError({ message: cause.message })),
49
- );
50
- }
51
-
52
- if (policy === "nativeVersion") {
53
- return yield* new RuntimeVersionError({
54
- message:
55
- 'runtimeVersion policy "nativeVersion" is not supported. Set a static runtimeVersion string in app.json.',
56
- });
57
- }
58
-
59
- return yield* new RuntimeVersionError({
60
- message: `Unsupported runtimeVersion policy "${policy}". Use a static string, "appVersion", or "fingerprint".`,
61
- });
62
- });