@akterjs/cli 0.0.0-stage → 0.1.1
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/LICENSE +201 -0
- package/README.md +47 -2
- package/dist/Argument-D81dqizB.mjs +57 -0
- package/dist/Array-B4kuTQeW.mjs +2323 -0
- package/dist/Boolean-kILIvmhE.mjs +26 -0
- package/dist/ChildProcess-BCIwxPGc.mjs +577 -0
- package/dist/CliError-Cf5xqkkj.mjs +633 -0
- package/dist/Command-Dzq9KM-h.mjs +5135 -0
- package/dist/Config-B6OND9WR.mjs +854 -0
- package/dist/Console-BKHUkH6a.mjs +141 -0
- package/dist/Data-CeCULr0-.mjs +251 -0
- package/dist/Digest-CBd3uv5B.mjs +348 -0
- package/dist/Duration-EH9Vy4U6.mjs +791 -0
- package/dist/Effect-CKFpqbmw.mjs +12911 -0
- package/dist/FileSystem-4pPcEb_i.mjs +1366 -0
- package/dist/HttpClientError-BUeMWx70.mjs +303 -0
- package/dist/HttpClientResponse-D5ZKICVV.mjs +855 -0
- package/dist/Match-CGPLZYrV.mjs +434 -0
- package/dist/MutableHashMap-f8nW8AJR.mjs +376 -0
- package/dist/MutableHashSet-_HI572Ya.mjs +155 -0
- package/dist/MutableRef-D4Qv592C.mjs +107 -0
- package/dist/Number-BvkRkfc-.mjs +137 -0
- package/dist/Option-DkxSnBhp.mjs +3967 -0
- package/dist/Order-C1JC991I.mjs +481 -0
- package/dist/Path-Djdv_eF5.mjs +130 -0
- package/dist/Queue-CO5UQ6os.mjs +1394 -0
- package/dist/Redacted-RkJrG5Me.mjs +160 -0
- package/dist/Ref-BIPOxJNH.mjs +267 -0
- package/dist/Runtime-BInIAwK3.mjs +297 -0
- package/dist/Schema-MiKt5K1v.mjs +10999 -0
- package/dist/SecureRandom-B_U4ysO3.mjs +279 -0
- package/dist/Semaphore-DOVJ4ZGS.mjs +177 -0
- package/dist/Stdio-KylmOBX_.mjs +74 -0
- package/dist/Stream-bK9QIETN.mjs +4012 -0
- package/dist/String-q5dcsVe4.mjs +351 -0
- package/dist/TerminalCapabilities-w_44MAca.mjs +1769 -0
- package/dist/WebCrypto-ZI_GNVlw.mjs +33 -0
- package/dist/actor-call.service-CGM_vD9u.mjs +1269 -0
- package/dist/actor-client.adapter-Coem7gF1.mjs +3691 -0
- package/dist/akter-program.config-D8yb9xQP.mjs +7034 -0
- package/dist/akter.cli.mjs +5 -0
- package/dist/app-cli.config.mjs +49 -0
- package/dist/app-entry.pure.mjs +31 -0
- package/dist/archive/akter-archive.schema.mjs +65 -0
- package/dist/archive/akter-archive.service.mjs +309 -0
- package/dist/archive/archive-cli.config.mjs +45 -0
- package/dist/archive/archive-invalid.errors.mjs +5 -0
- package/dist/archive/archived-lineage.pure.mjs +13 -0
- package/dist/archive/tar.pure.mjs +291 -0
- package/dist/archive/zstd.adapter.mjs +46 -0
- package/dist/bench/bench-report.pure.mjs +120 -0
- package/dist/bench/bench-runner.pure.mjs +89 -0
- package/dist/bench/bench.service.mjs +301 -0
- package/dist/cli-runtime.layer-BqK2mmVL.mjs +1191 -0
- package/dist/cli-runtime.layer.mjs +1 -0
- package/dist/cloud-client.adapter-Cy1BjDbG.mjs +3186 -0
- package/dist/cloud-client.adapter-DOVMoYAj.mjs +304 -0
- package/dist/command/akter-commands.config.mjs +32 -0
- package/dist/command/akter-program.config.mjs +1 -0
- package/dist/credentials/credentials.service.mjs +226 -0
- package/dist/credentials/keychain.adapter.mjs +92 -0
- package/dist/credentials/login.config.mjs +251 -0
- package/dist/deploy/auth/api-attempt.service.mjs +30 -0
- package/dist/deploy/auth/token-refresh.errors.mjs +4 -0
- package/dist/deploy/build-bundle.adapter.mjs +133 -0
- package/dist/deploy/build-manifest.pure.mjs +44 -0
- package/dist/deploy/cloud-client.adapter.mjs +1 -0
- package/dist/deploy/cloud-url.pure.mjs +17 -0
- package/dist/deploy/deploy-failed.errors.mjs +17 -0
- package/dist/deploy/deploy.service.mjs +156 -0
- package/dist/deploy/request-failure.pure.mjs +87 -0
- package/dist/dev-runner.pure.mjs +30 -0
- package/dist/docs/doc-topics.pure.mjs +21 -0
- package/dist/docs/docs.config.mjs +1 -0
- package/dist/docs.config-D2amjsje.mjs +190 -0
- package/dist/docs.config-D3zzLzLR.mjs +190 -0
- package/dist/export/actor-export.service.mjs +267 -0
- package/dist/export/export-failed.errors.mjs +4 -0
- package/dist/export/export-failure.pure.mjs +18 -0
- package/dist/export/ordered-lines.pure.mjs +43 -0
- package/dist/export/ranged-download.pure.mjs +66 -0
- package/dist/export/snapshot-download.pure.mjs +86 -0
- package/dist/history-runner.pure.mjs +186 -0
- package/dist/history-since.pure.mjs +17 -0
- package/dist/hosted-runtime.layer-Cx3IPsEd.mjs +16358 -0
- package/dist/local/environment-cli.config.mjs +90 -0
- package/dist/local/environment-runner.pure.mjs +75 -0
- package/dist/local/local-cli.config.mjs +159 -0
- package/dist/logs/log-follow.config.mjs +21 -0
- package/dist/logs/log-follow.service.mjs +87 -0
- package/dist/logs/log-lines.pure.mjs +44 -0
- package/dist/migrate-runner.pure.mjs +28 -0
- package/dist/operation-input.pure-CS9iUhqK.mjs +1383 -0
- package/dist/operation-registry.pure-DVC8EDgj.mjs +283 -0
- package/dist/registry/actor-call.service.mjs +1 -0
- package/dist/registry/actor-commands.config.mjs +127 -0
- package/dist/registry/actor-refusal.pure.mjs +34 -0
- package/dist/registry/approval.service.mjs +80 -0
- package/dist/registry/cloud-commands.config.mjs +151 -0
- package/dist/registry/cloud-served.pure.mjs +31 -0
- package/dist/registry/operation-call.service.mjs +262 -0
- package/dist/registry/operation-input.pure.mjs +1 -0
- package/dist/registry/operation-output.pure.mjs +91 -0
- package/dist/registry/served-command.config.mjs +57 -0
- package/dist/runner-exited.errors.mjs +10 -0
- package/dist/terminal/cli-exit.errors.mjs +38 -0
- package/dist/terminal/cli-failure.pure.mjs +31 -0
- package/dist/terminal/confirm.adapter.mjs +27 -0
- package/package.json +61 -3
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { t as match } from "../Boolean-kILIvmhE.mjs";
|
|
2
|
+
import { n as log } from "../Console-BKHUkH6a.mjs";
|
|
3
|
+
import { C as catch_, M as fail, U as fn, W as fnUntraced, et as gen, gn as void_, ht as mapError, in as timeout, l as as } from "../Effect-CKFpqbmw.mjs";
|
|
4
|
+
import { S as none, _ as isSome, f as getOrElse, h as isNone, p as getOrNull, v as liftPredicate, w as some, x as match$1 } from "../Option-DkxSnBhp.mjs";
|
|
5
|
+
import { i as value, n as make } from "../Redacted-RkJrG5Me.mjs";
|
|
6
|
+
import { B as TaggedStruct, F as String, I as Struct, S as Literals, mt as is, ot as decodeUnknownOption } from "../Schema-MiKt5K1v.mjs";
|
|
7
|
+
import { t as Stdio } from "../Stdio-KylmOBX_.mjs";
|
|
8
|
+
import { M as mkString, i as decodeText } from "../Stream-bK9QIETN.mjs";
|
|
9
|
+
import { o as isEmpty, v as trim } from "../String-q5dcsVe4.mjs";
|
|
10
|
+
import { f as optional, i as withDescription$1, l as String$1, m as withDescription, o as Boolean, p as withDefault, t as make$1 } from "../Command-Dzq9KM-h.mjs";
|
|
11
|
+
import { c as String$2, d as run, o as Password, t as TerminalCapabilities } from "../TerminalCapabilities-w_44MAca.mjs";
|
|
12
|
+
import { CliExitError } from "../terminal/cli-exit.errors.mjs";
|
|
13
|
+
import { Confirmation } from "../terminal/confirm.adapter.mjs";
|
|
14
|
+
import { Credentials } from "./credentials.service.mjs";
|
|
15
|
+
import { t as CloudClient } from "../cloud-client.adapter-DOVMoYAj.mjs";
|
|
16
|
+
import { compactJson } from "../registry/operation-output.pure.mjs";
|
|
17
|
+
// src/credentials/login.config.ts
|
|
18
|
+
const json = Boolean("json").pipe(withDefault(false), withDescription("Print the result as one line of JSON"));
|
|
19
|
+
const usage = (reason, message) => CliExitError.make({
|
|
20
|
+
code: CliExitError.codes.usage,
|
|
21
|
+
message,
|
|
22
|
+
reason
|
|
23
|
+
});
|
|
24
|
+
const noKey = () => usage("MissingApiKey", "akter login --with-api-key reads the API key from stdin, and stdin was empty.");
|
|
25
|
+
const readKey = gen(function* readKey() {
|
|
26
|
+
const { canPrompt } = yield* TerminalCapabilities;
|
|
27
|
+
if (canPrompt) return yield* run(Password({ message: "API key" })).pipe(mapError(noKey));
|
|
28
|
+
const stdio = yield* Stdio;
|
|
29
|
+
const text = yield* mkString(decodeText(stdio.stdin)).pipe(mapError(noKey));
|
|
30
|
+
return make(trim(text));
|
|
31
|
+
});
|
|
32
|
+
const refused = (origin) => (failure) => CliExitError.make({
|
|
33
|
+
code: failure.code,
|
|
34
|
+
message: `${failure.message} (${origin}); nothing was saved.`,
|
|
35
|
+
reason: failure.reason
|
|
36
|
+
});
|
|
37
|
+
const verified = fnUntraced(function* verified(secret, url) {
|
|
38
|
+
const cloud = yield* CloudClient.use((factory) => factory.connect({
|
|
39
|
+
apiKey: secret,
|
|
40
|
+
url
|
|
41
|
+
}));
|
|
42
|
+
return yield* mapError(cloud.signIn, refused(url.origin));
|
|
43
|
+
});
|
|
44
|
+
const whereOf = (saved) => match(saved.store === "keychain", {
|
|
45
|
+
onFalse: () => `${saved.file}, readable only by you, because no OS keychain was reachable`,
|
|
46
|
+
onTrue: () => "the OS keychain"
|
|
47
|
+
});
|
|
48
|
+
const report = fnUntraced(function* report(saved, url, options) {
|
|
49
|
+
if (options.json) {
|
|
50
|
+
const organization = getOrNull(options.organization);
|
|
51
|
+
const text = yield* compactJson({
|
|
52
|
+
file: saved.file,
|
|
53
|
+
organization,
|
|
54
|
+
store: saved.store,
|
|
55
|
+
url: url.href
|
|
56
|
+
});
|
|
57
|
+
return yield* log(text);
|
|
58
|
+
}
|
|
59
|
+
return yield* log(`Signed in to ${url.origin}. The key is saved in ${whereOf(saved)}.`);
|
|
60
|
+
});
|
|
61
|
+
const keyLogin = fn("keyLogin")(function* keyLogin(options) {
|
|
62
|
+
const credentials = yield* Credentials;
|
|
63
|
+
const url = yield* credentials.url(options.url);
|
|
64
|
+
const secret = yield* readKey;
|
|
65
|
+
if (isEmpty(value(secret))) return yield* noKey();
|
|
66
|
+
yield* verified(secret, url);
|
|
67
|
+
const saved = yield* credentials.save({
|
|
68
|
+
organization: options.organization,
|
|
69
|
+
secret,
|
|
70
|
+
url
|
|
71
|
+
});
|
|
72
|
+
return yield* report(saved, url, options);
|
|
73
|
+
});
|
|
74
|
+
const signInWait = "30 seconds";
|
|
75
|
+
const codeReasons = {
|
|
76
|
+
expired: "The code expired. Run akter login again for a new one.",
|
|
77
|
+
invalid: "The code is wrong, expired, already used, or replaced by a newer one. Run akter login again for a new one.",
|
|
78
|
+
tooManyAttempts: "The code took too many wrong guesses and no longer works. Run akter login again for a new one."
|
|
79
|
+
};
|
|
80
|
+
const Rejected = TaggedStruct("SignInVerifyCode401", { cause: Struct({ reason: Literals([
|
|
81
|
+
"expired",
|
|
82
|
+
"invalid",
|
|
83
|
+
"tooManyAttempts"
|
|
84
|
+
]) }) });
|
|
85
|
+
const TermsRefused = TaggedStruct("SignInVerifyCode422", { cause: Struct({ version: String }) });
|
|
86
|
+
const termsUnread = (version) => `Creating an account needs you to accept Akter's terms of service and privacy notice, version ${version}. Read them with akter docs legal/terms and akter docs legal/privacy, then run this again with --accept-terms and the same --code; the code still works.`;
|
|
87
|
+
const termsNotAccepted = (version) => CliExitError.make({
|
|
88
|
+
code: CliExitError.codes.confirmation,
|
|
89
|
+
message: termsUnread(version),
|
|
90
|
+
reason: "TermsNotAccepted"
|
|
91
|
+
});
|
|
92
|
+
const signInFailure = (origin) => (failure) => match$1(decodeUnknownOption(Rejected)(failure), {
|
|
93
|
+
onNone: () => match$1(decodeUnknownOption(TermsRefused)(failure), {
|
|
94
|
+
onNone: () => CliExitError.make({
|
|
95
|
+
code: CliExitError.codes.unavailable,
|
|
96
|
+
message: `${origin} didn't answer the sign-in, or asked to wait before another code; nothing was saved.`,
|
|
97
|
+
reason: "SignInUnavailable"
|
|
98
|
+
}),
|
|
99
|
+
onSome: ({ cause }) => termsNotAccepted(cause.version)
|
|
100
|
+
}),
|
|
101
|
+
onSome: ({ cause }) => CliExitError.make({
|
|
102
|
+
code: CliExitError.codes.authentication,
|
|
103
|
+
message: codeReasons[cause.reason],
|
|
104
|
+
reason: "CodeRejected"
|
|
105
|
+
})
|
|
106
|
+
});
|
|
107
|
+
const noEmail = () => usage("MissingEmail", "Pass --email: akter login emails a sign-in code there, then takes it with --code.");
|
|
108
|
+
const emailOf = fnUntraced(function* emailOf(given) {
|
|
109
|
+
if (isSome(given)) return given.value;
|
|
110
|
+
const { canPrompt } = yield* TerminalCapabilities;
|
|
111
|
+
if (!canPrompt) return yield* noEmail();
|
|
112
|
+
return yield* run(String$2({ message: "Email" })).pipe(mapError(noEmail));
|
|
113
|
+
});
|
|
114
|
+
const printSent = fnUntraced(function* printSent(sent, asJson) {
|
|
115
|
+
if (asJson) {
|
|
116
|
+
const text = yield* compactJson({
|
|
117
|
+
codeSent: true,
|
|
118
|
+
email: sent.email,
|
|
119
|
+
expiresAt: sent.expiresAt
|
|
120
|
+
});
|
|
121
|
+
return yield* log(text);
|
|
122
|
+
}
|
|
123
|
+
return yield* log(`Sent a sign-in code to ${sent.email}; it works until ${sent.expiresAt}. Finish with: akter login --email ${sent.email} --code <code>`);
|
|
124
|
+
});
|
|
125
|
+
const codeOf = fnUntraced(function* codeOf({ client, email, options, origin }) {
|
|
126
|
+
if (isSome(options.code)) return options.code;
|
|
127
|
+
const sent = yield* client.signInSendCode({ payload: { email } }).pipe(timeout(signInWait), mapError(signInFailure(origin)));
|
|
128
|
+
const { canPrompt } = yield* TerminalCapabilities;
|
|
129
|
+
if (!canPrompt) return yield* as(printSent(sent, options.json), none());
|
|
130
|
+
const typed = yield* run(String$2({ message: `The code emailed to ${sent.email}` }));
|
|
131
|
+
return some(trim(typed));
|
|
132
|
+
});
|
|
133
|
+
const acceptTerms = fnUntraced(function* acceptTerms(version, given) {
|
|
134
|
+
if (given) return true;
|
|
135
|
+
return yield* Confirmation.use((confirmation) => confirmation.confirm({
|
|
136
|
+
question: `Creating your account accepts Akter's terms of service and privacy notice, version ${version} (read them with akter docs legal/terms and akter docs legal/privacy). Accept them?`,
|
|
137
|
+
unattended: termsUnread(version)
|
|
138
|
+
}));
|
|
139
|
+
});
|
|
140
|
+
const verifyCode = fnUntraced(function* verifyCode({ client, code, email, options, origin }) {
|
|
141
|
+
const send = fnUntraced(function* send(acceptedTerms) {
|
|
142
|
+
const terms = match$1(acceptedTerms, {
|
|
143
|
+
onNone: () => ({}),
|
|
144
|
+
onSome: (version) => ({ acceptedTerms: version })
|
|
145
|
+
});
|
|
146
|
+
return yield* client.signInVerifyCode({ payload: {
|
|
147
|
+
code,
|
|
148
|
+
email,
|
|
149
|
+
...terms
|
|
150
|
+
} }).pipe(timeout(signInWait));
|
|
151
|
+
});
|
|
152
|
+
const accepted = fnUntraced(function* accepted(failure) {
|
|
153
|
+
const terms = decodeUnknownOption(TermsRefused)(failure);
|
|
154
|
+
if (isNone(terms)) return yield* fail(failure);
|
|
155
|
+
yield* acceptTerms(terms.value.cause.version, options.acceptTerms);
|
|
156
|
+
return yield* send(some(terms.value.cause.version));
|
|
157
|
+
});
|
|
158
|
+
return yield* send(none()).pipe(catch_(accepted), mapError((failure) => getOrElse(liftPredicate(failure, is(CliExitError)), () => signInFailure(origin)(failure))));
|
|
159
|
+
});
|
|
160
|
+
const reportSession = fnUntraced(function* reportSession({ asJson, saved, signedIn, url }) {
|
|
161
|
+
const { account, created, session } = signedIn;
|
|
162
|
+
if (asJson) {
|
|
163
|
+
const text = yield* compactJson({
|
|
164
|
+
account,
|
|
165
|
+
created,
|
|
166
|
+
file: saved.file,
|
|
167
|
+
session,
|
|
168
|
+
store: saved.store,
|
|
169
|
+
url: url.href
|
|
170
|
+
});
|
|
171
|
+
return yield* log(text);
|
|
172
|
+
}
|
|
173
|
+
const done = match(created, {
|
|
174
|
+
onFalse: () => "Signed in",
|
|
175
|
+
onTrue: () => "Created your account and signed in"
|
|
176
|
+
});
|
|
177
|
+
return yield* log(`${done} to ${url.origin} as ${account.email}. The session is saved in ${whereOf(saved)}.`);
|
|
178
|
+
});
|
|
179
|
+
const emailLogin = fn("emailLogin")(function* emailLogin(options) {
|
|
180
|
+
const credentials = yield* Credentials;
|
|
181
|
+
const url = yield* credentials.url(options.url);
|
|
182
|
+
const client = yield* CloudClient.use((factory) => factory.anonymous(url));
|
|
183
|
+
const email = yield* emailOf(options.email);
|
|
184
|
+
const code = yield* codeOf({
|
|
185
|
+
client,
|
|
186
|
+
email,
|
|
187
|
+
options,
|
|
188
|
+
origin: url.origin
|
|
189
|
+
});
|
|
190
|
+
if (isNone(code)) return yield* void_;
|
|
191
|
+
const signedIn = yield* verifyCode({
|
|
192
|
+
client,
|
|
193
|
+
code: code.value,
|
|
194
|
+
email,
|
|
195
|
+
options,
|
|
196
|
+
origin: url.origin
|
|
197
|
+
});
|
|
198
|
+
const saved = yield* credentials.save({
|
|
199
|
+
organization: options.organization,
|
|
200
|
+
secret: make(signedIn.secret),
|
|
201
|
+
url
|
|
202
|
+
});
|
|
203
|
+
return yield* reportSession({
|
|
204
|
+
asJson: options.json,
|
|
205
|
+
saved,
|
|
206
|
+
signedIn,
|
|
207
|
+
url
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
const saveLogin = fn("saveLogin")(function* saveLogin(options) {
|
|
211
|
+
if (options.withApiKey) return yield* keyLogin(options);
|
|
212
|
+
return yield* emailLogin(options);
|
|
213
|
+
});
|
|
214
|
+
const forget = fn("forget")(function* forget(options) {
|
|
215
|
+
const removed = yield* Credentials.use((credentials) => credentials.clear);
|
|
216
|
+
if (options.json) {
|
|
217
|
+
const text = yield* compactJson({ removed });
|
|
218
|
+
return yield* log(text);
|
|
219
|
+
}
|
|
220
|
+
return yield* log(match(removed, {
|
|
221
|
+
onFalse: () => "There was no saved login.",
|
|
222
|
+
onTrue: () => "Signed out; the saved login is removed."
|
|
223
|
+
}));
|
|
224
|
+
});
|
|
225
|
+
const organization = String$1("organization").pipe(optional, withDescription("The organization later commands use when they get no --organization"));
|
|
226
|
+
const url = String$1("url").pipe(optional, withDescription("The Cloud API's URL; defaults to AKTER_CLOUD_URL, then https://api.akter.dev"));
|
|
227
|
+
const email = String$1("email").pipe(optional, withDescription("Email a sign-in code to this address; prompted for when a person is at the terminal"));
|
|
228
|
+
const code = String$1("code").pipe(optional, withDescription("The six-digit code from the email; without one, a code is emailed and, with no terminal, the run ends"));
|
|
229
|
+
const acceptTermsFlag = Boolean("accept-terms").pipe(withDefault(false), withDescription("Accept Akter's terms of service and privacy notice (akter docs legal/terms, legal/privacy) if this code creates an account, without asking"));
|
|
230
|
+
const login = make$1("login", {
|
|
231
|
+
acceptTerms: acceptTermsFlag,
|
|
232
|
+
code,
|
|
233
|
+
email,
|
|
234
|
+
json,
|
|
235
|
+
organization,
|
|
236
|
+
url,
|
|
237
|
+
withApiKey: Boolean("with-api-key").pipe(withDefault(false), withDescription("Read an API key or agent key from stdin instead, check it with the API and save it"))
|
|
238
|
+
}, saveLogin).pipe(withDescription$1("Sign in with a code emailed to you, or an API key, and save the session in the OS keychain or a file only you can read"));
|
|
239
|
+
const signup = make$1("signup", {
|
|
240
|
+
acceptTerms: acceptTermsFlag,
|
|
241
|
+
code,
|
|
242
|
+
email,
|
|
243
|
+
json,
|
|
244
|
+
organization,
|
|
245
|
+
url
|
|
246
|
+
}, (options) => emailLogin({
|
|
247
|
+
...options,
|
|
248
|
+
withApiKey: false
|
|
249
|
+
})).pipe(withDescription$1("Create an Akter account with a code emailed to you, and save its session"));
|
|
250
|
+
const logout = make$1("logout", { json }, forget).pipe(withDescription$1("Remove the saved login; AKTER_API_KEY is unaffected"));
|
|
251
|
+
export { login, logout, signup };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { B as flatMap, Jt as serviceOption, Lt as retry, W as fnUntraced, aa as Service, gn as void_, in as timeout, jt as provideService, o as andThen, ti as isTimeoutError, y as catchIf } from "../../Effect-CKFpqbmw.mjs";
|
|
2
|
+
import { t as error } from "../../Console-BKHUkH6a.mjs";
|
|
3
|
+
import { x as match } from "../../Option-DkxSnBhp.mjs";
|
|
4
|
+
import { o as set, r as make, t as get } from "../../Ref-BIPOxJNH.mjs";
|
|
5
|
+
import { mt as is } from "../../Schema-MiKt5K1v.mjs";
|
|
6
|
+
import { TokenRefreshError } from "./token-refresh.errors.mjs";
|
|
7
|
+
import { reconnectBackoff, refreshMessage, requestFailure } from "../request-failure.pure.mjs";
|
|
8
|
+
// src/deploy/auth/api-attempt.service.ts
|
|
9
|
+
const isRefresh = is(TokenRefreshError);
|
|
10
|
+
const timedOut = fnUntraced(function* timedOut(phase, failure) {
|
|
11
|
+
const current = yield* get(phase);
|
|
12
|
+
if (current === "refresh") return yield* TokenRefreshError.make({ message: refreshMessage(failure) });
|
|
13
|
+
if (current === "initial") return yield* requestFailure("API key exchange")(failure);
|
|
14
|
+
return yield* failure;
|
|
15
|
+
});
|
|
16
|
+
var ApiAttempt = class ApiAttempt extends Service()("@akterjs/cli/deploy/auth/api-attempt.service/ApiAttempt") {
|
|
17
|
+
static mark = (phase) => flatMap(serviceOption(ApiAttempt), match({
|
|
18
|
+
onNone: () => void_,
|
|
19
|
+
onSome: (state) => set(state, phase)
|
|
20
|
+
}));
|
|
21
|
+
static request = fnUntraced(function* request(action, wait) {
|
|
22
|
+
const phase = yield* make("request");
|
|
23
|
+
const attempt = andThen(set(phase, "request"), action).pipe(timeout(wait), catchIf(isTimeoutError, (failure) => timedOut(phase, failure)), provideService(ApiAttempt, phase));
|
|
24
|
+
return yield* attempt.pipe(catchIf(isRefresh, (failure) => error(`${failure.message} Retrying; the request will resume after authentication recovers.`).pipe(andThen(retry(attempt, {
|
|
25
|
+
schedule: reconnectBackoff,
|
|
26
|
+
while: isRefresh
|
|
27
|
+
})))));
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
export { ApiAttempt };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { M as match, W as unfold, j as map } from "../Array-B4kuTQeW.mjs";
|
|
2
|
+
import { At as provideContext, B as flatMap, Q as fromOption, T as context, U as fn, Vn as effect, W as fnUntraced, _a as pick, aa as Service, cn as tryPromise, dt as map$1, et as gen, z as findFirst } from "../Effect-CKFpqbmw.mjs";
|
|
3
|
+
import { t as FileSystem } from "../FileSystem-4pPcEb_i.mjs";
|
|
4
|
+
import { b as map$2, f as getOrElse, i as filter, u as fromNullishOr, w as some } from "../Option-DkxSnBhp.mjs";
|
|
5
|
+
import { t as Path } from "../Path-Djdv_eF5.mjs";
|
|
6
|
+
import { F as String, I as Struct, n as ArraySchema, pt as fromJsonString, st as encodeEffect, tt as decodeEffect, v as Int } from "../Schema-MiKt5K1v.mjs";
|
|
7
|
+
import { i as sha256, t as Digest } from "../Digest-CBd3uv5B.mjs";
|
|
8
|
+
import { a as ChildProcessSpawner, n as make } from "../ChildProcess-BCIwxPGc.mjs";
|
|
9
|
+
import { i as hostInterfaceLine } from "../hosted-runtime.layer-Cx3IPsEd.mjs";
|
|
10
|
+
import { n as BuildManifest } from "../cloud-client.adapter-Cy1BjDbG.mjs";
|
|
11
|
+
import { manifestRunner } from "./build-manifest.pure.mjs";
|
|
12
|
+
import { DeployFailedError } from "./deploy-failed.errors.mjs";
|
|
13
|
+
import { build } from "esbuild";
|
|
14
|
+
// src/deploy/build-bundle.adapter.ts
|
|
15
|
+
const firstOutput = 0;
|
|
16
|
+
const successfulExit = 0;
|
|
17
|
+
const decodeManifest = decodeEffect(fromJsonString(BuildManifest));
|
|
18
|
+
/** Each actor's host interface version, which build.json carries beside the manifest Akter Cloud's API takes. */
|
|
19
|
+
const HostInterfaces = Struct({ actors: ArraySchema(Struct({ hostInterface: Int })) });
|
|
20
|
+
const decodeVersions = decodeEffect(fromJsonString(HostInterfaces));
|
|
21
|
+
const unnamed = 0;
|
|
22
|
+
/**
|
|
23
|
+
* The bundle with its host interface version as its first line: the oldest any of its actors was built with, which is
|
|
24
|
+
* the app's own akter, so the control plane reads it from the stored bundle. An app without actors names 0.
|
|
25
|
+
*/
|
|
26
|
+
const marked = (text, contents) => map$1(decodeVersions(text), ({ actors }) => {
|
|
27
|
+
const oldest = match(map(actors, (actor) => actor.hostInterface), {
|
|
28
|
+
onEmpty: () => unnamed,
|
|
29
|
+
onNonEmpty: (versions) => Math.min(...versions)
|
|
30
|
+
});
|
|
31
|
+
const line = new globalThis.TextEncoder().encode(hostInterfaceLine(oldest));
|
|
32
|
+
return Uint8Array.from([...line, ...contents]);
|
|
33
|
+
});
|
|
34
|
+
const compileApp = fn("compileApp")(function* compileApp(entry) {
|
|
35
|
+
const compiled = yield* tryPromise({
|
|
36
|
+
catch: () => DeployFailedError.make({ message: "Could not bundle the app. Check its entry and imports." }),
|
|
37
|
+
try: () => build({
|
|
38
|
+
banner: { js: "import { createRequire as __akterCreateRequire } from \"node:module\"; const require = __akterCreateRequire(import.meta.url);" },
|
|
39
|
+
bundle: true,
|
|
40
|
+
conditions: ["@akterjs/source"],
|
|
41
|
+
entryPoints: [entry],
|
|
42
|
+
format: "esm",
|
|
43
|
+
logLevel: "silent",
|
|
44
|
+
platform: "node",
|
|
45
|
+
target: "esnext",
|
|
46
|
+
write: false
|
|
47
|
+
})
|
|
48
|
+
});
|
|
49
|
+
return yield* fromOption(fromNullishOr(compiled.outputFiles.at(firstOutput)), () => DeployFailedError.make({ message: "The app build produced no JavaScript." }));
|
|
50
|
+
});
|
|
51
|
+
const writeRunner = fn("writeRunner")(function* writeRunner(files) {
|
|
52
|
+
const fs = yield* FileSystem;
|
|
53
|
+
const url = yield* (yield* Path).toFileUrl(files.bundle);
|
|
54
|
+
const quote = encodeEffect(fromJsonString(String));
|
|
55
|
+
const entry = yield* quote(url.href);
|
|
56
|
+
const output = yield* quote(files.manifest);
|
|
57
|
+
yield* fs.writeFileString(files.runner, manifestRunner({
|
|
58
|
+
entry,
|
|
59
|
+
output
|
|
60
|
+
}));
|
|
61
|
+
});
|
|
62
|
+
const describeApp = fn("describeApp")(function* describeApp(folder, bundle) {
|
|
63
|
+
const fs = yield* FileSystem;
|
|
64
|
+
const path = yield* Path;
|
|
65
|
+
const spawner = yield* ChildProcessSpawner;
|
|
66
|
+
const files = {
|
|
67
|
+
bundle: path.resolve(folder, "app.mjs"),
|
|
68
|
+
manifest: path.resolve(folder, "build.json"),
|
|
69
|
+
runner: path.resolve(folder, "manifest.mjs")
|
|
70
|
+
};
|
|
71
|
+
yield* fs.writeFile(files.bundle, bundle);
|
|
72
|
+
yield* writeRunner(files);
|
|
73
|
+
if ((yield* spawner.exitCode(make(globalThis.process.execPath, [files.runner], {
|
|
74
|
+
env: { AKTER_API_KEY: "" },
|
|
75
|
+
extendEnv: true,
|
|
76
|
+
stderr: "ignore",
|
|
77
|
+
stdout: "ignore"
|
|
78
|
+
}))) !== successfulExit) return yield* DeployFailedError.make({ message: "Could not read the app's contract and migration metadata." });
|
|
79
|
+
return yield* flatMap(fs.readFileString(files.manifest), (text) => map$1(decodeManifest(text), (manifest) => ({
|
|
80
|
+
manifest,
|
|
81
|
+
text
|
|
82
|
+
})));
|
|
83
|
+
});
|
|
84
|
+
/** The app's build.json manifest and its bundle with the host interface line its manifest names. */
|
|
85
|
+
const stampedBuild = fnUntraced(function* stampedBuild(folder, contents) {
|
|
86
|
+
const described = yield* describeApp(folder, contents);
|
|
87
|
+
return {
|
|
88
|
+
bundle: yield* marked(described.text, contents),
|
|
89
|
+
manifest: described.manifest
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
const appRoot = fn("appRoot")(function* appRoot(entryFolder) {
|
|
93
|
+
const fs = yield* FileSystem;
|
|
94
|
+
const path = yield* Path;
|
|
95
|
+
const folders = unfold(some(entryFolder), (current) => map$2(current, (folder) => {
|
|
96
|
+
const parent = path.dirname(folder);
|
|
97
|
+
return [folder, filter(some(parent), (above) => above !== folder)];
|
|
98
|
+
}));
|
|
99
|
+
const found = yield* findFirst(folders, (folder) => fs.exists(path.join(folder, "package.json")));
|
|
100
|
+
return getOrElse(found, () => entryFolder);
|
|
101
|
+
});
|
|
102
|
+
const buildFolder = fn("buildFolder")(function* buildFolder(entryPath) {
|
|
103
|
+
const fs = yield* FileSystem;
|
|
104
|
+
const path = yield* Path;
|
|
105
|
+
const root = yield* appRoot(path.dirname(entryPath));
|
|
106
|
+
const modules = path.join(root, "node_modules");
|
|
107
|
+
yield* fs.makeDirectory(modules, { recursive: true });
|
|
108
|
+
return yield* fs.makeTempDirectoryScoped({
|
|
109
|
+
directory: modules,
|
|
110
|
+
prefix: ".akter-deploy-"
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
const bundleApp = fn("bundleApp")(function* bundleApp(entry) {
|
|
114
|
+
const fs = yield* FileSystem;
|
|
115
|
+
const entryPath = (yield* Path).resolve(entry);
|
|
116
|
+
if (!(yield* fs.exists(entryPath))) return yield* DeployFailedError.make({ message: `App entry not found: ${entry}` });
|
|
117
|
+
const folder = yield* buildFolder(entryPath);
|
|
118
|
+
const output = yield* compileApp(entryPath);
|
|
119
|
+
const { bundle, manifest } = yield* stampedBuild(folder, output.contents);
|
|
120
|
+
return {
|
|
121
|
+
bundle,
|
|
122
|
+
digest: yield* Digest.use((digest) => digest.digestHex(sha256, bundle)),
|
|
123
|
+
manifest,
|
|
124
|
+
size: bundle.byteLength
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
var BuildBundle = class extends Service()("@akterjs/cli/deploy/build-bundle.adapter/BuildBundle", { make: gen(function* makeBuildBundle() {
|
|
128
|
+
const context$1 = yield* context().pipe(map$1(pick(FileSystem, Path, ChildProcessSpawner, Digest)));
|
|
129
|
+
return { bundle: (entry) => provideContext(bundleApp(entry), context$1) };
|
|
130
|
+
}) }) {
|
|
131
|
+
static layer = effect(this, this.make);
|
|
132
|
+
};
|
|
133
|
+
export { BuildBundle };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/deploy/build-manifest.pure.ts
|
|
2
|
+
const manifestRunner = ({ entry, output }) => `
|
|
3
|
+
import { layer as nodeServices } from "@effect/platform-node/NodeServices"
|
|
4
|
+
import { Context, Duration, Effect, FileSystem, Layer, Option, Schema } from "effect"
|
|
5
|
+
import { Digest } from "effect/crypto"
|
|
6
|
+
import { WorkflowEngine } from "effect/workflow"
|
|
7
|
+
import { MigrationHasher } from "@akterjs/effect"
|
|
8
|
+
import { ActorRegistry } from "@akterjs/effect/contract/actor-registry"
|
|
9
|
+
import { renderContract } from "@akterjs/effect/contract/description"
|
|
10
|
+
import { app } from ${entry}
|
|
11
|
+
|
|
12
|
+
const program = Effect.gen(function* () {
|
|
13
|
+
const built = yield* Layer.build(app.layer.pipe(Layer.provideMerge(ActorRegistry.layer)))
|
|
14
|
+
const registry = Context.get(built, ActorRegistry)
|
|
15
|
+
const hasher = yield* MigrationHasher
|
|
16
|
+
const digest = yield* Digest.Digest
|
|
17
|
+
const bytes = new TextEncoder()
|
|
18
|
+
const actors = yield* Effect.forEach(app.actors, (actor) => Effect.gen(function* () {
|
|
19
|
+
const hosted = yield* registry.lookup(actor.name)
|
|
20
|
+
const contract = yield* digest.digestHex(Digest.sha256, bytes.encode(yield* renderContract(actor)))
|
|
21
|
+
const migrations = yield* Effect.forEach(
|
|
22
|
+
Object.entries(hosted.migrations).sort(([left], [right]) => Number(left) - Number(right)),
|
|
23
|
+
([number, migration]) => Effect.map(hasher.hash(migration), (sha256) => ({ number: Number(number), sha256 })),
|
|
24
|
+
)
|
|
25
|
+
// Each query that declares staleFor, in milliseconds; an actor with none names no staleForMs, as before.
|
|
26
|
+
const staleForMs = Object.fromEntries(Object.entries(actor.queries).flatMap(([query, declared]) =>
|
|
27
|
+
Option.match(Option.flatten(Option.fromUndefinedOr(declared.staleFor)), {
|
|
28
|
+
onNone: () => [],
|
|
29
|
+
onSome: (age) => [[query, Duration.toMillis(age)]],
|
|
30
|
+
}),
|
|
31
|
+
))
|
|
32
|
+
const ages = Object.keys(staleForMs).length > 0 ? { staleForMs } : {}
|
|
33
|
+
return { name: actor.name, contract, migrations, hostInterface: hosted.hostInterface ?? 0, ...ages }
|
|
34
|
+
}))
|
|
35
|
+
const text = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Json))({ actors })
|
|
36
|
+
yield* FileSystem.FileSystem.use((fs) => fs.writeFileString(${output}, text))
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
Effect.runPromise(program.pipe(
|
|
40
|
+
Effect.scoped,
|
|
41
|
+
Effect.provide(Layer.mergeAll(nodeServices, MigrationHasher.layer, Digest.layerWebCrypto, WorkflowEngine.layerMemory)),
|
|
42
|
+
))
|
|
43
|
+
`;
|
|
44
|
+
export { manifestRunner };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { t as CloudClient } from "../cloud-client.adapter-DOVMoYAj.mjs";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { W as fnUntraced, ht as mapError } from "../Effect-CKFpqbmw.mjs";
|
|
2
|
+
import { G as URLFromString, tt as decodeEffect } from "../Schema-MiKt5K1v.mjs";
|
|
3
|
+
import { DeployFailedError } from "./deploy-failed.errors.mjs";
|
|
4
|
+
// src/deploy/cloud-url.pure.ts
|
|
5
|
+
const defaultCloudUrl = "https://api.akter.dev";
|
|
6
|
+
const cloudUrl = fnUntraced(function* cloudUrl(input) {
|
|
7
|
+
const url = yield* mapError(decodeEffect(URLFromString)(input), () => DeployFailedError.make({ message: "The Cloud API URL is invalid." }));
|
|
8
|
+
const loopback = [
|
|
9
|
+
"localhost",
|
|
10
|
+
"127.0.0.1",
|
|
11
|
+
"[::1]"
|
|
12
|
+
].includes(url.hostname);
|
|
13
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) return yield* DeployFailedError.make({ message: "Cloud API requests require HTTPS, except on loopback." });
|
|
14
|
+
if (url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "") return yield* DeployFailedError.make({ message: "The Cloud API URL must not contain credentials, a query or a fragment." });
|
|
15
|
+
return url;
|
|
16
|
+
});
|
|
17
|
+
export { cloudUrl, defaultCloudUrl };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { d as fromUndefinedOr, x as match } from "../Option-DkxSnBhp.mjs";
|
|
2
|
+
import { t as errorExitCode } from "../Runtime-BInIAwK3.mjs";
|
|
3
|
+
import { F as String, an as optionalKey, v as Int, z as TaggedError } from "../Schema-MiKt5K1v.mjs";
|
|
4
|
+
import { CliExitError } from "../terminal/cli-exit.errors.mjs";
|
|
5
|
+
// src/deploy/deploy-failed.errors.ts
|
|
6
|
+
var DeployFailedError = class extends TaggedError()("DeployFailedError", {
|
|
7
|
+
message: String,
|
|
8
|
+
status: optionalKey(Int)
|
|
9
|
+
}) {
|
|
10
|
+
get [errorExitCode]() {
|
|
11
|
+
return match(fromUndefinedOr(this.status), {
|
|
12
|
+
onNone: () => CliExitError.codes.failed,
|
|
13
|
+
onSome: CliExitError.ofStatus
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
export { DeployFailedError };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { R as some } from "../Array-B4kuTQeW.mjs";
|
|
2
|
+
import { t as match } from "../Boolean-kILIvmhE.mjs";
|
|
3
|
+
import { n as log, t as error } from "../Console-BKHUkH6a.mjs";
|
|
4
|
+
import { At as provideContext, B as flatMap, Et as orDie, K as forEach, Kn as provide, Lt as retry, M as fail, R as filterOrFail, T as context, U as fn, Vn as effect, Wn as merge, Xt as succeed, Yt as sleep, _a as pick, aa as Service, d as asVoid, dt as map, et as gen, gn as void_, ht as mapError, ia as Reference, in as timeout, jt as provideService, o as andThen, u as asSome, y as catchIf } from "../Effect-CKFpqbmw.mjs";
|
|
5
|
+
import { o as type, t as discriminatorsExhaustive } from "../Match-CGPLZYrV.mjs";
|
|
6
|
+
import { b as map$1, d as fromUndefinedOr, h as isNone, nn as isNotUndefined, x as match$1 } from "../Option-DkxSnBhp.mjs";
|
|
7
|
+
import { A as Option, G as URLFromString, I as Struct, mt as is, pt as fromJsonString, st as encodeEffect, tt as decodeEffect } from "../Schema-MiKt5K1v.mjs";
|
|
8
|
+
import { C as put, h as bodyUint8Array, w as setHeaders } from "../HttpClientResponse-D5ZKICVV.mjs";
|
|
9
|
+
import { a as Deploy, i as CustomerHostname, o as EnvironmentName, t as AppName, u as OrganizationName } from "../cloud-client.adapter-Cy1BjDbG.mjs";
|
|
10
|
+
import { BuildBundle } from "./build-bundle.adapter.mjs";
|
|
11
|
+
import { t as CloudClient } from "../cloud-client.adapter-DOVMoYAj.mjs";
|
|
12
|
+
import { cloudUrl } from "./cloud-url.pure.mjs";
|
|
13
|
+
import { DeployFailedError } from "./deploy-failed.errors.mjs";
|
|
14
|
+
import { progressFailure, progressMessage, progressWait, reconnectBackoff, requestFailure, retryable } from "./request-failure.pure.mjs";
|
|
15
|
+
// src/deploy/deploy.service.ts
|
|
16
|
+
const Target = Struct({
|
|
17
|
+
app: AppName,
|
|
18
|
+
domain: Option(CustomerHostname),
|
|
19
|
+
environment: EnvironmentName,
|
|
20
|
+
organization: OrganizationName
|
|
21
|
+
});
|
|
22
|
+
const lastNotice = -1;
|
|
23
|
+
const ProgressOnStderr = Reference("@akterjs/cli/deploy/deploy.service/ProgressOnStderr", { defaultValue: () => false });
|
|
24
|
+
const say = fn("say")(function* say(line) {
|
|
25
|
+
const onStderr = yield* ProgressOnStderr;
|
|
26
|
+
yield* match(onStderr, {
|
|
27
|
+
onFalse: () => log(line),
|
|
28
|
+
onTrue: () => error(line)
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
const timeoutFailure = (message) => (failure) => {
|
|
32
|
+
if (is(DeployFailedError)(failure)) return failure;
|
|
33
|
+
return DeployFailedError.make({ message });
|
|
34
|
+
};
|
|
35
|
+
const uploadBuild = fn("uploadBuild")(function* uploadBuild(cloud, built, upload) {
|
|
36
|
+
const destination = yield* decodeEffect(URLFromString)(upload.url).pipe(mapError(() => DeployFailedError.make({ message: "The API returned an invalid upload URL." })));
|
|
37
|
+
const local = [
|
|
38
|
+
"localhost",
|
|
39
|
+
"127.0.0.1",
|
|
40
|
+
"[::1]"
|
|
41
|
+
].includes(destination.hostname);
|
|
42
|
+
if (destination.username !== "" || destination.password !== "" || destination.protocol !== "https:" && !(local && destination.protocol === "http:")) return yield* DeployFailedError.make({ message: "The upload URL must use HTTPS, except on loopback." });
|
|
43
|
+
yield* say("Uploading build");
|
|
44
|
+
return yield* cloud.upload.execute(put(destination).pipe(bodyUint8Array(built.bundle), setHeaders(upload.headers))).pipe(flatMap((response) => response.arrayBuffer), timeout("5 minutes"), mapError(requestFailure("Build upload")), asVoid);
|
|
45
|
+
});
|
|
46
|
+
const confirmBuild = fn("confirmBuild")(function* confirmBuild(cloud, target, initial) {
|
|
47
|
+
let ready = initial;
|
|
48
|
+
while (ready.status !== "Ready") {
|
|
49
|
+
if (ready.status === "Failed") return yield* DeployFailedError.make({ message: "The uploaded build failed validation." });
|
|
50
|
+
ready = yield* cloud.request(cloud.client.buildsGet(target.organization, target.app, ready.id, {}), "30 seconds").pipe(mapError(requestFailure("Build confirmation")));
|
|
51
|
+
if (ready.status !== "Ready") yield* sleep("1 second");
|
|
52
|
+
}
|
|
53
|
+
return ready;
|
|
54
|
+
});
|
|
55
|
+
const prepareBuild = fn("prepareBuild")(function* prepareBuild(cloud, target, built) {
|
|
56
|
+
const created = yield* cloud.request(cloud.client.buildsCreate(target.organization, target.app, { payload: {
|
|
57
|
+
digest: built.digest,
|
|
58
|
+
manifest: built.manifest,
|
|
59
|
+
size: built.size
|
|
60
|
+
} }), "30 seconds").pipe(mapError(requestFailure("Build creation")));
|
|
61
|
+
if (isNotUndefined(created.upload)) yield* uploadBuild(cloud, built, created.upload);
|
|
62
|
+
return yield* confirmBuild(cloud, target, created.build).pipe(timeout("5 minutes"), mapError(timeoutFailure("Timed out confirming the uploaded build; no deploy was started.")));
|
|
63
|
+
});
|
|
64
|
+
const noticesOf = fn("noticesOf")(function* noticesOf(current, seen) {
|
|
65
|
+
const { notices, status } = current;
|
|
66
|
+
if (some(seen, (notice, index) => notices[index] !== notice) || notices.at(lastNotice) !== status) return yield* DeployFailedError.make({ message: "The API returned inconsistent deploy notices." });
|
|
67
|
+
yield* forEach(notices.slice(seen.length), (notice) => say(notice), { discard: true });
|
|
68
|
+
return notices;
|
|
69
|
+
});
|
|
70
|
+
const reasonText = type().pipe(discriminatorsExhaustive("code")({
|
|
71
|
+
"host-ceiling": ({ ceiling }) => `the environment already holds its ceiling of ${String(ceiling)} hosts, so a new one waits for a release`,
|
|
72
|
+
"load-failed": ({ message }) => `the app failed to load: ${message}`,
|
|
73
|
+
"no-answer": () => "the control plane never answered",
|
|
74
|
+
"no-host": () => "no host",
|
|
75
|
+
"no-host-up": () => "no host is up",
|
|
76
|
+
"owners-missing": ({ published, serving, shards }) => `${String(published)} of ${String(shards)} shards have a published owner (${String(serving)} serving)`
|
|
77
|
+
}));
|
|
78
|
+
const finished = (current) => {
|
|
79
|
+
if (current.status === "RolledBack") return fail(DeployFailedError.make({ message: `Deploy ${current.id} rolled back.` }));
|
|
80
|
+
if (current.status === "NotServing") {
|
|
81
|
+
const why = match$1(fromUndefinedOr(current.reason), {
|
|
82
|
+
onNone: () => "no reason given",
|
|
83
|
+
onSome: reasonText
|
|
84
|
+
});
|
|
85
|
+
return fail(DeployFailedError.make({ message: `Deploy ${current.id} is published but not serving: ${why}.` }));
|
|
86
|
+
}
|
|
87
|
+
return succeed(current);
|
|
88
|
+
};
|
|
89
|
+
const progressOf = (cloud, target, deploy) => {
|
|
90
|
+
const poll = cloud.request(cloud.progress.deploysGet(target.organization, target.app, deploy.id, {}), progressWait);
|
|
91
|
+
return poll.pipe(catchIf(retryable, (failure) => say(`${progressMessage(failure)} Reconnecting; the deploy continues on the server.`).pipe(andThen(retry(poll, {
|
|
92
|
+
schedule: reconnectBackoff,
|
|
93
|
+
while: retryable
|
|
94
|
+
})))), mapError(progressFailure));
|
|
95
|
+
};
|
|
96
|
+
const followDeploy = fn("followDeploy")(function* followDeploy(cloud, target, initial) {
|
|
97
|
+
let current = initial;
|
|
98
|
+
let seen = [];
|
|
99
|
+
while (current.status !== "Live" && current.status !== "NotServing" && current.status !== "RolledBack") {
|
|
100
|
+
seen = yield* noticesOf(current, seen);
|
|
101
|
+
yield* sleep("1 second");
|
|
102
|
+
current = yield* progressOf(cloud, target, initial);
|
|
103
|
+
}
|
|
104
|
+
yield* noticesOf(current, seen);
|
|
105
|
+
return yield* finished(current);
|
|
106
|
+
});
|
|
107
|
+
const claimDomain = fn("claimDomain")(function* claimDomain(cloud, target) {
|
|
108
|
+
if (isNone(target.domain)) return;
|
|
109
|
+
const hostname = target.domain.value;
|
|
110
|
+
const registered = yield* cloud.request(cloud.client.domainsCreate(target.organization, target.app, { payload: {
|
|
111
|
+
environment: target.environment,
|
|
112
|
+
hostname
|
|
113
|
+
} }), "30 seconds").pipe(mapError(requestFailure("Domain registration")), filterOrFail((domain) => domain.hostname === hostname && domain.environment === target.environment && domain.status !== "Removing", () => DeployFailedError.make({ message: "The API returned an unavailable or mismatched domain." })));
|
|
114
|
+
yield* say(`Add CNAME ${registered.hostname} -> ${registered.cname}`);
|
|
115
|
+
});
|
|
116
|
+
const deployed = fn("deployed")(function* deployed(options) {
|
|
117
|
+
const target = yield* decodeEffect(Target)(options).pipe(mapError(() => DeployFailedError.make({ message: "App, organization, environment and domain must be valid Cloud names." })));
|
|
118
|
+
const url = yield* cloudUrl(options.url);
|
|
119
|
+
const cloud = yield* CloudClient.use((factory) => factory.connect({
|
|
120
|
+
apiKey: options.apiKey,
|
|
121
|
+
url
|
|
122
|
+
}));
|
|
123
|
+
yield* claimDomain(cloud, target);
|
|
124
|
+
const built = yield* say("Building app").pipe(andThen(BuildBundle.use((builder) => builder.bundle(options.entry))), mapError(() => DeployFailedError.make({ message: "Could not build the app and its manifest." })));
|
|
125
|
+
yield* cloud.signIn;
|
|
126
|
+
const ready = yield* prepareBuild(cloud, target, built);
|
|
127
|
+
const current = yield* cloud.request(cloud.client.deploysCreate(target.organization, target.app, { payload: {
|
|
128
|
+
build: ready.id,
|
|
129
|
+
environment: target.environment
|
|
130
|
+
} }), "30 seconds").pipe(mapError(requestFailure("Deploy creation")));
|
|
131
|
+
yield* say(`Deploy ${current.id}`);
|
|
132
|
+
return yield* followDeploy(cloud, target, current).pipe(timeout("30 minutes"), mapError(timeoutFailure(`Stopped following deploy ${current.id} after 30 minutes; the deploy continues on the server.`)));
|
|
133
|
+
});
|
|
134
|
+
const DeployJson = fromJsonString(Deploy);
|
|
135
|
+
const announced = fn("announced")(function* announced(live, json) {
|
|
136
|
+
const line = yield* match(json, {
|
|
137
|
+
onFalse: () => succeed(map$1(fromUndefinedOr(live.url), (url) => `Actors at ${url}`)),
|
|
138
|
+
onTrue: () => asSome(orDie(encodeEffect(DeployJson)(live)))
|
|
139
|
+
});
|
|
140
|
+
yield* match$1(line, {
|
|
141
|
+
onNone: () => void_,
|
|
142
|
+
onSome: (text) => log(text)
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
const runDeploy = fn("runDeploy")(function* runDeploy(options) {
|
|
146
|
+
const live = yield* deployed(options).pipe(provideService(ProgressOnStderr, options.json));
|
|
147
|
+
yield* announced(live, options.json);
|
|
148
|
+
return live;
|
|
149
|
+
});
|
|
150
|
+
var Deployment = class extends Service()("@akterjs/cli/deploy/deploy.service/Deployment", { make: gen(function* makeDeployment() {
|
|
151
|
+
const context$1 = yield* context().pipe(map(pick(BuildBundle, CloudClient)));
|
|
152
|
+
return { run: (options) => provideContext(runDeploy(options), context$1) };
|
|
153
|
+
}) }) {
|
|
154
|
+
static layer = effect(this, this.make).pipe(provide(merge(BuildBundle.layer, CloudClient.layer)));
|
|
155
|
+
};
|
|
156
|
+
export { Deployment };
|