@wervt/cli 0.2.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.
Files changed (2) hide show
  1. package/dist/index.mjs +116 -7
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
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
4
  import { styleText } from "node:util";
5
5
  import * as p from "@clack/prompts";
@@ -71,6 +71,52 @@ async function saveConfig(config) {
71
71
  return file;
72
72
  }
73
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
74
120
  //#region src/index.ts
75
121
  const connection = {
76
122
  url: {
@@ -79,7 +125,7 @@ const connection = {
79
125
  },
80
126
  token: {
81
127
  type: "string",
82
- description: "Deploy or read token (or WERVT_TOKEN)"
128
+ description: "Admin, read or deploy token (or WERVT_TOKEN)"
83
129
  }
84
130
  };
85
131
  async function api(path, flags, init = {}) {
@@ -158,9 +204,13 @@ const deploy = defineCommand({
158
204
  const bundle = await createBundle(dir);
159
205
  spinner.stop(`Packed ${bundle.files} files (${(bundle.data.byteLength / 1024).toFixed(0)} kB)`);
160
206
  spinner.start("Uploading");
207
+ const source = await deploySource(dir);
161
208
  const res = await api(`/v1/apps/${name}/deploys`, args, {
162
209
  method: "POST",
163
- headers: { "content-type": "application/gzip" },
210
+ headers: {
211
+ "content-type": "application/gzip",
212
+ "x-wervt-source": encodeURIComponent(JSON.stringify(source))
213
+ },
164
214
  body: new Uint8Array(bundle.data)
165
215
  });
166
216
  const labels = {
@@ -184,6 +234,7 @@ const deploy = defineCommand({
184
234
  if (event.done) {
185
235
  spinner.stop(`Release ${event.release}`);
186
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`);
187
238
  return;
188
239
  }
189
240
  spinner.message(labels[event.step] ?? event.step);
@@ -236,7 +287,18 @@ const releases = defineCommand({
236
287
  },
237
288
  async run({ args }) {
238
289
  const info = await (await api(`/v1/apps/${args.app}`, args)).json();
239
- for (const release of [...info.releases].reverse()) console.log(`${release}${release === info.release ? " (active)" : ""}`);
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
+ }
240
302
  }
241
303
  });
242
304
  const activate = defineCommand({
@@ -378,7 +440,53 @@ const env = defineCommand({
378
440
  })
379
441
  }) : await api(`/v1/apps/${args.app}/env`, args)).json();
380
442
  console.log(keys.length ? keys.join("\n") : "No variables set.");
381
- if (Object.keys(set).length || unset.length) console.log("Applies to newly started isolates (next deploy or after idle timeout).");
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
+ }
382
490
  }
383
491
  });
384
492
  const remove = defineCommand({
@@ -506,12 +614,12 @@ const login = defineCommand({
506
614
  token: {
507
615
  type: "string",
508
616
  required: true,
509
- description: "Deploy token, or the read token"
617
+ description: "Admin, read or deploy token"
510
618
  }
511
619
  },
512
620
  async run({ args }) {
513
621
  const res = await fetch(`${args.url.replace(/\/$/, "")}/v1/apps`, { headers: { authorization: `Bearer ${args.token}` } });
514
- 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}`);
515
623
  const file = await saveConfig({
516
624
  url: args.url,
517
625
  token: args.token
@@ -534,6 +642,7 @@ runMain(defineCommand({
534
642
  reprovision,
535
643
  access,
536
644
  env,
645
+ tokens,
537
646
  delete: remove,
538
647
  login
539
648
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wervt/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Deploy and manage apps on wervt.",
5
5
  "keywords": [
6
6
  "agents",