@wervt/cli 0.2.2 → 0.2.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
3
- import { homedir } from "node:os";
3
+ import { homedir, hostname } from "node:os";
4
4
  import { dirname, join, relative, resolve } from "node:path";
5
5
  import { styleText } from "node:util";
6
6
  import * as p from "@clack/prompts";
@@ -59,12 +59,16 @@ async function resolveConfig(flags) {
59
59
  const saved = JSON.parse(await readFile(file, "utf8").catch(() => "{}"));
60
60
  const url = flags.url ?? process.env.WERVT_URL ?? saved.url;
61
61
  const token = flags.token ?? process.env.WERVT_TOKEN ?? saved.token;
62
- if (!url || !token) throw new Error("Not logged in. Run `wervt login --url <control url> --token <token>`.");
62
+ if (!url || !token) throw new Error("Not logged in. Run `wervt login --url <control url>`.");
63
63
  return {
64
64
  url: url.replace(/\/$/, ""),
65
65
  token
66
66
  };
67
67
  }
68
+ /** The saved control plane URL, if any. */
69
+ async function savedUrl() {
70
+ return JSON.parse(await readFile(file, "utf8").catch(() => "{}")).url;
71
+ }
68
72
  async function saveConfig(config) {
69
73
  await mkdir(dirname(file), { recursive: true });
70
74
  await writeFile(file, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
@@ -649,31 +653,97 @@ const skill = defineCommand({
649
653
  }
650
654
  }) }
651
655
  });
656
+ /** Opens a URL in the default browser; quietly does nothing where that isn't possible. */
657
+ async function openBrowser(url) {
658
+ const [command, ...args] = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? [
659
+ "cmd",
660
+ "/c",
661
+ "start",
662
+ "",
663
+ url
664
+ ] : ["xdg-open", url];
665
+ try {
666
+ await x(command, args, {
667
+ throwOnError: false,
668
+ nodeOptions: { stdio: "ignore" }
669
+ });
670
+ } catch {}
671
+ }
672
+ /** Browser login: an admin approves this machine in the dashboard, the CLI gets its own token. */
673
+ async function browserLogin(url) {
674
+ const start = await fetch(`${url}/v1/login`, {
675
+ method: "POST",
676
+ headers: { "content-type": "application/json" },
677
+ body: JSON.stringify({ name: hostname() })
678
+ });
679
+ if (!start.ok) throw new Error(`Could not start the login: ${start.status} ${await start.text()}`);
680
+ const login = await start.json();
681
+ p.note(`${styleText("bold", login.userCode)}\n\nEnter it at ${login.verifyUrl}`, "Your one-time code");
682
+ const go = await p.confirm({
683
+ message: "Open the browser?",
684
+ initialValue: true
685
+ });
686
+ if (p.isCancel(go)) throw new Error("Cancelled");
687
+ if (go) await openBrowser(login.verifyUrl);
688
+ const spinner = p.spinner();
689
+ spinner.start("Waiting for approval in the dashboard");
690
+ const deadline = new Date(login.expiresAt).getTime();
691
+ while (Date.now() < deadline) {
692
+ await new Promise((done) => setTimeout(done, login.interval * 1e3));
693
+ const res = await fetch(`${url}/v1/login/poll`, {
694
+ method: "POST",
695
+ headers: { "content-type": "application/json" },
696
+ body: JSON.stringify({ deviceCode: login.deviceCode })
697
+ }).catch(() => void 0);
698
+ if (!res?.ok) continue;
699
+ const result = await res.json();
700
+ if (result.status === "pending") continue;
701
+ if (result.status === "approved" && result.token) {
702
+ spinner.stop("Approved");
703
+ return result.token;
704
+ }
705
+ spinner.error(result.status === "denied" ? "Denied in the dashboard" : "The login expired");
706
+ process.exit(1);
707
+ }
708
+ spinner.error("The login expired");
709
+ process.exit(1);
710
+ }
652
711
  const login = defineCommand({
653
712
  meta: {
654
713
  name: "login",
655
- description: "Save the control plane URL and token"
714
+ description: "Sign in: approve this machine in the dashboard (or pass a token)"
656
715
  },
657
716
  args: {
658
717
  url: {
659
718
  type: "string",
660
- required: true,
661
- description: "Control plane URL"
719
+ description: "Control plane URL, e.g. https://api.example.com"
662
720
  },
663
721
  token: {
664
722
  type: "string",
665
- required: true,
666
- description: "Admin, read or deploy token"
723
+ description: "Save this token instead (admin, read or a deploy token, e.g. for CI)"
667
724
  }
668
725
  },
669
726
  async run({ args }) {
670
- const res = await fetch(`${args.url.replace(/\/$/, "")}/v1/apps`, { headers: { authorization: `Bearer ${args.token}` } });
727
+ p.intro("wervt login");
728
+ let url = args.url ?? process.env.WERVT_URL ?? await savedUrl();
729
+ if (!url) {
730
+ const answer = await p.text({
731
+ message: "Control plane URL",
732
+ placeholder: "https://api.example.com",
733
+ validate: (value) => /^https?:\/\/\S+$/.test(value ?? "") ? void 0 : "An http(s) URL"
734
+ });
735
+ if (p.isCancel(answer)) return p.cancel("Cancelled");
736
+ url = answer;
737
+ }
738
+ url = url.replace(/\/$/, "");
739
+ const token = args.token ?? await browserLogin(url);
740
+ const res = await fetch(`${url}/v1/apps`, { headers: { authorization: `Bearer ${token}` } });
671
741
  if (!res.ok && res.status !== 403) throw new Error(`Login failed: ${res.status} ${res.statusText}`);
672
742
  const file = await saveConfig({
673
- url: args.url,
674
- token: args.token
743
+ url,
744
+ token
675
745
  });
676
- console.log(`Saved to ${file}`);
746
+ p.outro(`Signed in to ${url} (saved to ${file})`);
677
747
  }
678
748
  });
679
749
  runMain(defineCommand({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wervt/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Deploy and manage apps on wervt.",
5
5
  "keywords": [
6
6
  "agents",
package/skill/SKILL.md CHANGED
@@ -18,9 +18,10 @@ npx -y @wervt/cli whoami
18
18
 
19
19
  It prints the control plane URL and what the saved token can do (`admin`, `read`, or a deploy
20
20
  token for one app). Shipping new apps needs `admin`. If it says "Not logged in", ask the user to
21
- run `wervt login --url <control plane URL> --token <admin token>` themselves (for example with
22
- `! npx -y @wervt/cli login --url … --token …`). Never look for tokens in files, env or
23
- keychains, and never print or ask to paste one into the chat.
21
+ run `! npx -y @wervt/cli login --url <control plane URL>` themselves: it shows a code, they
22
+ approve it in the dashboard (CLI page) and the machine gets its own admin session. Never run the
23
+ login for them, never look for tokens in files, env or keychains, and never print or ask to paste
24
+ one into the chat.
24
25
 
25
26
  ## 2. Scaffold
26
27