@better-update/cli 0.3.0
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/CHANGELOG.md +58 -0
- package/oxlint.config.ts +6 -0
- package/package.json +36 -0
- package/src/app-layer.ts +29 -0
- package/src/application/build-workflow.ts +222 -0
- package/src/application/command-exit.ts +13 -0
- package/src/application/login.ts +87 -0
- package/src/application/update-promote.ts +88 -0
- package/src/application/update-publish.ts +402 -0
- package/src/application/update-rollback.ts +275 -0
- package/src/commands/analytics/adoption.ts +40 -0
- package/src/commands/analytics/channels.ts +35 -0
- package/src/commands/analytics/helpers.ts +3 -0
- package/src/commands/analytics/index.ts +13 -0
- package/src/commands/analytics/platforms.ts +39 -0
- package/src/commands/analytics/updates.ts +35 -0
- package/src/commands/audit-logs/helpers.ts +3 -0
- package/src/commands/audit-logs/index.ts +8 -0
- package/src/commands/audit-logs/list.ts +66 -0
- package/src/commands/branches.ts +70 -0
- package/src/commands/build/android.ts +129 -0
- package/src/commands/build/index.ts +63 -0
- package/src/commands/build/ios.ts +199 -0
- package/src/commands/build/reserve-and-upload.test.ts +263 -0
- package/src/commands/build/reserve-and-upload.ts +160 -0
- package/src/commands/build/run-step.ts +131 -0
- package/src/commands/builds/compatibility-matrix.ts +48 -0
- package/src/commands/builds/delete.ts +15 -0
- package/src/commands/builds/get.ts +34 -0
- package/src/commands/builds/helpers.ts +3 -0
- package/src/commands/builds/index.ts +20 -0
- package/src/commands/builds/install-link.ts +20 -0
- package/src/commands/builds/list.ts +38 -0
- package/src/commands/channels/create.ts +37 -0
- package/src/commands/channels/delete.ts +15 -0
- package/src/commands/channels/helpers.ts +18 -0
- package/src/commands/channels/index.ts +24 -0
- package/src/commands/channels/list.ts +38 -0
- package/src/commands/channels/pause.ts +15 -0
- package/src/commands/channels/resume.ts +15 -0
- package/src/commands/channels/rollout/complete.ts +17 -0
- package/src/commands/channels/rollout/create.ts +36 -0
- package/src/commands/channels/rollout/index.ts +11 -0
- package/src/commands/channels/rollout/revert.ts +17 -0
- package/src/commands/channels/rollout/update.ts +23 -0
- package/src/commands/channels/update.ts +32 -0
- package/src/commands/credentials/delete.ts +24 -0
- package/src/commands/credentials/index.ts +10 -0
- package/src/commands/credentials/list.ts +33 -0
- package/src/commands/credentials/upload.ts +91 -0
- package/src/commands/env/delete.ts +35 -0
- package/src/commands/env/export.ts +27 -0
- package/src/commands/env/get.ts +25 -0
- package/src/commands/env/helpers.ts +13 -0
- package/src/commands/env/import.ts +31 -0
- package/src/commands/env/index.ts +24 -0
- package/src/commands/env/list.ts +44 -0
- package/src/commands/env/pull.ts +27 -0
- package/src/commands/env/set.ts +42 -0
- package/src/commands/fingerprint/compare.ts +25 -0
- package/src/commands/fingerprint/generate.ts +18 -0
- package/src/commands/fingerprint/index.ts +9 -0
- package/src/commands/init.ts +35 -0
- package/src/commands/login.ts +13 -0
- package/src/commands/logout.ts +12 -0
- package/src/commands/projects.ts +84 -0
- package/src/commands/status.ts +48 -0
- package/src/commands/update/delete.ts +15 -0
- package/src/commands/update/helpers.ts +22 -0
- package/src/commands/update/index.ts +22 -0
- package/src/commands/update/list.ts +60 -0
- package/src/commands/update/promote.ts +30 -0
- package/src/commands/update/publish.ts +94 -0
- package/src/commands/update/rollback.ts +42 -0
- package/src/commands/update/rollout/complete.ts +17 -0
- package/src/commands/update/rollout/index.ts +10 -0
- package/src/commands/update/rollout/revert.ts +17 -0
- package/src/commands/update/rollout/set.ts +23 -0
- package/src/index.ts +53 -0
- package/src/lib/android-keystore.test.ts +114 -0
- package/src/lib/android-keystore.ts +76 -0
- package/src/lib/android-signing-gradle.test.ts +95 -0
- package/src/lib/android-signing-gradle.ts +52 -0
- package/src/lib/app-json.ts +81 -0
- package/src/lib/apple-auth.test.ts +402 -0
- package/src/lib/apple-auth.ts +132 -0
- package/src/lib/artifact-finder.test.ts +195 -0
- package/src/lib/artifact-finder.ts +122 -0
- package/src/lib/browser-login.test.ts +88 -0
- package/src/lib/browser-login.ts +193 -0
- package/src/lib/build-profile.test.ts +290 -0
- package/src/lib/build-profile.ts +234 -0
- package/src/lib/cli-schemas.ts +39 -0
- package/src/lib/command-errors.ts +60 -0
- package/src/lib/credentials-downloader.ts +181 -0
- package/src/lib/credentials-manager.ts +354 -0
- package/src/lib/env-exporter.test.ts +96 -0
- package/src/lib/env-exporter.ts +28 -0
- package/src/lib/exit-codes.ts +82 -0
- package/src/lib/expo-config.ts +130 -0
- package/src/lib/expo-export.test.ts +94 -0
- package/src/lib/expo-export.ts +281 -0
- package/src/lib/fingerprint.ts +67 -0
- package/src/lib/format-error.ts +22 -0
- package/src/lib/git-context.ts +56 -0
- package/src/lib/gradle-config.ts +126 -0
- package/src/lib/ios-export-options.test.ts +98 -0
- package/src/lib/ios-export-options.ts +62 -0
- package/src/lib/ios-keychain.ts +181 -0
- package/src/lib/ios-provisioning.test.ts +115 -0
- package/src/lib/ios-provisioning.ts +179 -0
- package/src/lib/output.ts +32 -0
- package/src/lib/pkcs12.ts +73 -0
- package/src/lib/plist.ts +39 -0
- package/src/lib/post-build-validation.ts +146 -0
- package/src/lib/presigned-upload.test.ts +140 -0
- package/src/lib/presigned-upload.ts +35 -0
- package/src/lib/record.ts +5 -0
- package/src/lib/resolve-named-resource.ts +24 -0
- package/src/lib/runtime-version.test.ts +119 -0
- package/src/lib/runtime-version.ts +62 -0
- package/src/lib/sha256.test.ts +108 -0
- package/src/lib/sha256.ts +80 -0
- package/src/lib/signed-payloads.test.ts +181 -0
- package/src/lib/signed-payloads.ts +164 -0
- package/src/lib/string-utils.ts +4 -0
- package/src/lib/temp-dir.ts +14 -0
- package/src/lib/test-utils.ts +13 -0
- package/src/lib/update-platforms.test.ts +45 -0
- package/src/lib/update-platforms.ts +19 -0
- package/src/lib/xcpretty-formatter.ts +21 -0
- package/src/services/api-client.ts +42 -0
- package/src/services/apple-session-store.ts +100 -0
- package/src/services/auth-store.ts +85 -0
- package/src/services/cli-runtime.ts +46 -0
- package/src/services/config-store.ts +108 -0
- package/src/services/presigned-upload.ts +84 -0
- package/src/services/update-asset-uploader.ts +72 -0
- package/src/types/keychain.d.ts +22 -0
- package/tests/e2e/build.test.ts +270 -0
- package/tests/e2e/commands.test.ts +694 -0
- package/tests/e2e/ota-lifecycle.test.ts +275 -0
- package/tests/e2e/publish.test.ts +150 -0
- package/tests/helpers/cli-e2e.ts +426 -0
- package/tests/helpers/pty-driver.ts +142 -0
- package/tests/interactive/harness/provider-prompt.ts +54 -0
- package/tests/interactive/login.test.ts +47 -0
- package/tests/interactive/provider-select.test.ts +59 -0
- package/tsconfig.json +7 -0
- package/vitest.config.ts +38 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli";
|
|
2
|
+
import { Console, Effect, Option } from "effect";
|
|
3
|
+
|
|
4
|
+
import { readProjectId } from "../../lib/app-json";
|
|
5
|
+
import { printTable } from "../../lib/output";
|
|
6
|
+
import { apiClient } from "../../services/api-client";
|
|
7
|
+
import { handleAnalyticsCommandErrors } from "./helpers";
|
|
8
|
+
|
|
9
|
+
const period = Options.choice("period", ["1d", "7d", "30d", "90d"]).pipe(Options.optional);
|
|
10
|
+
|
|
11
|
+
export const adoptionCommand = Command.make("adoption", { period }, (opts) =>
|
|
12
|
+
Effect.gen(function* () {
|
|
13
|
+
const projectId = yield* readProjectId;
|
|
14
|
+
const api = yield* apiClient;
|
|
15
|
+
|
|
16
|
+
const periodFilter = Option.match(opts.period, {
|
|
17
|
+
onNone: () => ({}) as Record<string, string>,
|
|
18
|
+
onSome: (periodValue) => ({ period: periodValue }) as Record<string, string>,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const result = yield* api.analytics.adoption({
|
|
22
|
+
urlParams: { projectId, ...periodFilter },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (result.updates.length === 0) {
|
|
26
|
+
yield* Console.log("No adoption data found.");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
yield* printTable(
|
|
31
|
+
["Update ID", "Devices", "First Seen", "Last Seen"],
|
|
32
|
+
result.updates.map((update) => [
|
|
33
|
+
update.updateId,
|
|
34
|
+
String(update.devices),
|
|
35
|
+
update.firstSeen,
|
|
36
|
+
update.lastSeen,
|
|
37
|
+
]),
|
|
38
|
+
);
|
|
39
|
+
}).pipe(handleAnalyticsCommandErrors),
|
|
40
|
+
);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli";
|
|
2
|
+
import { Effect, Option } from "effect";
|
|
3
|
+
|
|
4
|
+
import { readProjectId } from "../../lib/app-json";
|
|
5
|
+
import { printKeyValue } from "../../lib/output";
|
|
6
|
+
import { apiClient } from "../../services/api-client";
|
|
7
|
+
import { handleAnalyticsCommandErrors } from "./helpers";
|
|
8
|
+
|
|
9
|
+
const channel = Options.text("channel");
|
|
10
|
+
const period = Options.choice("period", ["1d", "7d", "30d", "90d"]).pipe(Options.optional);
|
|
11
|
+
|
|
12
|
+
export const channelsCommand = Command.make("channels", { channel, period }, (opts) =>
|
|
13
|
+
Effect.gen(function* () {
|
|
14
|
+
const projectId = yield* readProjectId;
|
|
15
|
+
const api = yield* apiClient;
|
|
16
|
+
|
|
17
|
+
const periodFilter = Option.match(opts.period, {
|
|
18
|
+
onNone: () => ({}) as Record<string, string>,
|
|
19
|
+
onSome: (periodValue) => ({ period: periodValue }) as Record<string, string>,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const result = yield* api.analytics.channels({
|
|
23
|
+
urlParams: { projectId, channel: opts.channel, ...periodFilter },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
yield* printKeyValue([
|
|
27
|
+
["Channel", result.channel],
|
|
28
|
+
["Total Requests", String(result.totalRequests)],
|
|
29
|
+
["Unique Devices", String(result.uniqueDevices)],
|
|
30
|
+
["Manifest", String(result.responseTypeDistribution.manifest)],
|
|
31
|
+
["Directive", String(result.responseTypeDistribution.directive)],
|
|
32
|
+
["No Update", String(result.responseTypeDistribution.no_update)],
|
|
33
|
+
]);
|
|
34
|
+
}).pipe(handleAnalyticsCommandErrors),
|
|
35
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from "@effect/cli";
|
|
2
|
+
import { Console } from "effect";
|
|
3
|
+
|
|
4
|
+
import { adoptionCommand } from "./adoption";
|
|
5
|
+
import { channelsCommand } from "./channels";
|
|
6
|
+
import { platformsCommand } from "./platforms";
|
|
7
|
+
import { updatesCommand } from "./updates";
|
|
8
|
+
|
|
9
|
+
export const analyticsCommand = Command.make("analytics", {}, () =>
|
|
10
|
+
Console.log("View deployment analytics. Run with --help for subcommands."),
|
|
11
|
+
).pipe(
|
|
12
|
+
Command.withSubcommands([adoptionCommand, updatesCommand, channelsCommand, platformsCommand]),
|
|
13
|
+
);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli";
|
|
2
|
+
import { Console, Effect, Option } from "effect";
|
|
3
|
+
|
|
4
|
+
import { readProjectId } from "../../lib/app-json";
|
|
5
|
+
import { printTable } from "../../lib/output";
|
|
6
|
+
import { apiClient } from "../../services/api-client";
|
|
7
|
+
import { handleAnalyticsCommandErrors } from "./helpers";
|
|
8
|
+
|
|
9
|
+
const period = Options.choice("period", ["1d", "7d", "30d", "90d"]).pipe(Options.optional);
|
|
10
|
+
|
|
11
|
+
export const platformsCommand = Command.make("platforms", { period }, (opts) =>
|
|
12
|
+
Effect.gen(function* () {
|
|
13
|
+
const projectId = yield* readProjectId;
|
|
14
|
+
const api = yield* apiClient;
|
|
15
|
+
|
|
16
|
+
const periodFilter = Option.match(opts.period, {
|
|
17
|
+
onNone: () => ({}) as Record<string, string>,
|
|
18
|
+
onSome: (periodValue) => ({ period: periodValue }) as Record<string, string>,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const result = yield* api.analytics.platforms({
|
|
22
|
+
urlParams: { projectId, ...periodFilter },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (result.platforms.length === 0) {
|
|
26
|
+
yield* Console.log("No platform data found.");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
yield* printTable(
|
|
31
|
+
["Platform", "Requests", "Devices"],
|
|
32
|
+
result.platforms.map((platform) => [
|
|
33
|
+
platform.platform,
|
|
34
|
+
String(platform.requests),
|
|
35
|
+
String(platform.devices),
|
|
36
|
+
]),
|
|
37
|
+
);
|
|
38
|
+
}).pipe(handleAnalyticsCommandErrors),
|
|
39
|
+
);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli";
|
|
2
|
+
import { Effect, Option } from "effect";
|
|
3
|
+
|
|
4
|
+
import { readProjectId } from "../../lib/app-json";
|
|
5
|
+
import { printKeyValue } from "../../lib/output";
|
|
6
|
+
import { apiClient } from "../../services/api-client";
|
|
7
|
+
import { handleAnalyticsCommandErrors } from "./helpers";
|
|
8
|
+
|
|
9
|
+
const updateId = Options.text("update-id");
|
|
10
|
+
const period = Options.choice("period", ["1d", "7d", "30d", "90d"]).pipe(Options.optional);
|
|
11
|
+
|
|
12
|
+
export const updatesCommand = Command.make("updates", { updateId, period }, (opts) =>
|
|
13
|
+
Effect.gen(function* () {
|
|
14
|
+
const projectId = yield* readProjectId;
|
|
15
|
+
const api = yield* apiClient;
|
|
16
|
+
|
|
17
|
+
const periodFilter = Option.match(opts.period, {
|
|
18
|
+
onNone: () => ({}) as Record<string, string>,
|
|
19
|
+
onSome: (periodValue) => ({ period: periodValue }) as Record<string, string>,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const result = yield* api.analytics.updates({
|
|
23
|
+
urlParams: { projectId, updateId: opts.updateId, ...periodFilter },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
yield* printKeyValue([
|
|
27
|
+
["Update ID", result.updateId],
|
|
28
|
+
["Total Requests", String(result.totalRequests)],
|
|
29
|
+
["Unique Devices", String(result.uniqueDevices)],
|
|
30
|
+
["Manifest", String(result.byResponseType.manifest)],
|
|
31
|
+
["Directive", String(result.byResponseType.directive)],
|
|
32
|
+
["No Update", String(result.byResponseType.no_update)],
|
|
33
|
+
]);
|
|
34
|
+
}).pipe(handleAnalyticsCommandErrors),
|
|
35
|
+
);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Command } from "@effect/cli";
|
|
2
|
+
import { Console } from "effect";
|
|
3
|
+
|
|
4
|
+
import { listCommand } from "./list";
|
|
5
|
+
|
|
6
|
+
export const auditLogsCommand = Command.make("audit-logs", {}, () =>
|
|
7
|
+
Console.log("View audit logs. Run with --help for subcommands."),
|
|
8
|
+
).pipe(Command.withSubcommands([listCommand]));
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli";
|
|
2
|
+
import { Console, Effect, Option } from "effect";
|
|
3
|
+
|
|
4
|
+
import { printTable } from "../../lib/output";
|
|
5
|
+
import { apiClient } from "../../services/api-client";
|
|
6
|
+
import { handleAuditLogCommandErrors } from "./helpers";
|
|
7
|
+
|
|
8
|
+
const action = Options.text("action").pipe(Options.optional);
|
|
9
|
+
const resourceType = Options.text("resource-type").pipe(Options.optional);
|
|
10
|
+
const actorId = Options.text("actor-id").pipe(Options.optional);
|
|
11
|
+
const from = Options.text("from").pipe(Options.optional);
|
|
12
|
+
const to = Options.text("to").pipe(Options.optional);
|
|
13
|
+
|
|
14
|
+
export const listCommand = Command.make(
|
|
15
|
+
"list",
|
|
16
|
+
{ action, resourceType, actorId, from, to },
|
|
17
|
+
(opts) =>
|
|
18
|
+
Effect.gen(function* () {
|
|
19
|
+
const api = yield* apiClient;
|
|
20
|
+
|
|
21
|
+
const filters = {
|
|
22
|
+
...Option.match(opts.action, {
|
|
23
|
+
onNone: () => ({}),
|
|
24
|
+
onSome: (value) => ({ action: value }),
|
|
25
|
+
}),
|
|
26
|
+
...Option.match(opts.resourceType, {
|
|
27
|
+
onNone: () => ({}),
|
|
28
|
+
onSome: (value) => ({ resourceType: value }),
|
|
29
|
+
}),
|
|
30
|
+
...Option.match(opts.actorId, {
|
|
31
|
+
onNone: () => ({}),
|
|
32
|
+
onSome: (value) => ({ actorId: value }),
|
|
33
|
+
}),
|
|
34
|
+
...Option.match(opts.from, {
|
|
35
|
+
onNone: () => ({}),
|
|
36
|
+
onSome: (value) => ({ from: value }),
|
|
37
|
+
}),
|
|
38
|
+
...Option.match(opts.to, {
|
|
39
|
+
onNone: () => ({}),
|
|
40
|
+
onSome: (value) => ({ to: value }),
|
|
41
|
+
}),
|
|
42
|
+
} as Record<string, string>;
|
|
43
|
+
|
|
44
|
+
const { items } = yield* api["audit-logs"].list({
|
|
45
|
+
urlParams: { ...filters, page: 1, limit: 100 },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (items.length === 0) {
|
|
49
|
+
yield* Console.log("No audit log entries found.");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
yield* printTable(
|
|
54
|
+
["ID", "Action", "Resource Type", "Resource ID", "Actor", "Source", "Created"],
|
|
55
|
+
items.map((log) => [
|
|
56
|
+
log.id,
|
|
57
|
+
log.action,
|
|
58
|
+
log.resourceType,
|
|
59
|
+
log.resourceId ?? "-",
|
|
60
|
+
log.actorEmail,
|
|
61
|
+
log.source,
|
|
62
|
+
log.createdAt,
|
|
63
|
+
]),
|
|
64
|
+
);
|
|
65
|
+
}).pipe(handleAuditLogCommandErrors),
|
|
66
|
+
);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { readProjectId } from "../lib/app-json";
|
|
5
|
+
import { makeCommandErrorHandler } from "../lib/command-errors";
|
|
6
|
+
import { printKeyValue, printTable } from "../lib/output";
|
|
7
|
+
import { apiClient } from "../services/api-client";
|
|
8
|
+
|
|
9
|
+
const handleErrors = makeCommandErrorHandler();
|
|
10
|
+
|
|
11
|
+
const idArg = Args.text({ name: "id" });
|
|
12
|
+
const nameOption = Options.text("name");
|
|
13
|
+
|
|
14
|
+
const listCommand = Command.make("list", {}, () =>
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
const projectId = yield* readProjectId;
|
|
17
|
+
const api = yield* apiClient;
|
|
18
|
+
const { items } = yield* api.branches.list({
|
|
19
|
+
urlParams: { projectId, page: 1, limit: 1000 },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (items.length === 0) {
|
|
23
|
+
yield* Console.log("No branches found.");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
yield* printTable(
|
|
28
|
+
["ID", "Name", "Created"],
|
|
29
|
+
items.map((branch) => [branch.id, branch.name, branch.createdAt]),
|
|
30
|
+
);
|
|
31
|
+
}).pipe(handleErrors),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const createCommand = Command.make("create", { name: nameOption }, (opts) =>
|
|
35
|
+
Effect.gen(function* () {
|
|
36
|
+
const projectId = yield* readProjectId;
|
|
37
|
+
const api = yield* apiClient;
|
|
38
|
+
const branch = yield* api.branches.create({
|
|
39
|
+
payload: { projectId, name: opts.name },
|
|
40
|
+
});
|
|
41
|
+
yield* printKeyValue([
|
|
42
|
+
["ID", branch.id],
|
|
43
|
+
["Name", branch.name],
|
|
44
|
+
["Created", branch.createdAt],
|
|
45
|
+
]);
|
|
46
|
+
}).pipe(handleErrors),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const renameCommand = Command.make("rename", { id: idArg, name: nameOption }, (opts) =>
|
|
50
|
+
Effect.gen(function* () {
|
|
51
|
+
const api = yield* apiClient;
|
|
52
|
+
const branch = yield* api.branches.rename({
|
|
53
|
+
path: { id: opts.id },
|
|
54
|
+
payload: { name: opts.name },
|
|
55
|
+
});
|
|
56
|
+
yield* Console.log(`Branch renamed to "${branch.name}".`);
|
|
57
|
+
}).pipe(handleErrors),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const deleteCommand = Command.make("delete", { id: idArg }, (opts) =>
|
|
61
|
+
Effect.gen(function* () {
|
|
62
|
+
const api = yield* apiClient;
|
|
63
|
+
yield* api.branches.delete({ path: { id: opts.id } });
|
|
64
|
+
yield* Console.log(`Branch ${opts.id} deleted.`);
|
|
65
|
+
}).pipe(handleErrors),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
export const branchesCommand = Command.make("branches", {}, () =>
|
|
69
|
+
Console.log("Manage branches. Run with --help for subcommands."),
|
|
70
|
+
).pipe(Command.withSubcommands([listCommand, createCommand, renameCommand, deleteCommand]));
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { Command, FileSystem } from "@effect/platform";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
|
|
6
|
+
import type { CommandExecutor } from "@effect/platform";
|
|
7
|
+
import type { PlatformError } from "@effect/platform/Error";
|
|
8
|
+
|
|
9
|
+
import { renderSigningGradle } from "../../lib/android-signing-gradle";
|
|
10
|
+
import { findAndroidArtifact } from "../../lib/artifact-finder";
|
|
11
|
+
import { downloadAndroidCredentials } from "../../lib/credentials-downloader";
|
|
12
|
+
import { sha256File } from "../../lib/sha256";
|
|
13
|
+
import { capitalize } from "../../lib/string-utils";
|
|
14
|
+
import { CliRuntime } from "../../services/cli-runtime";
|
|
15
|
+
import { runStep } from "./run-step";
|
|
16
|
+
|
|
17
|
+
import type { AndroidProfile } from "../../lib/build-profile";
|
|
18
|
+
import type {
|
|
19
|
+
ArtifactNotFoundError,
|
|
20
|
+
BuildFailedError,
|
|
21
|
+
MissingCredentialsError,
|
|
22
|
+
} from "../../lib/exit-codes";
|
|
23
|
+
import type { ApiClient } from "../../services/api-client";
|
|
24
|
+
|
|
25
|
+
export interface RunAndroidBuildInput {
|
|
26
|
+
readonly api: ApiClient;
|
|
27
|
+
readonly tempDir: string;
|
|
28
|
+
readonly projectRoot: string;
|
|
29
|
+
readonly androidProfile: AndroidProfile;
|
|
30
|
+
readonly applicationIdentifier: string;
|
|
31
|
+
readonly envVars: Record<string, string>;
|
|
32
|
+
readonly projectId: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface RunAndroidBuildResult {
|
|
36
|
+
readonly artifactPath: string;
|
|
37
|
+
readonly byteSize: number;
|
|
38
|
+
readonly sha256: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Compose the Gradle task name from flavor, format, and buildType.
|
|
43
|
+
*
|
|
44
|
+
* Gradle naming convention: `<verb><Flavor><Variant>`, e.g.
|
|
45
|
+
* - no flavor + apk + release → `assembleRelease`
|
|
46
|
+
* - no flavor + aab + release → `bundleRelease`
|
|
47
|
+
* - flavor=prod + aab + release → `bundleProdRelease`
|
|
48
|
+
* - flavor=prod + apk + debug → `assembleProdDebug`
|
|
49
|
+
*/
|
|
50
|
+
const gradleTaskName = (
|
|
51
|
+
format: "apk" | "aab",
|
|
52
|
+
flavor: string | undefined,
|
|
53
|
+
buildType: "debug" | "release",
|
|
54
|
+
): string => {
|
|
55
|
+
const verb = format === "aab" ? "bundle" : "assemble";
|
|
56
|
+
return flavor
|
|
57
|
+
? `${verb}${capitalize(flavor)}${capitalize(buildType)}`
|
|
58
|
+
: `${verb}${capitalize(buildType)}`;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const runAndroidBuild = (
|
|
62
|
+
input: RunAndroidBuildInput,
|
|
63
|
+
): Effect.Effect<
|
|
64
|
+
RunAndroidBuildResult,
|
|
65
|
+
BuildFailedError | MissingCredentialsError | ArtifactNotFoundError | PlatformError,
|
|
66
|
+
CliRuntime | CommandExecutor.CommandExecutor | FileSystem.FileSystem
|
|
67
|
+
> =>
|
|
68
|
+
Effect.gen(function* () {
|
|
69
|
+
const { api, tempDir, projectRoot, androidProfile, applicationIdentifier, envVars, projectId } =
|
|
70
|
+
input;
|
|
71
|
+
const runtime = yield* CliRuntime;
|
|
72
|
+
|
|
73
|
+
// Record build start so artifact-finder can reject stale outputs from
|
|
74
|
+
// Earlier builds that may still live in `android/app/build/outputs/`.
|
|
75
|
+
const buildStartMs = Date.now();
|
|
76
|
+
|
|
77
|
+
const { format } = androidProfile;
|
|
78
|
+
const { flavor } = androidProfile;
|
|
79
|
+
const buildType = androidProfile.buildType ?? "release";
|
|
80
|
+
const androidDir = path.join(projectRoot, "android");
|
|
81
|
+
const commandEnv = yield* runtime.commandEnvironment(envVars);
|
|
82
|
+
|
|
83
|
+
const credentials = yield* downloadAndroidCredentials(api, {
|
|
84
|
+
projectId,
|
|
85
|
+
applicationIdentifier,
|
|
86
|
+
tempDir,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
yield* runStep(
|
|
90
|
+
Command.make("bunx", "expo", "prebuild", "--platform", "android", "--clean").pipe(
|
|
91
|
+
Command.workingDirectory(projectRoot),
|
|
92
|
+
Command.env(commandEnv),
|
|
93
|
+
),
|
|
94
|
+
"expo prebuild android",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const fs = yield* FileSystem.FileSystem;
|
|
98
|
+
const signingGradlePath = path.join(tempDir, "signing.gradle");
|
|
99
|
+
yield* fs.writeFileString(
|
|
100
|
+
signingGradlePath,
|
|
101
|
+
renderSigningGradle({
|
|
102
|
+
keystorePath: credentials.keystorePath,
|
|
103
|
+
storePassword: credentials.storePassword,
|
|
104
|
+
keyAlias: credentials.keyAlias,
|
|
105
|
+
keyPassword: credentials.keyPassword,
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const taskName = gradleTaskName(format, flavor, buildType);
|
|
110
|
+
yield* runStep(
|
|
111
|
+
Command.make("./gradlew", "--init-script", signingGradlePath, `:app:${taskName}`).pipe(
|
|
112
|
+
Command.workingDirectory(androidDir),
|
|
113
|
+
Command.env(commandEnv),
|
|
114
|
+
),
|
|
115
|
+
"gradlew",
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const artifactPath = yield* findAndroidArtifact({
|
|
119
|
+
projectRoot,
|
|
120
|
+
format,
|
|
121
|
+
...(flavor === undefined ? {} : { flavor }),
|
|
122
|
+
buildType,
|
|
123
|
+
minMtimeMs: buildStartMs,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const { sha256, byteSize } = yield* sha256File(artifactPath);
|
|
127
|
+
|
|
128
|
+
return { artifactPath, byteSize, sha256 };
|
|
129
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli";
|
|
2
|
+
import { Effect, Option } from "effect";
|
|
3
|
+
|
|
4
|
+
import type { BadArgument, SystemError } from "@effect/platform/Error";
|
|
5
|
+
|
|
6
|
+
import { runBuildWorkflow } from "../../application/build-workflow";
|
|
7
|
+
import { exitWith } from "../../application/command-exit";
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ArtifactNotFoundError,
|
|
11
|
+
AuthRequiredError,
|
|
12
|
+
BuildFailedError,
|
|
13
|
+
BuildProfileError,
|
|
14
|
+
CompleteError,
|
|
15
|
+
EnvExportError,
|
|
16
|
+
KeychainError,
|
|
17
|
+
MissingCredentialsError,
|
|
18
|
+
PresignedUrlExpiredError,
|
|
19
|
+
ProjectNotLinkedError,
|
|
20
|
+
ProvisioningError,
|
|
21
|
+
ReserveError,
|
|
22
|
+
RuntimeVersionError,
|
|
23
|
+
UploadFailedError,
|
|
24
|
+
} from "../../lib/exit-codes";
|
|
25
|
+
|
|
26
|
+
const platform = Options.choice("platform", ["ios", "android"] as const);
|
|
27
|
+
const profile = Options.text("profile").pipe(Options.withDefault("production"));
|
|
28
|
+
const message = Options.text("message").pipe(Options.optional);
|
|
29
|
+
const noUpload = Options.boolean("no-upload");
|
|
30
|
+
const rawOutput = Options.boolean("raw-output");
|
|
31
|
+
|
|
32
|
+
export const buildCommand = Command.make(
|
|
33
|
+
"build",
|
|
34
|
+
{ platform, profile, message, noUpload, rawOutput },
|
|
35
|
+
(opts) =>
|
|
36
|
+
runBuildWorkflow({
|
|
37
|
+
platform: opts.platform,
|
|
38
|
+
profileName: opts.profile,
|
|
39
|
+
message: Option.getOrUndefined(opts.message),
|
|
40
|
+
noUpload: opts.noUpload,
|
|
41
|
+
rawOutput: opts.rawOutput,
|
|
42
|
+
}).pipe(
|
|
43
|
+
Effect.catchTags({
|
|
44
|
+
AuthRequiredError: (err: AuthRequiredError) => exitWith(3, err.message),
|
|
45
|
+
ProjectNotLinkedError: (err: ProjectNotLinkedError) => exitWith(4, err.message),
|
|
46
|
+
BuildProfileError: (err: BuildProfileError) => exitWith(2, err.message),
|
|
47
|
+
RuntimeVersionError: (err: RuntimeVersionError) => exitWith(2, err.message),
|
|
48
|
+
MissingCredentialsError: (err: MissingCredentialsError) =>
|
|
49
|
+
exitWith(5, `${err.message}\n${err.hint}`),
|
|
50
|
+
BuildFailedError: (err: BuildFailedError) => exitWith(6, err.message),
|
|
51
|
+
KeychainError: (err: KeychainError) => exitWith(6, err.message),
|
|
52
|
+
ProvisioningError: (err: ProvisioningError) => exitWith(6, err.message),
|
|
53
|
+
ArtifactNotFoundError: (err: ArtifactNotFoundError) => exitWith(6, err.message),
|
|
54
|
+
ReserveError: (err: ReserveError) => exitWith(7, err.message),
|
|
55
|
+
UploadFailedError: (err: UploadFailedError) => exitWith(7, err.message),
|
|
56
|
+
PresignedUrlExpiredError: (err: PresignedUrlExpiredError) => exitWith(7, err.message),
|
|
57
|
+
CompleteError: (err: CompleteError) => exitWith(7, err.message),
|
|
58
|
+
EnvExportError: (err: EnvExportError) => exitWith(7, err.message),
|
|
59
|
+
SystemError: (err: SystemError) => exitWith(6, `Filesystem error: ${err.message}`),
|
|
60
|
+
BadArgument: (err: BadArgument) => exitWith(6, `Invalid argument: ${err.message}`),
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
);
|