nerdsystem 0.0.9 → 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": {
@@ -121,7 +121,7 @@ export async function runChecks(root, opts) {
121
121
  }
122
122
  if (opts.typecheck && problems.every((p) => p.endsWith("(warning)") || p.startsWith("Agent instruction files"))) {
123
123
  log.step("Typechecking");
124
- const code = await run("npx", ["tsc", "-b", "--pretty"], { cwd: root });
124
+ const code = await run("npx", ["tsc", "-b", "--pretty"], { cwd: root, stdoutToStderr: true });
125
125
  if (code !== 0)
126
126
  problems.push("Typecheck failed (see errors above).");
127
127
  }
@@ -144,7 +144,9 @@ export async function check(opts) {
144
144
  }
145
145
  }
146
146
  // Loaded here, not at the top: the gates import staticChecks from this file.
147
- const { runVerify } = await import("./verify.js");
147
+ const { runVerify, assertRuleHasGate } = await import("./verify.js");
148
+ if (opts.rule)
149
+ assertRuleHasGate(opts.rule);
148
150
  const out = await runVerify(root, { withBuild: !opts["no-build"], stopOnFail: false, quiet: opts.json, onlyRules: opts.rule ? [opts.rule] : undefined });
149
151
  const failed = out.results.filter((r) => r.status === "fail").length;
150
152
  if (opts.json)
@@ -65,7 +65,10 @@ async function probeAfterDeploy(deployment, url, skip) {
65
65
  return skippedProbe("--skip-probe was given");
66
66
  if (deployment.status !== "live")
67
67
  return skippedProbe(`the deployment status is ${deployment.status}, not live`);
68
- await waitForDeployment(url);
68
+ // Until the dispatcher knows the new script it answers its own 404 page, which has none of the app's
69
+ // headers or routes; probing that would report false auth failures. Wait, and say so when it never came.
70
+ if (!(await waitForDeployment(url)))
71
+ return skippedProbe("the app had not started answering within 30 seconds; open the URL to check, or run nerd deploy again to probe it");
69
72
  return probeDeployment(url);
70
73
  }
71
74
  function printProbe(url, probes) {
@@ -99,7 +102,7 @@ export async function deploy(opts) {
99
102
  if (!opts["skip-build"]) {
100
103
  log.step("Building with vite");
101
104
  ensureDeps(root);
102
- await runOrThrow("npx", ["vite", "build"], { cwd: root });
105
+ await runOrThrow("npx", ["vite", "build"], { cwd: root, stdoutToStderr: true });
103
106
  }
104
107
  const build = readBuildOutput(root);
105
108
  const built = await runGates(BUILD_GATES, { root, build, knownSecrets, log: (l) => log.info(l) }, { stopOnFail: true });
@@ -127,10 +130,12 @@ export async function deploy(opts) {
127
130
  }
128
131
  const { deployment } = await api.post(projectPath(nerd, "/deployments"), form);
129
132
  const { project } = await api.get(projectPath(nerd));
133
+ log.ok(`Deployed ${deployment.id} (${deployment.status}) → ${project.url}`);
134
+ if (!opts["skip-probe"] && deployment.status === "live")
135
+ log.step(`Probing ${project.url}`);
130
136
  const probes = await probeAfterDeploy(deployment, project.url, Boolean(opts["skip-probe"]));
131
137
  if (opts.json)
132
138
  return printJson({ deployment, url: project.url, migrations: mig, probes });
133
- log.ok(`Deployed ${deployment.id} (${deployment.status}) → ${project.url}`);
134
139
  printProbe(project.url, probes);
135
140
  log.info(`First time? Sign up on your app at ${project.url}, then make yourself admin in the dashboard's Auth tab: ${dashboardUrl(api.apiUrl)}/app/${nerd.org}/projects/${nerd.project}/auth`);
136
141
  }
@@ -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.9";
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}: one click signs you in (creating your account if you are new) and approves.`);
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) {
@@ -17,6 +17,8 @@ export declare function runVerify(root: string, opts: {
17
17
  onlyRules?: string[] | undefined;
18
18
  trackAttempts?: boolean;
19
19
  }): Promise<VerifyOutput>;
20
+ /** `--rule smoke/x` would select no gate and report "All 0 gates passed"; say which prefixes a gate owns instead. */
21
+ export declare function assertRuleHasGate(rule: string): void;
20
22
  export declare function verify(opts: {
21
23
  json: boolean;
22
24
  "no-build": boolean;
@@ -35,8 +35,19 @@ export async function runVerify(root, opts) {
35
35
  }
36
36
  return { ok: results.every((r) => r.status !== "fail"), failures, stop, results };
37
37
  }
38
+ /** `--rule smoke/x` would select no gate and report "All 0 gates passed"; say which prefixes a gate owns instead. */
39
+ export function assertRuleHasGate(rule) {
40
+ const prefixes = [...new Set([...SOURCE_GATES, ...BUILD_GATES].flatMap((g) => g.rulePrefixes))];
41
+ if (prefixes.some((p) => rule.startsWith(p)))
42
+ return;
43
+ if (rule.startsWith("smoke/"))
44
+ throw new CliError(`No gate runs ${rule} on the source: the live URL is probed by nerd deploy. Deploy again to re-check it.`, 2);
45
+ throw new CliError(`No gate runs ${rule}. Rule ids start with one of: ${prefixes.join(", ")}`, 2);
46
+ }
38
47
  export async function verify(opts) {
39
48
  const root = findProjectRoot();
49
+ if (opts.rule)
50
+ assertRuleHasGate(opts.rule);
40
51
  const out = await runVerify(root, { withBuild: !opts["no-build"], stopOnFail: false, quiet: opts.json, onlyRules: opts.rule ? [opts.rule] : undefined });
41
52
  if (opts.json)
42
53
  process.stdout.write(`${JSON.stringify({ ok: out.ok, failures: out.failures, stop: out.stop, skipped: skippedGates(out.results) }, null, 2)}\n`);
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";
@@ -73,14 +73,16 @@ export function depsRuleFor(severity) {
73
73
  }
74
74
  /** Known vulnerabilities in dependencies. Critical and high block; moderate warns. Package names come from the registry, not the repo. */
75
75
  /**
76
- * The lockfile for the project: in its root, or in an ancestor up to the repository root (the directory holding .git),
77
- * since a workspace member such as the template inside this monorepo shares the lockfile committed at the top.
76
+ * The lockfile for the project: in its root, or in an ancestor that is itself a package or workspace root
77
+ * (it holds a package.json or pnpm-workspace.yaml), searched up to the repository root (the directory holding
78
+ * .git). A workspace member such as the template inside this monorepo shares the lockfile committed at the top;
79
+ * a stray lockfile in an unrelated ancestor such as the home folder is never adopted.
78
80
  */
79
81
  function findLockfile(start) {
80
82
  let dir = start;
81
83
  for (;;) {
82
84
  const hit = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock"].find((f) => existsSync(path.join(dir, f)));
83
- if (hit)
85
+ if (hit && (dir === start || existsSync(path.join(dir, "package.json")) || existsSync(path.join(dir, "pnpm-workspace.yaml"))))
84
86
  return path.join(dir, hit);
85
87
  const parent = path.dirname(dir);
86
88
  if (parent === dir || existsSync(path.join(dir, ".git")))
@@ -101,7 +103,8 @@ export const depsGate = {
101
103
  if (!lock)
102
104
  findings.push(finding("deps/lockfile-required", {}));
103
105
  if (await hasTool("osv-scanner")) {
104
- const r = await exec("osv-scanner", ["scan", "source", "--format", "json", "--lockfile", lockPath ?? path.join(ctx.root, "pnpm-lock.yaml")], ctx.root);
106
+ // Run from the lockfile's own folder with a bare name: on Windows the shell would split an absolute path with a space.
107
+ const r = await exec("osv-scanner", ["scan", "source", "--format", "json", "--lockfile", lock ?? "pnpm-lock.yaml"], lockPath ? path.dirname(lockPath) : ctx.root);
105
108
  try {
106
109
  const json = JSON.parse(r.stdout);
107
110
  for (const res of json.results ?? [])
@@ -14,8 +14,10 @@ function withTimeout(init) {
14
14
  const signal = typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(PROBE_TIMEOUT_MS) : undefined;
15
15
  return { redirect: "manual", ...(signal ? { signal } : {}), ...init };
16
16
  }
17
+ /** No gate owns the smoke/ prefix, so `nerd verify --rule smoke/...` would run nothing; the way to re-check is another deploy. */
18
+ const VERIFY = "nerd deploy (the live URL is probed after every deploy)";
17
19
  function smokeFinding(ruleId, risk, key) {
18
- return finding(ruleId, { risk, fingerprint: fingerprint(ruleId, key) });
20
+ return { ...finding(ruleId, { risk, fingerprint: fingerprint(ruleId, key) }), verify: VERIFY };
19
21
  }
20
22
  function probeResult(findings, startedAt) {
21
23
  const status = verdict(findings);
@@ -28,7 +30,7 @@ function probeResult(findings, startedAt) {
28
30
  * 404 for a script it does not know yet). Returns true as soon as it does; false after the last attempt.
29
31
  */
30
32
  export async function waitForDeployment(url, opts = {}) {
31
- const attempts = opts.attempts ?? 3;
33
+ const attempts = opts.attempts ?? 8;
32
34
  const delayMs = opts.delayMs ?? 2000;
33
35
  const fetchImpl = opts.fetchImpl ?? defaultFetch;
34
36
  for (let i = 0; i < attempts; i++) {
package/dist/util.d.ts CHANGED
@@ -10,22 +10,22 @@ export declare class CliError extends Error {
10
10
  });
11
11
  }
12
12
  /** The one sentence every unauthenticated command prints, with both ways in. */
13
- export declare const NOT_LOGGED_IN = "Not logged in. Run `nerd login --email you@example.com` (one click in the email signs you in and creates your account the first time), or set NERD_TOKEN to an API token for scripts.";
13
+ export declare const NOT_LOGGED_IN = "Not logged in. Run `nerd login --email you@example.com` (the link in the email signs you in, creating your account the first time, and opens a page where you press Approve), or set NERD_TOKEN to an API token for scripts.";
14
14
  export declare const log: {
15
15
  info: (msg: string) => void;
16
16
  step: (msg: string) => void;
17
17
  ok: (msg: string) => void;
18
18
  warn: (msg: string) => void;
19
19
  };
20
- /** Runs a command, streaming output. Resolves with the exit code. */
21
- export declare function run(cmd: string, args: string[], opts?: {
22
- cwd?: string;
23
- env?: NodeJS.ProcessEnv;
24
- }): Promise<number>;
25
- export declare function runOrThrow(cmd: string, args: string[], opts?: {
20
+ export interface RunOptions {
26
21
  cwd?: string;
27
22
  env?: NodeJS.ProcessEnv;
28
- }): Promise<void>;
23
+ /** Send the child's stdout to our stderr, so stdout stays reserved for `--json` output. */
24
+ stdoutToStderr?: boolean;
25
+ }
26
+ /** Runs a command, streaming output. Resolves with the exit code. */
27
+ export declare function run(cmd: string, args: string[], opts?: RunOptions): Promise<number>;
28
+ export declare function runOrThrow(cmd: string, args: string[], opts?: RunOptions): Promise<void>;
29
29
  export declare function printJson(value: unknown): void;
30
30
  export declare function table(rows: Record<string, unknown>[], columns: string[]): void;
31
31
  /** The dashboard that goes with an API origin (https://api.nerdsystem.com -> https://nerdsystem.com). */
package/dist/util.js CHANGED
@@ -15,7 +15,7 @@ export class CliError extends Error {
15
15
  }
16
16
  }
17
17
  /** The one sentence every unauthenticated command prints, with both ways in. */
18
- export const NOT_LOGGED_IN = "Not logged in. Run `nerd login --email you@example.com` (one click in the email signs you in and creates your account the first time), or set NERD_TOKEN to an API token for scripts.";
18
+ export const NOT_LOGGED_IN = "Not logged in. Run `nerd login --email you@example.com` (the link in the email signs you in, creating your account the first time, and opens a page where you press Approve), or set NERD_TOKEN to an API token for scripts.";
19
19
  export const log = {
20
20
  info: (msg) => console.error(`${msg}`),
21
21
  step: (msg) => console.error(`\x1b[36m›\x1b[0m ${msg}`),
@@ -25,7 +25,8 @@ export const log = {
25
25
  /** Runs a command, streaming output. Resolves with the exit code. */
26
26
  export function run(cmd, args, opts = {}) {
27
27
  return new Promise((resolve, reject) => {
28
- const child = spawn(cmd, args, { cwd: opts.cwd, env: { ...process.env, ...opts.env }, stdio: "inherit", shell: process.platform === "win32" });
28
+ const child = spawn(cmd, args, { cwd: opts.cwd, env: { ...process.env, ...opts.env }, stdio: ["inherit", opts.stdoutToStderr ? "pipe" : "inherit", "inherit"], shell: process.platform === "win32" });
29
+ child.stdout?.pipe(process.stderr);
29
30
  child.on("error", reject);
30
31
  child.on("exit", (code) => resolve(code ?? 1));
31
32
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nerdsystem",
3
- "version": "0.0.9",
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
 
@@ -36,14 +36,14 @@ Tell the person plainly what does not carry. Their users sign up again on the ne
36
36
  1. Make the change.
37
37
  2. `nerd check` (static rules, then typecheck). Fix anything it reports.
38
38
  3. `nerd dev` to run locally when you need to try it. The local app talks to the real auth service, so signup and login work exactly as in production.
39
- 4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, and prints the URL.
39
+ 4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, prints the URL, and probes the live URL (headers and the sign-in routes). With `--json` the output carries `probes`, the same shape as a check result, with `status: "skip"` and the reason when it did not run; probe findings never fail the deploy.
40
40
  5. If something is wrong after deploying: `nerd logs` shows recent output from the deployed app (`--q` to search, `--pr 12` for a preview), `nerd deployments` shows status and errors, `nerd db query "..."` inspects production data.
41
41
 
42
42
  Run `nerd check --json` when you want a machine-readable list of problems. Its summary says how many gates ran and which were skipped (`skipped` in the JSON): semgrep and gitleaks only run locally when installed; git builds always run them in the builder.
43
43
 
44
44
  ## Deploying from GitHub
45
45
 
46
- Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically (for accounts younger than seven days, counted from the nerdsystem account's creation, git builds are deferred: deploy with `nerd deploy` from the machine until then), and every pull request gets a preview at its own URL, posted as a PR comment. `nerd builds` lists builds; `nerd builds show <id>` prints the log when one fails. A failing build reports the same `nerd check` problems you would see locally, so fix and push again. Every preview gets its own database and bucket, and pending migrations are applied to it on each preview build, so a PR that changes the schema can be tried end to end before it merges.
46
+ Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically, and every pull request gets a preview at its own URL, posted as a PR comment. `nerd builds` lists builds; `nerd builds show <id>` prints the log when one fails. A failing build reports the same `nerd check` problems you would see locally, so fix and push again. Every preview gets its own database and bucket, and pending migrations are applied to it on each preview build, so a PR that changes the schema can be tried end to end before it merges.
47
47
 
48
48
  ## Where things go
49
49
 
@@ -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 (f8685cd08ad1); 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
 
@@ -37,14 +37,14 @@ Tell the person plainly what does not carry. Their users sign up again on the ne
37
37
  1. Make the change.
38
38
  2. `nerd check` (static rules, then typecheck). Fix anything it reports.
39
39
  3. `nerd dev` to run locally when you need to try it. The local app talks to the real auth service, so signup and login work exactly as in production.
40
- 4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, and prints the URL.
40
+ 4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, prints the URL, and probes the live URL (headers and the sign-in routes). With `--json` the output carries `probes`, the same shape as a check result, with `status: "skip"` and the reason when it did not run; probe findings never fail the deploy.
41
41
  5. If something is wrong after deploying: `nerd logs` shows recent output from the deployed app (`--q` to search, `--pr 12` for a preview), `nerd deployments` shows status and errors, `nerd db query "..."` inspects production data.
42
42
 
43
43
  Run `nerd check --json` when you want a machine-readable list of problems. Its summary says how many gates ran and which were skipped (`skipped` in the JSON): semgrep and gitleaks only run locally when installed; git builds always run them in the builder.
44
44
 
45
45
  ## Deploying from GitHub
46
46
 
47
- Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically (for accounts younger than seven days, counted from the nerdsystem account's creation, git builds are deferred: deploy with `nerd deploy` from the machine until then), and every pull request gets a preview at its own URL, posted as a PR comment. `nerd builds` lists builds; `nerd builds show <id>` prints the log when one fails. A failing build reports the same `nerd check` problems you would see locally, so fix and push again. Every preview gets its own database and bucket, and pending migrations are applied to it on each preview build, so a PR that changes the schema can be tried end to end before it merges.
47
+ Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically, and every pull request gets a preview at its own URL, posted as a PR comment. `nerd builds` lists builds; `nerd builds show <id>` prints the log when one fails. A failing build reports the same `nerd check` problems you would see locally, so fix and push again. Every preview gets its own database and bucket, and pending migrations are applied to it on each preview build, so a PR that changes the schema can be tried end to end before it merges.
48
48
 
49
49
  ## Where things go
50
50
 
@@ -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 (f8685cd08ad1); 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
 
@@ -33,14 +33,14 @@ Tell the person plainly what does not carry. Their users sign up again on the ne
33
33
  1. Make the change.
34
34
  2. `nerd check` (static rules, then typecheck). Fix anything it reports.
35
35
  3. `nerd dev` to run locally when you need to try it. The local app talks to the real auth service, so signup and login work exactly as in production.
36
- 4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, and prints the URL.
36
+ 4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, prints the URL, and probes the live URL (headers and the sign-in routes). With `--json` the output carries `probes`, the same shape as a check result, with `status: "skip"` and the reason when it did not run; probe findings never fail the deploy.
37
37
  5. If something is wrong after deploying: `nerd logs` shows recent output from the deployed app (`--q` to search, `--pr 12` for a preview), `nerd deployments` shows status and errors, `nerd db query "..."` inspects production data.
38
38
 
39
39
  Run `nerd check --json` when you want a machine-readable list of problems. Its summary says how many gates ran and which were skipped (`skipped` in the JSON): semgrep and gitleaks only run locally when installed; git builds always run them in the builder.
40
40
 
41
41
  ## Deploying from GitHub
42
42
 
43
- Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically (for accounts younger than seven days, counted from the nerdsystem account's creation, git builds are deferred: deploy with `nerd deploy` from the machine until then), and every pull request gets a preview at its own URL, posted as a PR comment. `nerd builds` lists builds; `nerd builds show <id>` prints the log when one fails. A failing build reports the same `nerd check` problems you would see locally, so fix and push again. Every preview gets its own database and bucket, and pending migrations are applied to it on each preview build, so a PR that changes the schema can be tried end to end before it merges.
43
+ Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically, and every pull request gets a preview at its own URL, posted as a PR comment. `nerd builds` lists builds; `nerd builds show <id>` prints the log when one fails. A failing build reports the same `nerd check` problems you would see locally, so fix and push again. Every preview gets its own database and bucket, and pending migrations are applied to it on each preview build, so a PR that changes the schema can be tried end to end before it merges.
44
44
 
45
45
  ## Where things go
46
46
 
@@ -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 (f8685cd08ad1); 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
 
@@ -33,14 +33,14 @@ Tell the person plainly what does not carry. Their users sign up again on the ne
33
33
  1. Make the change.
34
34
  2. `nerd check` (static rules, then typecheck). Fix anything it reports.
35
35
  3. `nerd dev` to run locally when you need to try it. The local app talks to the real auth service, so signup and login work exactly as in production.
36
- 4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, and prints the URL.
36
+ 4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, prints the URL, and probes the live URL (headers and the sign-in routes). With `--json` the output carries `probes`, the same shape as a check result, with `status: "skip"` and the reason when it did not run; probe findings never fail the deploy.
37
37
  5. If something is wrong after deploying: `nerd logs` shows recent output from the deployed app (`--q` to search, `--pr 12` for a preview), `nerd deployments` shows status and errors, `nerd db query "..."` inspects production data.
38
38
 
39
39
  Run `nerd check --json` when you want a machine-readable list of problems. Its summary says how many gates ran and which were skipped (`skipped` in the JSON): semgrep and gitleaks only run locally when installed; git builds always run them in the builder.
40
40
 
41
41
  ## Deploying from GitHub
42
42
 
43
- Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically (for accounts younger than seven days, counted from the nerdsystem account's creation, git builds are deferred: deploy with `nerd deploy` from the machine until then), and every pull request gets a preview at its own URL, posted as a PR comment. `nerd builds` lists builds; `nerd builds show <id>` prints the log when one fails. A failing build reports the same `nerd check` problems you would see locally, so fix and push again. Every preview gets its own database and bucket, and pending migrations are applied to it on each preview build, so a PR that changes the schema can be tried end to end before it merges.
43
+ Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically, and every pull request gets a preview at its own URL, posted as a PR comment. `nerd builds` lists builds; `nerd builds show <id>` prints the log when one fails. A failing build reports the same `nerd check` problems you would see locally, so fix and push again. Every preview gets its own database and bucket, and pending migrations are applied to it on each preview build, so a PR that changes the schema can be tried end to end before it merges.
44
44
 
45
45
  ## Where things go
46
46
 
@@ -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 (f8685cd08ad1); 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
 
@@ -33,14 +33,14 @@ Tell the person plainly what does not carry. Their users sign up again on the ne
33
33
  1. Make the change.
34
34
  2. `nerd check` (static rules, then typecheck). Fix anything it reports.
35
35
  3. `nerd dev` to run locally when you need to try it. The local app talks to the real auth service, so signup and login work exactly as in production.
36
- 4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, and prints the URL.
36
+ 4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, prints the URL, and probes the live URL (headers and the sign-in routes). With `--json` the output carries `probes`, the same shape as a check result, with `status: "skip"` and the reason when it did not run; probe findings never fail the deploy.
37
37
  5. If something is wrong after deploying: `nerd logs` shows recent output from the deployed app (`--q` to search, `--pr 12` for a preview), `nerd deployments` shows status and errors, `nerd db query "..."` inspects production data.
38
38
 
39
39
  Run `nerd check --json` when you want a machine-readable list of problems. Its summary says how many gates ran and which were skipped (`skipped` in the JSON): semgrep and gitleaks only run locally when installed; git builds always run them in the builder.
40
40
 
41
41
  ## Deploying from GitHub
42
42
 
43
- Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically (for accounts younger than seven days, counted from the nerdsystem account's creation, git builds are deferred: deploy with `nerd deploy` from the machine until then), and every pull request gets a preview at its own URL, posted as a PR comment. `nerd builds` lists builds; `nerd builds show <id>` prints the log when one fails. A failing build reports the same `nerd check` problems you would see locally, so fix and push again. Every preview gets its own database and bucket, and pending migrations are applied to it on each preview build, so a PR that changes the schema can be tried end to end before it merges.
43
+ Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically, and every pull request gets a preview at its own URL, posted as a PR comment. `nerd builds` lists builds; `nerd builds show <id>` prints the log when one fails. A failing build reports the same `nerd check` problems you would see locally, so fix and push again. Every preview gets its own database and bucket, and pending migrations are applied to it on each preview build, so a PR that changes the schema can be tried end to end before it merges.
44
44
 
45
45
  ## Where things go
46
46
 
@@ -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