nerdsystem 0.0.4 → 0.0.5

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/api.js CHANGED
@@ -10,7 +10,7 @@ export class ApiClient {
10
10
  static fromConfig(opts = {}) {
11
11
  const cfg = loadConfig();
12
12
  if (opts.requireAuth !== false && !cfg.token) {
13
- throw new CliError("Not logged in. Run `nerd login --token nsk_...` (or set NERD_TOKEN).");
13
+ throw new CliError("Not logged in. Run `nerd login` (or set NERD_TOKEN).");
14
14
  }
15
15
  return new ApiClient(cfg.apiUrl, cfg.token);
16
16
  }
@@ -15,7 +15,7 @@ export async function doctor(opts) {
15
15
  const gh = await exec("gh", ["auth", "status"], process.cwd());
16
16
  checks.push({ name: "GitHub CLI signed in (optional)", ok: gh.code === 0, detail: gh.code === 0 ? "ok" : "not signed in", fix: "Only needed to create repos from the terminal: https://cli.github.com then `gh auth login`." });
17
17
  const cfg = loadConfig();
18
- checks.push({ name: "nerd login", ok: Boolean(cfg.token), detail: cfg.token ? cfg.apiUrl : "no token saved", fix: "Create a token in the dashboard (API tokens), then `nerd login --token nsk_...`." });
18
+ checks.push({ name: "nerd login", ok: Boolean(cfg.token), detail: cfg.token ? cfg.apiUrl : "no token saved", fix: "Run `nerd login` and approve the link it prints." });
19
19
  let root = null;
20
20
  try {
21
21
  root = findProjectRoot();
@@ -104,4 +104,4 @@ export async function init(dir, args) {
104
104
  log.ok(`Ready. Next:\n cd ${dir}\n pnpm install\n nerd dev\n\nThen open the folder in your coding agent and ask it to build the first feature; the nerdsystem skill walks it through.\nDeploys go to ${project.url}`);
105
105
  }
106
106
  // Kept in sync with packages/sdk/package.json by the release script.
107
- const SDK_VERSION = "0.0.4";
107
+ const SDK_VERSION = "0.0.5";
@@ -1,6 +1,14 @@
1
+ /**
2
+ * `nerd login` with no token starts a device login: prints a link, the person approves it in the
3
+ * dashboard (or from the email when --email is given), and the token arrives here. `--token` still
4
+ * works for scripts. A pending login is remembered, so running `nerd login` again after approving
5
+ * picks it up instead of starting over.
6
+ */
1
7
  export declare function login(args: {
2
8
  token?: string;
3
9
  "api-url"?: string;
10
+ email?: string;
11
+ json?: boolean;
4
12
  }): Promise<void>;
5
13
  export declare function whoami(opts: {
6
14
  json: boolean;
@@ -1,13 +1,76 @@
1
+ import os from "node:os";
1
2
  import { ApiClient } from "../api.js";
2
3
  import { DEFAULT_API_URL, loadConfig, saveConfig } from "../config.js";
3
4
  import { CliError, log, printJson } from "../util.js";
5
+ /** How long one `nerd login` call waits before handing back; agents run commands with short timeouts, so a rerun resumes. */
6
+ const WAIT_S = 90;
7
+ /**
8
+ * `nerd login` with no token starts a device login: prints a link, the person approves it in the
9
+ * dashboard (or from the email when --email is given), and the token arrives here. `--token` still
10
+ * works for scripts. A pending login is remembered, so running `nerd login` again after approving
11
+ * picks it up instead of starting over.
12
+ */
4
13
  export async function login(args) {
14
+ const cfg = loadConfig();
15
+ const apiUrl = (args["api-url"] ?? cfg.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
5
16
  const token = args.token ?? process.env.NERD_TOKEN;
6
- if (!token)
7
- throw new CliError("Pass --token nsk_... (get one from the dashboard or `POST /v1/tokens`).");
8
- const apiUrl = (args["api-url"] ?? loadConfig().apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
17
+ if (token)
18
+ return finish(apiUrl, token, args.json);
19
+ const anon = new ApiClient(apiUrl, undefined);
20
+ let pending = cfg.pendingLogin && cfg.pendingLogin.apiUrl === apiUrl && cfg.pendingLogin.expiresAt > Date.now() ? cfg.pendingLogin : undefined;
21
+ if (!pending) {
22
+ const client = `nerd CLI on ${os.hostname()} (${os.platform()})`;
23
+ const start = await anon.post("/session/device", { client, ...(args.email ? { email: args.email } : {}) });
24
+ pending = { apiUrl, deviceCode: start.deviceCode, userCode: start.userCode, verificationUrl: start.verificationUrl, interval: start.interval, expiresAt: Date.now() + start.expiresIn * 1000 };
25
+ saveConfig({ ...cfg, pendingLogin: pending });
26
+ if (args.json)
27
+ printJson({ status: "pending", userCode: start.userCode, verificationUrl: start.verificationUrl, expiresIn: start.expiresIn });
28
+ else {
29
+ log.info(`Open this link in your browser and approve code ${start.userCode}:\n\n ${start.verificationUrl}\n`);
30
+ if (args.email)
31
+ log.info(`An email with the same link went to ${args.email}.`);
32
+ log.info("Waiting for approval…");
33
+ }
34
+ }
35
+ else if (!args.json) {
36
+ log.info(`Still waiting for code ${pending.userCode} to be approved at:\n\n ${pending.verificationUrl}\n`);
37
+ }
38
+ const deadline = Math.min(Date.now() + WAIT_S * 1000, pending.expiresAt);
39
+ while (Date.now() < deadline) {
40
+ let r;
41
+ try {
42
+ r = await anon.post("/session/device/token", { deviceCode: pending.deviceCode });
43
+ }
44
+ catch (err) {
45
+ // 410: expired or already collected. Forget it so the next run starts fresh.
46
+ clearPending();
47
+ throw err;
48
+ }
49
+ if (r.status === "ok") {
50
+ clearPending();
51
+ return finish(apiUrl, r.token, args.json);
52
+ }
53
+ await new Promise((res) => setTimeout(res, pending.interval * 1000));
54
+ }
55
+ if (pending.expiresAt <= Date.now()) {
56
+ clearPending();
57
+ throw new CliError("The login code expired before it was approved. Run `nerd login` again.");
58
+ }
59
+ const msg = `Not approved yet. Approve code ${pending.userCode} at ${pending.verificationUrl}, then run \`nerd login\` again to finish.`;
60
+ if (args.json)
61
+ printJson({ status: "pending", userCode: pending.userCode, verificationUrl: pending.verificationUrl });
62
+ else
63
+ log.warn(msg);
64
+ }
65
+ function clearPending() {
66
+ const { pendingLogin: _drop, ...rest } = loadConfig();
67
+ saveConfig(rest);
68
+ }
69
+ async function finish(apiUrl, token, json) {
9
70
  const me = await new ApiClient(apiUrl, token).get("/v1/me");
10
71
  const file = saveConfig({ apiUrl, token });
72
+ if (json)
73
+ return printJson({ status: "ok", apiUrl, user: me.user, file });
11
74
  log.ok(`Logged in as ${me.user.email} (${apiUrl}). Saved to ${file}`);
12
75
  }
13
76
  export async function whoami(opts) {
package/dist/config.d.ts CHANGED
@@ -1,6 +1,15 @@
1
1
  export interface GlobalConfig {
2
2
  apiUrl: string;
3
3
  token?: string;
4
+ /** A device login that was started but not yet approved; `nerd login` resumes it. */
5
+ pendingLogin?: {
6
+ apiUrl: string;
7
+ deviceCode: string;
8
+ userCode: string;
9
+ verificationUrl: string;
10
+ interval: number;
11
+ expiresAt: number;
12
+ };
4
13
  }
5
14
  export declare const DEFAULT_API_URL = "https://api.nerdsystem.com";
6
15
  export declare function loadConfig(): GlobalConfig;
package/dist/config.js CHANGED
@@ -12,11 +12,15 @@ export function loadConfig() {
12
12
  return {
13
13
  apiUrl: process.env.NERD_API_URL ?? file.apiUrl ?? DEFAULT_API_URL,
14
14
  ...(process.env.NERD_TOKEN ?? file.token ? { token: process.env.NERD_TOKEN ?? file.token } : {}),
15
+ ...(file.pendingLogin ? { pendingLogin: file.pendingLogin } : {}),
15
16
  };
16
17
  }
17
18
  export function saveConfig(cfg) {
18
19
  const p = configPath();
19
20
  mkdirSync(path.dirname(p), { recursive: true, mode: 0o700 });
20
- writeFileSync(p, `${JSON.stringify(cfg, null, 2)}\n`, { mode: 0o600 });
21
+ const out = { ...cfg };
22
+ if (!out.pendingLogin)
23
+ delete out.pendingLogin;
24
+ writeFileSync(p, `${JSON.stringify(out, null, 2)}\n`, { mode: 0o600 });
21
25
  return p;
22
26
  }
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import { findProjectRoot } from "./project.js";
19
19
  import { CliError, log } from "./util.js";
20
20
  const HELP = `nerd — deploy apps to nerdsystem
21
21
 
22
- nerd login --token nsk_... Save an API token (also: NERD_TOKEN, NERD_API_URL)
22
+ nerd login [--email you@x.com] Sign in: prints a link to approve in the browser (or --token nsk_...)
23
23
  nerd whoami Show the current user and orgs
24
24
  nerd doctor Check Node, pnpm, git, login, and the project
25
25
  nerd init <dir> [--org] [--name] [--describe "what it does"] Create an app and a project for it
@@ -69,7 +69,7 @@ async function main(argv) {
69
69
  const parse = (options, allowPositionals = true) => parseArgs({ args: rest, options: { json: BOOL, ...options }, allowPositionals, strict: cmd !== "dev" });
70
70
  switch (cmd) {
71
71
  case "login": {
72
- const { values } = parse({ token: STR, "api-url": STR });
72
+ const { values } = parse({ token: STR, "api-url": STR, email: STR });
73
73
  return login(values);
74
74
  }
75
75
  case "doctor": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nerdsystem",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "nerd: deploy apps to nerdsystem from the terminal or a coding agent.",
5
5
  "type": "module",
6
6
  "bin": {