nerdsystem 0.0.24 → 0.0.26

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 { reviewChecklist, reviewReport } from "./commands/session-review.js";
12
13
  import { review, status } from "./commands/status.js";
13
14
  import { billingPortal, billingShow, billingUpgrade } from "./commands/billing.js";
14
15
  import { auditExport, clientsCreate, clientsList, clientsRelease, domainsAdd, domainsList, domainsRemove, logs, membersInvite, membersList, membersRemove } from "./commands/org.js";
@@ -61,7 +62,9 @@ const HELP = `nerd: start, check, and deploy a web app on nerdsystem
61
62
  nerd agents sync Regenerate AGENTS.md, Cursor, Copilot, Gemini files from the skill
62
63
  nerd upgrade Bring this app up to the current SDK, agent instructions, and test harness
63
64
  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
65
+ nerd review What is open from the latest review of this app, with the fix for each
66
+ nerd review checklist What to look for when reviewing this app's code before a deploy, and which files changed
67
+ nerd review report [--agent name] Send the review written to .nerd/review.json (see the checklist for the format)
65
68
  nerd billing [--org slug] Plan, limits, and usage for the org
66
69
  nerd billing upgrade --plan pro|studio [--annual] Start checkout
67
70
  nerd billing portal Manage cards, invoices, and plan
@@ -143,8 +146,15 @@ export async function main(argv) {
143
146
  return status({ json: Boolean(values.json), hook: Boolean(values.hook) });
144
147
  }
145
148
  case "review": {
146
- const { values } = parse({}, false);
147
- return review({ json: Boolean(values.json) });
149
+ const { values, positionals } = parseArgs({ args: rest, options: { json: BOOL, file: STR, agent: STR }, allowPositionals: true });
150
+ const json = Boolean(values.json);
151
+ if (positionals[0] === "checklist")
152
+ return reviewChecklist({ json });
153
+ if (positionals[0] === "report")
154
+ return reviewReport({ json, ...(values.file ? { file: String(values.file) } : {}), ...(values.agent ? { agent: String(values.agent) } : {}) });
155
+ if (positionals.length)
156
+ throw new CliError("Usage: nerd review | nerd review checklist | nerd review report [--file path] [--agent name]");
157
+ return review({ json });
148
158
  }
149
159
  case "upgrade": {
150
160
  const { values } = parse({}, false);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The sha256 of every version of the starter app's CLAUDE.md this project has shipped (line endings as LF).
3
+ * `nerd upgrade` uses it to tell a CLAUDE.md that is still exactly the platform's text, which it replaces
4
+ * without ceremony, from one a person or an agent has added to, which it keeps beside the new one.
5
+ *
6
+ * When packages/template/CLAUDE.md changes, add the new hash here; a test fails until it is listed.
7
+ * To print it: `tr -d '\\r' < packages/template/CLAUDE.md | sha256sum`.
8
+ */
9
+ export declare const KNOWN_CLAUDE_MD: Set<string>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The sha256 of every version of the starter app's CLAUDE.md this project has shipped (line endings as LF).
3
+ * `nerd upgrade` uses it to tell a CLAUDE.md that is still exactly the platform's text, which it replaces
4
+ * without ceremony, from one a person or an agent has added to, which it keeps beside the new one.
5
+ *
6
+ * When packages/template/CLAUDE.md changes, add the new hash here; a test fails until it is listed.
7
+ * To print it: `tr -d '\\r' < packages/template/CLAUDE.md | sha256sum`.
8
+ */
9
+ export const KNOWN_CLAUDE_MD = new Set([
10
+ "02eb1063cb99fca0106b0a3052872a7839782564d285527ac74a08b1ca19b9ed",
11
+ "30b969614ba066a6faa930d35df67da48c3a692aba66ce009543e7c0d58114ba",
12
+ "31b9481c380810c87dff5e6a0812274cfb7068a35e11941867ad5bd4b2dc8dbf",
13
+ "440751d827c9f112f584157ea08495b56c8ff111024cc52177e5aa98b07622b8",
14
+ "4dc0c5234ad2115e1b932396db2a6efb0329bd93cb4f8ddc9968d978ddd48389",
15
+ "65f97940d79e9d657bc117d1dbbf13f21e84c578301b20b13c726e00739a305b",
16
+ "6f21b6478d50bd599bc8833068ffa82bc7ad5434439636a81b5b35d9d8554947",
17
+ "887cebefb74b2187ccaea027a40074d0cd163c3e6ed6fcdb0edbcbe766c0ccdd",
18
+ ]);
@@ -11,6 +11,12 @@ export interface BuildOutput {
11
11
  compatibilityFlags: string[];
12
12
  }
13
13
  /** Locates the Vite build output: dist/<worker>/wrangler.json names the entry and the assets dir. */
14
+ /**
15
+ * The Vite plugin copies `.dev.vars` next to the built Worker so a local preview can read it. A deploy
16
+ * never uploads it, and it is rewritten by every build, so the copy is removed here instead of being
17
+ * reported on every run with nothing for anyone to do about it. The file in the app's own folder stays.
18
+ */
19
+ export declare function dropLocalSecretCopies(build: BuildOutput): void;
14
20
  export declare function readBuildOutput(root: string): BuildOutput;
15
21
  export declare function applyRemoteMigrations(root: string, api: ApiClient): Promise<{
16
22
  applied: string[];
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { existsSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { ApiClient } from "../api.js";
5
5
  import { findProjectRoot, projectPath, readDevVars, readNerdJson } from "../project.js";
@@ -8,6 +8,7 @@ import { CliError, log, printJson, runOrThrow, table, dashboardUrl, ensureDeps }
8
8
  import { runChecks } from "./check.js";
9
9
  import { appendLineage, cliVersion, LOCAL_ONLY_NOTE, packProject, readLineage, uploadSnapshot, uploadStopMessage } from "./source.js";
10
10
  import { lockedGetPaths } from "../gates/tests.js";
11
+ import { ENV_FILES } from "../gates/text.js";
11
12
  import { platformScan } from "./platform-scan.js";
12
13
  import { platformReview, printReview } from "./platform-review.js";
13
14
  import { BUILD_GATES, gateSummary, knownSecretsFrom, probeDeployment, runGates, skippedProbe, SOURCE_GATES, waitForDeployment } from "../gates/index.js";
@@ -24,6 +25,18 @@ function walk(dir, base = dir) {
24
25
  return out;
25
26
  }
26
27
  /** Locates the Vite build output: dist/<worker>/wrangler.json names the entry and the assets dir. */
28
+ /**
29
+ * The Vite plugin copies `.dev.vars` next to the built Worker so a local preview can read it. A deploy
30
+ * never uploads it, and it is rewritten by every build, so the copy is removed here instead of being
31
+ * reported on every run with nothing for anyone to do about it. The file in the app's own folder stays.
32
+ */
33
+ export function dropLocalSecretCopies(build) {
34
+ if (!existsSync(build.workerDir))
35
+ return;
36
+ for (const name of readdirSync(build.workerDir))
37
+ if (ENV_FILES.test(name))
38
+ rmSync(path.join(build.workerDir, name), { force: true });
39
+ }
27
40
  export function readBuildOutput(root) {
28
41
  const dist = path.join(root, "dist");
29
42
  if (!existsSync(dist))
@@ -76,10 +89,11 @@ async function probeAfterDeploy(root, deployment, url, skip) {
76
89
  return skippedProbe("the app had not started answering within 30 seconds; open the URL to check, or run nerd deploy again to probe it");
77
90
  return probeDeployment(url, undefined, { lockedGets: lockedGetPaths(root) });
78
91
  }
79
- function printProbe(url, probes) {
92
+ function printProbe(url, probes, ownDomain) {
80
93
  if (probes.status === "skip")
81
94
  return log.info(`Live URL not probed: ${probes.summary}`);
82
- const line = `Probed ${url}: ${probes.status === "pass" ? "all probes passed" : probes.summary}`;
95
+ // With the owner's domain on the app, the address above is theirs and this one is not: say why it is the one tried.
96
+ const line = `Probed ${url}${ownDomain ? " (the app's platform address, which keeps answering whatever happens to the domain's DNS)" : ""}: ${probes.status === "pass" ? "all probes passed" : probes.summary}`;
83
97
  if (probes.status === "pass")
84
98
  log.ok(line);
85
99
  else
@@ -224,18 +238,19 @@ export async function deploy(opts) {
224
238
  await runOrThrow("npx", ["vite", "build"], { cwd: root, stdoutToStderr: true });
225
239
  }
226
240
  const build = readBuildOutput(root);
241
+ dropLocalSecretCopies(build);
227
242
  const built = await runGates(BUILD_GATES, { root, build, knownSecrets, log: (l) => log.info(l) }, { stopOnFail: true });
228
243
  if (!built.ok)
229
244
  throw new CliError(`Blocked by: ${built.failed.map((g) => g.title).join(", ")}.`, 2);
230
245
  const r = await shipBuild({ root, nerd, api, build, results: [...gates.results, ...built.results], opts });
231
- // The app is live at this point. The model review reads the stored code and is advice for the next change.
246
+ // The app is live at this point. The platform says what the review on file holds, and now and then reads the code itself.
232
247
  const reviewPage = `${dashboardUrl(api.apiUrl)}/app/${nerd.org}/projects/${nerd.project}/review`;
233
248
  const wantReview = r.deployment.status === "live" && Boolean(r.source);
234
249
  if (opts.json) {
235
250
  const review = wantReview ? await platformReview(api, `${projectPath(nerd)}/sources/${r.source.id}`, reviewPage) : null;
236
251
  return printJson({ deployment: r.deployment, url: r.url, customUrl: r.customUrl, migrations: r.migrations, probes: r.probes, source: r.source, sourceSkipped: r.sourceSkipped, lineage: r.lineage.passed, warnings: r.warnings, review });
237
252
  }
238
- printProbe(r.url, r.probes);
253
+ printProbe(r.url, r.probes, Boolean(r.customUrl));
239
254
  if (r.deployment.status === "live") {
240
255
  if (r.deployment.sourceId)
241
256
  log.ok(sourceLine(r));
@@ -243,10 +258,13 @@ export async function deploy(opts) {
243
258
  log.warn(sourceLine(r));
244
259
  }
245
260
  if (wantReview) {
246
- log.step("Your app is live. Asking for a model review of how it is put together (up to a minute or two; safe to stop waiting)");
261
+ log.step("Your app is live. Checking in on its review (now and then nerdsystem reads the code itself, which takes a minute or two; safe to stop waiting)");
247
262
  printReview(await platformReview(api, `${projectPath(nerd)}/sources/${r.source.id}`, reviewPage));
248
263
  }
249
- log.info(`First time? Sign up on your app at ${r.customUrl ?? r.url}, then make yourself admin in the dashboard's Auth tab: ${dashboardUrl(api.apiUrl)}/app/${nerd.org}/projects/${nerd.project}/auth`);
264
+ // Said once, after the app's first deploy, which is when it is news.
265
+ const earlier = await api.get(projectPath(nerd, "/deployments?limit=2")).catch(() => null);
266
+ if (earlier && earlier.deployments.length <= 1)
267
+ log.info(`First time? Sign up on your app at ${r.customUrl ?? r.url}, then make yourself admin in the dashboard's Auth tab: ${dashboardUrl(api.apiUrl)}/app/${nerd.org}/projects/${nerd.project}/auth`);
250
268
  }
251
269
  export async function deployments(opts) {
252
270
  const nerd = readNerdJson(findProjectRoot());
@@ -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.24";
195
+ const SDK_VERSION = "0.0.26";
@@ -12,16 +12,24 @@ export type ReviewOutcome = {
12
12
  status: "done" | "declined";
13
13
  findings: ReviewFindingOut[];
14
14
  dashboard: string;
15
+ /** Whether the platform read the code itself just now. False means these are the findings already on file. */
16
+ platformRead: boolean;
17
+ /** Whose review the findings on file come from. */
18
+ kind: "platform" | "session";
15
19
  } | {
16
20
  status: "skipped";
17
21
  reason: string;
18
22
  };
19
23
  /**
20
- * After a deploy is live, asks the platform for the model's review of the stored code and waits for it.
24
+ * After a deploy is live, checks in with the platform about the review of the stored code.
25
+ *
26
+ * The platform reads the code itself only now and then (an app's first deploy, and about weekly on paid
27
+ * plans when the code has changed), which takes a minute or two; otherwise it answers at once with the
28
+ * review on file, which is usually the one this app's agent reported with `nerd review report`.
21
29
  *
22
30
  * Only the findings come back into the terminal, and only as a rule id with a file and a line: the words
23
- * printed are this CLI's own catalog, looked up by id, so nothing the model wrote is ever put in front of
24
- * the agent reading this output. The model's plain-language summary is for the person and stays in the
31
+ * printed are this CLI's own catalog, looked up by id, so nothing a model wrote is ever put in front of
32
+ * the agent reading this output. The plain-language summary is for the person and stays in the
25
33
  * dashboard. Any failure, limit, or missing configuration is a skip with a reason; a review never changes
26
34
  * whether the deploy succeeded.
27
35
  */
@@ -1,11 +1,15 @@
1
1
  import { RULES } from "../gates/rules.js";
2
2
  import { CliError, log } from "../util.js";
3
3
  /**
4
- * After a deploy is live, asks the platform for the model's review of the stored code and waits for it.
4
+ * After a deploy is live, checks in with the platform about the review of the stored code.
5
+ *
6
+ * The platform reads the code itself only now and then (an app's first deploy, and about weekly on paid
7
+ * plans when the code has changed), which takes a minute or two; otherwise it answers at once with the
8
+ * review on file, which is usually the one this app's agent reported with `nerd review report`.
5
9
  *
6
10
  * Only the findings come back into the terminal, and only as a rule id with a file and a line: the words
7
- * printed are this CLI's own catalog, looked up by id, so nothing the model wrote is ever put in front of
8
- * the agent reading this output. The model's plain-language summary is for the person and stays in the
11
+ * printed are this CLI's own catalog, looked up by id, so nothing a model wrote is ever put in front of
12
+ * the agent reading this output. The plain-language summary is for the person and stays in the
9
13
  * dashboard. Any failure, limit, or missing configuration is a skip with a reason; a review never changes
10
14
  * whether the deploy succeeded.
11
15
  */
@@ -13,18 +17,23 @@ export async function platformReview(api, sourcePath, dashboard) {
13
17
  if (process.env.NERD_NO_REVIEW)
14
18
  return { status: "skipped", reason: "turned off with NERD_NO_REVIEW" };
15
19
  let review;
20
+ let platformRead = true;
16
21
  try {
17
- review = (await api.post(`${sourcePath}/review`)).review;
22
+ const got = await api.post(`${sourcePath}/review`);
23
+ review = got.review;
24
+ // A platform that does not say is an older one, which read the code on every call.
25
+ platformRead = got.read ? got.read.ran : true;
18
26
  }
19
27
  catch (err) {
20
28
  const status = err instanceof CliError ? err.status : undefined;
21
29
  // The 402's wording is the platform's own (plan and allowance), safe to repeat.
22
30
  if (status === 402 && err instanceof Error)
23
31
  return { status: "skipped", reason: err.message.replace(/\s*\[[^\]]*\]\s*$/, "") };
24
- return { status: "skipped", reason: status === 503 ? "the model review is not available on this platform yet" : "the review could not be completed this time" };
32
+ return { status: "skipped", reason: status === 503 ? "the platform's own review is not available here yet" : "the review could not be completed this time" };
25
33
  }
34
+ const kind = review.kind === "session" ? "session" : "platform";
26
35
  if (review.status !== "done")
27
- return { status: "declined", findings: [], dashboard };
36
+ return { status: "declined", findings: [], dashboard, platformRead, kind };
28
37
  const findings = [];
29
38
  for (const f of review.findings ?? []) {
30
39
  const rule = typeof f.rule_id === "string" && f.rule_id.startsWith("review/") ? RULES[f.rule_id] : undefined;
@@ -33,22 +42,23 @@ export async function platformReview(api, sourcePath, dashboard) {
33
42
  continue;
34
43
  findings.push({ rule_id: f.rule_id, severity: rule.severity, audience: rule.audience, file: f.file, line: f.line, risk: rule.risk, fix: rule.fix });
35
44
  }
36
- return { status: "done", findings, dashboard };
45
+ return { status: "done", findings, dashboard, platformRead, kind };
37
46
  }
38
47
  export function printReview(r) {
39
48
  if (r.status === "skipped") {
40
- log.info(`Model review: not run (${r.reason}).`);
49
+ log.info(`Review by nerdsystem: not run (${r.reason}). Your own review before each deploy (\`nerd review checklist\`) is not affected.`);
41
50
  return;
42
51
  }
43
52
  if (r.status === "declined") {
44
- log.info("Model review: the reviewer gave no answer for this app.");
53
+ log.info("Review by nerdsystem: the reviewer gave no answer for this app.");
45
54
  return;
46
55
  }
56
+ const whose = r.platformRead ? "nerdsystem read the code itself after this deploy" : r.kind === "session" ? "from the review this app's agent last reported" : "from nerdsystem's last own read of the code";
47
57
  if (!r.findings.length) {
48
- log.ok(`Model review: nothing to raise. A plain-language summary for the person is at ${r.dashboard}`);
58
+ log.ok(`Review: nothing open (${whose}). A plain-language summary for the person is at ${r.dashboard}`);
49
59
  return;
50
60
  }
51
- log.warn(`Model review: ${r.findings.length} thing(s) to look at. This is a second opinion from a model reading the code, so check each one against the code before changing anything; it can be wrong.`);
61
+ log.warn(`Review: ${r.findings.length} thing(s) open (${whose}). A review is a second opinion and can be wrong, so check each one against the code before changing anything. Say what you did about them in the next \`nerd review report\`.`);
52
62
  for (const f of r.findings) {
53
63
  log.info(` ${f.severity === "info" ? "·" : "!"} ${f.rule_id} ${f.file}:${f.line}${f.audience === "human" ? " (for the person to decide)" : ""}`);
54
64
  log.info(` ${f.risk}`);
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The session review: the app's coding agent reads its own changes against the platform's `review/`
3
+ * catalog before a deploy, and reports what it found.
4
+ *
5
+ * `nerd review checklist` prints what to look for, which files changed since the last report from this
6
+ * folder, the findings still open from earlier reviews, and the report's format. `nerd review report`
7
+ * stores a snapshot of the code as reviewed and sends the report with it. The platform checks every
8
+ * finding against that stored code and keeps the report as the agent's own word; it does its own
9
+ * independent read only now and then, and compares the two.
10
+ *
11
+ * What changed is worked out on this computer, from a list of file hashes kept in `.nerd/reviewed.json`
12
+ * at the last report. A fresh folder has no list, so its first review covers everything.
13
+ */
14
+ /** Where the agent writes its report for `nerd review report` to send. */
15
+ export declare const REPORT_FILE = ".nerd/review.json";
16
+ export declare const REVIEW_RULES: string[];
17
+ type Hashes = Record<string, string>;
18
+ /** Every file a review reads in this folder, with a hash of its contents. */
19
+ export declare function reviewFiles(root: string): Hashes;
20
+ /** Records the code as it stands as reviewed. */
21
+ export declare function writeReviewed(root: string, files: Hashes): void;
22
+ export interface ReviewDue {
23
+ /** Whether code a review reads has changed since the last report from this folder (styles alone do not count). */
24
+ due: boolean;
25
+ /** No report has been made from this folder: everything is unreviewed as far as this computer knows. */
26
+ first: boolean;
27
+ /** Files to read: the ones added or changed since the last report, or all of them the first time. */
28
+ changed: string[];
29
+ }
30
+ export declare function reviewDue(root: string): ReviewDue;
31
+ interface OpenFinding {
32
+ rule_id: string;
33
+ file: string;
34
+ line: number;
35
+ origin: "platform" | "session";
36
+ /** Recorded earlier as real and deliberately left as it is. */
37
+ leftAsIs: boolean;
38
+ }
39
+ declare const FORMAT: {
40
+ summary: string;
41
+ does: string[];
42
+ missing: string[];
43
+ findings: {
44
+ rule: string;
45
+ file: string;
46
+ line: number;
47
+ }[];
48
+ leftAsIs: {
49
+ rule: string;
50
+ file: string;
51
+ line: number;
52
+ }[];
53
+ fixed: {
54
+ rule: string;
55
+ file: string;
56
+ }[];
57
+ dismissed: {
58
+ rule: string;
59
+ file: string;
60
+ }[];
61
+ };
62
+ export interface Checklist {
63
+ app: string;
64
+ first: boolean;
65
+ changed: string[];
66
+ open: OpenFinding[];
67
+ rules: {
68
+ rule_id: string;
69
+ title: string;
70
+ audience: "agent" | "human";
71
+ lookFor: string;
72
+ fix: string;
73
+ }[];
74
+ reportFile: string;
75
+ format: typeof FORMAT;
76
+ }
77
+ export declare function checklistText(c: Checklist): string;
78
+ /** `nerd review checklist`: what the agent reads before it reviews. */
79
+ export declare function reviewChecklist(opts: {
80
+ json: boolean;
81
+ }): Promise<void>;
82
+ export interface Report {
83
+ findings: {
84
+ rule: string;
85
+ file: string;
86
+ line: number;
87
+ }[];
88
+ leftAsIs: {
89
+ rule: string;
90
+ file: string;
91
+ line: number;
92
+ }[];
93
+ fixed: {
94
+ rule: string;
95
+ file: string;
96
+ }[];
97
+ dismissed: {
98
+ rule: string;
99
+ file: string;
100
+ }[];
101
+ summary?: string;
102
+ does?: string[];
103
+ missing?: string[];
104
+ }
105
+ /**
106
+ * Reads the agent's report strictly, so a slip is corrected here with a sentence that says what to change,
107
+ * instead of being dropped without a word by the platform. `files` is what a review reads in this folder.
108
+ */
109
+ export declare function parseReport(text: string, files: Hashes, lineCount: (file: string) => number): Report;
110
+ /** `nerd review report`: store the code as reviewed, and send the agent's report with it. */
111
+ export declare function reviewReport(opts: {
112
+ json: boolean;
113
+ file?: string;
114
+ agent?: string;
115
+ }): Promise<void>;
116
+ export {};