nerdsystem 0.0.10 → 0.0.11

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/cli.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ /** The published version, read from package.json so a release bump is the only place it changes. */
2
+ export declare function cliVersion(): string;
1
3
  export declare function main(argv: string[]): Promise<void>;
2
4
  /**
3
5
  * Runs one command and turns any failure into a non-zero exit code. `process.exitCode` is set
package/dist/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createRequire } from "node:module";
1
2
  import { parseArgs } from "node:util";
2
3
  import { authConfig, authSecret } from "./commands/auth.js";
3
4
  import { check } from "./commands/check.js";
@@ -60,8 +61,12 @@ const HELP = `nerd: start, check, and deploy a web app on nerdsystem
60
61
  nerd logs [--since 30m] [--limit 100] [--q text] [--pr n] Logs from the deployed app
61
62
  nerd audit [--out file.csv] Export deployments and builds (Studio)
62
63
 
63
- Every command accepts --json for machine-readable output.
64
+ Every command accepts --json for machine-readable output. nerd --version prints the version.
64
65
  Operator: nerd admin --help`;
66
+ /** The published version, read from package.json so a release bump is the only place it changes. */
67
+ export function cliVersion() {
68
+ return createRequire(import.meta.url)("../package.json").version;
69
+ }
65
70
  const BOOL = { type: "boolean", default: false };
66
71
  const STR = { type: "string" };
67
72
  export async function main(argv) {
@@ -70,6 +75,18 @@ export async function main(argv) {
70
75
  console.log(HELP);
71
76
  return;
72
77
  }
78
+ if (cmd === "version" || cmd === "--version" || cmd === "-v") {
79
+ if (rest.includes("--json"))
80
+ console.log(JSON.stringify({ version: cliVersion() }));
81
+ else
82
+ console.log(cliVersion());
83
+ return;
84
+ }
85
+ // `nerd <command> --help` prints the help instead of "Unknown option"; admin keeps its own help text.
86
+ if (cmd !== "admin" && (rest.includes("--help") || rest.includes("-h"))) {
87
+ console.log(HELP);
88
+ return;
89
+ }
73
90
  const parse = (options, allowPositionals = true) => parseArgs({ args: rest, options: { json: BOOL, ...options }, allowPositionals, strict: cmd !== "dev" });
74
91
  switch (cmd) {
75
92
  case "login": {
@@ -1,3 +1,7 @@
1
+ /** The template's Vite needs Node 22.12 or newer (its engines field: ^20.19.0 || >=22.12.0); the CLI asks for 22, so 22.12 is the floor. */
2
+ export declare const NODE_FLOOR = "22.12";
3
+ /** Whether a Node version string ("22.11.0") meets the floor; a 22.0-22.11 machine only failed at `nerd deploy` before. */
4
+ export declare function nodeMeetsFloor(version: string, floor?: string): boolean;
1
5
  /** Checks the machine, not the app: the things that stop a newcomer before the first deploy. */
2
6
  export declare function doctor(opts: {
3
7
  json: boolean;
@@ -3,11 +3,18 @@ import { exec, hasTool } from "../gates/code.js";
3
3
  import { loadConfig } from "../config.js";
4
4
  import { findProjectRoot } from "../project.js";
5
5
  import { log, printJson } from "../util.js";
6
+ /** The template's Vite needs Node 22.12 or newer (its engines field: ^20.19.0 || >=22.12.0); the CLI asks for 22, so 22.12 is the floor. */
7
+ export const NODE_FLOOR = "22.12";
8
+ /** Whether a Node version string ("22.11.0") meets the floor; a 22.0-22.11 machine only failed at `nerd deploy` before. */
9
+ export function nodeMeetsFloor(version, floor = NODE_FLOOR) {
10
+ const [major = 0, minor = 0] = version.split(".").map(Number);
11
+ const [fMajor = 0, fMinor = 0] = floor.split(".").map(Number);
12
+ return major > fMajor || (major === fMajor && minor >= fMinor);
13
+ }
6
14
  /** Checks the machine, not the app: the things that stop a newcomer before the first deploy. */
7
15
  export async function doctor(opts) {
8
16
  const checks = [];
9
- const nodeMajor = Number(process.versions.node.split(".")[0]);
10
- checks.push({ name: "Node.js 22+", ok: nodeMajor >= 22, detail: `v${process.versions.node}`, fix: "Install Node 22 from https://nodejs.org or with your version manager." });
17
+ checks.push({ name: `Node.js ${NODE_FLOOR}+`, ok: nodeMeetsFloor(process.versions.node), detail: `v${process.versions.node}`, fix: `Install Node ${NODE_FLOOR} or newer from https://nodejs.org` });
11
18
  for (const [tool, fix] of [["pnpm", "npm install -g pnpm"], ["git", "Install git from https://git-scm.com"]]) {
12
19
  const r = await exec(tool, ["--version"], process.cwd());
13
20
  checks.push({ name: tool, ok: r.code === 0, detail: r.code === 0 ? r.stdout.trim().split("\n")[0] ?? "" : "not found", fix });
@@ -12,4 +12,5 @@ export declare function init(dir: string | undefined, args: {
12
12
  slug?: string;
13
13
  describe?: string;
14
14
  "no-create": boolean;
15
+ json?: boolean;
15
16
  }): Promise<void>;
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { ApiClient } from "../api.js";
6
6
  import { projectPath, writeNerdJson } from "../project.js";
7
- import { CliError, log } from "../util.js";
7
+ import { CliError, log, printJson } from "../util.js";
8
8
  const SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,28}[a-z0-9])?$/;
9
9
  function templateDir() {
10
10
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -168,10 +168,18 @@ export async function init(dir, args) {
168
168
  }
169
169
  catch (err) {
170
170
  const reason = err instanceof Error ? err.message : String(err);
171
- throw new CliError(`The folder is ready but the project was not created: ${reason}\nFix that, then run: cd ${dir} && nerd link --slug ${slug}${args.org ? ` --org ${args.org}` : ""}`);
171
+ const code = err instanceof CliError && err.code ? err.code : "error";
172
+ // A plan limit is not something to fix in the folder: the API's message says how to free a slot.
173
+ const link = `cd ${dir} && nerd link --slug ${slug}${args.org ? ` --org ${args.org}` : ""}`;
174
+ const next = code === "plan_limit" ? `When a slot is free, run: ${link}` : `Fix that, then run: ${link}`;
175
+ const message = `The folder is ready but the project was not created: ${reason}`;
176
+ // Same pattern as `nerd check --json`: the JSON goes to stdout, the exit code stays non-zero.
177
+ if (args.json)
178
+ printJson({ error: { code, message }, dir, slug, next });
179
+ throw new CliError(`${message}\n${next}`, 1, { code });
172
180
  }
173
181
  initGitRepo(root);
174
182
  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}`);
175
183
  }
176
184
  // Kept in sync with packages/sdk/package.json by the release script.
177
- const SDK_VERSION = "0.0.10";
185
+ const SDK_VERSION = "0.0.11";
@@ -2,7 +2,8 @@
2
2
  * `nerd login` with no token starts a device login: prints a link, the person approves it in the
3
3
  * dashboard (or from the email when --email is given), and the token arrives here. `--token` still
4
4
  * works for scripts. A pending login is remembered, so running `nerd login` again after approving
5
- * picks it up instead of starting over.
5
+ * picks it up instead of starting over. `--email` with a different address than the pending login's
6
+ * starts a fresh one, so a typo does not lock the person out until the code expires.
6
7
  */
7
8
  export declare function login(args: {
8
9
  token?: string;
@@ -8,7 +8,8 @@ const WAIT_S = 90;
8
8
  * `nerd login` with no token starts a device login: prints a link, the person approves it in the
9
9
  * dashboard (or from the email when --email is given), and the token arrives here. `--token` still
10
10
  * works for scripts. A pending login is remembered, so running `nerd login` again after approving
11
- * picks it up instead of starting over.
11
+ * picks it up instead of starting over. `--email` with a different address than the pending login's
12
+ * starts a fresh one, so a typo does not lock the person out until the code expires.
12
13
  */
13
14
  export async function login(args) {
14
15
  const cfg = loadConfig();
@@ -17,23 +18,30 @@ export async function login(args) {
17
18
  if (token)
18
19
  return finish(apiUrl, token, args.json);
19
20
  const anon = new ApiClient(apiUrl, undefined);
20
- let pending = cfg.pendingLogin && cfg.pendingLogin.apiUrl === apiUrl && cfg.pendingLogin.expiresAt > Date.now() ? cfg.pendingLogin : undefined;
21
+ const email = args.email?.trim().toLowerCase() || undefined;
22
+ const saved = cfg.pendingLogin;
23
+ // Resume only a login for this API that has not expired and, when --email is given, that went to the same address.
24
+ const resumable = saved && saved.apiUrl === apiUrl && saved.expiresAt > Date.now() && (!email || saved.email === email);
25
+ let pending = resumable ? saved : undefined;
21
26
  if (!pending) {
22
27
  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 };
28
+ const start = await anon.post("/session/device", { client, ...(email ? { email } : {}) });
29
+ pending = { apiUrl, deviceCode: start.deviceCode, userCode: start.userCode, verificationUrl: start.verificationUrl, interval: start.interval, expiresAt: Date.now() + start.expiresIn * 1000, ...(email ? { email } : {}) };
25
30
  saveConfig({ ...cfg, pendingLogin: pending });
26
31
  if (args.json)
27
32
  printJson({ status: "pending", userCode: start.userCode, verificationUrl: start.verificationUrl, expiresIn: start.expiresIn });
28
33
  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 went to ${args.email}: the link signs you in (creating your account if you are new) and opens a page where you press Approve.`);
34
+ if (email) {
35
+ log.info(`An email went to ${email}. Click the link in it (check spam if nothing arrives within a minute), then press Approve on the page it opens. The link works once and expires in 15 minutes. If it never arrives, open ${start.verificationUrl} and sign in there.`);
36
+ }
37
+ else {
38
+ log.info(`Open this link in your browser and approve code ${start.userCode}:\n\n ${start.verificationUrl}\n`);
39
+ }
32
40
  log.info("Waiting for approval…");
33
41
  }
34
42
  }
35
43
  else if (!args.json) {
36
- log.info(`Still waiting for code ${pending.userCode} to be approved at:\n\n ${pending.verificationUrl}\n`);
44
+ log.info(`Still waiting for code ${pending.userCode} to be approved at:\n\n ${pending.verificationUrl}\n\n(run nerd login --email <address> with a different address to start over)`);
37
45
  }
38
46
  const deadline = Math.min(Date.now() + WAIT_S * 1000, pending.expiresAt);
39
47
  while (Date.now() < deadline) {
package/dist/config.d.ts CHANGED
@@ -1,7 +1,7 @@
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. */
4
+ /** A device login that was started but not yet approved; `nerd login` resumes it. `email` is the address the link went to, so `--email` with another address starts over. */
5
5
  pendingLogin?: {
6
6
  apiUrl: string;
7
7
  deviceCode: string;
@@ -9,6 +9,7 @@ export interface GlobalConfig {
9
9
  verificationUrl: string;
10
10
  interval: number;
11
11
  expiresAt: number;
12
+ email?: string;
12
13
  };
13
14
  }
14
15
  export declare const DEFAULT_API_URL = "https://api.nerdsystem.com";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nerdsystem",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "description": "nerd: start, check, and deploy a web app on nerdsystem from the terminal or a coding agent.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,7 +14,7 @@
14
14
  "LICENSE"
15
15
  ],
16
16
  "engines": {
17
- "node": ">=22"
17
+ "node": ">=22.12"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@types/node": "^22.0.0",
@@ -11,10 +11,10 @@ This app is deployed by nerdsystem, which also provides authentication, authoriz
11
11
 
12
12
  If this project still has the template's todo app (the `todos` table and `src/server/routes/todos.ts`), treat the person as new and offer a short walkthrough before building anything:
13
13
 
14
- 1. Ask one question: what should the app do, in a sentence or two. Do not ask about stacks, databases, or hosting; those are decided.
14
+ 1. Ask one question: what should the app do, in a sentence or two, unless CLAUDE.md already says what the app does (`nerd init` writes the description at the top). Do not ask about stacks, databases, or hosting; those are decided.
15
15
  2. Say what you will build first, in plain words: the one table and the one screen that make the idea real. Keep the first feature small enough to deploy within the session.
16
16
  3. Replace the todo example with that feature: schema, migration (`pnpm db:generate`), a router under `/api/...`, and the client screen. The template is a normal website: a public homepage at `/`, auth pages at `/login`, `/signup`, `/forgot-password`, `/reset-password`, and a signed-in area. Reshape it to the idea (a public landing page with sign-in in the header, a members-only area, an admin section) rather than forcing a login wall; the forms already work.
17
- 4. Run `nerd check`, then `nerd dev`, and tell the person to open the local URL, create an account, and try it. Fix what they report.
17
+ 4. Run `nerd check`. Then, unless the person asked to go straight to a live URL, run `nerd dev` and tell them to open the local URL, create an account, and try it. Fix what they report. When they want the live URL first, skip `nerd dev` and go to the next step; they can try it on the deployed app.
18
18
  5. Run `nerd deploy` and give them the live URL. Tell them to sign up on their own app first, then make themselves admin in the dashboard's Auth tab (the URL is printed after the deploy). Explain in two lines that every deploy is checked for leaked secrets and vulnerable dependencies, and that they can run `nerd check` any time.
19
19
  6. Offer the next steps once, briefly: link the GitHub repo for previews on pull requests (`nerd github connect`), add a custom domain, or invite a teammate from the dashboard.
20
20
 
@@ -127,7 +127,7 @@ Without MCP, `nerd check --json` returns the same shape, and `nerd check --rule
127
127
 
128
128
  - **Scheduled jobs**: put the work in the `POST /api/cron` handler (already in `src/server/index.ts`, guarded by `verifyCron()`), then turn the schedule on with `nerd cron hourly` or `nerd cron daily`. The platform calls it; the app never needs its own timer.
129
129
  - **Secrets** (Stripe keys, API tokens): never in code or in the client. Locally they live in `.dev.vars` (gitignored). For production, ask the person to run `nerd secret set NAME` and paste the value; it reaches the app as `c.env.NAME` on the next deploy. Pull request previews do not get it unless it was set with `--previews` (use that only for keys that are safe in a preview, such as a test-mode key). `nerd secret list` shows names only. Do not put a real secret value in any command you run yourself.
130
- - **Email**: `sendEmail(c.env, { to, subject, text, html? })` from `@nerdsystem/sdk` sends through the platform. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price.
130
+ - **Email**: `sendEmail(c.env, { to, subject, text, html? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price.
131
131
 
132
132
  ## Debugging production
133
133
 
@@ -2,7 +2,7 @@
2
2
  description: How to build, run, and deploy this app on nerdsystem. Applies to server, database, auth, and deploy work.
3
3
  alwaysApply: true
4
4
  ---
5
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (10dcd79a44bd); edit the skill, then run: nerd agents sync -->
5
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (31894f024834); edit the skill, then run: nerd agents sync -->
6
6
 
7
7
  # Building on nerdsystem
8
8
 
@@ -12,10 +12,10 @@ This app is deployed by nerdsystem, which also provides authentication, authoriz
12
12
 
13
13
  If this project still has the template's todo app (the `todos` table and `src/server/routes/todos.ts`), treat the person as new and offer a short walkthrough before building anything:
14
14
 
15
- 1. Ask one question: what should the app do, in a sentence or two. Do not ask about stacks, databases, or hosting; those are decided.
15
+ 1. Ask one question: what should the app do, in a sentence or two, unless CLAUDE.md already says what the app does (`nerd init` writes the description at the top). Do not ask about stacks, databases, or hosting; those are decided.
16
16
  2. Say what you will build first, in plain words: the one table and the one screen that make the idea real. Keep the first feature small enough to deploy within the session.
17
17
  3. Replace the todo example with that feature: schema, migration (`pnpm db:generate`), a router under `/api/...`, and the client screen. The template is a normal website: a public homepage at `/`, auth pages at `/login`, `/signup`, `/forgot-password`, `/reset-password`, and a signed-in area. Reshape it to the idea (a public landing page with sign-in in the header, a members-only area, an admin section) rather than forcing a login wall; the forms already work.
18
- 4. Run `nerd check`, then `nerd dev`, and tell the person to open the local URL, create an account, and try it. Fix what they report.
18
+ 4. Run `nerd check`. Then, unless the person asked to go straight to a live URL, run `nerd dev` and tell them to open the local URL, create an account, and try it. Fix what they report. When they want the live URL first, skip `nerd dev` and go to the next step; they can try it on the deployed app.
19
19
  5. Run `nerd deploy` and give them the live URL. Tell them to sign up on their own app first, then make themselves admin in the dashboard's Auth tab (the URL is printed after the deploy). Explain in two lines that every deploy is checked for leaked secrets and vulnerable dependencies, and that they can run `nerd check` any time.
20
20
  6. Offer the next steps once, briefly: link the GitHub repo for previews on pull requests (`nerd github connect`), add a custom domain, or invite a teammate from the dashboard.
21
21
 
@@ -128,7 +128,7 @@ Without MCP, `nerd check --json` returns the same shape, and `nerd check --rule
128
128
 
129
129
  - **Scheduled jobs**: put the work in the `POST /api/cron` handler (already in `src/server/index.ts`, guarded by `verifyCron()`), then turn the schedule on with `nerd cron hourly` or `nerd cron daily`. The platform calls it; the app never needs its own timer.
130
130
  - **Secrets** (Stripe keys, API tokens): never in code or in the client. Locally they live in `.dev.vars` (gitignored). For production, ask the person to run `nerd secret set NAME` and paste the value; it reaches the app as `c.env.NAME` on the next deploy. Pull request previews do not get it unless it was set with `--previews` (use that only for keys that are safe in a preview, such as a test-mode key). `nerd secret list` shows names only. Do not put a real secret value in any command you run yourself.
131
- - **Email**: `sendEmail(c.env, { to, subject, text, html? })` from `@nerdsystem/sdk` sends through the platform. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price.
131
+ - **Email**: `sendEmail(c.env, { to, subject, text, html? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price.
132
132
 
133
133
  ## Debugging production
134
134
 
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (10dcd79a44bd); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (31894f024834); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -8,10 +8,10 @@ This app is deployed by nerdsystem, which also provides authentication, authoriz
8
8
 
9
9
  If this project still has the template's todo app (the `todos` table and `src/server/routes/todos.ts`), treat the person as new and offer a short walkthrough before building anything:
10
10
 
11
- 1. Ask one question: what should the app do, in a sentence or two. Do not ask about stacks, databases, or hosting; those are decided.
11
+ 1. Ask one question: what should the app do, in a sentence or two, unless CLAUDE.md already says what the app does (`nerd init` writes the description at the top). Do not ask about stacks, databases, or hosting; those are decided.
12
12
  2. Say what you will build first, in plain words: the one table and the one screen that make the idea real. Keep the first feature small enough to deploy within the session.
13
13
  3. Replace the todo example with that feature: schema, migration (`pnpm db:generate`), a router under `/api/...`, and the client screen. The template is a normal website: a public homepage at `/`, auth pages at `/login`, `/signup`, `/forgot-password`, `/reset-password`, and a signed-in area. Reshape it to the idea (a public landing page with sign-in in the header, a members-only area, an admin section) rather than forcing a login wall; the forms already work.
14
- 4. Run `nerd check`, then `nerd dev`, and tell the person to open the local URL, create an account, and try it. Fix what they report.
14
+ 4. Run `nerd check`. Then, unless the person asked to go straight to a live URL, run `nerd dev` and tell them to open the local URL, create an account, and try it. Fix what they report. When they want the live URL first, skip `nerd dev` and go to the next step; they can try it on the deployed app.
15
15
  5. Run `nerd deploy` and give them the live URL. Tell them to sign up on their own app first, then make themselves admin in the dashboard's Auth tab (the URL is printed after the deploy). Explain in two lines that every deploy is checked for leaked secrets and vulnerable dependencies, and that they can run `nerd check` any time.
16
16
  6. Offer the next steps once, briefly: link the GitHub repo for previews on pull requests (`nerd github connect`), add a custom domain, or invite a teammate from the dashboard.
17
17
 
@@ -124,7 +124,7 @@ Without MCP, `nerd check --json` returns the same shape, and `nerd check --rule
124
124
 
125
125
  - **Scheduled jobs**: put the work in the `POST /api/cron` handler (already in `src/server/index.ts`, guarded by `verifyCron()`), then turn the schedule on with `nerd cron hourly` or `nerd cron daily`. The platform calls it; the app never needs its own timer.
126
126
  - **Secrets** (Stripe keys, API tokens): never in code or in the client. Locally they live in `.dev.vars` (gitignored). For production, ask the person to run `nerd secret set NAME` and paste the value; it reaches the app as `c.env.NAME` on the next deploy. Pull request previews do not get it unless it was set with `--previews` (use that only for keys that are safe in a preview, such as a test-mode key). `nerd secret list` shows names only. Do not put a real secret value in any command you run yourself.
127
- - **Email**: `sendEmail(c.env, { to, subject, text, html? })` from `@nerdsystem/sdk` sends through the platform. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price.
127
+ - **Email**: `sendEmail(c.env, { to, subject, text, html? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price.
128
128
 
129
129
  ## Debugging production
130
130
 
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (10dcd79a44bd); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (31894f024834); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -8,10 +8,10 @@ This app is deployed by nerdsystem, which also provides authentication, authoriz
8
8
 
9
9
  If this project still has the template's todo app (the `todos` table and `src/server/routes/todos.ts`), treat the person as new and offer a short walkthrough before building anything:
10
10
 
11
- 1. Ask one question: what should the app do, in a sentence or two. Do not ask about stacks, databases, or hosting; those are decided.
11
+ 1. Ask one question: what should the app do, in a sentence or two, unless CLAUDE.md already says what the app does (`nerd init` writes the description at the top). Do not ask about stacks, databases, or hosting; those are decided.
12
12
  2. Say what you will build first, in plain words: the one table and the one screen that make the idea real. Keep the first feature small enough to deploy within the session.
13
13
  3. Replace the todo example with that feature: schema, migration (`pnpm db:generate`), a router under `/api/...`, and the client screen. The template is a normal website: a public homepage at `/`, auth pages at `/login`, `/signup`, `/forgot-password`, `/reset-password`, and a signed-in area. Reshape it to the idea (a public landing page with sign-in in the header, a members-only area, an admin section) rather than forcing a login wall; the forms already work.
14
- 4. Run `nerd check`, then `nerd dev`, and tell the person to open the local URL, create an account, and try it. Fix what they report.
14
+ 4. Run `nerd check`. Then, unless the person asked to go straight to a live URL, run `nerd dev` and tell them to open the local URL, create an account, and try it. Fix what they report. When they want the live URL first, skip `nerd dev` and go to the next step; they can try it on the deployed app.
15
15
  5. Run `nerd deploy` and give them the live URL. Tell them to sign up on their own app first, then make themselves admin in the dashboard's Auth tab (the URL is printed after the deploy). Explain in two lines that every deploy is checked for leaked secrets and vulnerable dependencies, and that they can run `nerd check` any time.
16
16
  6. Offer the next steps once, briefly: link the GitHub repo for previews on pull requests (`nerd github connect`), add a custom domain, or invite a teammate from the dashboard.
17
17
 
@@ -124,7 +124,7 @@ Without MCP, `nerd check --json` returns the same shape, and `nerd check --rule
124
124
 
125
125
  - **Scheduled jobs**: put the work in the `POST /api/cron` handler (already in `src/server/index.ts`, guarded by `verifyCron()`), then turn the schedule on with `nerd cron hourly` or `nerd cron daily`. The platform calls it; the app never needs its own timer.
126
126
  - **Secrets** (Stripe keys, API tokens): never in code or in the client. Locally they live in `.dev.vars` (gitignored). For production, ask the person to run `nerd secret set NAME` and paste the value; it reaches the app as `c.env.NAME` on the next deploy. Pull request previews do not get it unless it was set with `--previews` (use that only for keys that are safe in a preview, such as a test-mode key). `nerd secret list` shows names only. Do not put a real secret value in any command you run yourself.
127
- - **Email**: `sendEmail(c.env, { to, subject, text, html? })` from `@nerdsystem/sdk` sends through the platform. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price.
127
+ - **Email**: `sendEmail(c.env, { to, subject, text, html? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price.
128
128
 
129
129
  ## Debugging production
130
130
 
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (10dcd79a44bd); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (31894f024834); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -8,10 +8,10 @@ This app is deployed by nerdsystem, which also provides authentication, authoriz
8
8
 
9
9
  If this project still has the template's todo app (the `todos` table and `src/server/routes/todos.ts`), treat the person as new and offer a short walkthrough before building anything:
10
10
 
11
- 1. Ask one question: what should the app do, in a sentence or two. Do not ask about stacks, databases, or hosting; those are decided.
11
+ 1. Ask one question: what should the app do, in a sentence or two, unless CLAUDE.md already says what the app does (`nerd init` writes the description at the top). Do not ask about stacks, databases, or hosting; those are decided.
12
12
  2. Say what you will build first, in plain words: the one table and the one screen that make the idea real. Keep the first feature small enough to deploy within the session.
13
13
  3. Replace the todo example with that feature: schema, migration (`pnpm db:generate`), a router under `/api/...`, and the client screen. The template is a normal website: a public homepage at `/`, auth pages at `/login`, `/signup`, `/forgot-password`, `/reset-password`, and a signed-in area. Reshape it to the idea (a public landing page with sign-in in the header, a members-only area, an admin section) rather than forcing a login wall; the forms already work.
14
- 4. Run `nerd check`, then `nerd dev`, and tell the person to open the local URL, create an account, and try it. Fix what they report.
14
+ 4. Run `nerd check`. Then, unless the person asked to go straight to a live URL, run `nerd dev` and tell them to open the local URL, create an account, and try it. Fix what they report. When they want the live URL first, skip `nerd dev` and go to the next step; they can try it on the deployed app.
15
15
  5. Run `nerd deploy` and give them the live URL. Tell them to sign up on their own app first, then make themselves admin in the dashboard's Auth tab (the URL is printed after the deploy). Explain in two lines that every deploy is checked for leaked secrets and vulnerable dependencies, and that they can run `nerd check` any time.
16
16
  6. Offer the next steps once, briefly: link the GitHub repo for previews on pull requests (`nerd github connect`), add a custom domain, or invite a teammate from the dashboard.
17
17
 
@@ -124,7 +124,7 @@ Without MCP, `nerd check --json` returns the same shape, and `nerd check --rule
124
124
 
125
125
  - **Scheduled jobs**: put the work in the `POST /api/cron` handler (already in `src/server/index.ts`, guarded by `verifyCron()`), then turn the schedule on with `nerd cron hourly` or `nerd cron daily`. The platform calls it; the app never needs its own timer.
126
126
  - **Secrets** (Stripe keys, API tokens): never in code or in the client. Locally they live in `.dev.vars` (gitignored). For production, ask the person to run `nerd secret set NAME` and paste the value; it reaches the app as `c.env.NAME` on the next deploy. Pull request previews do not get it unless it was set with `--previews` (use that only for keys that are safe in a preview, such as a test-mode key). `nerd secret list` shows names only. Do not put a real secret value in any command you run yourself.
127
- - **Email**: `sendEmail(c.env, { to, subject, text, html? })` from `@nerdsystem/sdk` sends through the platform. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price.
127
+ - **Email**: `sendEmail(c.env, { to, subject, text, html? })` from `@nerdsystem/sdk` sends through the platform. The relay only sends to addresses that have an account on this app (someone who signed up); any other recipient is refused with a 400, and the sender name is always the app's name. It is limited per day by plan (50 on Free, 1,000 on Pro at $39 a month, 2,000 on Studio at $99 a month), so use it for transactional mail such as receipts and notifications, not bulk. Plans can be paid monthly or yearly at ten times the monthly price.
128
128
 
129
129
  ## Debugging production
130
130