@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,33 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli";
|
|
2
|
+
import { Console, Effect, Option } from "effect";
|
|
3
|
+
|
|
4
|
+
import { filterCredentials, listAllCredentials } from "../../lib/credentials-manager";
|
|
5
|
+
import { printTable } from "../../lib/output";
|
|
6
|
+
import { apiClient } from "../../services/api-client";
|
|
7
|
+
|
|
8
|
+
const platform = Options.choice("platform", ["ios", "android"] as const).pipe(Options.optional);
|
|
9
|
+
|
|
10
|
+
export const listCommand = Command.make("list", { platform }, (opts) =>
|
|
11
|
+
Effect.gen(function* () {
|
|
12
|
+
const api = yield* apiClient;
|
|
13
|
+
const rows = yield* listAllCredentials(api);
|
|
14
|
+
|
|
15
|
+
const filtered = filterCredentials(
|
|
16
|
+
rows,
|
|
17
|
+
Option.match(opts.platform, {
|
|
18
|
+
onNone: () => ({}),
|
|
19
|
+
onSome: (platformValue) => ({ platform: platformValue }),
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (filtered.length === 0) {
|
|
24
|
+
yield* Console.log("No credentials found.");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
yield* printTable(
|
|
29
|
+
["ID", "Name", "Platform", "Type", "Distribution"],
|
|
30
|
+
filtered.map((row) => [row.id, row.name, row.platform, row.type, row.distribution ?? "-"]),
|
|
31
|
+
);
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli";
|
|
2
|
+
import { Console, Effect, Option } from "effect";
|
|
3
|
+
|
|
4
|
+
import { uploadCredential } from "../../lib/credentials-manager";
|
|
5
|
+
import { printKeyValue } from "../../lib/output";
|
|
6
|
+
import { apiClient } from "../../services/api-client";
|
|
7
|
+
|
|
8
|
+
import type { CliCredentialType } from "../../lib/credentials-manager";
|
|
9
|
+
|
|
10
|
+
const platform = Options.choice("platform", ["ios", "android"] as const);
|
|
11
|
+
const type = Options.choice("type", [
|
|
12
|
+
"distribution-certificate",
|
|
13
|
+
"provisioning-profile",
|
|
14
|
+
"push-key",
|
|
15
|
+
"asc-api-key",
|
|
16
|
+
"keystore",
|
|
17
|
+
"google-service-account-key",
|
|
18
|
+
] as const);
|
|
19
|
+
const name = Options.text("name");
|
|
20
|
+
const file = Options.text("file");
|
|
21
|
+
|
|
22
|
+
const password = Options.text("password").pipe(Options.optional);
|
|
23
|
+
const keyAlias = Options.text("key-alias").pipe(Options.optional);
|
|
24
|
+
const keyPassword = Options.text("key-password").pipe(Options.optional);
|
|
25
|
+
const keyId = Options.text("key-id").pipe(Options.optional);
|
|
26
|
+
const issuerId = Options.text("issuer-id").pipe(Options.optional);
|
|
27
|
+
const appleTeamIdentifier = Options.text("apple-team-identifier").pipe(Options.optional);
|
|
28
|
+
|
|
29
|
+
export const uploadCommand = Command.make(
|
|
30
|
+
"upload",
|
|
31
|
+
{
|
|
32
|
+
platform,
|
|
33
|
+
type,
|
|
34
|
+
name,
|
|
35
|
+
file,
|
|
36
|
+
password,
|
|
37
|
+
keyAlias,
|
|
38
|
+
keyPassword,
|
|
39
|
+
keyId,
|
|
40
|
+
issuerId,
|
|
41
|
+
appleTeamIdentifier,
|
|
42
|
+
},
|
|
43
|
+
(opts) =>
|
|
44
|
+
Effect.gen(function* () {
|
|
45
|
+
const api = yield* apiClient;
|
|
46
|
+
|
|
47
|
+
const passwordOpt = Option.getOrUndefined(opts.password);
|
|
48
|
+
const keyAliasOpt = Option.getOrUndefined(opts.keyAlias);
|
|
49
|
+
const keyPasswordOpt = Option.getOrUndefined(opts.keyPassword);
|
|
50
|
+
const keyIdOpt = Option.getOrUndefined(opts.keyId);
|
|
51
|
+
const issuerIdOpt = Option.getOrUndefined(opts.issuerId);
|
|
52
|
+
const appleTeamIdentifierOpt = Option.getOrUndefined(opts.appleTeamIdentifier);
|
|
53
|
+
|
|
54
|
+
const input: {
|
|
55
|
+
readonly platform: typeof opts.platform;
|
|
56
|
+
readonly type: CliCredentialType;
|
|
57
|
+
readonly name: string;
|
|
58
|
+
readonly filePath: string;
|
|
59
|
+
readonly password?: string;
|
|
60
|
+
readonly keyAlias?: string;
|
|
61
|
+
readonly keyPassword?: string;
|
|
62
|
+
readonly keyId?: string;
|
|
63
|
+
readonly issuerId?: string;
|
|
64
|
+
readonly appleTeamIdentifier?: string;
|
|
65
|
+
} = {
|
|
66
|
+
platform: opts.platform,
|
|
67
|
+
type: opts.type,
|
|
68
|
+
name: opts.name,
|
|
69
|
+
filePath: opts.file,
|
|
70
|
+
...(passwordOpt === undefined ? {} : { password: passwordOpt }),
|
|
71
|
+
...(keyAliasOpt === undefined ? {} : { keyAlias: keyAliasOpt }),
|
|
72
|
+
...(keyPasswordOpt === undefined ? {} : { keyPassword: keyPasswordOpt }),
|
|
73
|
+
...(keyIdOpt === undefined ? {} : { keyId: keyIdOpt }),
|
|
74
|
+
...(issuerIdOpt === undefined ? {} : { issuerId: issuerIdOpt }),
|
|
75
|
+
...(appleTeamIdentifierOpt === undefined
|
|
76
|
+
? {}
|
|
77
|
+
: { appleTeamIdentifier: appleTeamIdentifierOpt }),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const credential = yield* uploadCredential(api, input);
|
|
81
|
+
|
|
82
|
+
yield* Console.log("Credential uploaded successfully.");
|
|
83
|
+
yield* Console.log("");
|
|
84
|
+
yield* printKeyValue([
|
|
85
|
+
["ID", credential.id],
|
|
86
|
+
["Name", credential.name],
|
|
87
|
+
["Platform", credential.platform],
|
|
88
|
+
["Type", credential.type],
|
|
89
|
+
]);
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
@@ -0,0 +1,35 @@
|
|
|
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 { EnvResourceNotFoundError, handleEnvCommandErrors } from "./helpers";
|
|
7
|
+
|
|
8
|
+
const keyArg = Args.text({ name: "KEY" });
|
|
9
|
+
const environmentOption = Options.text("environment").pipe(Options.withDefault("production"));
|
|
10
|
+
|
|
11
|
+
export const deleteCommand = Command.make(
|
|
12
|
+
"delete",
|
|
13
|
+
{ key: keyArg, environment: environmentOption },
|
|
14
|
+
({ key, environment }) =>
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
const projectId = yield* readProjectId;
|
|
17
|
+
const api = yield* apiClient;
|
|
18
|
+
|
|
19
|
+
const existing = yield* api["env-vars"].list({
|
|
20
|
+
urlParams: { projectId, environment },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const match = existing.items.find((item) => item.key === key);
|
|
24
|
+
|
|
25
|
+
if (!match) {
|
|
26
|
+
return yield* new EnvResourceNotFoundError({
|
|
27
|
+
message: `Environment variable ${key} not found in ${environment}`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
yield* api["env-vars"].delete({ path: { id: match.id } });
|
|
32
|
+
yield* Console.log(`Deleted ${key} from ${environment}`);
|
|
33
|
+
return undefined;
|
|
34
|
+
}).pipe(handleEnvCommandErrors),
|
|
35
|
+
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { 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 { handleEnvCommandErrors } from "./helpers";
|
|
7
|
+
|
|
8
|
+
const environmentOption = Options.text("environment").pipe(Options.withDefault("production"));
|
|
9
|
+
|
|
10
|
+
export const exportCommand = Command.make(
|
|
11
|
+
"export",
|
|
12
|
+
{ environment: environmentOption },
|
|
13
|
+
({ environment }) =>
|
|
14
|
+
Effect.gen(function* () {
|
|
15
|
+
const projectId = yield* readProjectId;
|
|
16
|
+
const api = yield* apiClient;
|
|
17
|
+
|
|
18
|
+
const result = yield* api["env-vars"].export({
|
|
19
|
+
urlParams: { projectId, environment },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
for (const item of result.items) {
|
|
23
|
+
const escaped = item.value.replaceAll("'", String.raw`'\''`);
|
|
24
|
+
yield* Console.log(`${item.key}='${escaped}'`);
|
|
25
|
+
}
|
|
26
|
+
}).pipe(handleEnvCommandErrors),
|
|
27
|
+
);
|
|
@@ -0,0 +1,25 @@
|
|
|
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 { handleEnvCommandErrors } 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 envVar = yield* api["env-vars"].get({ path: { id: opts.id } });
|
|
14
|
+
yield* printKeyValue([
|
|
15
|
+
["ID", envVar.id],
|
|
16
|
+
["Key", envVar.key],
|
|
17
|
+
["Environment", envVar.environment],
|
|
18
|
+
["Visibility", envVar.visibility],
|
|
19
|
+
// eslint-disable-next-line eslint-js/no-restricted-syntax -- EnvVar.value nullable at storage; display empty when absent
|
|
20
|
+
["Value", envVar.visibility === "plaintext" ? (envVar.value ?? "") : "******"],
|
|
21
|
+
["Created", envVar.createdAt],
|
|
22
|
+
["Updated", envVar.updatedAt],
|
|
23
|
+
]);
|
|
24
|
+
}).pipe(handleEnvCommandErrors),
|
|
25
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Data } from "effect";
|
|
2
|
+
|
|
3
|
+
import { makeCommandErrorHandler } from "../../lib/command-errors";
|
|
4
|
+
|
|
5
|
+
export class EnvResourceNotFoundError extends Data.TaggedError("EnvResourceNotFoundError")<{
|
|
6
|
+
readonly message: string;
|
|
7
|
+
}> {}
|
|
8
|
+
|
|
9
|
+
export const handleEnvCommandErrors = makeCommandErrorHandler({
|
|
10
|
+
EnvResourceNotFoundError: 1,
|
|
11
|
+
SystemError: 6,
|
|
12
|
+
BadArgument: 6,
|
|
13
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
+
import { FileSystem } from "@effect/platform";
|
|
3
|
+
import { Console, Effect } from "effect";
|
|
4
|
+
|
|
5
|
+
import { readProjectId } from "../../lib/app-json";
|
|
6
|
+
import { apiClient } from "../../services/api-client";
|
|
7
|
+
import { handleEnvCommandErrors } from "./helpers";
|
|
8
|
+
|
|
9
|
+
const fileArg = Args.text({ name: "file" });
|
|
10
|
+
const environmentOption = Options.text("environment").pipe(Options.withDefault("production"));
|
|
11
|
+
|
|
12
|
+
export const importCommand = Command.make(
|
|
13
|
+
"import",
|
|
14
|
+
{ file: fileArg, environment: environmentOption },
|
|
15
|
+
({ file, environment }) =>
|
|
16
|
+
Effect.gen(function* () {
|
|
17
|
+
const fs = yield* FileSystem.FileSystem;
|
|
18
|
+
const content = yield* fs.readFileString(file);
|
|
19
|
+
|
|
20
|
+
const projectId = yield* readProjectId;
|
|
21
|
+
const api = yield* apiClient;
|
|
22
|
+
|
|
23
|
+
const result = yield* api["env-vars"].bulkImport({
|
|
24
|
+
payload: { projectId, environment, content, visibility: "plaintext" },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
yield* Console.log(
|
|
28
|
+
`Imported: ${String(result.created)} created, ${String(result.updated)} updated, ${String(result.skipped)} skipped`,
|
|
29
|
+
);
|
|
30
|
+
}).pipe(handleEnvCommandErrors),
|
|
31
|
+
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Command } from "@effect/cli";
|
|
2
|
+
import { Console } from "effect";
|
|
3
|
+
|
|
4
|
+
import { deleteCommand } from "./delete";
|
|
5
|
+
import { exportCommand } from "./export";
|
|
6
|
+
import { getCommand } from "./get";
|
|
7
|
+
import { importCommand } from "./import";
|
|
8
|
+
import { listCommand } from "./list";
|
|
9
|
+
import { pullCommand } from "./pull";
|
|
10
|
+
import { setCommand } from "./set";
|
|
11
|
+
|
|
12
|
+
export const envCommand = Command.make("env", {}, () =>
|
|
13
|
+
Console.log("Manage environment variables. Run with --help for subcommands."),
|
|
14
|
+
).pipe(
|
|
15
|
+
Command.withSubcommands([
|
|
16
|
+
listCommand,
|
|
17
|
+
getCommand,
|
|
18
|
+
setCommand,
|
|
19
|
+
deleteCommand,
|
|
20
|
+
importCommand,
|
|
21
|
+
exportCommand,
|
|
22
|
+
pullCommand,
|
|
23
|
+
]),
|
|
24
|
+
);
|
|
@@ -0,0 +1,44 @@
|
|
|
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 { handleEnvCommandErrors } from "./helpers";
|
|
8
|
+
|
|
9
|
+
const environmentOption = Options.text("environment").pipe(Options.optional);
|
|
10
|
+
|
|
11
|
+
export const listCommand = Command.make(
|
|
12
|
+
"list",
|
|
13
|
+
{ environment: environmentOption },
|
|
14
|
+
({ environment }) =>
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
const projectId = yield* readProjectId;
|
|
17
|
+
const api = yield* apiClient;
|
|
18
|
+
|
|
19
|
+
const envFilter = Option.match(environment, {
|
|
20
|
+
onNone: () => ({}),
|
|
21
|
+
onSome: (value) => ({ environment: value }),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const result = yield* api["env-vars"].list({
|
|
25
|
+
urlParams: { projectId, ...envFilter },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (result.items.length === 0) {
|
|
29
|
+
yield* Console.log("No environment variables found.");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
yield* printTable(
|
|
34
|
+
["Key", "Environment", "Visibility", "Value"],
|
|
35
|
+
result.items.map((item) => [
|
|
36
|
+
item.key,
|
|
37
|
+
item.environment,
|
|
38
|
+
item.visibility,
|
|
39
|
+
// eslint-disable-next-line eslint-js/no-restricted-syntax -- EnvVar.value nullable at storage; display empty when absent
|
|
40
|
+
item.visibility === "plaintext" ? (item.value ?? "") : "••••••",
|
|
41
|
+
]),
|
|
42
|
+
);
|
|
43
|
+
}).pipe(handleEnvCommandErrors),
|
|
44
|
+
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { 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 { handleEnvCommandErrors } from "./helpers";
|
|
7
|
+
|
|
8
|
+
const environmentOption = Options.text("environment").pipe(Options.withDefault("production"));
|
|
9
|
+
|
|
10
|
+
export const pullCommand = Command.make(
|
|
11
|
+
"pull",
|
|
12
|
+
{ environment: environmentOption },
|
|
13
|
+
({ environment }) =>
|
|
14
|
+
Effect.gen(function* () {
|
|
15
|
+
const projectId = yield* readProjectId;
|
|
16
|
+
const api = yield* apiClient;
|
|
17
|
+
|
|
18
|
+
const result = yield* api["env-vars"].export({
|
|
19
|
+
urlParams: { projectId, environment },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
for (const item of result.items) {
|
|
23
|
+
const escaped = item.value.replaceAll("'", String.raw`'\''`);
|
|
24
|
+
yield* Console.log(`export ${item.key}='${escaped}'`);
|
|
25
|
+
}
|
|
26
|
+
}).pipe(handleEnvCommandErrors),
|
|
27
|
+
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { readProjectId } from "../../lib/app-json";
|
|
5
|
+
import { keyValueArg } from "../../lib/cli-schemas";
|
|
6
|
+
import { apiClient } from "../../services/api-client";
|
|
7
|
+
import { handleEnvCommandErrors } from "./helpers";
|
|
8
|
+
|
|
9
|
+
const keyValue = keyValueArg("KEY=VALUE");
|
|
10
|
+
const environmentOption = Options.text("environment").pipe(Options.withDefault("production"));
|
|
11
|
+
const visibilityOption = Options.choice("visibility", ["plaintext", "sensitive", "secret"]).pipe(
|
|
12
|
+
Options.withDefault("plaintext" as const),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const setCommand = Command.make(
|
|
16
|
+
"set",
|
|
17
|
+
{ keyValue, environment: environmentOption, visibility: visibilityOption },
|
|
18
|
+
({ keyValue: { key, value }, environment, visibility }) =>
|
|
19
|
+
Effect.gen(function* () {
|
|
20
|
+
const projectId = yield* readProjectId;
|
|
21
|
+
const api = yield* apiClient;
|
|
22
|
+
|
|
23
|
+
const existing = yield* api["env-vars"].list({
|
|
24
|
+
urlParams: { projectId, environment },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const match = existing.items.find((item) => item.key === key);
|
|
28
|
+
|
|
29
|
+
if (match) {
|
|
30
|
+
yield* api["env-vars"].update({
|
|
31
|
+
path: { id: match.id },
|
|
32
|
+
payload: { value, visibility },
|
|
33
|
+
});
|
|
34
|
+
yield* Console.log(`Updated ${key} in ${environment}`);
|
|
35
|
+
} else {
|
|
36
|
+
yield* api["env-vars"].create({
|
|
37
|
+
payload: { projectId, environment, key, value, visibility },
|
|
38
|
+
});
|
|
39
|
+
yield* Console.log(`Created ${key} in ${environment}`);
|
|
40
|
+
}
|
|
41
|
+
}).pipe(handleEnvCommandErrors),
|
|
42
|
+
);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Args, Command } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { exitWith } from "../../application/command-exit";
|
|
5
|
+
import { runFingerprintFull } from "../../lib/fingerprint";
|
|
6
|
+
import { CliRuntime } from "../../services/cli-runtime";
|
|
7
|
+
|
|
8
|
+
const hash = Args.text({ name: "hash" });
|
|
9
|
+
|
|
10
|
+
export const compareCommand = Command.make("compare", { hash }, (opts) =>
|
|
11
|
+
Effect.gen(function* () {
|
|
12
|
+
const runtime = yield* CliRuntime;
|
|
13
|
+
const projectRoot = yield* runtime.cwd;
|
|
14
|
+
const result = yield* runFingerprintFull(projectRoot);
|
|
15
|
+
|
|
16
|
+
if (result.hash === opts.hash) {
|
|
17
|
+
yield* Console.log("Fingerprints match.");
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
yield* Console.log("Fingerprints differ.");
|
|
21
|
+
yield* Console.log(` Local: ${result.hash}`);
|
|
22
|
+
yield* Console.log(` Provided: ${opts.hash}`);
|
|
23
|
+
return yield* exitWith(1, "Fingerprint mismatch");
|
|
24
|
+
}).pipe(Effect.catchTag("FingerprintError", (error) => exitWith(2, error.message))),
|
|
25
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Command } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { exitWith } from "../../application/command-exit";
|
|
5
|
+
import { runFingerprintFull } from "../../lib/fingerprint";
|
|
6
|
+
import { CliRuntime } from "../../services/cli-runtime";
|
|
7
|
+
|
|
8
|
+
export const generateCommand = Command.make("generate", {}, () =>
|
|
9
|
+
Effect.gen(function* () {
|
|
10
|
+
const runtime = yield* CliRuntime;
|
|
11
|
+
const projectRoot = yield* runtime.cwd;
|
|
12
|
+
const result = yield* runFingerprintFull(projectRoot);
|
|
13
|
+
yield* Console.log(result.hash);
|
|
14
|
+
if (result.sources.length > 0) {
|
|
15
|
+
yield* Console.log(`${result.sources.length} sources`);
|
|
16
|
+
}
|
|
17
|
+
}).pipe(Effect.catchTag("FingerprintError", (error) => exitWith(2, error.message))),
|
|
18
|
+
);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from "@effect/cli";
|
|
2
|
+
import { Console } from "effect";
|
|
3
|
+
|
|
4
|
+
import { compareCommand } from "./compare";
|
|
5
|
+
import { generateCommand } from "./generate";
|
|
6
|
+
|
|
7
|
+
export const fingerprintCommand = Command.make("fingerprint", {}, () =>
|
|
8
|
+
Console.log("Fingerprint utilities. Use --help for subcommands."),
|
|
9
|
+
).pipe(Command.withSubcommands([generateCommand, compareCommand]));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { readAppJson, readSlug, writeProjectId } from "../lib/app-json";
|
|
5
|
+
import { asString } from "../lib/build-profile";
|
|
6
|
+
import { asRecord } from "../lib/record";
|
|
7
|
+
import { apiClient } from "../services/api-client";
|
|
8
|
+
|
|
9
|
+
export const initCommand = Command.make("init", {}, () =>
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
const appJson = yield* readAppJson;
|
|
12
|
+
const expo = asRecord(appJson["expo"]);
|
|
13
|
+
const name = asString(expo?.["name"]) ?? asString(expo?.["slug"]) ?? "untitled";
|
|
14
|
+
const slug = yield* readSlug;
|
|
15
|
+
|
|
16
|
+
yield* Console.log(`Linking project: ${name} (${slug})`);
|
|
17
|
+
|
|
18
|
+
const api = yield* apiClient;
|
|
19
|
+
const { items } = yield* api.projects.list({ urlParams: { page: 1, limit: 100 } });
|
|
20
|
+
|
|
21
|
+
const existing = items.find((project) => project.slug === slug);
|
|
22
|
+
|
|
23
|
+
if (existing) {
|
|
24
|
+
yield* Console.log(`Found existing project: ${existing.name} (${existing.id})`);
|
|
25
|
+
yield* writeProjectId(existing.id);
|
|
26
|
+
} else {
|
|
27
|
+
yield* Console.log("No existing project found. Creating new project...");
|
|
28
|
+
const project = yield* api.projects.create({ payload: { name, slug } });
|
|
29
|
+
yield* Console.log(`Created project: ${project.name} (${project.id})`);
|
|
30
|
+
yield* writeProjectId(project.id);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
yield* Console.log("Project linked successfully. ID saved to app.json.");
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command as CliCommand, Options } from "@effect/cli";
|
|
2
|
+
import { Cause, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { exitWith } from "../application/command-exit";
|
|
5
|
+
import { runLogin } from "../application/login";
|
|
6
|
+
|
|
7
|
+
const manualApiKey = Options.boolean("api-key");
|
|
8
|
+
|
|
9
|
+
const loginFailed = (cause: Cause.Cause<unknown>) => exitWith(1, Cause.pretty(cause));
|
|
10
|
+
|
|
11
|
+
export const loginCommand = CliCommand.make("login", { manualApiKey }, (opts) =>
|
|
12
|
+
runLogin({ manualApiKey: opts.manualApiKey }).pipe(Effect.catchAllCause(loginFailed)),
|
|
13
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { AuthStore } from "../services/auth-store";
|
|
5
|
+
|
|
6
|
+
export const logoutCommand = Command.make("logout", {}, () =>
|
|
7
|
+
Effect.gen(function* () {
|
|
8
|
+
const authStore = yield* AuthStore;
|
|
9
|
+
yield* authStore.clearToken;
|
|
10
|
+
yield* Console.log("Logged out. Auth token removed.");
|
|
11
|
+
}),
|
|
12
|
+
);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { makeCommandErrorHandler } from "../lib/command-errors";
|
|
5
|
+
import { printKeyValue, printTable } from "../lib/output";
|
|
6
|
+
import { apiClient } from "../services/api-client";
|
|
7
|
+
|
|
8
|
+
const handleErrors = makeCommandErrorHandler();
|
|
9
|
+
|
|
10
|
+
const idArg = Args.text({ name: "id" });
|
|
11
|
+
const nameOption = Options.text("name");
|
|
12
|
+
const slugOption = Options.text("slug");
|
|
13
|
+
|
|
14
|
+
const listCommand = Command.make("list", {}, () =>
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
const api = yield* apiClient;
|
|
17
|
+
const { items } = yield* api.projects.list({
|
|
18
|
+
urlParams: { page: 1, limit: 1000 },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (items.length === 0) {
|
|
22
|
+
yield* Console.log("No projects found.");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
yield* printTable(
|
|
27
|
+
["ID", "Name", "Slug", "Created"],
|
|
28
|
+
items.map((project) => [project.id, project.name, project.slug, project.createdAt]),
|
|
29
|
+
);
|
|
30
|
+
}).pipe(handleErrors),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const createCommand = Command.make("create", { name: nameOption, slug: slugOption }, (opts) =>
|
|
34
|
+
Effect.gen(function* () {
|
|
35
|
+
const api = yield* apiClient;
|
|
36
|
+
const project = yield* api.projects.create({
|
|
37
|
+
payload: { name: opts.name, slug: opts.slug },
|
|
38
|
+
});
|
|
39
|
+
yield* printKeyValue([
|
|
40
|
+
["ID", project.id],
|
|
41
|
+
["Name", project.name],
|
|
42
|
+
["Slug", project.slug],
|
|
43
|
+
["Created", project.createdAt],
|
|
44
|
+
]);
|
|
45
|
+
}).pipe(handleErrors),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const getCommand = Command.make("get", { id: idArg }, (opts) =>
|
|
49
|
+
Effect.gen(function* () {
|
|
50
|
+
const api = yield* apiClient;
|
|
51
|
+
const project = yield* api.projects.get({ path: { id: opts.id } });
|
|
52
|
+
yield* printKeyValue([
|
|
53
|
+
["ID", project.id],
|
|
54
|
+
["Name", project.name],
|
|
55
|
+
["Slug", project.slug],
|
|
56
|
+
["Created", project.createdAt],
|
|
57
|
+
]);
|
|
58
|
+
}).pipe(handleErrors),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const renameCommand = Command.make("rename", { id: idArg, name: nameOption }, (opts) =>
|
|
62
|
+
Effect.gen(function* () {
|
|
63
|
+
const api = yield* apiClient;
|
|
64
|
+
const project = yield* api.projects.rename({
|
|
65
|
+
path: { id: opts.id },
|
|
66
|
+
payload: { name: opts.name },
|
|
67
|
+
});
|
|
68
|
+
yield* Console.log(`Project renamed to "${project.name}".`);
|
|
69
|
+
}).pipe(handleErrors),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const deleteCommand = Command.make("delete", { id: idArg }, (opts) =>
|
|
73
|
+
Effect.gen(function* () {
|
|
74
|
+
const api = yield* apiClient;
|
|
75
|
+
yield* api.projects.delete({ path: { id: opts.id } });
|
|
76
|
+
yield* Console.log(`Project ${opts.id} deleted.`);
|
|
77
|
+
}).pipe(handleErrors),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
export const projectsCommand = Command.make("projects", {}, () =>
|
|
81
|
+
Console.log("Manage projects. Run with --help for subcommands."),
|
|
82
|
+
).pipe(
|
|
83
|
+
Command.withSubcommands([listCommand, createCommand, getCommand, renameCommand, deleteCommand]),
|
|
84
|
+
);
|
|
@@ -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 { listAllCredentials } from "../lib/credentials-manager";
|
|
6
|
+
import { printKeyValue } from "../lib/output";
|
|
7
|
+
import { apiClient } from "../services/api-client";
|
|
8
|
+
|
|
9
|
+
export const statusCommand = Command.make("status", {}, () =>
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
const projectId = yield* readProjectId;
|
|
12
|
+
const api = yield* apiClient;
|
|
13
|
+
|
|
14
|
+
const { project, credentials, builds } = yield* Effect.all(
|
|
15
|
+
{
|
|
16
|
+
project: api.projects.get({ path: { id: projectId } }),
|
|
17
|
+
credentials: listAllCredentials(api),
|
|
18
|
+
builds: api.builds.list({ urlParams: { projectId } }),
|
|
19
|
+
},
|
|
20
|
+
{ concurrency: "unbounded" },
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
yield* Console.log("Project");
|
|
24
|
+
yield* Console.log("-------");
|
|
25
|
+
yield* printKeyValue([
|
|
26
|
+
["Name", project.name],
|
|
27
|
+
["ID", project.id],
|
|
28
|
+
["Slug", project.slug],
|
|
29
|
+
["Created", project.createdAt],
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
yield* Console.log("");
|
|
33
|
+
yield* Console.log("Credentials");
|
|
34
|
+
yield* Console.log("-----------");
|
|
35
|
+
const iosCreds = credentials.filter((cred) => cred.platform === "ios").length;
|
|
36
|
+
const androidCreds = credentials.filter((cred) => cred.platform === "android").length;
|
|
37
|
+
yield* printKeyValue([
|
|
38
|
+
["iOS", String(iosCreds)],
|
|
39
|
+
["Android", String(androidCreds)],
|
|
40
|
+
["Total", String(credentials.length)],
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
yield* Console.log("");
|
|
44
|
+
yield* Console.log("Builds");
|
|
45
|
+
yield* Console.log("------");
|
|
46
|
+
yield* printKeyValue([["Total", String(builds.total)]]);
|
|
47
|
+
}),
|
|
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 { handleUpdateCommandErrors } from "./helpers";
|
|
6
|
+
|
|
7
|
+
const groupId = Args.text({ name: "groupId" });
|
|
8
|
+
|
|
9
|
+
export const deleteCommand = Command.make("delete", { groupId }, (opts) =>
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
const api = yield* apiClient;
|
|
12
|
+
const result = yield* api.updates.deleteGroup({ path: { groupId: opts.groupId } });
|
|
13
|
+
yield* Console.log(`Deleted ${String(result.deleted)} update(s) from group ${opts.groupId}.`);
|
|
14
|
+
}).pipe(handleUpdateCommandErrors),
|
|
15
|
+
);
|