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 +1 -1
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/login.d.ts +8 -0
- package/dist/commands/login.js +66 -3
- package/dist/config.d.ts +9 -0
- package/dist/config.js +5 -1
- package/dist/index.js +2 -2
- package/package.json +1 -1
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
|
|
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
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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: "
|
|
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();
|
package/dist/commands/init.js
CHANGED
|
@@ -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.
|
|
107
|
+
const SDK_VERSION = "0.0.5";
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -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;
|
package/dist/commands/login.js
CHANGED
|
@@ -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 (
|
|
7
|
-
|
|
8
|
-
const
|
|
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
|
-
|
|
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 --
|
|
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": {
|