nerdsystem 0.0.8 → 0.0.9

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # nerdsystem
2
2
 
3
- `nerd`: deploy apps built with coding agents to [nerdsystem](https://nerdsystem.com), with sign-in, roles, a database, file storage, previews per pull request, and security gates on every deploy.
3
+ `nerd`: start a web app with your coding agent and deploy it to [nerdsystem](https://nerdsystem.com), with sign-in, roles, a database, file storage, previews per pull request, and security gates on every deploy.
4
4
 
5
5
  ```sh
6
6
  npx nerdsystem init my-app
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ import { login, whoami } from "./commands/login.js";
17
17
  import { deleteProject, trashList, trashRestore } from "./commands/trash.js";
18
18
  import { findProjectRoot } from "./project.js";
19
19
  import { CliError, log } from "./util.js";
20
- const HELP = `nerd — deploy apps to nerdsystem
20
+ const HELP = `nerd: start, check, and deploy a web app on nerdsystem
21
21
 
22
22
  nerd login [--email you@x.com] Sign in or sign up: prints a link to approve in the browser (--email sends it; --token nsk_... for scripts)
23
23
  nerd whoami Show the current user and orgs
@@ -26,7 +26,7 @@ const HELP = `nerd — deploy apps to nerdsystem
26
26
  nerd link --slug <project> [--org] Link this directory to an existing or new project
27
27
  nerd dev [vite args] Run locally (writes .dev.vars, applies local migrations)
28
28
  nerd check [--no-build] [--rule id] [--json] Run every gate a deploy runs; --json gives failures the agent can fix (alias: nerd verify)
29
- nerd deploy [--note "what changed"] Check, build, migrate, upload. Prints the URL
29
+ nerd deploy [--note "what changed"] [--skip-probe] Check, build, migrate, upload, probe the live URL. Prints the URL
30
30
  nerd rollback [deploymentId] Re-publish the previous production bundle
31
31
  nerd cron [off|hourly|daily] Scheduled job: the platform calls POST /api/cron
32
32
  nerd transfer <org> Move this project to another org you administer
@@ -111,7 +111,7 @@ export async function main(argv) {
111
111
  return check({ json: Boolean(values.json), "no-build": Boolean(values["no-build"]), "no-remote": Boolean(values["no-remote"]), ...(values.rule ? { rule: values.rule } : {}) });
112
112
  }
113
113
  case "deploy": {
114
- const { values } = parse({ "skip-check": BOOL, "skip-build": BOOL, note: STR });
114
+ const { values } = parse({ "skip-build": BOOL, "skip-probe": BOOL, note: STR });
115
115
  return deploy(values);
116
116
  }
117
117
  case "rollback": {
@@ -301,6 +301,15 @@ export async function runCli(argv) {
301
301
  log.warn(err.message);
302
302
  code = err.exitCode > 0 ? err.exitCode : 1;
303
303
  }
304
+ else if (err instanceof Error && typeof err.code === "string" && err.code.startsWith("ERR_PARSE_ARGS")) {
305
+ // A typo in a flag gets one sentence, not a Node stack trace.
306
+ const message = `${err.message.replace(/\. To specify a positional argument.*$/s, "")}. Run \`nerd --help\` for the flags each command takes.`;
307
+ if (argv.includes("--json"))
308
+ console.log(JSON.stringify({ error: { code: "usage", message } }));
309
+ else
310
+ log.warn(message);
311
+ code = 2;
312
+ }
304
313
  else {
305
314
  console.error(err);
306
315
  code = 1;
@@ -16,8 +16,8 @@ export declare function applyRemoteMigrations(root: string, api: ApiClient): Pro
16
16
  }>;
17
17
  export declare function deploy(opts: {
18
18
  json: boolean;
19
- "skip-check": boolean;
20
19
  "skip-build": boolean;
20
+ "skip-probe"?: boolean | undefined;
21
21
  note?: string | undefined;
22
22
  }): Promise<void>;
23
23
  export declare function deployments(opts: {
@@ -4,7 +4,7 @@ import { ApiClient } from "../api.js";
4
4
  import { findProjectRoot, projectPath, readNerdJson } from "../project.js";
5
5
  import { CliError, log, printJson, runOrThrow, table, dashboardUrl, ensureDeps } from "../util.js";
6
6
  import { runChecks } from "./check.js";
7
- import { BUILD_GATES, gateSummary, knownSecretsFrom, runGates, SOURCE_GATES } from "../gates/index.js";
7
+ import { BUILD_GATES, gateSummary, knownSecretsFrom, probeDeployment, runGates, skippedProbe, SOURCE_GATES, waitForDeployment } from "../gates/index.js";
8
8
  import { readDevVars } from "../project.js";
9
9
  const MODULE_EXT = new Set([".js", ".mjs", ".wasm"]);
10
10
  function walk(dir, base = dir) {
@@ -56,35 +56,55 @@ export async function applyRemoteMigrations(root, api) {
56
56
  return { applied: [], skipped: [] };
57
57
  return api.post(projectPath(readNerdJson(root), "/db/migrations"), { migrations });
58
58
  }
59
+ /**
60
+ * Probes the live URL after an upload, the same checks the builder runs after a git build.
61
+ * Informational: the app is already live, so findings are printed, never thrown.
62
+ */
63
+ async function probeAfterDeploy(deployment, url, skip) {
64
+ if (skip)
65
+ return skippedProbe("--skip-probe was given");
66
+ if (deployment.status !== "live")
67
+ return skippedProbe(`the deployment status is ${deployment.status}, not live`);
68
+ await waitForDeployment(url);
69
+ return probeDeployment(url);
70
+ }
71
+ function printProbe(url, probes) {
72
+ if (probes.status === "skip")
73
+ return log.info(`Live URL not probed: ${probes.summary}`);
74
+ const line = `Probed ${url}: ${probes.status === "pass" ? "all probes passed" : probes.summary}`;
75
+ if (probes.status === "pass")
76
+ log.ok(line);
77
+ else
78
+ log.warn(line);
79
+ for (const f of probes.findings)
80
+ log.info(` ${f.severity === "block" ? "✗" : "!"} ${f.rule_id}: ${f.risk}`);
81
+ }
59
82
  export async function deploy(opts) {
60
83
  const root = findProjectRoot();
61
84
  const nerd = readNerdJson(root);
62
85
  const api = ApiClient.fromConfig();
63
86
  const knownSecrets = knownSecretsFrom(readDevVars(root));
64
- if (!opts["skip-check"]) {
65
- const remote = await runChecks(root, { remote: true, typecheck: false });
66
- if (!remote.ok) {
67
- for (const p of remote.problems)
68
- log.warn(p);
69
- throw new CliError("Fix the problems above, then deploy again.", 2);
70
- }
71
- const { approvedSuppressions } = await import("./verify.js");
72
- const gates = await runGates(SOURCE_GATES, { root, knownSecrets, approvedSuppressions: await approvedSuppressions(root), log: (l) => log.info(l) }, { stopOnFail: true });
73
- if (!gates.ok)
74
- throw new CliError(`Blocked by: ${gates.failed.map((g) => g.title).join(", ")}. Fix the findings above, then deploy again.`, 2);
75
- log.ok(gateSummary(gates.results));
87
+ // Every deploy runs every gate. There is no flag to skip them: suppressions need a person's approval in the dashboard.
88
+ const remote = await runChecks(root, { remote: true, typecheck: false });
89
+ if (!remote.ok) {
90
+ for (const p of remote.problems)
91
+ log.warn(p);
92
+ throw new CliError("Fix the problems above, then deploy again.", 2);
76
93
  }
94
+ const { approvedSuppressions } = await import("./verify.js");
95
+ const gates = await runGates(SOURCE_GATES, { root, knownSecrets, approvedSuppressions: await approvedSuppressions(root), log: (l) => log.info(l) }, { stopOnFail: true });
96
+ if (!gates.ok)
97
+ throw new CliError(`Blocked by: ${gates.failed.map((g) => g.title).join(", ")}. Fix the findings above, then deploy again.`, 2);
98
+ log.ok(gateSummary(gates.results));
77
99
  if (!opts["skip-build"]) {
78
100
  log.step("Building with vite");
79
101
  ensureDeps(root);
80
102
  await runOrThrow("npx", ["vite", "build"], { cwd: root });
81
103
  }
82
104
  const build = readBuildOutput(root);
83
- if (!opts["skip-check"]) {
84
- const built = await runGates(BUILD_GATES, { root, build, knownSecrets, log: (l) => log.info(l) }, { stopOnFail: true });
85
- if (!built.ok)
86
- throw new CliError(`Blocked by: ${built.failed.map((g) => g.title).join(", ")}.`, 2);
87
- }
105
+ const built = await runGates(BUILD_GATES, { root, build, knownSecrets, log: (l) => log.info(l) }, { stopOnFail: true });
106
+ if (!built.ok)
107
+ throw new CliError(`Blocked by: ${built.failed.map((g) => g.title).join(", ")}.`, 2);
88
108
  log.step("Applying migrations to the production database");
89
109
  const mig = await applyRemoteMigrations(root, api);
90
110
  if (mig.applied.length)
@@ -107,9 +127,11 @@ export async function deploy(opts) {
107
127
  }
108
128
  const { deployment } = await api.post(projectPath(nerd, "/deployments"), form);
109
129
  const { project } = await api.get(projectPath(nerd));
130
+ const probes = await probeAfterDeploy(deployment, project.url, Boolean(opts["skip-probe"]));
110
131
  if (opts.json)
111
- return printJson({ deployment, url: project.url, migrations: mig });
132
+ return printJson({ deployment, url: project.url, migrations: mig, probes });
112
133
  log.ok(`Deployed ${deployment.id} (${deployment.status}) → ${project.url}`);
134
+ printProbe(project.url, probes);
113
135
  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`);
114
136
  }
115
137
  export async function deployments(opts) {
@@ -12,6 +12,10 @@ export async function doctor(opts) {
12
12
  const r = await exec(tool, ["--version"], process.cwd());
13
13
  checks.push({ name: tool, ok: r.code === 0, detail: r.code === 0 ? r.stdout.trim().split("\n")[0] ?? "" : "not found", fix });
14
14
  }
15
+ if (process.platform === "win32" && !process.env.MSYSTEM && !process.env.WSL_DISTRO_NAME) {
16
+ // npm installs nerd and pnpm as .ps1 scripts too, and PowerShell's default policy refuses those; Git Bash (MSYSTEM) and WSL are not affected.
17
+ checks.push({ name: "Windows shell (optional)", ok: true, detail: "if PowerShell says running scripts is disabled, run `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` once, or use Git Bash or WSL" });
18
+ }
15
19
  const gh = await exec("gh", ["auth", "status"], process.cwd());
16
20
  checks.push({ name: "GitHub CLI signed in (optional)", ok: gh.code === 0, detail: gh.code === 0 ? "ok" : "not signed in", fix: "Only needed to create repos from the terminal: https://cli.github.com then `gh auth login`." });
17
21
  const cfg = loadConfig();
@@ -174,4 +174,4 @@ export async function init(dir, args) {
174
174
  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
175
  }
176
176
  // Kept in sync with packages/sdk/package.json by the release script.
177
- const SDK_VERSION = "0.0.8";
177
+ const SDK_VERSION = "0.0.9";
@@ -11,7 +11,6 @@ export declare const rulesGate: Gate;
11
11
  export declare const typecheckGate: Gate;
12
12
  /** The catalog rule for an advisory severity. Critical and high block, as the homepage promises; moderate warns; low and unrated inform. */
13
13
  export declare function depsRuleFor(severity: AdvisorySeverity): string;
14
- /** Known vulnerabilities in dependencies. Critical and high block; moderate warns. Package names come from the registry, not the repo. */
15
14
  export declare const depsGate: Gate;
16
15
  export declare const policyGate: Gate;
17
16
  /** Heuristics for careless handling of personal data. Advisory: warns, never blocks. */
@@ -72,6 +72,22 @@ export function depsRuleFor(severity) {
72
72
  return severity === "critical" ? "deps/critical-vulnerability" : severity === "high" ? "deps/high-vulnerability" : severity === "moderate" ? "deps/moderate-vulnerability" : severity === "low" ? "deps/low-vulnerability" : "deps/advisory";
73
73
  }
74
74
  /** Known vulnerabilities in dependencies. Critical and high block; moderate warns. Package names come from the registry, not the repo. */
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.
78
+ */
79
+ function findLockfile(start) {
80
+ let dir = start;
81
+ for (;;) {
82
+ const hit = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock"].find((f) => existsSync(path.join(dir, f)));
83
+ if (hit)
84
+ return path.join(dir, hit);
85
+ const parent = path.dirname(dir);
86
+ if (parent === dir || existsSync(path.join(dir, ".git")))
87
+ return undefined;
88
+ dir = parent;
89
+ }
90
+ }
75
91
  export const depsGate = {
76
92
  name: "dependencies",
77
93
  title: "Dependency vulnerabilities",
@@ -80,11 +96,12 @@ export const depsGate = {
80
96
  async run(ctx) {
81
97
  const t = Date.now();
82
98
  const findings = [];
83
- const lock = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock"].find((f) => existsSync(path.join(ctx.root, f)));
99
+ const lockPath = findLockfile(ctx.root);
100
+ const lock = lockPath ? path.basename(lockPath) : undefined;
84
101
  if (!lock)
85
102
  findings.push(finding("deps/lockfile-required", {}));
86
103
  if (await hasTool("osv-scanner")) {
87
- const r = await exec("osv-scanner", ["scan", "source", "--format", "json", "--lockfile", lock ?? "pnpm-lock.yaml"], ctx.root);
104
+ const r = await exec("osv-scanner", ["scan", "source", "--format", "json", "--lockfile", lockPath ?? path.join(ctx.root, "pnpm-lock.yaml")], ctx.root);
88
105
  try {
89
106
  const json = JSON.parse(r.stdout);
90
107
  for (const res of json.results ?? [])
@@ -4,6 +4,7 @@ export { gateSummary, skippedGates } from "./types.js";
4
4
  export { RULES, ruleFor, type RuleDef } from "./rules.js";
5
5
  export { mask } from "./secrets.js";
6
6
  export { MAX_ATTEMPTS, readAttempts } from "./attempts.js";
7
+ export { probeDeployment, skippedProbe, waitForDeployment, type FetchLike } from "./smoke.js";
7
8
  /** Gates that run on source, before the build. Suppressions first so a human sees them before anything else. */
8
9
  export declare const SOURCE_GATES: Gate[];
9
10
  /** Gates that need the built output. */
@@ -7,6 +7,7 @@ export { gateSummary, skippedGates } from "./types.js";
7
7
  export { RULES, ruleFor } from "./rules.js";
8
8
  export { mask } from "./secrets.js";
9
9
  export { MAX_ATTEMPTS, readAttempts } from "./attempts.js";
10
+ export { probeDeployment, skippedProbe, waitForDeployment } from "./smoke.js";
10
11
  /** Gates that run on source, before the build. Suppressions first so a human sees them before anything else. */
11
12
  export const SOURCE_GATES = [suppressionsGate, secretsGate, gitleaksGate, depsGate, rulesGate, semgrepGate, policyGate, piiGate, typecheckGate];
12
13
  /** Gates that need the built output. */
@@ -34,6 +34,7 @@ export const RULES = {
34
34
  "deps/audit-unavailable": { title: "Dependency audit could not run", severity: "warn", audience: "human", risk: "Vulnerable packages would not be detected this run.", fix: "Check network access and the package manager, then run `nerd verify` again." },
35
35
  "rules/platform": { title: "Platform rule", severity: "block", audience: "agent", risk: "The app would not run correctly on the platform.", fix: "Follow the instruction in the finding; the rules are listed in the nerdsystem skill." },
36
36
  "rules/advisory": { title: "Platform advisory", severity: "warn", audience: "agent", risk: "Not blocking, but the project is missing something the platform expects.", fix: "Follow the instruction in the finding." },
37
+ "rules/shape": { title: "Not in the nerdsystem shape", severity: "warn", audience: "human", risk: "Every app on nerdsystem is one shape: a Vite client, a Hono server on @nerdsystem/sdk, SQLite on D1, and a bucket. This repository is not in that shape. A clean scan says nothing about whether it would deploy here.", fix: "Start a project with `nerd init` and have the agent port the screens into it. The server and the login are rebuilt on the platform's sign-in and database. The compare pages on nerdsystem.com say what carries over." },
37
38
  "typecheck/error": { title: "TypeScript error", severity: "block", audience: "agent", risk: "The code would not compile reliably; the build may fail or behave unexpectedly.", fix: "Fix the type error at the file and line shown. Run `npx tsc -b` for the full message." },
38
39
  "typecheck/failed": { title: "Typecheck failed", severity: "block", audience: "agent", risk: "The project does not compile.", fix: "Run `npx tsc -b` and fix the errors it prints." },
39
40
  "policy/blocked-package": { title: "Package not allowed", severity: "block", audience: "agent", risk: "The package is a miner, proxy, or headless browser, which the acceptable use policy forbids and the platform cannot run.", fix: "Remove the package and the code that uses it. If the app genuinely needs a browser or scraping, say so to the person; it is not supported here." },
@@ -0,0 +1,18 @@
1
+ import { type GateResult } from "./types.js";
2
+ export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
3
+ /**
4
+ * Waits until GET / on the deployed app answers (a response below 500 that is not the dispatcher's
5
+ * 404 for a script it does not know yet). Returns true as soon as it does; false after the last attempt.
6
+ */
7
+ export declare function waitForDeployment(url: string, opts?: {
8
+ attempts?: number;
9
+ delayMs?: number;
10
+ fetchImpl?: FetchLike;
11
+ }): Promise<boolean>;
12
+ /**
13
+ * Runs the live-URL probes: security headers on GET /, GET /api/auth/me answers 200 with
14
+ * {"user":null} when signed out, and POST /api/auth/login with wrong credentials answers 401.
15
+ */
16
+ export declare function probeDeployment(url: string, fetchImpl?: FetchLike): Promise<GateResult>;
17
+ /** The result recorded when the probe did not run, so --json consumers see why rather than a missing key. */
18
+ export declare function skippedProbe(reason: string): GateResult;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Post-deploy probes against the live URL. The same checks the builder runs after a git build
3
+ * (apps/builder/image/build.mjs smokeGate), so `nerd deploy` reports the same findings.
4
+ * Deterministic and non-disruptive: three GETs and one login with made-up credentials.
5
+ * Findings are informational for the deploy command; the app is already live when they run.
6
+ */
7
+ import { finding, fingerprint } from "./secrets.js";
8
+ import { verdict } from "./types.js";
9
+ const PROBE_TIMEOUT_MS = 10_000;
10
+ const REQUIRED_HEADERS = ["x-content-type-options", "content-security-policy", "x-frame-options"];
11
+ /** Default fetch wrapped so it is never called with a foreign `this` (workerd throws "Illegal invocation"). */
12
+ const defaultFetch = (input, init) => fetch(input, init);
13
+ function withTimeout(init) {
14
+ const signal = typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(PROBE_TIMEOUT_MS) : undefined;
15
+ return { redirect: "manual", ...(signal ? { signal } : {}), ...init };
16
+ }
17
+ function smokeFinding(ruleId, risk, key) {
18
+ return finding(ruleId, { risk, fingerprint: fingerprint(ruleId, key) });
19
+ }
20
+ function probeResult(findings, startedAt) {
21
+ const status = verdict(findings);
22
+ const blocks = findings.filter((f) => f.severity === "block").length;
23
+ const summary = blocks ? `${blocks} probe${blocks === 1 ? "" : "s"} failed` : findings.length ? `${findings.length} warning${findings.length === 1 ? "" : "s"}` : "All probes passed";
24
+ return { name: "smoke", title: "Deployed app probes", status, summary, findings, durationMs: Date.now() - startedAt, tool: "builtin" };
25
+ }
26
+ /**
27
+ * Waits until GET / on the deployed app answers (a response below 500 that is not the dispatcher's
28
+ * 404 for a script it does not know yet). Returns true as soon as it does; false after the last attempt.
29
+ */
30
+ export async function waitForDeployment(url, opts = {}) {
31
+ const attempts = opts.attempts ?? 3;
32
+ const delayMs = opts.delayMs ?? 2000;
33
+ const fetchImpl = opts.fetchImpl ?? defaultFetch;
34
+ for (let i = 0; i < attempts; i++) {
35
+ try {
36
+ const r = await fetchImpl(`${url}/`, withTimeout(undefined));
37
+ await r.body?.cancel().catch(() => { });
38
+ if (r.status < 500 && r.status !== 404)
39
+ return true;
40
+ }
41
+ catch {
42
+ // not reachable yet
43
+ }
44
+ if (i < attempts - 1)
45
+ await new Promise((r) => setTimeout(r, delayMs));
46
+ }
47
+ return false;
48
+ }
49
+ /**
50
+ * Runs the live-URL probes: security headers on GET /, GET /api/auth/me answers 200 with
51
+ * {"user":null} when signed out, and POST /api/auth/login with wrong credentials answers 401.
52
+ */
53
+ export async function probeDeployment(url, fetchImpl = defaultFetch) {
54
+ const startedAt = Date.now();
55
+ const base = url.replace(/\/$/, "");
56
+ const findings = [];
57
+ const get = async (p, init) => {
58
+ try {
59
+ return await fetchImpl(`${base}${p}`, withTimeout(init));
60
+ }
61
+ catch (err) {
62
+ findings.push(smokeFinding("smoke/unreachable", `${p} could not be fetched: ${err instanceof Error ? err.message : String(err)}`, p));
63
+ return null;
64
+ }
65
+ };
66
+ const home = await get("/");
67
+ if (home) {
68
+ await home.body?.cancel().catch(() => { });
69
+ if (home.status >= 500)
70
+ findings.push(smokeFinding("smoke/unreachable", `GET / returned ${home.status}.`, "/:status"));
71
+ for (const h of REQUIRED_HEADERS) {
72
+ if (!home.headers.get(h))
73
+ findings.push(smokeFinding("smoke/header-missing", `The ${h} header is missing, so browsers lose a protection against clickjacking, content sniffing, or script injection.`, h));
74
+ }
75
+ if (base.startsWith("https://") && !home.headers.get("strict-transport-security"))
76
+ findings.push(smokeFinding("smoke/header-missing", "The strict-transport-security header is missing.", "strict-transport-security"));
77
+ }
78
+ const me = await get("/api/auth/me");
79
+ if (me) {
80
+ const body = await me.text().catch(() => "");
81
+ if (me.status !== 200 || !body.includes('"user":null'))
82
+ findings.push(smokeFinding("smoke/auth-me", `GET /api/auth/me without a session returned ${me.status} instead of 200 with {"user":null}.`, "/api/auth/me"));
83
+ }
84
+ const login = await get("/api/auth/login", {
85
+ method: "POST",
86
+ headers: { "content-type": "application/json" },
87
+ body: JSON.stringify({ email: "probe@nerdsystem.invalid", password: "definitely-wrong-password" }),
88
+ });
89
+ if (login) {
90
+ await login.body?.cancel().catch(() => { });
91
+ if (login.status !== 401)
92
+ findings.push(smokeFinding("smoke/auth-login", `Wrong credentials returned ${login.status} instead of 401.`, "/api/auth/login"));
93
+ }
94
+ return probeResult(findings, startedAt);
95
+ }
96
+ /** The result recorded when the probe did not run, so --json consumers see why rather than a missing key. */
97
+ export function skippedProbe(reason) {
98
+ return { name: "smoke", title: "Deployed app probes", status: "skip", summary: reason, findings: [], durationMs: 0, tool: "builtin" };
99
+ }
package/dist/project.js CHANGED
@@ -8,7 +8,7 @@ export function findProjectRoot(start = process.cwd()) {
8
8
  return dir;
9
9
  const parent = path.dirname(dir);
10
10
  if (parent === dir)
11
- throw new CliError("No nerd.json found. Run this inside a nerdsystem app, or create one with `nerd init <dir>`.");
11
+ throw new CliError("No nerd.json found. Run this inside an app created by `nerd init`, link a folder already in the nerdsystem shape with `nerd link --slug <project>`, or create one with `nerd init <dir>`.");
12
12
  dir = parent;
13
13
  }
14
14
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nerdsystem",
3
- "version": "0.0.8",
4
- "description": "nerd: deploy apps to nerdsystem from the terminal or a coding agent.",
3
+ "version": "0.0.9",
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": {
7
7
  "nerd": "./dist/index.js"
@@ -44,6 +44,8 @@
44
44
  "deploy",
45
45
  "cli",
46
46
  "claude-code",
47
+ "cursor",
48
+ "codex",
47
49
  "vibe-coding"
48
50
  ],
49
51
  "publishConfig": {
@@ -20,6 +20,17 @@ If this project still has the template's todo app (the `todos` table and `src/se
20
20
 
21
21
  Throughout, show commands rather than describing them, and never ask the person to edit config files; the platform owns `wrangler.jsonc` and `nerd.json`.
22
22
 
23
+ ## Porting an app built somewhere else into this project
24
+
25
+ When the person built the app on Lovable, Bolt, Replit, Vercel, or another host and wants it here, the screens carry over and the server is rebuilt on the platform's sign-in and database. Say that in one line before starting, then work in this order and run `nerd check` after each step:
26
+
27
+ 1. Keep `src/client/`. Port the screens into the template's client and route them with `src/client/router.tsx` or a router of your choice. Replace the old sign-in, sign-up, and password UI with the forms from `src/client/auth.tsx`; the old auth client code (Supabase, Firebase, Clerk, hand-rolled tokens) is deleted, not adapted.
28
+ 2. Rebuild the server as routers under `/api/...` on `createNerdApp()`. Each old endpoint, serverless function, or edge function becomes a Hono route in `src/server/routes/<name>.ts`. The login is a service the agent does not write.
29
+ 3. Define the schema in `src/server/db/schema.ts` (Drizzle, SQLite dialect) and run `pnpm db:generate`. Every table that holds user data carries `user_id`, and every query filters by `c.var.user.id`.
30
+ 4. Any key the old app held goes into `.dev.vars` locally and `nerd secret set NAME` for production, never into the code.
31
+
32
+ Tell the person plainly what does not carry. Their users sign up again on the new login; there is no account import. Postgres-only features (row-level security policies, extensions), Next.js server features (server components, API routes, middleware), Lovable Cloud functions, Supabase edge functions, and any long-running process are rewritten or dropped. Say which ones you hit as you go rather than at the end.
33
+
23
34
  ## The loop
24
35
 
25
36
  1. Make the change.
@@ -116,7 +127,7 @@ Without MCP, `nerd check --json` returns the same shape, and `nerd check --rule
116
127
 
117
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.
118
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.
119
- - **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, 2,000 on Pro at $39 a month, 10,000 on Studio), 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. 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.
120
131
 
121
132
  ## Debugging production
122
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 (088a9335c558); edit the skill, then run: nerd agents sync -->
5
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (f8685cd08ad1); edit the skill, then run: nerd agents sync -->
6
6
 
7
7
  # Building on nerdsystem
8
8
 
@@ -21,6 +21,17 @@ If this project still has the template's todo app (the `todos` table and `src/se
21
21
 
22
22
  Throughout, show commands rather than describing them, and never ask the person to edit config files; the platform owns `wrangler.jsonc` and `nerd.json`.
23
23
 
24
+ ## Porting an app built somewhere else into this project
25
+
26
+ When the person built the app on Lovable, Bolt, Replit, Vercel, or another host and wants it here, the screens carry over and the server is rebuilt on the platform's sign-in and database. Say that in one line before starting, then work in this order and run `nerd check` after each step:
27
+
28
+ 1. Keep `src/client/`. Port the screens into the template's client and route them with `src/client/router.tsx` or a router of your choice. Replace the old sign-in, sign-up, and password UI with the forms from `src/client/auth.tsx`; the old auth client code (Supabase, Firebase, Clerk, hand-rolled tokens) is deleted, not adapted.
29
+ 2. Rebuild the server as routers under `/api/...` on `createNerdApp()`. Each old endpoint, serverless function, or edge function becomes a Hono route in `src/server/routes/<name>.ts`. The login is a service the agent does not write.
30
+ 3. Define the schema in `src/server/db/schema.ts` (Drizzle, SQLite dialect) and run `pnpm db:generate`. Every table that holds user data carries `user_id`, and every query filters by `c.var.user.id`.
31
+ 4. Any key the old app held goes into `.dev.vars` locally and `nerd secret set NAME` for production, never into the code.
32
+
33
+ Tell the person plainly what does not carry. Their users sign up again on the new login; there is no account import. Postgres-only features (row-level security policies, extensions), Next.js server features (server components, API routes, middleware), Lovable Cloud functions, Supabase edge functions, and any long-running process are rewritten or dropped. Say which ones you hit as you go rather than at the end.
34
+
24
35
  ## The loop
25
36
 
26
37
  1. Make the change.
@@ -117,7 +128,7 @@ Without MCP, `nerd check --json` returns the same shape, and `nerd check --rule
117
128
 
118
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.
119
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.
120
- - **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, 2,000 on Pro at $39 a month, 10,000 on Studio), 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. 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.
121
132
 
122
133
  ## Debugging production
123
134
 
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (088a9335c558); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (f8685cd08ad1); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -17,6 +17,17 @@ If this project still has the template's todo app (the `todos` table and `src/se
17
17
 
18
18
  Throughout, show commands rather than describing them, and never ask the person to edit config files; the platform owns `wrangler.jsonc` and `nerd.json`.
19
19
 
20
+ ## Porting an app built somewhere else into this project
21
+
22
+ When the person built the app on Lovable, Bolt, Replit, Vercel, or another host and wants it here, the screens carry over and the server is rebuilt on the platform's sign-in and database. Say that in one line before starting, then work in this order and run `nerd check` after each step:
23
+
24
+ 1. Keep `src/client/`. Port the screens into the template's client and route them with `src/client/router.tsx` or a router of your choice. Replace the old sign-in, sign-up, and password UI with the forms from `src/client/auth.tsx`; the old auth client code (Supabase, Firebase, Clerk, hand-rolled tokens) is deleted, not adapted.
25
+ 2. Rebuild the server as routers under `/api/...` on `createNerdApp()`. Each old endpoint, serverless function, or edge function becomes a Hono route in `src/server/routes/<name>.ts`. The login is a service the agent does not write.
26
+ 3. Define the schema in `src/server/db/schema.ts` (Drizzle, SQLite dialect) and run `pnpm db:generate`. Every table that holds user data carries `user_id`, and every query filters by `c.var.user.id`.
27
+ 4. Any key the old app held goes into `.dev.vars` locally and `nerd secret set NAME` for production, never into the code.
28
+
29
+ Tell the person plainly what does not carry. Their users sign up again on the new login; there is no account import. Postgres-only features (row-level security policies, extensions), Next.js server features (server components, API routes, middleware), Lovable Cloud functions, Supabase edge functions, and any long-running process are rewritten or dropped. Say which ones you hit as you go rather than at the end.
30
+
20
31
  ## The loop
21
32
 
22
33
  1. Make the change.
@@ -113,7 +124,7 @@ Without MCP, `nerd check --json` returns the same shape, and `nerd check --rule
113
124
 
114
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.
115
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.
116
- - **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, 2,000 on Pro at $39 a month, 10,000 on Studio), 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. 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.
117
128
 
118
129
  ## Debugging production
119
130
 
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (088a9335c558); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (f8685cd08ad1); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -17,6 +17,17 @@ If this project still has the template's todo app (the `todos` table and `src/se
17
17
 
18
18
  Throughout, show commands rather than describing them, and never ask the person to edit config files; the platform owns `wrangler.jsonc` and `nerd.json`.
19
19
 
20
+ ## Porting an app built somewhere else into this project
21
+
22
+ When the person built the app on Lovable, Bolt, Replit, Vercel, or another host and wants it here, the screens carry over and the server is rebuilt on the platform's sign-in and database. Say that in one line before starting, then work in this order and run `nerd check` after each step:
23
+
24
+ 1. Keep `src/client/`. Port the screens into the template's client and route them with `src/client/router.tsx` or a router of your choice. Replace the old sign-in, sign-up, and password UI with the forms from `src/client/auth.tsx`; the old auth client code (Supabase, Firebase, Clerk, hand-rolled tokens) is deleted, not adapted.
25
+ 2. Rebuild the server as routers under `/api/...` on `createNerdApp()`. Each old endpoint, serverless function, or edge function becomes a Hono route in `src/server/routes/<name>.ts`. The login is a service the agent does not write.
26
+ 3. Define the schema in `src/server/db/schema.ts` (Drizzle, SQLite dialect) and run `pnpm db:generate`. Every table that holds user data carries `user_id`, and every query filters by `c.var.user.id`.
27
+ 4. Any key the old app held goes into `.dev.vars` locally and `nerd secret set NAME` for production, never into the code.
28
+
29
+ Tell the person plainly what does not carry. Their users sign up again on the new login; there is no account import. Postgres-only features (row-level security policies, extensions), Next.js server features (server components, API routes, middleware), Lovable Cloud functions, Supabase edge functions, and any long-running process are rewritten or dropped. Say which ones you hit as you go rather than at the end.
30
+
20
31
  ## The loop
21
32
 
22
33
  1. Make the change.
@@ -113,7 +124,7 @@ Without MCP, `nerd check --json` returns the same shape, and `nerd check --rule
113
124
 
114
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.
115
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.
116
- - **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, 2,000 on Pro at $39 a month, 10,000 on Studio), 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. 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.
117
128
 
118
129
  ## Debugging production
119
130
 
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (088a9335c558); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (f8685cd08ad1); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -17,6 +17,17 @@ If this project still has the template's todo app (the `todos` table and `src/se
17
17
 
18
18
  Throughout, show commands rather than describing them, and never ask the person to edit config files; the platform owns `wrangler.jsonc` and `nerd.json`.
19
19
 
20
+ ## Porting an app built somewhere else into this project
21
+
22
+ When the person built the app on Lovable, Bolt, Replit, Vercel, or another host and wants it here, the screens carry over and the server is rebuilt on the platform's sign-in and database. Say that in one line before starting, then work in this order and run `nerd check` after each step:
23
+
24
+ 1. Keep `src/client/`. Port the screens into the template's client and route them with `src/client/router.tsx` or a router of your choice. Replace the old sign-in, sign-up, and password UI with the forms from `src/client/auth.tsx`; the old auth client code (Supabase, Firebase, Clerk, hand-rolled tokens) is deleted, not adapted.
25
+ 2. Rebuild the server as routers under `/api/...` on `createNerdApp()`. Each old endpoint, serverless function, or edge function becomes a Hono route in `src/server/routes/<name>.ts`. The login is a service the agent does not write.
26
+ 3. Define the schema in `src/server/db/schema.ts` (Drizzle, SQLite dialect) and run `pnpm db:generate`. Every table that holds user data carries `user_id`, and every query filters by `c.var.user.id`.
27
+ 4. Any key the old app held goes into `.dev.vars` locally and `nerd secret set NAME` for production, never into the code.
28
+
29
+ Tell the person plainly what does not carry. Their users sign up again on the new login; there is no account import. Postgres-only features (row-level security policies, extensions), Next.js server features (server components, API routes, middleware), Lovable Cloud functions, Supabase edge functions, and any long-running process are rewritten or dropped. Say which ones you hit as you go rather than at the end.
30
+
20
31
  ## The loop
21
32
 
22
33
  1. Make the change.
@@ -113,7 +124,7 @@ Without MCP, `nerd check --json` returns the same shape, and `nerd check --rule
113
124
 
114
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.
115
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.
116
- - **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, 2,000 on Pro at $39 a month, 10,000 on Studio), 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. 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.
117
128
 
118
129
  ## Debugging production
119
130