@wervt/cli 0.1.0 → 0.2.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/dist/index.mjs +201 -8
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import { styleText } from "node:util";
|
|
4
5
|
import * as p from "@clack/prompts";
|
|
5
6
|
import { defineCommand, runMain } from "citty";
|
|
6
7
|
import { detectPackageManager } from "nypm";
|
|
@@ -70,6 +71,52 @@ async function saveConfig(config) {
|
|
|
70
71
|
return file;
|
|
71
72
|
}
|
|
72
73
|
//#endregion
|
|
74
|
+
//#region src/source.ts
|
|
75
|
+
async function git(dir, ...args) {
|
|
76
|
+
try {
|
|
77
|
+
const result = await x("git", args, {
|
|
78
|
+
nodeOptions: { cwd: dir },
|
|
79
|
+
throwOnError: false
|
|
80
|
+
});
|
|
81
|
+
return result.exitCode === 0 ? result.stdout.trim() || void 0 : void 0;
|
|
82
|
+
} catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** `git@github.com:owner/repo.git` and `https://…/repo.git` as `https://github.com/owner/repo`. */
|
|
87
|
+
function webUrl(remote) {
|
|
88
|
+
if (!remote) return void 0;
|
|
89
|
+
const ssh = remote.match(/^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/);
|
|
90
|
+
if (ssh) return `https://${ssh[1]}/${ssh[2]}`;
|
|
91
|
+
const https = remote.match(/^https:\/\/(?:[^@/]+@)?(.+?)(?:\.git)?$/);
|
|
92
|
+
return https ? `https://${https[1]}` : void 0;
|
|
93
|
+
}
|
|
94
|
+
async function deploySource(dir) {
|
|
95
|
+
const env = process.env;
|
|
96
|
+
const message = await git(dir, "log", "-1", "--format=%s");
|
|
97
|
+
if (env.GITHUB_ACTIONS === "true") {
|
|
98
|
+
const repo = `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}`;
|
|
99
|
+
return {
|
|
100
|
+
commit: env.GITHUB_SHA,
|
|
101
|
+
branch: env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME,
|
|
102
|
+
message,
|
|
103
|
+
repo,
|
|
104
|
+
actor: env.GITHUB_ACTOR,
|
|
105
|
+
run: `${repo}/actions/runs/${env.GITHUB_RUN_ID}`
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const status = await git(dir, "status", "--porcelain", "--", ".");
|
|
109
|
+
const commit = await git(dir, "rev-parse", "HEAD");
|
|
110
|
+
return {
|
|
111
|
+
commit,
|
|
112
|
+
branch: await git(dir, "branch", "--show-current"),
|
|
113
|
+
message,
|
|
114
|
+
dirty: commit ? Boolean(status) : void 0,
|
|
115
|
+
repo: webUrl(await git(dir, "remote", "get-url", "origin")),
|
|
116
|
+
actor: await git(dir, "config", "user.name")
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
73
120
|
//#region src/index.ts
|
|
74
121
|
const connection = {
|
|
75
122
|
url: {
|
|
@@ -78,7 +125,7 @@ const connection = {
|
|
|
78
125
|
},
|
|
79
126
|
token: {
|
|
80
127
|
type: "string",
|
|
81
|
-
description: "
|
|
128
|
+
description: "Admin, read or deploy token (or WERVT_TOKEN)"
|
|
82
129
|
}
|
|
83
130
|
};
|
|
84
131
|
async function api(path, flags, init = {}) {
|
|
@@ -91,7 +138,12 @@ async function api(path, flags, init = {}) {
|
|
|
91
138
|
});
|
|
92
139
|
if (!res.ok) {
|
|
93
140
|
const body = await res.text();
|
|
94
|
-
|
|
141
|
+
let message = body;
|
|
142
|
+
try {
|
|
143
|
+
message = JSON.parse(body).error ?? body;
|
|
144
|
+
} catch {}
|
|
145
|
+
console.error(styleText("red", `Error (${res.status}): ${message}`));
|
|
146
|
+
process.exit(1);
|
|
95
147
|
}
|
|
96
148
|
return res;
|
|
97
149
|
}
|
|
@@ -152,9 +204,13 @@ const deploy = defineCommand({
|
|
|
152
204
|
const bundle = await createBundle(dir);
|
|
153
205
|
spinner.stop(`Packed ${bundle.files} files (${(bundle.data.byteLength / 1024).toFixed(0)} kB)`);
|
|
154
206
|
spinner.start("Uploading");
|
|
207
|
+
const source = await deploySource(dir);
|
|
155
208
|
const res = await api(`/v1/apps/${name}/deploys`, args, {
|
|
156
209
|
method: "POST",
|
|
157
|
-
headers: {
|
|
210
|
+
headers: {
|
|
211
|
+
"content-type": "application/gzip",
|
|
212
|
+
"x-wervt-source": encodeURIComponent(JSON.stringify(source))
|
|
213
|
+
},
|
|
158
214
|
body: new Uint8Array(bundle.data)
|
|
159
215
|
});
|
|
160
216
|
const labels = {
|
|
@@ -178,6 +234,7 @@ const deploy = defineCommand({
|
|
|
178
234
|
if (event.done) {
|
|
179
235
|
spinner.stop(`Release ${event.release}`);
|
|
180
236
|
p.outro(`Deployed → ${event.url}`);
|
|
237
|
+
if (process.env.GITHUB_OUTPUT) await appendFile(process.env.GITHUB_OUTPUT, `url=${event.url}\nrelease=${event.release}\n`);
|
|
181
238
|
return;
|
|
182
239
|
}
|
|
183
240
|
spinner.message(labels[event.step] ?? event.step);
|
|
@@ -230,7 +287,18 @@ const releases = defineCommand({
|
|
|
230
287
|
},
|
|
231
288
|
async run({ args }) {
|
|
232
289
|
const info = await (await api(`/v1/apps/${args.app}`, args)).json();
|
|
233
|
-
|
|
290
|
+
const history = new Map(info.history?.map((entry) => [entry.release, entry]));
|
|
291
|
+
for (const release of [...info.releases].reverse()) {
|
|
292
|
+
const { source, deployedBy } = history.get(release) ?? {};
|
|
293
|
+
const details = [
|
|
294
|
+
source?.commit ? `${source.commit.slice(0, 7)}${source.dirty ? "+" : ""}` : void 0,
|
|
295
|
+
source?.branch,
|
|
296
|
+
source?.actor ?? deployedBy,
|
|
297
|
+
source?.message && styleText("dim", source.message)
|
|
298
|
+
].filter(Boolean);
|
|
299
|
+
const active = release === info.release ? styleText("green", " (active)") : "";
|
|
300
|
+
console.log(`${release}${active}${details.length ? ` ${details.join(" ")}` : ""}`);
|
|
301
|
+
}
|
|
234
302
|
}
|
|
235
303
|
});
|
|
236
304
|
const activate = defineCommand({
|
|
@@ -372,7 +440,53 @@ const env = defineCommand({
|
|
|
372
440
|
})
|
|
373
441
|
}) : await api(`/v1/apps/${args.app}/env`, args)).json();
|
|
374
442
|
console.log(keys.length ? keys.join("\n") : "No variables set.");
|
|
375
|
-
if (Object.keys(set).length || unset.length) console.log("Applies
|
|
443
|
+
if (Object.keys(set).length || unset.length) console.log("Applies from the next request.");
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
const tokens = defineCommand({
|
|
447
|
+
meta: {
|
|
448
|
+
name: "tokens",
|
|
449
|
+
description: "List, create or revoke an app's deploy tokens (for CI)"
|
|
450
|
+
},
|
|
451
|
+
args: {
|
|
452
|
+
app: {
|
|
453
|
+
type: "positional",
|
|
454
|
+
required: true,
|
|
455
|
+
description: "App name"
|
|
456
|
+
},
|
|
457
|
+
create: {
|
|
458
|
+
type: "string",
|
|
459
|
+
description: "Create a token with this name, e.g. github"
|
|
460
|
+
},
|
|
461
|
+
revoke: {
|
|
462
|
+
type: "string",
|
|
463
|
+
description: "Revoke the token with this id"
|
|
464
|
+
},
|
|
465
|
+
...connection
|
|
466
|
+
},
|
|
467
|
+
async run({ args }) {
|
|
468
|
+
const path = `/v1/apps/${args.app}/tokens`;
|
|
469
|
+
if (args.create) {
|
|
470
|
+
const token = await (await api(path, args, {
|
|
471
|
+
method: "POST",
|
|
472
|
+
headers: { "content-type": "application/json" },
|
|
473
|
+
body: JSON.stringify({ name: args.create })
|
|
474
|
+
})).json();
|
|
475
|
+
console.error(`Deploy token "${token.name}" for ${args.app} (shown once):`);
|
|
476
|
+
console.log(token.token);
|
|
477
|
+
console.error(styleText("dim", "It can deploy this app and nothing else. Store it as the WERVT_TOKEN secret."));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (args.revoke) {
|
|
481
|
+
await api(`${path}/${args.revoke}`, args, { method: "DELETE" });
|
|
482
|
+
return console.log(`Revoked ${args.revoke}`);
|
|
483
|
+
}
|
|
484
|
+
const list = await (await api(path, args)).json();
|
|
485
|
+
if (!list.length) return console.log(`No deploy tokens for ${args.app}.`);
|
|
486
|
+
for (const token of list) {
|
|
487
|
+
const used = token.lastUsedAt ? `used ${token.lastUsedAt.slice(0, 10)}` : "never used";
|
|
488
|
+
console.log(`${token.id} ${token.name.padEnd(20)} created ${token.createdAt.slice(0, 10)} ${used}`);
|
|
489
|
+
}
|
|
376
490
|
}
|
|
377
491
|
});
|
|
378
492
|
const remove = defineCommand({
|
|
@@ -409,6 +523,83 @@ const remove = defineCommand({
|
|
|
409
523
|
console.log(`Deleted ${args.app}${dropDb ? " and its database" : ""}`);
|
|
410
524
|
}
|
|
411
525
|
});
|
|
526
|
+
const LEVEL_COLORS = {
|
|
527
|
+
error: "red",
|
|
528
|
+
warn: "yellow",
|
|
529
|
+
info: "cyan",
|
|
530
|
+
debug: "gray"
|
|
531
|
+
};
|
|
532
|
+
/** `09:07:52.060 error <message>`, or the entry as one JSON line. */
|
|
533
|
+
function printLog(entry, asJson) {
|
|
534
|
+
if (asJson) return console.log(JSON.stringify(entry));
|
|
535
|
+
const time = new Date(entry._time).toISOString().slice(11, 23);
|
|
536
|
+
const level = String(entry.level ?? "info");
|
|
537
|
+
const color = LEVEL_COLORS[level] ?? "white";
|
|
538
|
+
console.log(`${styleText("dim", time)} ${styleText(color, level.padEnd(5))} ${entry._msg ?? ""}`);
|
|
539
|
+
}
|
|
540
|
+
const logs = defineCommand({
|
|
541
|
+
meta: {
|
|
542
|
+
name: "logs",
|
|
543
|
+
description: "Show an app's logs: console output, request events and isolate events"
|
|
544
|
+
},
|
|
545
|
+
args: {
|
|
546
|
+
app: {
|
|
547
|
+
type: "positional",
|
|
548
|
+
required: true,
|
|
549
|
+
description: "App name"
|
|
550
|
+
},
|
|
551
|
+
query: {
|
|
552
|
+
type: "positional",
|
|
553
|
+
required: false,
|
|
554
|
+
description: "LogsQL filter, e.g. 'level:error' or 'status:>=500' (default: all)"
|
|
555
|
+
},
|
|
556
|
+
since: {
|
|
557
|
+
type: "string",
|
|
558
|
+
default: "1h",
|
|
559
|
+
description: "How far back, e.g. 15m, 6h, 2d"
|
|
560
|
+
},
|
|
561
|
+
until: {
|
|
562
|
+
type: "string",
|
|
563
|
+
description: "End of the range (default: now)"
|
|
564
|
+
},
|
|
565
|
+
limit: {
|
|
566
|
+
type: "string",
|
|
567
|
+
default: "100",
|
|
568
|
+
description: "Newest entries to show (max 1000)"
|
|
569
|
+
},
|
|
570
|
+
follow: {
|
|
571
|
+
type: "boolean",
|
|
572
|
+
alias: "f",
|
|
573
|
+
description: "Keep printing new entries"
|
|
574
|
+
},
|
|
575
|
+
json: {
|
|
576
|
+
type: "boolean",
|
|
577
|
+
description: "One JSON object per line, with every field"
|
|
578
|
+
},
|
|
579
|
+
...connection
|
|
580
|
+
},
|
|
581
|
+
async run({ args }) {
|
|
582
|
+
const params = new URLSearchParams();
|
|
583
|
+
if (args.query) params.set("query", args.query);
|
|
584
|
+
if (args.follow) {
|
|
585
|
+
params.set("since", args.since);
|
|
586
|
+
const res = await api(`/v1/apps/${args.app}/logs/tail?${params.toString()}`, args);
|
|
587
|
+
let rest = "";
|
|
588
|
+
for await (const chunk of res.body.pipeThrough(new TextDecoderStream())) {
|
|
589
|
+
const lines = (rest + chunk).split("\n");
|
|
590
|
+
rest = lines.pop() ?? "";
|
|
591
|
+
for (const line of lines) if (line) printLog(JSON.parse(line), Boolean(args.json));
|
|
592
|
+
}
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
params.set("start", args.since);
|
|
596
|
+
if (args.until) params.set("end", args.until);
|
|
597
|
+
params.set("limit", args.limit);
|
|
598
|
+
const entries = await (await api(`/v1/apps/${args.app}/logs?${params.toString()}`, args)).json();
|
|
599
|
+
if (!entries.length && !args.json) return console.error(`No logs in the last ${args.since}.`);
|
|
600
|
+
for (const entry of entries.reverse()) printLog(entry, Boolean(args.json));
|
|
601
|
+
}
|
|
602
|
+
});
|
|
412
603
|
const login = defineCommand({
|
|
413
604
|
meta: {
|
|
414
605
|
name: "login",
|
|
@@ -423,12 +614,12 @@ const login = defineCommand({
|
|
|
423
614
|
token: {
|
|
424
615
|
type: "string",
|
|
425
616
|
required: true,
|
|
426
|
-
description: "
|
|
617
|
+
description: "Admin, read or deploy token"
|
|
427
618
|
}
|
|
428
619
|
},
|
|
429
620
|
async run({ args }) {
|
|
430
621
|
const res = await fetch(`${args.url.replace(/\/$/, "")}/v1/apps`, { headers: { authorization: `Bearer ${args.token}` } });
|
|
431
|
-
if (!res.ok) throw new Error(`Login failed: ${res.status} ${res.statusText}`);
|
|
622
|
+
if (!res.ok && res.status !== 403) throw new Error(`Login failed: ${res.status} ${res.statusText}`);
|
|
432
623
|
const file = await saveConfig({
|
|
433
624
|
url: args.url,
|
|
434
625
|
token: args.token
|
|
@@ -445,11 +636,13 @@ runMain(defineCommand({
|
|
|
445
636
|
deploy,
|
|
446
637
|
apps,
|
|
447
638
|
releases,
|
|
639
|
+
logs,
|
|
448
640
|
activate,
|
|
449
641
|
rollback,
|
|
450
642
|
reprovision,
|
|
451
643
|
access,
|
|
452
644
|
env,
|
|
645
|
+
tokens,
|
|
453
646
|
delete: remove,
|
|
454
647
|
login
|
|
455
648
|
}
|