@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,42 +0,0 @@
1
- import { ManagementApi } from "@better-update/api";
2
- import { HttpApiClient, HttpClient, HttpClientRequest } from "@effect/platform";
3
- import { Context, Effect, Layer } from "effect";
4
-
5
- import { AuthStore } from "./auth-store";
6
- import { ConfigStore } from "./config-store";
7
-
8
- import type { AuthRequiredError } from "../lib/exit-codes";
9
-
10
- const client = HttpApiClient.make(ManagementApi);
11
- export type ApiClient = Effect.Effect.Success<typeof client>;
12
-
13
- export class ApiClientService extends Context.Tag("cli/ApiClient")<
14
- ApiClientService,
15
- {
16
- readonly get: Effect.Effect<ApiClient, AuthRequiredError>;
17
- }
18
- >() {}
19
-
20
- export const apiClient: Effect.Effect<ApiClient, AuthRequiredError, ApiClientService> =
21
- // eslint-disable-next-line unicorn/no-array-method-this-argument -- Effect.flatMap, not Array.prototype.flatMap; the second arg is a continuation, not a thisArg
22
- Effect.flatMap(ApiClientService, ({ get }) => get);
23
-
24
- export const ApiClientLive = Layer.effect(
25
- ApiClientService,
26
- Effect.gen(function* () {
27
- const clientService = yield* HttpClient.HttpClient;
28
- const authStore = yield* AuthStore;
29
- const configStore = yield* ConfigStore;
30
-
31
- return {
32
- get: Effect.gen(function* () {
33
- const token = yield* authStore.getToken;
34
- const baseUrl = yield* configStore.getBaseUrl;
35
- return yield* HttpApiClient.make(ManagementApi, {
36
- transformClient: HttpClient.mapRequest(HttpClientRequest.bearerToken(token)),
37
- baseUrl,
38
- }).pipe(Effect.provideService(HttpClient.HttpClient, clientService));
39
- }),
40
- };
41
- }),
42
- );
@@ -1,100 +0,0 @@
1
- import path from "node:path";
2
-
3
- import { safeJsonParse } from "@better-update/safe-json";
4
- import { FileSystem } from "@effect/platform";
5
- import { Context, Effect, Layer } from "effect";
6
-
7
- import type { Auth } from "@expo/apple-utils";
8
-
9
- import { AppleAuthError } from "../lib/exit-codes";
10
- import { formatCause } from "../lib/format-error";
11
- import { isRecord } from "../lib/record";
12
- import { CliRuntime } from "./cli-runtime";
13
-
14
- // The cookies payload accepted by @expo/apple-utils Auth.loginWithCookiesAsync.
15
- // Derived structurally so we don't depend on the un-exported `CookiesJSON` alias.
16
- export type AppleSessionCookies = Parameters<typeof Auth.loginWithCookiesAsync>[0]["cookies"];
17
-
18
- export interface SerializedAppleSession {
19
- readonly cookies: AppleSessionCookies;
20
- readonly teamId: string;
21
- readonly username: string;
22
- readonly providerId?: number;
23
- }
24
-
25
- export class AppleSessionStore extends Context.Tag("cli/AppleSessionStore")<
26
- AppleSessionStore,
27
- {
28
- readonly loadSession: Effect.Effect<SerializedAppleSession | null>;
29
- readonly saveSession: (session: SerializedAppleSession) => Effect.Effect<void, AppleAuthError>;
30
- readonly clearSession: Effect.Effect<void>;
31
- }
32
- >() {}
33
-
34
- export const AppleSessionStoreLive = Layer.effect(
35
- AppleSessionStore,
36
- Effect.gen(function* () {
37
- const fs = yield* FileSystem.FileSystem;
38
- const runtime = yield* CliRuntime;
39
- const homeDirectory = yield* runtime.homeDirectory;
40
- const sessionDir = path.join(homeDirectory, ".better-update");
41
- const sessionFile = path.join(sessionDir, "apple-session.json");
42
-
43
- return {
44
- loadSession: Effect.gen(function* () {
45
- const content = yield* fs
46
- .readFileString(sessionFile)
47
- .pipe(Effect.catchAll(() => Effect.succeed(null)));
48
-
49
- if (!content) {
50
- return null;
51
- }
52
-
53
- const parsed = safeJsonParse(content);
54
- if (!isRecord(parsed)) {
55
- return null;
56
- }
57
-
58
- if (
59
- typeof parsed["teamId"] !== "string" ||
60
- typeof parsed["username"] !== "string" ||
61
- !parsed["cookies"]
62
- ) {
63
- return null;
64
- }
65
-
66
- const providerIdRaw = parsed["providerId"];
67
- const hasProviderId = typeof providerIdRaw === "number" && Number.isInteger(providerIdRaw);
68
-
69
- // eslint-disable-next-line typescript/no-unsafe-type-assertion, typescript/no-unsafe-assignment -- AppleSessionCookies is an opaque cookies payload from @expo/apple-utils; round-tripped verbatim from disk
70
- const cookies = parsed["cookies"] as AppleSessionCookies;
71
-
72
- const session: SerializedAppleSession = {
73
- // eslint-disable-next-line typescript/no-unsafe-assignment -- see disable on the `cookies` declaration above; same opaque value
74
- cookies,
75
- teamId: parsed["teamId"],
76
- username: parsed["username"],
77
- ...(hasProviderId ? { providerId: providerIdRaw } : {}),
78
- };
79
- return session;
80
- }),
81
-
82
- saveSession: (session: SerializedAppleSession) =>
83
- Effect.gen(function* () {
84
- yield* fs.makeDirectory(sessionDir, { recursive: true });
85
- yield* fs.chmod(sessionDir, 0o700);
86
- yield* fs.writeFileString(sessionFile, `${JSON.stringify(session, null, 2)}\n`);
87
- yield* fs.chmod(sessionFile, 0o600);
88
- }).pipe(
89
- Effect.mapError(
90
- (cause) =>
91
- new AppleAuthError({
92
- message: `Failed to save Apple session: ${formatCause(cause)}`,
93
- }),
94
- ),
95
- ),
96
-
97
- clearSession: fs.remove(sessionFile).pipe(Effect.catchAll(() => Effect.void)),
98
- };
99
- }),
100
- );
@@ -1,85 +0,0 @@
1
- import path from "node:path";
2
-
3
- import { FileSystem } from "@effect/platform";
4
- import { Context, Effect, Layer } from "effect";
5
-
6
- import { AuthRequiredError } from "../lib/exit-codes";
7
- import { formatCause } from "../lib/format-error";
8
- import { isRecord } from "../lib/record";
9
- import { CliRuntime } from "./cli-runtime";
10
-
11
- export class AuthStore extends Context.Tag("cli/AuthStore")<
12
- AuthStore,
13
- {
14
- readonly getToken: Effect.Effect<string, AuthRequiredError>;
15
- readonly saveToken: (token: string) => Effect.Effect<void, AuthRequiredError>;
16
- readonly clearToken: Effect.Effect<void>;
17
- }
18
- >() {}
19
-
20
- export const AuthStoreLive = Layer.effect(
21
- AuthStore,
22
- Effect.gen(function* () {
23
- const fs = yield* FileSystem.FileSystem;
24
- const runtime = yield* CliRuntime;
25
- const homeDirectory = yield* runtime.homeDirectory;
26
- const authDir = path.join(homeDirectory, ".better-update");
27
- const authFile = path.join(authDir, "auth.json");
28
-
29
- return {
30
- getToken: Effect.gen(function* () {
31
- const envToken = yield* runtime.getEnv("BETTER_UPDATE_TOKEN");
32
- if (envToken) {
33
- return envToken;
34
- }
35
-
36
- const content = yield* fs.readFileString(authFile).pipe(
37
- Effect.mapError(
38
- () =>
39
- new AuthRequiredError({
40
- message: "Not logged in. Run `better-update login` to authenticate.",
41
- }),
42
- ),
43
- );
44
-
45
- const parsed = yield* Effect.try({
46
- try: (): unknown => JSON.parse(content),
47
- catch: () =>
48
- new AuthRequiredError({
49
- message: "Corrupted auth file. Run `better-update login` to re-authenticate.",
50
- }),
51
- });
52
- if (!isRecord(parsed)) {
53
- return yield* new AuthRequiredError({
54
- message: "Invalid auth file. Run `better-update login` to re-authenticate.",
55
- });
56
- }
57
- const { token } = parsed;
58
- if (typeof token !== "string") {
59
- return yield* new AuthRequiredError({
60
- message: "Invalid auth file. Run `better-update login` to re-authenticate.",
61
- });
62
- }
63
-
64
- return token;
65
- }),
66
-
67
- saveToken: (token: string) =>
68
- Effect.gen(function* () {
69
- yield* fs.makeDirectory(authDir, { recursive: true });
70
- yield* fs.chmod(authDir, 0o700);
71
- yield* fs.writeFileString(authFile, `${JSON.stringify({ token }, null, 2)}\n`);
72
- yield* fs.chmod(authFile, 0o600);
73
- }).pipe(
74
- Effect.mapError(
75
- (cause) =>
76
- new AuthRequiredError({
77
- message: `Failed to save auth token: ${formatCause(cause)}`,
78
- }),
79
- ),
80
- ),
81
-
82
- clearToken: fs.remove(authFile).pipe(Effect.catchAll(() => Effect.void)),
83
- };
84
- }),
85
- );
@@ -1,46 +0,0 @@
1
- import process from "node:process";
2
-
3
- import { Context, Effect, Layer } from "effect";
4
-
5
- const definedEnvironment = (): Readonly<Record<string, string>> =>
6
- Object.fromEntries(
7
- Object.entries(process.env).flatMap(([key, value]) =>
8
- typeof value === "string" ? [[key, value] as const] : [],
9
- ),
10
- );
11
-
12
- export class CliRuntime extends Context.Tag("cli/CliRuntime")<
13
- CliRuntime,
14
- {
15
- readonly argv: readonly string[];
16
- readonly platform: NodeJS.Platform;
17
- readonly cwd: Effect.Effect<string>;
18
- readonly getEnv: (name: string) => Effect.Effect<string | undefined>;
19
- readonly homeDirectory: Effect.Effect<string>;
20
- readonly userName: Effect.Effect<string>;
21
- readonly commandEnvironment: (
22
- overrides?: Readonly<Record<string, string>>,
23
- ) => Effect.Effect<Readonly<Record<string, string>>>;
24
- readonly setExitCode: (code: number) => Effect.Effect<void>;
25
- }
26
- >() {}
27
-
28
- export const CliRuntimeLive = Layer.succeed(CliRuntime, {
29
- argv: [...process.argv],
30
- platform: process.platform,
31
- cwd: Effect.sync(() => process.cwd()),
32
- getEnv: (name: string) => Effect.sync(() => process.env[name]),
33
- homeDirectory: Effect.sync(
34
- () => process.env["HOME"] ?? process.env["USERPROFILE"] ?? process.cwd(),
35
- ),
36
- userName: Effect.sync(() => process.env["USER"] ?? process.env["USERNAME"] ?? "better-update"),
37
- commandEnvironment: (overrides = {}) =>
38
- Effect.sync(() => ({
39
- ...definedEnvironment(),
40
- ...overrides,
41
- })),
42
- setExitCode: (code: number) =>
43
- Effect.sync(() => {
44
- process.exitCode = code;
45
- }),
46
- });
@@ -1,108 +0,0 @@
1
- import path from "node:path";
2
-
3
- import { FileSystem } from "@effect/platform";
4
- import { Context, Data, Effect, Layer } from "effect";
5
-
6
- import { isRecord } from "../lib/record";
7
- import { CliRuntime } from "./cli-runtime";
8
-
9
- const DEFAULT_BASE_URL = "https://api.better-update.dev";
10
- const DEFAULT_DASHBOARD_URL = "https://better-update.dev";
11
-
12
- class ConfigStoreParseError extends Data.TaggedError("ConfigStoreParseError")<{
13
- readonly message: string;
14
- readonly cause: unknown;
15
- }> {}
16
-
17
- const normalizeUrl = (value: string): string => value.replace(/\/$/, "");
18
-
19
- const deriveDashboardUrl = (serverUrl: string): string => {
20
- const normalized = normalizeUrl(serverUrl);
21
- if (!URL.canParse(normalized)) {
22
- return DEFAULT_DASHBOARD_URL;
23
- }
24
-
25
- const url = new URL(normalized);
26
-
27
- if (url.hostname.startsWith("api.")) {
28
- url.hostname = url.hostname.slice(4);
29
- return normalizeUrl(url.toString());
30
- }
31
-
32
- if (url.pathname === "/api") {
33
- url.pathname = "/";
34
- return normalizeUrl(url.toString());
35
- }
36
-
37
- return normalized;
38
- };
39
-
40
- export class ConfigStore extends Context.Tag("cli/ConfigStore")<
41
- ConfigStore,
42
- {
43
- readonly getBaseUrl: Effect.Effect<string>;
44
- readonly getDashboardUrl: Effect.Effect<string>;
45
- }
46
- >() {}
47
-
48
- export const ConfigStoreLive = Layer.effect(
49
- ConfigStore,
50
- Effect.gen(function* () {
51
- const fs = yield* FileSystem.FileSystem;
52
- const runtime = yield* CliRuntime;
53
- const homeDirectory = yield* runtime.homeDirectory;
54
- const configFile = path.join(homeDirectory, ".better-update", "config.json");
55
- const readConfig = fs.readFileString(configFile).pipe(
56
- Effect.catchAll(() => Effect.succeed("")),
57
- Effect.flatMap((content) =>
58
- content.length === 0
59
- ? Effect.succeed(undefined)
60
- : Effect.try({
61
- try: (): unknown => JSON.parse(content),
62
- catch: (cause) =>
63
- new ConfigStoreParseError({
64
- message: "Config file contains invalid JSON",
65
- cause,
66
- }),
67
- }).pipe(
68
- Effect.map((parsed) => (isRecord(parsed) ? parsed : undefined)),
69
- Effect.catchAll(() => Effect.succeed(undefined)),
70
- ),
71
- ),
72
- );
73
- const resolveBaseUrl = Effect.gen(function* () {
74
- const envUrl = yield* runtime.getEnv("BETTER_UPDATE_URL");
75
- if (envUrl) {
76
- return normalizeUrl(envUrl);
77
- }
78
-
79
- const parsed = yield* readConfig;
80
- const serverUrl = parsed?.["serverUrl"];
81
- if (typeof serverUrl === "string") {
82
- return normalizeUrl(serverUrl);
83
- }
84
-
85
- return DEFAULT_BASE_URL;
86
- });
87
-
88
- return {
89
- getBaseUrl: resolveBaseUrl,
90
-
91
- getDashboardUrl: Effect.gen(function* () {
92
- const envUrl = yield* runtime.getEnv("BETTER_UPDATE_DASHBOARD_URL");
93
- if (envUrl) {
94
- return normalizeUrl(envUrl);
95
- }
96
-
97
- const parsed = yield* readConfig;
98
- const dashboardUrl = parsed?.["dashboardUrl"];
99
- if (typeof dashboardUrl === "string") {
100
- return normalizeUrl(dashboardUrl);
101
- }
102
-
103
- const serverUrl = yield* resolveBaseUrl;
104
- return deriveDashboardUrl(serverUrl);
105
- }),
106
- };
107
- }),
108
- );
@@ -1,84 +0,0 @@
1
- import { FileSystem, HttpClient, HttpClientRequest } from "@effect/platform";
2
- import { Context, Effect, Layer } from "effect";
3
-
4
- import { PresignedUrlExpiredError, UploadFailedError } from "../lib/exit-codes";
5
-
6
- const EXPIRY_SAFETY_MARGIN_MS = 30_000;
7
-
8
- export interface PutToPresignedUrlInput {
9
- readonly url: string;
10
- readonly filePath: string;
11
- readonly byteSize: number;
12
- readonly expiresAt: string;
13
- readonly headers?: Record<string, string>;
14
- }
15
-
16
- export class PresignedUploadClient extends Context.Tag("cli/PresignedUploadClient")<
17
- PresignedUploadClient,
18
- {
19
- readonly putToPresignedUrl: (
20
- input: PutToPresignedUrlInput,
21
- ) => Effect.Effect<void, PresignedUrlExpiredError | UploadFailedError>;
22
- }
23
- >() {}
24
-
25
- export const PresignedUploadClientLive = Layer.effect(
26
- PresignedUploadClient,
27
- Effect.gen(function* () {
28
- const client = yield* HttpClient.HttpClient;
29
- const fileSystem = yield* FileSystem.FileSystem;
30
-
31
- return {
32
- putToPresignedUrl: ({
33
- url,
34
- filePath,
35
- byteSize,
36
- expiresAt,
37
- headers,
38
- }: PutToPresignedUrlInput) =>
39
- Effect.gen(function* () {
40
- const now = Date.now();
41
- const expiryMs = new Date(expiresAt).getTime();
42
- if (Number.isNaN(expiryMs) || now > expiryMs - EXPIRY_SAFETY_MARGIN_MS) {
43
- return yield* new PresignedUrlExpiredError({
44
- message: `Presigned upload URL expired or too close to expiry (expiresAt=${expiresAt}).`,
45
- });
46
- }
47
-
48
- const request = yield* HttpClientRequest.put(url).pipe(
49
- HttpClientRequest.bodyFile(filePath),
50
- Effect.provideService(FileSystem.FileSystem, fileSystem),
51
- Effect.map(
52
- HttpClientRequest.setHeaders({
53
- "content-length": String(byteSize),
54
- ...headers,
55
- }),
56
- ),
57
- Effect.mapError(
58
- (cause) =>
59
- new UploadFailedError({
60
- message: `Failed to open artifact for upload: ${String(cause)}`,
61
- }),
62
- ),
63
- );
64
-
65
- const response = yield* client.execute(request).pipe(
66
- Effect.mapError(
67
- (cause) =>
68
- new UploadFailedError({
69
- message: `HTTP request to presigned URL failed: ${String(cause)}`,
70
- }),
71
- ),
72
- );
73
-
74
- if (response.status < 200 || response.status >= 300) {
75
- const body = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
76
- return yield* new UploadFailedError({
77
- message: `Presigned URL upload failed with status ${response.status}: ${body}`,
78
- });
79
- }
80
- return undefined;
81
- }),
82
- };
83
- }),
84
- );
@@ -1,72 +0,0 @@
1
- import { Context, Effect, Layer } from "effect";
2
-
3
- import { UpdatePublishError } from "../lib/exit-codes";
4
- import { formatCause } from "../lib/format-error";
5
- import { ApiClientService } from "./api-client";
6
- import { PresignedUploadClient } from "./presigned-upload";
7
-
8
- export interface UploadUpdateAssetInput {
9
- readonly path: string;
10
- readonly hash: string;
11
- readonly byteSize: number;
12
- readonly uploadUrl: string;
13
- readonly uploadExpiresAt: string;
14
- readonly uploadHeaders: Record<string, string>;
15
- }
16
-
17
- export class UpdateAssetUploader extends Context.Tag("cli/UpdateAssetUploader")<
18
- UpdateAssetUploader,
19
- {
20
- readonly uploadAssetBinary: (
21
- input: UploadUpdateAssetInput,
22
- ) => Effect.Effect<void, UpdatePublishError>;
23
- }
24
- >() {}
25
-
26
- export const UpdateAssetUploaderLive = Layer.effect(
27
- UpdateAssetUploader,
28
- Effect.gen(function* () {
29
- const presignedUploadClient = yield* PresignedUploadClient;
30
- const apiService = yield* ApiClientService;
31
-
32
- return {
33
- uploadAssetBinary: (asset: UploadUpdateAssetInput) =>
34
- Effect.gen(function* () {
35
- const api = yield* apiService.get.pipe(
36
- Effect.mapError(
37
- (cause) =>
38
- new UpdatePublishError({
39
- message: `Asset upload requires authentication: ${formatCause(cause)}`,
40
- }),
41
- ),
42
- );
43
-
44
- yield* presignedUploadClient
45
- .putToPresignedUrl({
46
- url: asset.uploadUrl,
47
- filePath: asset.path,
48
- byteSize: asset.byteSize,
49
- expiresAt: asset.uploadExpiresAt,
50
- headers: asset.uploadHeaders,
51
- })
52
- .pipe(
53
- Effect.mapError(
54
- (cause) =>
55
- new UpdatePublishError({
56
- message: `Asset upload failed for ${asset.hash}: ${formatCause(cause)}`,
57
- }),
58
- ),
59
- );
60
-
61
- yield* api.assets.finalize({ path: { hash: asset.hash } }).pipe(
62
- Effect.mapError(
63
- (cause) =>
64
- new UpdatePublishError({
65
- message: `Asset finalize failed for ${asset.hash}: ${formatCause(cause)}`,
66
- }),
67
- ),
68
- );
69
- }),
70
- };
71
- }),
72
- );
@@ -1,22 +0,0 @@
1
- export type KeychainModuleMarker = never;
2
-
3
- declare module "keychain" {
4
- interface KeychainOptions {
5
- account: string;
6
- service: string;
7
- type?: string;
8
- }
9
-
10
- interface SetPasswordOptions extends KeychainOptions {
11
- password: string;
12
- }
13
-
14
- interface KeychainAccess {
15
- getPassword(opts: KeychainOptions, cb: (err: Error | null, password?: string) => void): void;
16
- setPassword(opts: SetPasswordOptions, cb: (err: Error | null) => void): void;
17
- deletePassword(opts: KeychainOptions, cb: (err: Error | null) => void): void;
18
- }
19
-
20
- const keychain: KeychainAccess;
21
- export default keychain;
22
- }