@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,131 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
|
|
3
|
+
import { Command } from "@effect/platform";
|
|
4
|
+
import { Effect, Fiber, Stream } from "effect";
|
|
5
|
+
|
|
6
|
+
import type { CommandExecutor } from "@effect/platform";
|
|
7
|
+
import type { Scope } from "effect";
|
|
8
|
+
|
|
9
|
+
import { BuildFailedError } from "../../lib/exit-codes";
|
|
10
|
+
|
|
11
|
+
import type { XcodebuildFormatter } from "../../lib/xcpretty-formatter";
|
|
12
|
+
|
|
13
|
+
export const runStep = (
|
|
14
|
+
cmd: Command.Command,
|
|
15
|
+
step: string,
|
|
16
|
+
): Effect.Effect<void, BuildFailedError, CommandExecutor.CommandExecutor> =>
|
|
17
|
+
Command.exitCode(cmd.pipe(Command.stdout("inherit"), Command.stderr("inherit"))).pipe(
|
|
18
|
+
Effect.mapError(
|
|
19
|
+
(cause) =>
|
|
20
|
+
new BuildFailedError({
|
|
21
|
+
step,
|
|
22
|
+
exitCode: 1,
|
|
23
|
+
message: `${step} failed to spawn: ${String(cause)}`,
|
|
24
|
+
}),
|
|
25
|
+
),
|
|
26
|
+
Effect.flatMap((code) =>
|
|
27
|
+
code === 0
|
|
28
|
+
? Effect.void
|
|
29
|
+
: Effect.fail(
|
|
30
|
+
new BuildFailedError({
|
|
31
|
+
step,
|
|
32
|
+
exitCode: code,
|
|
33
|
+
message: `${step} exited with code ${code}`,
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Run a build step with stdout piped through a formatter (e.g., xcpretty).
|
|
41
|
+
* stderr passes through to the terminal directly.
|
|
42
|
+
*/
|
|
43
|
+
export const runStepFormatted = (
|
|
44
|
+
cmd: Command.Command,
|
|
45
|
+
step: string,
|
|
46
|
+
formatter: XcodebuildFormatter,
|
|
47
|
+
): Effect.Effect<void, BuildFailedError, CommandExecutor.CommandExecutor | Scope.Scope> =>
|
|
48
|
+
Effect.gen(function* () {
|
|
49
|
+
const proc = yield* Command.start(
|
|
50
|
+
cmd.pipe(Command.stdout("pipe"), Command.stderr("pipe")),
|
|
51
|
+
).pipe(
|
|
52
|
+
Effect.mapError(
|
|
53
|
+
(cause) =>
|
|
54
|
+
new BuildFailedError({
|
|
55
|
+
step,
|
|
56
|
+
exitCode: 1,
|
|
57
|
+
message: `${step} failed to spawn: ${String(cause)}`,
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const stdoutFiber = yield* proc.stdout.pipe(
|
|
63
|
+
Stream.decodeText(),
|
|
64
|
+
Stream.splitLines,
|
|
65
|
+
Stream.runForEach((line) => {
|
|
66
|
+
const formatted = formatter.pipe(line);
|
|
67
|
+
return formatted.length > 0
|
|
68
|
+
? Effect.sync(() => {
|
|
69
|
+
for (const output of formatted) {
|
|
70
|
+
process.stdout.write(`${output}\n`);
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
: Effect.void;
|
|
74
|
+
}),
|
|
75
|
+
Effect.mapError(
|
|
76
|
+
(cause) =>
|
|
77
|
+
new BuildFailedError({
|
|
78
|
+
step,
|
|
79
|
+
exitCode: 1,
|
|
80
|
+
message: `${step} stdout stream error: ${String(cause)}`,
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
83
|
+
Effect.fork,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const stderrFiber = yield* proc.stderr.pipe(
|
|
87
|
+
Stream.decodeText(),
|
|
88
|
+
Stream.splitLines,
|
|
89
|
+
Stream.runForEach((line) => Effect.sync(() => process.stderr.write(`${line}\n`))),
|
|
90
|
+
Effect.mapError(
|
|
91
|
+
(cause) =>
|
|
92
|
+
new BuildFailedError({
|
|
93
|
+
step,
|
|
94
|
+
exitCode: 1,
|
|
95
|
+
message: `${step} stderr stream error: ${String(cause)}`,
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
Effect.fork,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Join fibers concurrently — stream errors are non-fatal, exit code takes precedence.
|
|
102
|
+
yield* Effect.all([Fiber.join(stdoutFiber), Fiber.join(stderrFiber)], {
|
|
103
|
+
concurrency: 2,
|
|
104
|
+
}).pipe(Effect.catchAll(() => Effect.void));
|
|
105
|
+
|
|
106
|
+
const code = yield* proc.exitCode.pipe(
|
|
107
|
+
Effect.mapError(
|
|
108
|
+
(cause) =>
|
|
109
|
+
new BuildFailedError({
|
|
110
|
+
step,
|
|
111
|
+
exitCode: 1,
|
|
112
|
+
message: `${step} exit code error: ${String(cause)}`,
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (code !== 0) {
|
|
118
|
+
// Print build summary on failure for xcpretty diagnostics
|
|
119
|
+
const summary = formatter.getBuildSummary();
|
|
120
|
+
if (summary) {
|
|
121
|
+
process.stderr.write(`${summary}\n`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return yield* new BuildFailedError({
|
|
125
|
+
step,
|
|
126
|
+
exitCode: code,
|
|
127
|
+
message: `${step} exited with code ${code}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Command } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } 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 { handleBuildsCommandErrors } from "./helpers";
|
|
8
|
+
|
|
9
|
+
export const compatibilityMatrixCommand = Command.make("compatibility-matrix", {}, () =>
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
const projectId = yield* readProjectId;
|
|
12
|
+
const api = yield* apiClient;
|
|
13
|
+
const result = yield* api.builds.compatibilityMatrix({
|
|
14
|
+
urlParams: { projectId },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (result.rows.length === 0 && result.missingRuntimeVersions.length === 0) {
|
|
18
|
+
yield* Console.log("No compatibility data found.");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (result.rows.length > 0) {
|
|
23
|
+
yield* Console.log("Build-to-Channel Compatibility:");
|
|
24
|
+
yield* printTable(
|
|
25
|
+
["Build ID", "Platform", "Runtime Version", "Channels"],
|
|
26
|
+
result.rows.map((row) => [
|
|
27
|
+
row.id,
|
|
28
|
+
row.platform,
|
|
29
|
+
row.runtimeVersion ?? "-",
|
|
30
|
+
row.channels.map((channel) => channel.channelName).join(", ") || "-",
|
|
31
|
+
]),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (result.missingRuntimeVersions.length > 0) {
|
|
36
|
+
yield* Console.log("\nMissing Runtime Versions:");
|
|
37
|
+
yield* printTable(
|
|
38
|
+
["Channel", "Platform", "Runtime Version", "Updates"],
|
|
39
|
+
result.missingRuntimeVersions.map((missing) => [
|
|
40
|
+
missing.channelName,
|
|
41
|
+
missing.platform,
|
|
42
|
+
missing.runtimeVersion,
|
|
43
|
+
String(missing.updateCount),
|
|
44
|
+
]),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}).pipe(handleBuildsCommandErrors),
|
|
48
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Args, Command } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { apiClient } from "../../services/api-client";
|
|
5
|
+
import { handleBuildsCommandErrors } from "./helpers";
|
|
6
|
+
|
|
7
|
+
const id = Args.text({ name: "id" });
|
|
8
|
+
|
|
9
|
+
export const deleteCommand = Command.make("delete", { id }, (opts) =>
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
const api = yield* apiClient;
|
|
12
|
+
yield* api.builds.delete({ path: { id: opts.id } });
|
|
13
|
+
yield* Console.log(`Build ${opts.id} deleted.`);
|
|
14
|
+
}).pipe(handleBuildsCommandErrors),
|
|
15
|
+
);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Args, Command } from "@effect/cli";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { printKeyValue } from "../../lib/output";
|
|
5
|
+
import { apiClient } from "../../services/api-client";
|
|
6
|
+
import { handleBuildsCommandErrors } from "./helpers";
|
|
7
|
+
|
|
8
|
+
const id = Args.text({ name: "id" });
|
|
9
|
+
|
|
10
|
+
export const getCommand = Command.make("get", { id }, (opts) =>
|
|
11
|
+
Effect.gen(function* () {
|
|
12
|
+
const api = yield* apiClient;
|
|
13
|
+
const build = yield* api.builds.get({ path: { id: opts.id } });
|
|
14
|
+
yield* printKeyValue([
|
|
15
|
+
["ID", build.id],
|
|
16
|
+
["Platform", build.platform],
|
|
17
|
+
["Profile", build.profile],
|
|
18
|
+
["Distribution", build.distribution],
|
|
19
|
+
["Version", build.appVersion ?? "-"],
|
|
20
|
+
["Build Number", build.buildNumber ?? "-"],
|
|
21
|
+
["Runtime Version", build.runtimeVersion ?? "-"],
|
|
22
|
+
["Bundle ID", build.bundleId ?? "-"],
|
|
23
|
+
["Git Ref", build.gitRef ?? "-"],
|
|
24
|
+
["Message", build.message ?? "-"],
|
|
25
|
+
[
|
|
26
|
+
"Artifact",
|
|
27
|
+
build.artifact
|
|
28
|
+
? `${build.artifact.format} (${String(build.artifact.byteSize)} bytes)`
|
|
29
|
+
: "none",
|
|
30
|
+
],
|
|
31
|
+
["Created", build.createdAt],
|
|
32
|
+
]);
|
|
33
|
+
}).pipe(handleBuildsCommandErrors),
|
|
34
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from "@effect/cli";
|
|
2
|
+
import { Console } from "effect";
|
|
3
|
+
|
|
4
|
+
import { compatibilityMatrixCommand } from "./compatibility-matrix";
|
|
5
|
+
import { deleteCommand } from "./delete";
|
|
6
|
+
import { getCommand } from "./get";
|
|
7
|
+
import { installLinkCommand } from "./install-link";
|
|
8
|
+
import { listCommand } from "./list";
|
|
9
|
+
|
|
10
|
+
export const buildsCommand = Command.make("builds", {}, () =>
|
|
11
|
+
Console.log("Manage builds. Run with --help for subcommands."),
|
|
12
|
+
).pipe(
|
|
13
|
+
Command.withSubcommands([
|
|
14
|
+
listCommand,
|
|
15
|
+
getCommand,
|
|
16
|
+
deleteCommand,
|
|
17
|
+
installLinkCommand,
|
|
18
|
+
compatibilityMatrixCommand,
|
|
19
|
+
]),
|
|
20
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Args, Command } from "@effect/cli";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { printKeyValue } from "../../lib/output";
|
|
5
|
+
import { apiClient } from "../../services/api-client";
|
|
6
|
+
import { handleBuildsCommandErrors } from "./helpers";
|
|
7
|
+
|
|
8
|
+
const id = Args.text({ name: "id" });
|
|
9
|
+
|
|
10
|
+
export const installLinkCommand = Command.make("install-link", { id }, (opts) =>
|
|
11
|
+
Effect.gen(function* () {
|
|
12
|
+
const api = yield* apiClient;
|
|
13
|
+
const result = yield* api.builds.getInstallLink({ path: { id: opts.id } });
|
|
14
|
+
yield* printKeyValue([
|
|
15
|
+
["Artifact URL", result.artifactUrl],
|
|
16
|
+
["Install URL", result.installUrl ?? "-"],
|
|
17
|
+
["Expires", String(result.expires)],
|
|
18
|
+
]);
|
|
19
|
+
}).pipe(handleBuildsCommandErrors),
|
|
20
|
+
);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli";
|
|
2
|
+
import { 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 { handleBuildsCommandErrors } from "./helpers";
|
|
8
|
+
|
|
9
|
+
const platform = Options.choice("platform", ["ios", "android"]).pipe(Options.optional);
|
|
10
|
+
const limit = Options.integer("limit").pipe(Options.withDefault(10));
|
|
11
|
+
|
|
12
|
+
export const listCommand = Command.make("list", { platform, limit }, (opts) =>
|
|
13
|
+
Effect.gen(function* () {
|
|
14
|
+
const projectId = yield* readProjectId;
|
|
15
|
+
const api = yield* apiClient;
|
|
16
|
+
|
|
17
|
+
const platformFilter = Option.match(opts.platform, {
|
|
18
|
+
onNone: () => ({}) as Record<string, string>,
|
|
19
|
+
onSome: (platformValue) => ({ platform: platformValue }) as Record<string, string>,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const { items } = yield* api.builds.list({
|
|
23
|
+
urlParams: { projectId, ...platformFilter, page: 1, limit: opts.limit },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
yield* printTable(
|
|
27
|
+
["ID", "Platform", "Profile", "Distribution", "Version", "Created"],
|
|
28
|
+
items.map((build) => [
|
|
29
|
+
build.id,
|
|
30
|
+
build.platform,
|
|
31
|
+
build.profile,
|
|
32
|
+
build.distribution,
|
|
33
|
+
build.appVersion ?? "-",
|
|
34
|
+
build.createdAt,
|
|
35
|
+
]),
|
|
36
|
+
);
|
|
37
|
+
}).pipe(handleBuildsCommandErrors),
|
|
38
|
+
);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli";
|
|
2
|
+
import { Effect } 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 { handleChannelCommandErrors, resolveNamedResourceId } from "./helpers";
|
|
8
|
+
|
|
9
|
+
const name = Options.text("name");
|
|
10
|
+
const branch = Options.text("branch");
|
|
11
|
+
|
|
12
|
+
export const createCommand = Command.make("create", { name, branch }, (opts) =>
|
|
13
|
+
Effect.gen(function* () {
|
|
14
|
+
const projectId = yield* readProjectId;
|
|
15
|
+
const api = yield* apiClient;
|
|
16
|
+
|
|
17
|
+
const { items: branches } = yield* api.branches.list({
|
|
18
|
+
urlParams: { projectId, page: 1, limit: 1000 },
|
|
19
|
+
});
|
|
20
|
+
const branchId = yield* resolveNamedResourceId({
|
|
21
|
+
items: branches,
|
|
22
|
+
kind: "Branch",
|
|
23
|
+
name: opts.branch,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const channel = yield* api.channels.create({
|
|
27
|
+
payload: { projectId, name: opts.name, branchId },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
yield* printKeyValue([
|
|
31
|
+
["ID", channel.id],
|
|
32
|
+
["Name", channel.name],
|
|
33
|
+
["Branch", opts.branch],
|
|
34
|
+
["Created", channel.createdAt],
|
|
35
|
+
]);
|
|
36
|
+
}).pipe(handleChannelCommandErrors),
|
|
37
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Args, Command } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { apiClient } from "../../services/api-client";
|
|
5
|
+
import { handleChannelCommandErrors } from "./helpers";
|
|
6
|
+
|
|
7
|
+
const id = Args.text({ name: "id" });
|
|
8
|
+
|
|
9
|
+
export const deleteCommand = Command.make("delete", { id }, (opts) =>
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
const api = yield* apiClient;
|
|
12
|
+
yield* api.channels.delete({ path: { id: opts.id } });
|
|
13
|
+
yield* Console.log(`Channel ${opts.id} deleted.`);
|
|
14
|
+
}).pipe(handleChannelCommandErrors),
|
|
15
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Data } from "effect";
|
|
2
|
+
|
|
3
|
+
import { makeCommandErrorHandler } from "../../lib/command-errors";
|
|
4
|
+
import { resolveNamedResourceId as resolveNamedResourceIdBase } from "../../lib/resolve-named-resource";
|
|
5
|
+
|
|
6
|
+
export class ChannelCommandError extends Data.TaggedError("ChannelCommandError")<{
|
|
7
|
+
readonly message: string;
|
|
8
|
+
}> {}
|
|
9
|
+
|
|
10
|
+
export const handleChannelCommandErrors = makeCommandErrorHandler({
|
|
11
|
+
ChannelCommandError: 2,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const resolveNamedResourceId = (params: {
|
|
15
|
+
readonly items: readonly { readonly id: string; readonly name: string }[];
|
|
16
|
+
readonly kind: string;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
}) => resolveNamedResourceIdBase(params, (message) => new ChannelCommandError({ message }));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Command } from "@effect/cli";
|
|
2
|
+
import { Console } from "effect";
|
|
3
|
+
|
|
4
|
+
import { createCommand } from "./create";
|
|
5
|
+
import { deleteCommand } from "./delete";
|
|
6
|
+
import { listCommand } from "./list";
|
|
7
|
+
import { pauseCommand } from "./pause";
|
|
8
|
+
import { resumeCommand } from "./resume";
|
|
9
|
+
import { rolloutCommand } from "./rollout";
|
|
10
|
+
import { updateCommand } from "./update";
|
|
11
|
+
|
|
12
|
+
export const channelsCommand = Command.make("channels", {}, () =>
|
|
13
|
+
Console.log("Manage channels. Run with --help for subcommands."),
|
|
14
|
+
).pipe(
|
|
15
|
+
Command.withSubcommands([
|
|
16
|
+
listCommand,
|
|
17
|
+
createCommand,
|
|
18
|
+
updateCommand,
|
|
19
|
+
pauseCommand,
|
|
20
|
+
resumeCommand,
|
|
21
|
+
deleteCommand,
|
|
22
|
+
rolloutCommand,
|
|
23
|
+
]),
|
|
24
|
+
);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Command } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } 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 { handleChannelCommandErrors } from "./helpers";
|
|
8
|
+
|
|
9
|
+
export const listCommand = Command.make("list", {}, () =>
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
const projectId = yield* readProjectId;
|
|
12
|
+
const api = yield* apiClient;
|
|
13
|
+
|
|
14
|
+
const [{ items }, { items: branches }] = yield* Effect.all([
|
|
15
|
+
api.channels.list({ urlParams: { projectId, page: 1, limit: 1000 } }),
|
|
16
|
+
api.branches.list({ urlParams: { projectId, page: 1, limit: 1000 } }),
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
if (items.length === 0) {
|
|
20
|
+
yield* Console.log("No channels found.");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const branchNames = new Map(branches.map((branch) => [branch.id, branch.name]));
|
|
25
|
+
|
|
26
|
+
yield* printTable(
|
|
27
|
+
["ID", "Name", "Branch", "Paused", "Rollout", "Created"],
|
|
28
|
+
items.map((channel) => [
|
|
29
|
+
channel.id,
|
|
30
|
+
channel.name,
|
|
31
|
+
branchNames.get(channel.branchId) ?? channel.branchId,
|
|
32
|
+
channel.isPaused ? "yes" : "no",
|
|
33
|
+
channel.branchMappingJson === null ? "-" : "active",
|
|
34
|
+
channel.createdAt,
|
|
35
|
+
]),
|
|
36
|
+
);
|
|
37
|
+
}).pipe(handleChannelCommandErrors),
|
|
38
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Args, Command } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { apiClient } from "../../services/api-client";
|
|
5
|
+
import { handleChannelCommandErrors } from "./helpers";
|
|
6
|
+
|
|
7
|
+
const id = Args.text({ name: "id" });
|
|
8
|
+
|
|
9
|
+
export const pauseCommand = Command.make("pause", { id }, (opts) =>
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
const api = yield* apiClient;
|
|
12
|
+
const channel = yield* api.channels.pause({ path: { id: opts.id } });
|
|
13
|
+
yield* Console.log(`Channel "${channel.name}" paused.`);
|
|
14
|
+
}).pipe(handleChannelCommandErrors),
|
|
15
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Args, Command } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { apiClient } from "../../services/api-client";
|
|
5
|
+
import { handleChannelCommandErrors } from "./helpers";
|
|
6
|
+
|
|
7
|
+
const id = Args.text({ name: "id" });
|
|
8
|
+
|
|
9
|
+
export const resumeCommand = Command.make("resume", { id }, (opts) =>
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
const api = yield* apiClient;
|
|
12
|
+
const channel = yield* api.channels.resume({ path: { id: opts.id } });
|
|
13
|
+
yield* Console.log(`Channel "${channel.name}" resumed.`);
|
|
14
|
+
}).pipe(handleChannelCommandErrors),
|
|
15
|
+
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Args, Command } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { apiClient } from "../../../services/api-client";
|
|
5
|
+
import { handleChannelCommandErrors } from "../helpers";
|
|
6
|
+
|
|
7
|
+
const channelId = Args.text({ name: "channelId" });
|
|
8
|
+
|
|
9
|
+
export const completeCommand = Command.make("complete", { channelId }, (opts) =>
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
const api = yield* apiClient;
|
|
12
|
+
const channel = yield* api.channels.completeBranchRollout({
|
|
13
|
+
path: { id: opts.channelId },
|
|
14
|
+
});
|
|
15
|
+
yield* Console.log(`Completed rollout on channel "${channel.name}".`);
|
|
16
|
+
}).pipe(handleChannelCommandErrors),
|
|
17
|
+
);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { readProjectId } from "../../../lib/app-json";
|
|
5
|
+
import { rolloutPercentageOption } from "../../../lib/cli-schemas";
|
|
6
|
+
import { apiClient } from "../../../services/api-client";
|
|
7
|
+
import { handleChannelCommandErrors, resolveNamedResourceId } from "../helpers";
|
|
8
|
+
|
|
9
|
+
const channelId = Args.text({ name: "channelId" });
|
|
10
|
+
const branch = Options.text("branch");
|
|
11
|
+
const percentage = rolloutPercentageOption("percentage");
|
|
12
|
+
|
|
13
|
+
export const createCommand = Command.make("create", { channelId, branch, percentage }, (opts) =>
|
|
14
|
+
Effect.gen(function* () {
|
|
15
|
+
const projectId = yield* readProjectId;
|
|
16
|
+
const api = yield* apiClient;
|
|
17
|
+
|
|
18
|
+
const { items: branches } = yield* api.branches.list({
|
|
19
|
+
urlParams: { projectId, page: 1, limit: 1000 },
|
|
20
|
+
});
|
|
21
|
+
const newBranchId = yield* resolveNamedResourceId({
|
|
22
|
+
items: branches,
|
|
23
|
+
kind: "Branch",
|
|
24
|
+
name: opts.branch,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const channel = yield* api.channels.createBranchRollout({
|
|
28
|
+
path: { id: opts.channelId },
|
|
29
|
+
payload: { newBranchId, percentage: opts.percentage },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
yield* Console.log(
|
|
33
|
+
`Started rollout on channel "${channel.name}" to branch "${opts.branch}" at ${String(opts.percentage)}%.`,
|
|
34
|
+
);
|
|
35
|
+
}).pipe(handleChannelCommandErrors),
|
|
36
|
+
);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from "@effect/cli";
|
|
2
|
+
import { Console } from "effect";
|
|
3
|
+
|
|
4
|
+
import { completeCommand } from "./complete";
|
|
5
|
+
import { createCommand } from "./create";
|
|
6
|
+
import { revertCommand } from "./revert";
|
|
7
|
+
import { updateCommand } from "./update";
|
|
8
|
+
|
|
9
|
+
export const rolloutCommand = Command.make("rollout", {}, () =>
|
|
10
|
+
Console.log("Manage channel branch rollouts. Run with --help for subcommands."),
|
|
11
|
+
).pipe(Command.withSubcommands([createCommand, updateCommand, completeCommand, revertCommand]));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Args, Command } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { apiClient } from "../../../services/api-client";
|
|
5
|
+
import { handleChannelCommandErrors } from "../helpers";
|
|
6
|
+
|
|
7
|
+
const channelId = Args.text({ name: "channelId" });
|
|
8
|
+
|
|
9
|
+
export const revertCommand = Command.make("revert", { channelId }, (opts) =>
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
const api = yield* apiClient;
|
|
12
|
+
const channel = yield* api.channels.revertBranchRollout({
|
|
13
|
+
path: { id: opts.channelId },
|
|
14
|
+
});
|
|
15
|
+
yield* Console.log(`Reverted rollout on channel "${channel.name}".`);
|
|
16
|
+
}).pipe(handleChannelCommandErrors),
|
|
17
|
+
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Args, Command } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { rolloutPercentageOption } from "../../../lib/cli-schemas";
|
|
5
|
+
import { apiClient } from "../../../services/api-client";
|
|
6
|
+
import { handleChannelCommandErrors } from "../helpers";
|
|
7
|
+
|
|
8
|
+
const channelId = Args.text({ name: "channelId" });
|
|
9
|
+
const percentage = rolloutPercentageOption("percentage");
|
|
10
|
+
|
|
11
|
+
export const updateCommand = Command.make("update", { channelId, percentage }, (opts) =>
|
|
12
|
+
Effect.gen(function* () {
|
|
13
|
+
const api = yield* apiClient;
|
|
14
|
+
const channel = yield* api.channels.updateBranchRollout({
|
|
15
|
+
path: { id: opts.channelId },
|
|
16
|
+
payload: { percentage: opts.percentage },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
yield* Console.log(
|
|
20
|
+
`Updated rollout on channel "${channel.name}" to ${String(opts.percentage)}%.`,
|
|
21
|
+
);
|
|
22
|
+
}).pipe(handleChannelCommandErrors),
|
|
23
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { readProjectId } from "../../lib/app-json";
|
|
5
|
+
import { apiClient } from "../../services/api-client";
|
|
6
|
+
import { handleChannelCommandErrors, resolveNamedResourceId } from "./helpers";
|
|
7
|
+
|
|
8
|
+
const id = Args.text({ name: "id" });
|
|
9
|
+
const branch = Options.text("branch");
|
|
10
|
+
|
|
11
|
+
export const updateCommand = Command.make("update", { id, branch }, (opts) =>
|
|
12
|
+
Effect.gen(function* () {
|
|
13
|
+
const projectId = yield* readProjectId;
|
|
14
|
+
const api = yield* apiClient;
|
|
15
|
+
|
|
16
|
+
const { items: branches } = yield* api.branches.list({
|
|
17
|
+
urlParams: { projectId, page: 1, limit: 1000 },
|
|
18
|
+
});
|
|
19
|
+
const branchId = yield* resolveNamedResourceId({
|
|
20
|
+
items: branches,
|
|
21
|
+
kind: "Branch",
|
|
22
|
+
name: opts.branch,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const channel = yield* api.channels.update({
|
|
26
|
+
path: { id: opts.id },
|
|
27
|
+
payload: { branchId },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
yield* Console.log(`Channel "${channel.name}" relinked to branch "${opts.branch}".`);
|
|
31
|
+
}).pipe(handleChannelCommandErrors),
|
|
32
|
+
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { deleteCredential } from "../../lib/credentials-manager";
|
|
5
|
+
import { apiClient } from "../../services/api-client";
|
|
6
|
+
|
|
7
|
+
const id = Args.text({ name: "id" });
|
|
8
|
+
const platform = Options.choice("platform", ["ios", "android"] as const);
|
|
9
|
+
const type = Options.choice("type", [
|
|
10
|
+
"distribution-certificate",
|
|
11
|
+
"provisioning-profile",
|
|
12
|
+
"push-key",
|
|
13
|
+
"asc-api-key",
|
|
14
|
+
"keystore",
|
|
15
|
+
"google-service-account-key",
|
|
16
|
+
] as const);
|
|
17
|
+
|
|
18
|
+
export const deleteCommand = Command.make("delete", { id, platform, type }, (opts) =>
|
|
19
|
+
Effect.gen(function* () {
|
|
20
|
+
const api = yield* apiClient;
|
|
21
|
+
yield* deleteCredential(api, { id: opts.id, platform: opts.platform, type: opts.type });
|
|
22
|
+
yield* Console.log(`Credential ${opts.id} deleted.`);
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from "@effect/cli";
|
|
2
|
+
import { Console } from "effect";
|
|
3
|
+
|
|
4
|
+
import { deleteCommand } from "./delete";
|
|
5
|
+
import { listCommand } from "./list";
|
|
6
|
+
import { uploadCommand } from "./upload";
|
|
7
|
+
|
|
8
|
+
export const credentialsCommand = Command.make("credentials", {}, () =>
|
|
9
|
+
Console.log("Manage credentials. Run with --help for subcommands."),
|
|
10
|
+
).pipe(Command.withSubcommands([listCommand, uploadCommand, deleteCommand]));
|