nerdsystem 0.0.21 → 0.0.23

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.js CHANGED
@@ -9,6 +9,7 @@ import { cron, health, rollback, secretList, secretRemove, secretSet, transfer,
9
9
  import { adminHelp, adminSuspend, adminUnsuspend } from "./commands/admin.js";
10
10
  import { agentsSync } from "./commands/agents.js";
11
11
  import { upgrade } from "./commands/upgrade.js";
12
+ import { review, status } from "./commands/status.js";
12
13
  import { billingPortal, billingShow, billingUpgrade } from "./commands/billing.js";
13
14
  import { auditExport, clientsCreate, clientsList, clientsRelease, domainsAdd, domainsList, domainsRemove, logs, membersInvite, membersList, membersRemove } from "./commands/org.js";
14
15
  import { buildsList, buildsShow, buildsTrigger } from "./commands/builds.js";
@@ -59,12 +60,14 @@ const HELP = `nerd: start, check, and deploy a web app on nerdsystem
59
60
  nerd builds trigger [--branch b] [--commit sha]
60
61
  nerd agents sync Regenerate AGENTS.md, Cursor, Copilot, Gemini files from the skill
61
62
  nerd upgrade Bring this app up to the current SDK, agent instructions, and test harness
63
+ nerd status What the platform has found for this app that needs attention (updates, review findings, vulnerable packages)
64
+ nerd review The latest model review's findings for this app, with the fix for each
62
65
  nerd billing [--org slug] Plan, limits, and usage for the org
63
66
  nerd billing upgrade --plan pro|studio [--annual] Start checkout
64
67
  nerd billing portal Manage cards, invoices, and plan
65
68
  nerd members [invite <email> --role r | remove <userId>] Org members and invitations
66
69
  nerd clients [create <slug> --name n | release <slug>] Studio: one org per client, billed through yours
67
- nerd domains [add <hostname> | remove <id>] Custom domains for this project
70
+ nerd domains [add <domain> | remove <id>] The app on your own domain, with the DNS steps for it
68
71
  nerd logs [--since 30m] [--limit 100] [--q text] [--pr n] Logs from the deployed app
69
72
  nerd audit [--out file.csv] Export deployments and builds (Studio)
70
73
 
@@ -135,6 +138,14 @@ export async function main(argv) {
135
138
  const { values } = parse({ "skip-build": BOOL, "skip-probe": BOOL, force: BOOL, note: STR });
136
139
  return deploy(values);
137
140
  }
141
+ case "status": {
142
+ const { values } = parse({ hook: BOOL }, false);
143
+ return status({ json: Boolean(values.json), hook: Boolean(values.hook) });
144
+ }
145
+ case "review": {
146
+ const { values } = parse({}, false);
147
+ return review({ json: Boolean(values.json) });
148
+ }
138
149
  case "upgrade": {
139
150
  const { values } = parse({}, false);
140
151
  return upgrade({ json: Boolean(values.json) });
@@ -192,4 +192,4 @@ export async function init(dir, args) {
192
192
  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}`);
193
193
  }
194
194
  // Kept in sync with packages/sdk/package.json by the release script.
195
- const SDK_VERSION = "0.0.21";
195
+ const SDK_VERSION = "0.0.23";
@@ -11,6 +11,23 @@ export declare function membersRemove(userId: string | undefined, opts: {
11
11
  json: boolean;
12
12
  org?: string;
13
13
  }): Promise<void>;
14
+ /** The platform's steps for the company that runs the domain's DNS. Absent from an older platform. */
15
+ interface DomainSetup {
16
+ address: string;
17
+ forwards: string | null;
18
+ dnsHost: {
19
+ id: string;
20
+ name: string;
21
+ };
22
+ method: string;
23
+ steps: string[];
24
+ after: string;
25
+ }
26
+ /**
27
+ * The steps as the platform wrote them, numbered, with a line telling an agent how to pass them on. The person
28
+ * who owns the domain is usually not technical: they get these steps and nothing to choose between.
29
+ */
30
+ export declare function setupText(s: DomainSetup): string;
14
31
  export declare function domainsList(opts: {
15
32
  json: boolean;
16
33
  }): Promise<void>;
@@ -45,3 +62,4 @@ export declare function clientsRelease(slug: string | undefined, opts: {
45
62
  json: boolean;
46
63
  org?: string;
47
64
  }): Promise<void>;
65
+ export {};
@@ -35,23 +35,46 @@ export async function membersRemove(userId, opts) {
35
35
  return printJson(r);
36
36
  log.ok("Removed.");
37
37
  }
38
+ /**
39
+ * The steps as the platform wrote them, numbered, with a line telling an agent how to pass them on. The person
40
+ * who owns the domain is usually not technical: they get these steps and nothing to choose between.
41
+ */
42
+ export function setupText(s) {
43
+ const known = s.dnsHost.id !== "unknown";
44
+ return [
45
+ `The app's address will be https://${s.address}${s.forwards ? `, and ${s.forwards} forwards to it` : ""}.`,
46
+ known ? `This domain's DNS is run by ${s.dnsHost.name}${s.method === "move-dns" ? ", which cannot point a bare domain name at another service, so the DNS moves to Cloudflare (free) first" : ""}. The steps for the domain's owner:` : "The steps for the domain's owner:",
47
+ "",
48
+ ...s.steps.map((step, i) => ` ${i + 1}. ${step}`),
49
+ "",
50
+ s.after,
51
+ "",
52
+ "If you are a coding agent: give the owner these steps as they are, numbered, and nothing else to decide. Do not ask which company they use (it was looked up) and do not offer other record types. When they say it is done, run `nerd domains` until the status is active, then open the address to confirm.",
53
+ ].join("\n");
54
+ }
38
55
  export async function domainsList(opts) {
39
56
  const nerd = readNerdJson(findProjectRoot());
40
57
  const r = await ApiClient.fromConfig().get(projectPath(nerd, "/domains"));
41
58
  if (opts.json)
42
59
  return printJson(r);
43
60
  if (!r.domains.length)
44
- return log.info(`No custom domains. Add one: nerd domains add app.example.com`);
45
- table(r.domains.map((d) => ({ hostname: d.hostname, status: d.status, ssl: d.sslStatus ?? "", error: d.lastError ?? "", id: d.id })), ["hostname", "status", "ssl", "error", "id"]);
46
- log.info(`\nPoint each hostname at ${r.cnameTarget} with a CNAME record.`);
61
+ return log.info(`No custom domains. Add one: nerd domains add example.com`);
62
+ table(r.domains.map((d) => ({ hostname: d.hostname, status: d.status, ssl: d.sslStatus ?? "", does: d.redirectTo ? `forwards to ${d.redirectTo}` : "the app", error: d.lastError ?? "", id: d.id })), ["hostname", "status", "ssl", "does", "error", "id"]);
63
+ // Only addresses still waiting for their DNS records have steps left to do.
64
+ for (const s of r.setups ?? [])
65
+ log.info(`\n${setupText(s)}`);
66
+ if (!r.setups && r.domains.some((d) => d.status !== "active"))
67
+ log.info(`\nPoint each hostname at ${r.cnameTarget} with a CNAME record.`);
47
68
  }
48
69
  export async function domainsAdd(hostname, opts) {
49
70
  if (!hostname)
50
- throw new CliError("Usage: nerd domains add app.example.com");
71
+ throw new CliError("Usage: nerd domains add example.com");
51
72
  const nerd = readNerdJson(findProjectRoot());
52
73
  const r = await ApiClient.fromConfig().post(projectPath(nerd, "/domains"), { hostname });
53
74
  if (opts.json)
54
75
  return printJson(r);
76
+ if (r.setup)
77
+ return log.ok(`Added ${r.domain.hostname}.\n\n${setupText(r.setup)}`);
55
78
  log.ok(`Added ${r.domain.hostname}. Create this DNS record, then check with \`nerd domains\`:\n\n ${r.domain.hostname} CNAME ${r.cnameTarget}`);
56
79
  }
57
80
  export async function domainsRemove(idOrHostname, opts) {
@@ -0,0 +1,47 @@
1
+ import { ApiClient } from "../api.js";
2
+ /** One thing the platform wants looked at, in the catalog's words. */
3
+ export interface AttentionItem {
4
+ rule_id: string;
5
+ title: string;
6
+ severity: "block" | "warn" | "info";
7
+ audience: "agent" | "human";
8
+ /** A file and line, or a package and version. Never free text from the app or from a model. */
9
+ where?: string;
10
+ risk: string;
11
+ fix: string;
12
+ }
13
+ export interface Attention {
14
+ app: string | null;
15
+ items: AttentionItem[];
16
+ /** When the review the findings come from was made, so a finding already fixed since is not a surprise. */
17
+ reviewedAt?: string;
18
+ }
19
+ /** The latest model review's findings for this app, mapped through this CLI's own catalog. */
20
+ export declare function reviewItems(api: ApiClient, base: string): Promise<{
21
+ items: AttentionItem[];
22
+ reviewedAt?: string;
23
+ }>;
24
+ /**
25
+ * Everything the platform has noticed about this app that someone should act on, gathered in a few
26
+ * seconds without building or testing anything: a newer nerd or SDK, what the model review found in the
27
+ * live code, and packages with a serious vulnerability reported since the last deploy. Never throws and
28
+ * never waits long; whatever cannot be reached is simply left out.
29
+ *
30
+ * This is what a coding agent is shown when a session starts, so the app's owner does not have to know
31
+ * that any of it exists, let alone open a dashboard to find it.
32
+ */
33
+ export declare function gatherAttention(root: string): Promise<Attention>;
34
+ /** Short enough to sit at the top of a session: what, where, and one line on what to do about each kind. */
35
+ export declare function attentionText(a: Attention): string;
36
+ /**
37
+ * `nerd status`: what needs attention. With `--hook` it is the session-start hook's output: it prints
38
+ * nothing when there is nothing to say, and exits 0 whatever happens, so a session never opens on an error.
39
+ */
40
+ export declare function status(opts: {
41
+ json: boolean;
42
+ hook: boolean;
43
+ }): Promise<void>;
44
+ /** `nerd review`: only the model review's findings, with every fix written out. */
45
+ export declare function review(opts: {
46
+ json: boolean;
47
+ }): Promise<void>;
@@ -0,0 +1,140 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { ApiClient } from "../api.js";
4
+ import { RULES } from "../gates/rules.js";
5
+ import { currentVersions, pinned, versionFindings } from "../gates/versions.js";
6
+ import { findProjectRoot, projectPath, readNerdJson } from "../project.js";
7
+ import { log } from "../util.js";
8
+ import { cliVersion } from "./source.js";
9
+ const within = (p, ms) => Promise.race([p.catch(() => null), new Promise((r) => setTimeout(() => r(null), ms))]);
10
+ const item = (rule_id, extra = {}) => {
11
+ const r = RULES[rule_id];
12
+ return r ? { rule_id, title: r.title, severity: r.severity, audience: r.audience, risk: r.risk, fix: extra.fix ?? r.fix, ...(extra.where ? { where: extra.where } : {}) } : null;
13
+ };
14
+ /** The latest model review's findings for this app, mapped through this CLI's own catalog. */
15
+ export async function reviewItems(api, base) {
16
+ const got = await within(api.get(`${base}/review`), 5000);
17
+ const review = got?.review;
18
+ if (!review || review.status !== "done")
19
+ return { items: [] };
20
+ const items = [];
21
+ for (const f of review.findings ?? []) {
22
+ if (typeof f.rule_id !== "string" || !f.rule_id.startsWith("review/") || typeof f.file !== "string" || !/^[\w./@-]{1,200}$/.test(f.file) || !Number.isInteger(f.line))
23
+ continue;
24
+ const it = item(f.rule_id, { where: `${f.file}:${f.line}` });
25
+ if (it)
26
+ items.push(it);
27
+ }
28
+ return { items, reviewedAt: review.createdAt };
29
+ }
30
+ /**
31
+ * Everything the platform has noticed about this app that someone should act on, gathered in a few
32
+ * seconds without building or testing anything: a newer nerd or SDK, what the model review found in the
33
+ * live code, and packages with a serious vulnerability reported since the last deploy. Never throws and
34
+ * never waits long; whatever cannot be reached is simply left out.
35
+ *
36
+ * This is what a coding agent is shown when a session starts, so the app's owner does not have to know
37
+ * that any of it exists, let alone open a dashboard to find it.
38
+ */
39
+ export async function gatherAttention(root) {
40
+ const items = [];
41
+ let app = null;
42
+ let sdk = null;
43
+ try {
44
+ sdk = pinned(JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).dependencies?.["@nerdsystem/sdk"]);
45
+ }
46
+ catch {
47
+ // no package.json: nothing to say about the SDK
48
+ }
49
+ const versions = process.env.NERD_NO_VERSION_CHECK ? null : await within(currentVersions(), 5000);
50
+ if (versions)
51
+ for (const f of versionFindings({ cli: cliVersion(), sdk, versions })) {
52
+ const it = item(f.rule_id, { fix: f.fix, ...(f.file ? { where: f.file } : {}) });
53
+ if (it)
54
+ items.push(it);
55
+ }
56
+ let reviewedAt;
57
+ try {
58
+ const nerd = readNerdJson(root);
59
+ app = `${nerd.org}/${nerd.project}`;
60
+ const api = ApiClient.fromConfig();
61
+ const base = projectPath(nerd);
62
+ const [review, project] = await Promise.all([reviewItems(api, base), within(api.get(base), 5000)]);
63
+ items.push(...review.items);
64
+ reviewedAt = review.reviewedAt;
65
+ for (const v of project?.project.depsStatus?.vulnerable ?? []) {
66
+ if (v.severity !== "critical" && v.severity !== "high")
67
+ continue;
68
+ // Names and versions come from the platform's look-up of the registry, and are checked again for shape.
69
+ if (!/^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(v.name) || !/^\d+\.\d+\.\d+$/.test(v.version))
70
+ continue;
71
+ const it = item("platform/vulnerable-package", { where: `${v.name} ${v.version}` });
72
+ if (it)
73
+ items.push(it);
74
+ }
75
+ }
76
+ catch {
77
+ // not linked, or not signed in: the versions above are all there is to say
78
+ }
79
+ return { app, items, ...(reviewedAt ? { reviewedAt } : {}) };
80
+ }
81
+ /** Short enough to sit at the top of a session: what, where, and one line on what to do about each kind. */
82
+ export function attentionText(a) {
83
+ if (!a.items.length)
84
+ return "";
85
+ const lines = [`nerdsystem has ${a.items.length} thing(s) for this app${a.app ? ` (${a.app})` : ""} that someone should look at:`, ""];
86
+ const seen = new Set();
87
+ for (const it of a.items) {
88
+ lines.push(`- ${it.title}${it.where ? ` (${it.where})` : ""}${it.audience === "human" ? " [for the person to decide]" : ""}`);
89
+ if (!seen.has(it.rule_id)) {
90
+ seen.add(it.rule_id);
91
+ lines.push(` Why: ${it.risk}`, ` Fix: ${it.fix}`);
92
+ }
93
+ }
94
+ if (a.reviewedAt)
95
+ lines.push("", `Findings with a file and a line come from a model's review of the version deployed on ${a.reviewedAt.slice(0, 10)}. It is a second opinion and can be wrong, and code changed since may already have fixed one: read the code at each place before changing anything.`);
96
+ lines.push("", "Before starting on what the person asked for, tell them about these in one or two plain sentences, with no file names or jargon, and offer to take care of them. If they say yes, fix what is real, run `nerd check`, and deploy. Anything marked for the person to decide, put to them as a plain question and wait for the answer. If they would rather get on with their own request, do that first and remind them once at the end.");
97
+ return lines.join("\n");
98
+ }
99
+ /**
100
+ * `nerd status`: what needs attention. With `--hook` it is the session-start hook's output: it prints
101
+ * nothing when there is nothing to say, and exits 0 whatever happens, so a session never opens on an error.
102
+ */
103
+ export async function status(opts) {
104
+ let a = { app: null, items: [] };
105
+ try {
106
+ a = await gatherAttention(findProjectRoot());
107
+ }
108
+ catch {
109
+ if (!opts.hook && !opts.json)
110
+ log.info("This folder is not a nerdsystem app (no nerd.json found).");
111
+ }
112
+ if (opts.json) {
113
+ process.stdout.write(`${JSON.stringify(a, null, 2)}\n`);
114
+ return;
115
+ }
116
+ const text = attentionText(a);
117
+ if (text)
118
+ process.stdout.write(`${text}\n`);
119
+ else if (!opts.hook)
120
+ log.ok(`Nothing needs attention${a.app ? ` for ${a.app}` : ""}.`);
121
+ }
122
+ /** `nerd review`: only the model review's findings, with every fix written out. */
123
+ export async function review(opts) {
124
+ const root = findProjectRoot();
125
+ const nerd = readNerdJson(root);
126
+ const r = await reviewItems(ApiClient.fromConfig(), projectPath(nerd));
127
+ if (opts.json) {
128
+ process.stdout.write(`${JSON.stringify(r, null, 2)}\n`);
129
+ return;
130
+ }
131
+ if (!r.reviewedAt) {
132
+ log.info("No model review of this app yet. One runs at the end of `nerd deploy`.");
133
+ return;
134
+ }
135
+ if (!r.items.length) {
136
+ log.ok(`The review of the version deployed on ${r.reviewedAt.slice(0, 10)} found nothing to raise.`);
137
+ return;
138
+ }
139
+ process.stdout.write(`${attentionText({ app: `${nerd.org}/${nerd.project}`, items: r.items, reviewedAt: r.reviewedAt })}\n`);
140
+ }
@@ -87,6 +87,26 @@ export function upgradeProject(root, tpl, version) {
87
87
  // The files other agents read are generated from the skill that was just replaced.
88
88
  for (const [rel, content] of Object.entries(renderAgentFiles(root).files))
89
89
  write(rel, content);
90
+ // Claude Code's settings for this folder: keep whatever the person or agent added, and make sure the
91
+ // session-start hook and the platform's read-only commands are there.
92
+ const tplSettings = read(path.join(tpl, ".claude/settings.json"));
93
+ if (tplSettings !== null) {
94
+ const want = JSON.parse(tplSettings);
95
+ let have = {};
96
+ try {
97
+ have = JSON.parse(read(path.join(root, ".claude/settings.json")) ?? "{}");
98
+ }
99
+ catch {
100
+ have = {}; // an unreadable settings file is replaced rather than left broken
101
+ }
102
+ const allow = new Set([...(have.permissions?.allow ?? []), ...(want.permissions?.allow ?? [])]);
103
+ const hooks = { ...(have.hooks ?? {}) };
104
+ for (const [event, groups] of Object.entries(want.hooks ?? {})) {
105
+ const kept = (hooks[event] ?? []).filter((g) => !(g.hooks ?? []).some((h) => typeof h.command === "string" && h.command.startsWith("nerd status")));
106
+ hooks[event] = [...kept, ...groups];
107
+ }
108
+ write(".claude/settings.json", `${JSON.stringify({ ...have, permissions: { ...(have.permissions ?? {}), allow: [...allow] }, hooks }, null, 2)}\n`);
109
+ }
90
110
  const ignore = read(path.join(root, ".gitignore")) ?? "";
91
111
  if (!/^\.nerd\/?$/m.test(ignore))
92
112
  write(".gitignore", `${ignore}${ignore.endsWith("\n") || !ignore ? "" : "\n"}.nerd/\n`);
@@ -4,6 +4,7 @@ import { gitleaksGate, semgrepGate } from "./tools.js";
4
4
  import { suppressionsGate } from "./suppressions.js";
5
5
  import { staticGate } from "./static.js";
6
6
  import { designGate } from "./design.js";
7
+ import { reviewGate } from "./review.js";
7
8
  import { testsGate } from "./tests.js";
8
9
  import { versionsGate } from "./versions.js";
9
10
  import { applyAttempts, MAX_ATTEMPTS } from "./attempts.js";
@@ -13,7 +14,7 @@ export { mask } from "./secrets.js";
13
14
  export { MAX_ATTEMPTS, readAttempts } from "./attempts.js";
14
15
  export { probeDeployment, skippedProbe, waitForDeployment } from "./smoke.js";
15
16
  /** Gates that run on source, before the build. Suppressions first so a human sees them before anything else. */
16
- export const SOURCE_GATES = [suppressionsGate, secretsGate, gitleaksGate, depsGate, rulesGate, versionsGate, staticGate, semgrepGate, policyGate, piiGate, typecheckGate, testsGate, designGate];
17
+ export const SOURCE_GATES = [suppressionsGate, secretsGate, gitleaksGate, depsGate, rulesGate, versionsGate, staticGate, semgrepGate, policyGate, piiGate, typecheckGate, testsGate, designGate, reviewGate];
17
18
  /** Gates that need the built output. */
18
19
  export const BUILD_GATES = [bundleGate, policyGate];
19
20
  function selectGates(gates, onlyRules) {
@@ -0,0 +1,8 @@
1
+ import { type Gate } from "./types.js";
2
+ /**
3
+ * Brings the platform's latest model review of this app into the same list as every other check, so an
4
+ * agent meets its findings in the loop it already runs and nobody has to go and look for them. Advice
5
+ * only. The words are this CLI's catalog; the review itself contributes a rule id, a file, and a line.
6
+ * Skipped without comment when there is no sign-in, no linked project, no review, or no network.
7
+ */
8
+ export declare const reviewGate: Gate;
@@ -0,0 +1,37 @@
1
+ import { ApiClient } from "../api.js";
2
+ import { reviewItems } from "../commands/status.js";
3
+ import { projectPath, readNerdJson } from "../project.js";
4
+ import { counts, result, verdict } from "./types.js";
5
+ /**
6
+ * Brings the platform's latest model review of this app into the same list as every other check, so an
7
+ * agent meets its findings in the loop it already runs and nobody has to go and look for them. Advice
8
+ * only. The words are this CLI's catalog; the review itself contributes a rule id, a file, and a line.
9
+ * Skipped without comment when there is no sign-in, no linked project, no review, or no network.
10
+ */
11
+ export const reviewGate = {
12
+ name: "review",
13
+ title: "Model review of the deployed version",
14
+ blocking: false,
15
+ rulePrefixes: ["review/"],
16
+ async run(ctx) {
17
+ const t = Date.now();
18
+ // A git build has no person's sign-in to ask with, and the review is of code already deployed.
19
+ if (ctx.committed || process.env.NERD_NO_REVIEW)
20
+ return result(this, "skip", "Not asked for here", [], t);
21
+ let got;
22
+ try {
23
+ got = await reviewItems(ApiClient.fromConfig(), projectPath(readNerdJson(ctx.root)));
24
+ }
25
+ catch {
26
+ return result(this, "skip", "No linked project or sign-in to ask with", [], t);
27
+ }
28
+ if (!got.reviewedAt)
29
+ return result(this, "skip", "No review yet; one runs at the end of nerd deploy", [], t);
30
+ const findings = got.items.map((i) => {
31
+ const [file, line] = (i.where ?? "").split(":");
32
+ return { rule_id: i.rule_id, severity: i.severity, audience: i.audience, file, line: Number(line) || undefined, risk: i.risk, fix: i.fix, verify: "nerd deploy (the code is reviewed again after the next deploy)", attempt: 1, fingerprint: `${i.rule_id}:${i.where}` };
33
+ });
34
+ const when = got.reviewedAt.slice(0, 10);
35
+ return result(this, verdict(findings), findings.length ? `${counts(findings)}, from the review of the version deployed on ${when}; a second opinion that can be wrong, so read the code first` : `Nothing raised in the review of the version deployed on ${when}`, findings, t);
36
+ },
37
+ };
@@ -56,6 +56,7 @@ export const RULES = {
56
56
  "design/unused-table": { title: "A table nothing uses", severity: "warn", audience: "agent", risk: "A table left over from the starter or from a removed feature confuses the next change and may still hold people's data.", fix: "If the feature is gone, remove the table from the schema and write a migration that drops it; tell the person first if it may hold data they want. If it is meant to be used, connect it." },
57
57
  "design/long-file": { title: "A very long file", severity: "info", audience: "agent", risk: "Changes to a file this long are more likely to break something elsewhere in it, for a person or an agent.", fix: "Split it along its natural parts (one screen, one group of routes per file) the next time you change it. Do not reorganise working code just for this." },
58
58
  "design/unfinished": { title: "Something marked as unfinished", severity: "info", audience: "agent", risk: "A note to finish something later, or placeholder text, is about to go live.", fix: "Finish it, remove it, or tell the person it is not done yet so they can decide." },
59
+ "platform/vulnerable-package": { title: "A package this app uses has a serious vulnerability reported since it was deployed", severity: "warn", audience: "agent", risk: "The app passed its checks when it went live. Since then a flaw has been made public in something it depends on, and the live app still carries it.", fix: "Run `nerd check`: it names the package and the version to move to. Update it, run the tests, and deploy." },
59
60
  "versions/cli-outdated": { title: "The nerd command is behind", severity: "warn", audience: "agent", risk: "An older nerd runs fewer checks and may not know about newer platform features.", fix: "Run `npm install -g nerdsystem@latest`." },
60
61
  "versions/sdk-outdated": { title: "The app's SDK is behind", severity: "warn", audience: "agent", risk: "The app is missing fixes and safeguards that newer versions of the platform's library and instructions carry.", fix: "Run `nerd upgrade`, then `pnpm install` and `nerd check`." },
61
62
  "versions/sdk-unsupported": { title: "The app's SDK is too old for the platform", severity: "block", audience: "agent", risk: "This version of the platform's library lacks something the platform now depends on to keep apps safe, so it is no longer accepted.", fix: "Run `nerd upgrade`, then `pnpm install` and `nerd check`." },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nerdsystem",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
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": {
@@ -6,7 +6,22 @@
6
6
  "Bash(nerd db migrations:*)",
7
7
  "Bash(nerd auth config)",
8
8
  "Bash(pnpm db:generate)",
9
- "Bash(pnpm typecheck)"
9
+ "Bash(pnpm typecheck)",
10
+ "Bash(nerd status:*)",
11
+ "Bash(nerd review:*)"
12
+ ]
13
+ },
14
+ "hooks": {
15
+ "SessionStart": [
16
+ {
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "nerd status --hook 2>/dev/null || true",
21
+ "timeout": 20
22
+ }
23
+ ]
24
+ }
10
25
  ]
11
26
  }
12
27
  }
@@ -39,6 +39,8 @@ Tell the person plainly what does not carry. Their users sign up again on the ne
39
39
  4. `nerd deploy` when it is ready. It runs the checks, builds, stores a copy of the code, applies pending migrations, uploads, prints the URL, and probes the live URL (headers, the sign-in routes, and a signed-out GET of every route the tests found locked; a `smoke/route-open` finding means a locked route is answering strangers on the live app, so run `nerd rollback` first and investigate second). 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
+ **At the start of every session in this app, run `nerd status` before anything else** (in Claude Code it runs by itself and its output is already above). It lists what the platform has found for this app that someone should act on: a newer `nerd` or SDK, what the model review found in the live code, a package with a newly reported vulnerability. The person who owns this app does not know these exist and should never have to open a dashboard or learn a command to find out. So tell them, in one or two plain sentences with no file names or jargon, and offer to take care of it; if they say yes, fix what is real, run `nerd check`, and deploy. If it prints nothing, say nothing about it. `nerd review` prints the review's findings again with every fix written out.
43
+
42
44
  `nerd check` also says when the `nerd` command or this app's SDK has fallen behind (the `versions/` rules; it asks the platform once a day). For the command, run `npm install -g nerdsystem@latest`. For the app, run `nerd upgrade`: it updates the SDK version, these instructions, and the test harness, and leaves the app's own code alone; then `pnpm install`, `nerd check`, and work through what it reports. Tell the person what changed before you deploy. Never update the app's dependencies or instructions on your own initiative in the middle of other work; do it when the check asks or the person does.
43
45
 
44
46
  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. `nerd deploy` has the platform run whichever is missing on the stored copy of the code before anything goes live (a finding from it stops the deploy like any other), and git builds always run both in the builder.
@@ -176,6 +178,17 @@ After a deploy is live, `nerd deploy` asks the platform for a model review of th
176
178
  - **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 add it under the app's Settings tab in the dashboard, or 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.
177
179
  - **Email**: `sendEmail(c.env, { to, subject, text, html?, replyTo? })` 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. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. 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. Pass `replyTo` (the owner's address, or one they give you) whenever a person might answer the message; without it replies go to a mailbox nobody reads. Mail goes out under the app's name from the platform's address; on Pro and Studio the owner can switch it to their own domain in the dashboard under Settings, which needs no code change.
178
180
 
181
+ ## Putting the app on the owner's own domain
182
+
183
+ Paid plans only; on Free the command answers with the plan that includes it, so say that and stop.
184
+
185
+ 1. Run `nerd domains add example.com` with the bare domain the owner gives you. The platform makes `www.example.com` the app's address and sets the bare name up to forward to it. It also looks up which company runs the domain's DNS and prints the exact steps for that company.
186
+ 2. Give the owner those steps as printed, numbered. Do not ask who their registrar is, do not show a table of record types, and do not offer alternatives: the platform already chose. The steps are things only they can do (signing in where the domain is managed).
187
+ 3. When they say it is done, run `nerd domains` every minute or two until the status is `active`, then open the address and confirm it loads with https. If it is still pending after an hour, run `nerd domains` and read the steps again with them; the usual cause is a leftover parking record.
188
+ 4. Tell them two things once it is live: they sign in once more on the new address (same account, same data), and links to the old `.nerdsystem.app` address keep working.
189
+
190
+ For a subdomain such as `app.example.com`, the same command gives a single record to add.
191
+
179
192
  ## Debugging production
180
193
 
181
194
  When something is wrong on the deployed app: `nerd logs --since 1h` (add `--q "text"` to search, `--pr 12` for a preview) shows errors with the request that caused them. Reproduce locally with `nerd dev` using the same input, fix it, run `nerd verify`, and deploy or open a pull request. If a deploy made things worse, `nerd rollback` puts the previous production bundle back without touching the database. Write a one-line note on every deploy (`nerd deploy --note "fix: todos query filtered by user"`); the dashboard shows these as a changelog people can read.
@@ -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 (df4afc8243ef); edit the skill, then run: nerd agents sync -->
5
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (1794b8a1a530); edit the skill, then run: nerd agents sync -->
6
6
 
7
7
  # Building on nerdsystem
8
8
 
@@ -40,6 +40,8 @@ Tell the person plainly what does not carry. Their users sign up again on the ne
40
40
  4. `nerd deploy` when it is ready. It runs the checks, builds, stores a copy of the code, applies pending migrations, uploads, prints the URL, and probes the live URL (headers, the sign-in routes, and a signed-out GET of every route the tests found locked; a `smoke/route-open` finding means a locked route is answering strangers on the live app, so run `nerd rollback` first and investigate second). 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
+ **At the start of every session in this app, run `nerd status` before anything else** (in Claude Code it runs by itself and its output is already above). It lists what the platform has found for this app that someone should act on: a newer `nerd` or SDK, what the model review found in the live code, a package with a newly reported vulnerability. The person who owns this app does not know these exist and should never have to open a dashboard or learn a command to find out. So tell them, in one or two plain sentences with no file names or jargon, and offer to take care of it; if they say yes, fix what is real, run `nerd check`, and deploy. If it prints nothing, say nothing about it. `nerd review` prints the review's findings again with every fix written out.
44
+
43
45
  `nerd check` also says when the `nerd` command or this app's SDK has fallen behind (the `versions/` rules; it asks the platform once a day). For the command, run `npm install -g nerdsystem@latest`. For the app, run `nerd upgrade`: it updates the SDK version, these instructions, and the test harness, and leaves the app's own code alone; then `pnpm install`, `nerd check`, and work through what it reports. Tell the person what changed before you deploy. Never update the app's dependencies or instructions on your own initiative in the middle of other work; do it when the check asks or the person does.
44
46
 
45
47
  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. `nerd deploy` has the platform run whichever is missing on the stored copy of the code before anything goes live (a finding from it stops the deploy like any other), and git builds always run both in the builder.
@@ -177,6 +179,17 @@ After a deploy is live, `nerd deploy` asks the platform for a model review of th
177
179
  - **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 add it under the app's Settings tab in the dashboard, or 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.
178
180
  - **Email**: `sendEmail(c.env, { to, subject, text, html?, replyTo? })` 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. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. 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. Pass `replyTo` (the owner's address, or one they give you) whenever a person might answer the message; without it replies go to a mailbox nobody reads. Mail goes out under the app's name from the platform's address; on Pro and Studio the owner can switch it to their own domain in the dashboard under Settings, which needs no code change.
179
181
 
182
+ ## Putting the app on the owner's own domain
183
+
184
+ Paid plans only; on Free the command answers with the plan that includes it, so say that and stop.
185
+
186
+ 1. Run `nerd domains add example.com` with the bare domain the owner gives you. The platform makes `www.example.com` the app's address and sets the bare name up to forward to it. It also looks up which company runs the domain's DNS and prints the exact steps for that company.
187
+ 2. Give the owner those steps as printed, numbered. Do not ask who their registrar is, do not show a table of record types, and do not offer alternatives: the platform already chose. The steps are things only they can do (signing in where the domain is managed).
188
+ 3. When they say it is done, run `nerd domains` every minute or two until the status is `active`, then open the address and confirm it loads with https. If it is still pending after an hour, run `nerd domains` and read the steps again with them; the usual cause is a leftover parking record.
189
+ 4. Tell them two things once it is live: they sign in once more on the new address (same account, same data), and links to the old `.nerdsystem.app` address keep working.
190
+
191
+ For a subdomain such as `app.example.com`, the same command gives a single record to add.
192
+
180
193
  ## Debugging production
181
194
 
182
195
  When something is wrong on the deployed app: `nerd logs --since 1h` (add `--q "text"` to search, `--pr 12` for a preview) shows errors with the request that caused them. Reproduce locally with `nerd dev` using the same input, fix it, run `nerd verify`, and deploy or open a pull request. If a deploy made things worse, `nerd rollback` puts the previous production bundle back without touching the database. Write a one-line note on every deploy (`nerd deploy --note "fix: todos query filtered by user"`); the dashboard shows these as a changelog people can read.
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (df4afc8243ef); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (1794b8a1a530); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -36,6 +36,8 @@ Tell the person plainly what does not carry. Their users sign up again on the ne
36
36
  4. `nerd deploy` when it is ready. It runs the checks, builds, stores a copy of the code, applies pending migrations, uploads, prints the URL, and probes the live URL (headers, the sign-in routes, and a signed-out GET of every route the tests found locked; a `smoke/route-open` finding means a locked route is answering strangers on the live app, so run `nerd rollback` first and investigate second). 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
+ **At the start of every session in this app, run `nerd status` before anything else** (in Claude Code it runs by itself and its output is already above). It lists what the platform has found for this app that someone should act on: a newer `nerd` or SDK, what the model review found in the live code, a package with a newly reported vulnerability. The person who owns this app does not know these exist and should never have to open a dashboard or learn a command to find out. So tell them, in one or two plain sentences with no file names or jargon, and offer to take care of it; if they say yes, fix what is real, run `nerd check`, and deploy. If it prints nothing, say nothing about it. `nerd review` prints the review's findings again with every fix written out.
40
+
39
41
  `nerd check` also says when the `nerd` command or this app's SDK has fallen behind (the `versions/` rules; it asks the platform once a day). For the command, run `npm install -g nerdsystem@latest`. For the app, run `nerd upgrade`: it updates the SDK version, these instructions, and the test harness, and leaves the app's own code alone; then `pnpm install`, `nerd check`, and work through what it reports. Tell the person what changed before you deploy. Never update the app's dependencies or instructions on your own initiative in the middle of other work; do it when the check asks or the person does.
40
42
 
41
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. `nerd deploy` has the platform run whichever is missing on the stored copy of the code before anything goes live (a finding from it stops the deploy like any other), and git builds always run both in the builder.
@@ -173,6 +175,17 @@ After a deploy is live, `nerd deploy` asks the platform for a model review of th
173
175
  - **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 add it under the app's Settings tab in the dashboard, or 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.
174
176
  - **Email**: `sendEmail(c.env, { to, subject, text, html?, replyTo? })` 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. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. 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. Pass `replyTo` (the owner's address, or one they give you) whenever a person might answer the message; without it replies go to a mailbox nobody reads. Mail goes out under the app's name from the platform's address; on Pro and Studio the owner can switch it to their own domain in the dashboard under Settings, which needs no code change.
175
177
 
178
+ ## Putting the app on the owner's own domain
179
+
180
+ Paid plans only; on Free the command answers with the plan that includes it, so say that and stop.
181
+
182
+ 1. Run `nerd domains add example.com` with the bare domain the owner gives you. The platform makes `www.example.com` the app's address and sets the bare name up to forward to it. It also looks up which company runs the domain's DNS and prints the exact steps for that company.
183
+ 2. Give the owner those steps as printed, numbered. Do not ask who their registrar is, do not show a table of record types, and do not offer alternatives: the platform already chose. The steps are things only they can do (signing in where the domain is managed).
184
+ 3. When they say it is done, run `nerd domains` every minute or two until the status is `active`, then open the address and confirm it loads with https. If it is still pending after an hour, run `nerd domains` and read the steps again with them; the usual cause is a leftover parking record.
185
+ 4. Tell them two things once it is live: they sign in once more on the new address (same account, same data), and links to the old `.nerdsystem.app` address keep working.
186
+
187
+ For a subdomain such as `app.example.com`, the same command gives a single record to add.
188
+
176
189
  ## Debugging production
177
190
 
178
191
  When something is wrong on the deployed app: `nerd logs --since 1h` (add `--q "text"` to search, `--pr 12` for a preview) shows errors with the request that caused them. Reproduce locally with `nerd dev` using the same input, fix it, run `nerd verify`, and deploy or open a pull request. If a deploy made things worse, `nerd rollback` puts the previous production bundle back without touching the database. Write a one-line note on every deploy (`nerd deploy --note "fix: todos query filtered by user"`); the dashboard shows these as a changelog people can read.
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (df4afc8243ef); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (1794b8a1a530); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -36,6 +36,8 @@ Tell the person plainly what does not carry. Their users sign up again on the ne
36
36
  4. `nerd deploy` when it is ready. It runs the checks, builds, stores a copy of the code, applies pending migrations, uploads, prints the URL, and probes the live URL (headers, the sign-in routes, and a signed-out GET of every route the tests found locked; a `smoke/route-open` finding means a locked route is answering strangers on the live app, so run `nerd rollback` first and investigate second). 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
+ **At the start of every session in this app, run `nerd status` before anything else** (in Claude Code it runs by itself and its output is already above). It lists what the platform has found for this app that someone should act on: a newer `nerd` or SDK, what the model review found in the live code, a package with a newly reported vulnerability. The person who owns this app does not know these exist and should never have to open a dashboard or learn a command to find out. So tell them, in one or two plain sentences with no file names or jargon, and offer to take care of it; if they say yes, fix what is real, run `nerd check`, and deploy. If it prints nothing, say nothing about it. `nerd review` prints the review's findings again with every fix written out.
40
+
39
41
  `nerd check` also says when the `nerd` command or this app's SDK has fallen behind (the `versions/` rules; it asks the platform once a day). For the command, run `npm install -g nerdsystem@latest`. For the app, run `nerd upgrade`: it updates the SDK version, these instructions, and the test harness, and leaves the app's own code alone; then `pnpm install`, `nerd check`, and work through what it reports. Tell the person what changed before you deploy. Never update the app's dependencies or instructions on your own initiative in the middle of other work; do it when the check asks or the person does.
40
42
 
41
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. `nerd deploy` has the platform run whichever is missing on the stored copy of the code before anything goes live (a finding from it stops the deploy like any other), and git builds always run both in the builder.
@@ -173,6 +175,17 @@ After a deploy is live, `nerd deploy` asks the platform for a model review of th
173
175
  - **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 add it under the app's Settings tab in the dashboard, or 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.
174
176
  - **Email**: `sendEmail(c.env, { to, subject, text, html?, replyTo? })` 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. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. 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. Pass `replyTo` (the owner's address, or one they give you) whenever a person might answer the message; without it replies go to a mailbox nobody reads. Mail goes out under the app's name from the platform's address; on Pro and Studio the owner can switch it to their own domain in the dashboard under Settings, which needs no code change.
175
177
 
178
+ ## Putting the app on the owner's own domain
179
+
180
+ Paid plans only; on Free the command answers with the plan that includes it, so say that and stop.
181
+
182
+ 1. Run `nerd domains add example.com` with the bare domain the owner gives you. The platform makes `www.example.com` the app's address and sets the bare name up to forward to it. It also looks up which company runs the domain's DNS and prints the exact steps for that company.
183
+ 2. Give the owner those steps as printed, numbered. Do not ask who their registrar is, do not show a table of record types, and do not offer alternatives: the platform already chose. The steps are things only they can do (signing in where the domain is managed).
184
+ 3. When they say it is done, run `nerd domains` every minute or two until the status is `active`, then open the address and confirm it loads with https. If it is still pending after an hour, run `nerd domains` and read the steps again with them; the usual cause is a leftover parking record.
185
+ 4. Tell them two things once it is live: they sign in once more on the new address (same account, same data), and links to the old `.nerdsystem.app` address keep working.
186
+
187
+ For a subdomain such as `app.example.com`, the same command gives a single record to add.
188
+
176
189
  ## Debugging production
177
190
 
178
191
  When something is wrong on the deployed app: `nerd logs --since 1h` (add `--q "text"` to search, `--pr 12` for a preview) shows errors with the request that caused them. Reproduce locally with `nerd dev` using the same input, fix it, run `nerd verify`, and deploy or open a pull request. If a deploy made things worse, `nerd rollback` puts the previous production bundle back without touching the database. Write a one-line note on every deploy (`nerd deploy --note "fix: todos query filtered by user"`); the dashboard shows these as a changelog people can read.
@@ -13,6 +13,10 @@ nerdsystem deploys this app to Cloudflare and provides authentication and author
13
13
  - With a linked GitHub repo, pushes to the production branch and pull requests deploy automatically. `nerd builds` and `nerd builds show <id>` show what happened.
14
14
  - `pnpm db:generate` creates a migration after editing `src/server/db/schema.ts`. Never edit the database by hand.
15
15
 
16
+ ## When a session starts
17
+
18
+ Run `nerd status` first (Claude Code does this by itself). It lists what the platform has found for this app: updates, findings from the model review of the live code, packages with a new vulnerability. The app's owner does not know these exist. Tell them in plain words and offer to take care of them; they should never need a command or a dashboard to find out.
19
+
16
20
  ## Layout
17
21
 
18
22
  - `src/server/index.ts` is the Worker entry. Build the API here with Hono; mount routers under `/api/...`.
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (df4afc8243ef); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (1794b8a1a530); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -36,6 +36,8 @@ Tell the person plainly what does not carry. Their users sign up again on the ne
36
36
  4. `nerd deploy` when it is ready. It runs the checks, builds, stores a copy of the code, applies pending migrations, uploads, prints the URL, and probes the live URL (headers, the sign-in routes, and a signed-out GET of every route the tests found locked; a `smoke/route-open` finding means a locked route is answering strangers on the live app, so run `nerd rollback` first and investigate second). 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
+ **At the start of every session in this app, run `nerd status` before anything else** (in Claude Code it runs by itself and its output is already above). It lists what the platform has found for this app that someone should act on: a newer `nerd` or SDK, what the model review found in the live code, a package with a newly reported vulnerability. The person who owns this app does not know these exist and should never have to open a dashboard or learn a command to find out. So tell them, in one or two plain sentences with no file names or jargon, and offer to take care of it; if they say yes, fix what is real, run `nerd check`, and deploy. If it prints nothing, say nothing about it. `nerd review` prints the review's findings again with every fix written out.
40
+
39
41
  `nerd check` also says when the `nerd` command or this app's SDK has fallen behind (the `versions/` rules; it asks the platform once a day). For the command, run `npm install -g nerdsystem@latest`. For the app, run `nerd upgrade`: it updates the SDK version, these instructions, and the test harness, and leaves the app's own code alone; then `pnpm install`, `nerd check`, and work through what it reports. Tell the person what changed before you deploy. Never update the app's dependencies or instructions on your own initiative in the middle of other work; do it when the check asks or the person does.
40
42
 
41
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. `nerd deploy` has the platform run whichever is missing on the stored copy of the code before anything goes live (a finding from it stops the deploy like any other), and git builds always run both in the builder.
@@ -173,6 +175,17 @@ After a deploy is live, `nerd deploy` asks the platform for a model review of th
173
175
  - **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 add it under the app's Settings tab in the dashboard, or 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.
174
176
  - **Email**: `sendEmail(c.env, { to, subject, text, html?, replyTo? })` 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. On the Free plan the recipient must also have confirmed their address by finishing a password reset (403 otherwise); paid plans can write to every signed-up user. 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. Pass `replyTo` (the owner's address, or one they give you) whenever a person might answer the message; without it replies go to a mailbox nobody reads. Mail goes out under the app's name from the platform's address; on Pro and Studio the owner can switch it to their own domain in the dashboard under Settings, which needs no code change.
175
177
 
178
+ ## Putting the app on the owner's own domain
179
+
180
+ Paid plans only; on Free the command answers with the plan that includes it, so say that and stop.
181
+
182
+ 1. Run `nerd domains add example.com` with the bare domain the owner gives you. The platform makes `www.example.com` the app's address and sets the bare name up to forward to it. It also looks up which company runs the domain's DNS and prints the exact steps for that company.
183
+ 2. Give the owner those steps as printed, numbered. Do not ask who their registrar is, do not show a table of record types, and do not offer alternatives: the platform already chose. The steps are things only they can do (signing in where the domain is managed).
184
+ 3. When they say it is done, run `nerd domains` every minute or two until the status is `active`, then open the address and confirm it loads with https. If it is still pending after an hour, run `nerd domains` and read the steps again with them; the usual cause is a leftover parking record.
185
+ 4. Tell them two things once it is live: they sign in once more on the new address (same account, same data), and links to the old `.nerdsystem.app` address keep working.
186
+
187
+ For a subdomain such as `app.example.com`, the same command gives a single record to add.
188
+
176
189
  ## Debugging production
177
190
 
178
191
  When something is wrong on the deployed app: `nerd logs --since 1h` (add `--q "text"` to search, `--pr 12` for a preview) shows errors with the request that caused them. Reproduce locally with `nerd dev` using the same input, fix it, run `nerd verify`, and deploy or open a pull request. If a deploy made things worse, `nerd rollback` puts the previous production bundle back without touching the database. Write a one-line note on every deploy (`nerd deploy --note "fix: todos query filtered by user"`); the dashboard shows these as a changelog people can read.