nerdsystem 0.0.8 → 0.0.10
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 +1 -1
- package/dist/cli.js +12 -3
- package/dist/commands/check.js +4 -2
- package/dist/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.js +48 -21
- package/dist/commands/doctor.js +4 -0
- package/dist/commands/init.js +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/commands/verify.d.ts +2 -0
- package/dist/commands/verify.js +11 -0
- package/dist/gates/code.d.ts +0 -1
- package/dist/gates/code.js +22 -2
- package/dist/gates/index.d.ts +1 -0
- package/dist/gates/index.js +1 -0
- package/dist/gates/rules.js +1 -0
- package/dist/gates/smoke.d.ts +18 -0
- package/dist/gates/smoke.js +101 -0
- package/dist/project.js +1 -1
- package/dist/util.d.ts +8 -8
- package/dist/util.js +3 -2
- package/package.json +4 -2
- package/template/.claude/skills/nerdsystem/SKILL.md +14 -3
- package/template/.cursor/rules/nerdsystem.mdc +15 -4
- package/template/.github/copilot-instructions.md +15 -4
- package/template/AGENTS.md +15 -4
- package/template/GEMINI.md +15 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# nerdsystem
|
|
2
2
|
|
|
3
|
-
`nerd`:
|
|
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
|
|
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-
|
|
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;
|
package/dist/commands/check.js
CHANGED
|
@@ -121,7 +121,7 @@ export async function runChecks(root, opts) {
|
|
|
121
121
|
}
|
|
122
122
|
if (opts.typecheck && problems.every((p) => p.endsWith("(warning)") || p.startsWith("Agent instruction files"))) {
|
|
123
123
|
log.step("Typechecking");
|
|
124
|
-
const code = await run("npx", ["tsc", "-b", "--pretty"], { cwd: root });
|
|
124
|
+
const code = await run("npx", ["tsc", "-b", "--pretty"], { cwd: root, stdoutToStderr: true });
|
|
125
125
|
if (code !== 0)
|
|
126
126
|
problems.push("Typecheck failed (see errors above).");
|
|
127
127
|
}
|
|
@@ -144,7 +144,9 @@ export async function check(opts) {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
// Loaded here, not at the top: the gates import staticChecks from this file.
|
|
147
|
-
const { runVerify } = await import("./verify.js");
|
|
147
|
+
const { runVerify, assertRuleHasGate } = await import("./verify.js");
|
|
148
|
+
if (opts.rule)
|
|
149
|
+
assertRuleHasGate(opts.rule);
|
|
148
150
|
const out = await runVerify(root, { withBuild: !opts["no-build"], stopOnFail: false, quiet: opts.json, onlyRules: opts.rule ? [opts.rule] : undefined });
|
|
149
151
|
const failed = out.results.filter((r) => r.status === "fail").length;
|
|
150
152
|
if (opts.json)
|
|
@@ -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: {
|
package/dist/commands/deploy.js
CHANGED
|
@@ -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,58 @@ 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
|
+
// Until the dispatcher knows the new script it answers its own 404 page, which has none of the app's
|
|
69
|
+
// headers or routes; probing that would report false auth failures. Wait, and say so when it never came.
|
|
70
|
+
if (!(await waitForDeployment(url)))
|
|
71
|
+
return skippedProbe("the app had not started answering within 30 seconds; open the URL to check, or run nerd deploy again to probe it");
|
|
72
|
+
return probeDeployment(url);
|
|
73
|
+
}
|
|
74
|
+
function printProbe(url, probes) {
|
|
75
|
+
if (probes.status === "skip")
|
|
76
|
+
return log.info(`Live URL not probed: ${probes.summary}`);
|
|
77
|
+
const line = `Probed ${url}: ${probes.status === "pass" ? "all probes passed" : probes.summary}`;
|
|
78
|
+
if (probes.status === "pass")
|
|
79
|
+
log.ok(line);
|
|
80
|
+
else
|
|
81
|
+
log.warn(line);
|
|
82
|
+
for (const f of probes.findings)
|
|
83
|
+
log.info(` ${f.severity === "block" ? "✗" : "!"} ${f.rule_id}: ${f.risk}`);
|
|
84
|
+
}
|
|
59
85
|
export async function deploy(opts) {
|
|
60
86
|
const root = findProjectRoot();
|
|
61
87
|
const nerd = readNerdJson(root);
|
|
62
88
|
const api = ApiClient.fromConfig();
|
|
63
89
|
const knownSecrets = knownSecretsFrom(readDevVars(root));
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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));
|
|
90
|
+
// Every deploy runs every gate. There is no flag to skip them: suppressions need a person's approval in the dashboard.
|
|
91
|
+
const remote = await runChecks(root, { remote: true, typecheck: false });
|
|
92
|
+
if (!remote.ok) {
|
|
93
|
+
for (const p of remote.problems)
|
|
94
|
+
log.warn(p);
|
|
95
|
+
throw new CliError("Fix the problems above, then deploy again.", 2);
|
|
76
96
|
}
|
|
97
|
+
const { approvedSuppressions } = await import("./verify.js");
|
|
98
|
+
const gates = await runGates(SOURCE_GATES, { root, knownSecrets, approvedSuppressions: await approvedSuppressions(root), log: (l) => log.info(l) }, { stopOnFail: true });
|
|
99
|
+
if (!gates.ok)
|
|
100
|
+
throw new CliError(`Blocked by: ${gates.failed.map((g) => g.title).join(", ")}. Fix the findings above, then deploy again.`, 2);
|
|
101
|
+
log.ok(gateSummary(gates.results));
|
|
77
102
|
if (!opts["skip-build"]) {
|
|
78
103
|
log.step("Building with vite");
|
|
79
104
|
ensureDeps(root);
|
|
80
|
-
await runOrThrow("npx", ["vite", "build"], { cwd: root });
|
|
105
|
+
await runOrThrow("npx", ["vite", "build"], { cwd: root, stdoutToStderr: true });
|
|
81
106
|
}
|
|
82
107
|
const build = readBuildOutput(root);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
throw new CliError(`Blocked by: ${built.failed.map((g) => g.title).join(", ")}.`, 2);
|
|
87
|
-
}
|
|
108
|
+
const built = await runGates(BUILD_GATES, { root, build, knownSecrets, log: (l) => log.info(l) }, { stopOnFail: true });
|
|
109
|
+
if (!built.ok)
|
|
110
|
+
throw new CliError(`Blocked by: ${built.failed.map((g) => g.title).join(", ")}.`, 2);
|
|
88
111
|
log.step("Applying migrations to the production database");
|
|
89
112
|
const mig = await applyRemoteMigrations(root, api);
|
|
90
113
|
if (mig.applied.length)
|
|
@@ -107,9 +130,13 @@ export async function deploy(opts) {
|
|
|
107
130
|
}
|
|
108
131
|
const { deployment } = await api.post(projectPath(nerd, "/deployments"), form);
|
|
109
132
|
const { project } = await api.get(projectPath(nerd));
|
|
110
|
-
if (opts.json)
|
|
111
|
-
return printJson({ deployment, url: project.url, migrations: mig });
|
|
112
133
|
log.ok(`Deployed ${deployment.id} (${deployment.status}) → ${project.url}`);
|
|
134
|
+
if (!opts["skip-probe"] && deployment.status === "live")
|
|
135
|
+
log.step(`Probing ${project.url}`);
|
|
136
|
+
const probes = await probeAfterDeploy(deployment, project.url, Boolean(opts["skip-probe"]));
|
|
137
|
+
if (opts.json)
|
|
138
|
+
return printJson({ deployment, url: project.url, migrations: mig, probes });
|
|
139
|
+
printProbe(project.url, probes);
|
|
113
140
|
log.info(`First time? Sign up on your app at ${project.url}, then make yourself admin in the dashboard's Auth tab: ${dashboardUrl(api.apiUrl)}/app/${nerd.org}/projects/${nerd.project}/auth`);
|
|
114
141
|
}
|
|
115
142
|
export async function deployments(opts) {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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();
|
package/dist/commands/init.js
CHANGED
|
@@ -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.
|
|
177
|
+
const SDK_VERSION = "0.0.10";
|
package/dist/commands/login.js
CHANGED
|
@@ -28,7 +28,7 @@ export async function login(args) {
|
|
|
28
28
|
else {
|
|
29
29
|
log.info(`Open this link in your browser and approve code ${start.userCode}:\n\n ${start.verificationUrl}\n`);
|
|
30
30
|
if (args.email)
|
|
31
|
-
log.info(`An email went to ${args.email}:
|
|
31
|
+
log.info(`An email went to ${args.email}: the link signs you in (creating your account if you are new) and opens a page where you press Approve.`);
|
|
32
32
|
log.info("Waiting for approval…");
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -17,6 +17,8 @@ export declare function runVerify(root: string, opts: {
|
|
|
17
17
|
onlyRules?: string[] | undefined;
|
|
18
18
|
trackAttempts?: boolean;
|
|
19
19
|
}): Promise<VerifyOutput>;
|
|
20
|
+
/** `--rule smoke/x` would select no gate and report "All 0 gates passed"; say which prefixes a gate owns instead. */
|
|
21
|
+
export declare function assertRuleHasGate(rule: string): void;
|
|
20
22
|
export declare function verify(opts: {
|
|
21
23
|
json: boolean;
|
|
22
24
|
"no-build": boolean;
|
package/dist/commands/verify.js
CHANGED
|
@@ -35,8 +35,19 @@ export async function runVerify(root, opts) {
|
|
|
35
35
|
}
|
|
36
36
|
return { ok: results.every((r) => r.status !== "fail"), failures, stop, results };
|
|
37
37
|
}
|
|
38
|
+
/** `--rule smoke/x` would select no gate and report "All 0 gates passed"; say which prefixes a gate owns instead. */
|
|
39
|
+
export function assertRuleHasGate(rule) {
|
|
40
|
+
const prefixes = [...new Set([...SOURCE_GATES, ...BUILD_GATES].flatMap((g) => g.rulePrefixes))];
|
|
41
|
+
if (prefixes.some((p) => rule.startsWith(p)))
|
|
42
|
+
return;
|
|
43
|
+
if (rule.startsWith("smoke/"))
|
|
44
|
+
throw new CliError(`No gate runs ${rule} on the source: the live URL is probed by nerd deploy. Deploy again to re-check it.`, 2);
|
|
45
|
+
throw new CliError(`No gate runs ${rule}. Rule ids start with one of: ${prefixes.join(", ")}`, 2);
|
|
46
|
+
}
|
|
38
47
|
export async function verify(opts) {
|
|
39
48
|
const root = findProjectRoot();
|
|
49
|
+
if (opts.rule)
|
|
50
|
+
assertRuleHasGate(opts.rule);
|
|
40
51
|
const out = await runVerify(root, { withBuild: !opts["no-build"], stopOnFail: false, quiet: opts.json, onlyRules: opts.rule ? [opts.rule] : undefined });
|
|
41
52
|
if (opts.json)
|
|
42
53
|
process.stdout.write(`${JSON.stringify({ ok: out.ok, failures: out.failures, stop: out.stop, skipped: skippedGates(out.results) }, null, 2)}\n`);
|
package/dist/gates/code.d.ts
CHANGED
|
@@ -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. */
|
package/dist/gates/code.js
CHANGED
|
@@ -72,6 +72,24 @@ 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 that is itself a package or workspace root
|
|
77
|
+
* (it holds a package.json or pnpm-workspace.yaml), searched up to the repository root (the directory holding
|
|
78
|
+
* .git). A workspace member such as the template inside this monorepo shares the lockfile committed at the top;
|
|
79
|
+
* a stray lockfile in an unrelated ancestor such as the home folder is never adopted.
|
|
80
|
+
*/
|
|
81
|
+
function findLockfile(start) {
|
|
82
|
+
let dir = start;
|
|
83
|
+
for (;;) {
|
|
84
|
+
const hit = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock"].find((f) => existsSync(path.join(dir, f)));
|
|
85
|
+
if (hit && (dir === start || existsSync(path.join(dir, "package.json")) || existsSync(path.join(dir, "pnpm-workspace.yaml"))))
|
|
86
|
+
return path.join(dir, hit);
|
|
87
|
+
const parent = path.dirname(dir);
|
|
88
|
+
if (parent === dir || existsSync(path.join(dir, ".git")))
|
|
89
|
+
return undefined;
|
|
90
|
+
dir = parent;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
75
93
|
export const depsGate = {
|
|
76
94
|
name: "dependencies",
|
|
77
95
|
title: "Dependency vulnerabilities",
|
|
@@ -80,11 +98,13 @@ export const depsGate = {
|
|
|
80
98
|
async run(ctx) {
|
|
81
99
|
const t = Date.now();
|
|
82
100
|
const findings = [];
|
|
83
|
-
const
|
|
101
|
+
const lockPath = findLockfile(ctx.root);
|
|
102
|
+
const lock = lockPath ? path.basename(lockPath) : undefined;
|
|
84
103
|
if (!lock)
|
|
85
104
|
findings.push(finding("deps/lockfile-required", {}));
|
|
86
105
|
if (await hasTool("osv-scanner")) {
|
|
87
|
-
|
|
106
|
+
// Run from the lockfile's own folder with a bare name: on Windows the shell would split an absolute path with a space.
|
|
107
|
+
const r = await exec("osv-scanner", ["scan", "source", "--format", "json", "--lockfile", lock ?? "pnpm-lock.yaml"], lockPath ? path.dirname(lockPath) : ctx.root);
|
|
88
108
|
try {
|
|
89
109
|
const json = JSON.parse(r.stdout);
|
|
90
110
|
for (const res of json.results ?? [])
|
package/dist/gates/index.d.ts
CHANGED
|
@@ -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. */
|
package/dist/gates/index.js
CHANGED
|
@@ -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. */
|
package/dist/gates/rules.js
CHANGED
|
@@ -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,101 @@
|
|
|
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
|
+
/** No gate owns the smoke/ prefix, so `nerd verify --rule smoke/...` would run nothing; the way to re-check is another deploy. */
|
|
18
|
+
const VERIFY = "nerd deploy (the live URL is probed after every deploy)";
|
|
19
|
+
function smokeFinding(ruleId, risk, key) {
|
|
20
|
+
return { ...finding(ruleId, { risk, fingerprint: fingerprint(ruleId, key) }), verify: VERIFY };
|
|
21
|
+
}
|
|
22
|
+
function probeResult(findings, startedAt) {
|
|
23
|
+
const status = verdict(findings);
|
|
24
|
+
const blocks = findings.filter((f) => f.severity === "block").length;
|
|
25
|
+
const summary = blocks ? `${blocks} probe${blocks === 1 ? "" : "s"} failed` : findings.length ? `${findings.length} warning${findings.length === 1 ? "" : "s"}` : "All probes passed";
|
|
26
|
+
return { name: "smoke", title: "Deployed app probes", status, summary, findings, durationMs: Date.now() - startedAt, tool: "builtin" };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Waits until GET / on the deployed app answers (a response below 500 that is not the dispatcher's
|
|
30
|
+
* 404 for a script it does not know yet). Returns true as soon as it does; false after the last attempt.
|
|
31
|
+
*/
|
|
32
|
+
export async function waitForDeployment(url, opts = {}) {
|
|
33
|
+
const attempts = opts.attempts ?? 8;
|
|
34
|
+
const delayMs = opts.delayMs ?? 2000;
|
|
35
|
+
const fetchImpl = opts.fetchImpl ?? defaultFetch;
|
|
36
|
+
for (let i = 0; i < attempts; i++) {
|
|
37
|
+
try {
|
|
38
|
+
const r = await fetchImpl(`${url}/`, withTimeout(undefined));
|
|
39
|
+
await r.body?.cancel().catch(() => { });
|
|
40
|
+
if (r.status < 500 && r.status !== 404)
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// not reachable yet
|
|
45
|
+
}
|
|
46
|
+
if (i < attempts - 1)
|
|
47
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Runs the live-URL probes: security headers on GET /, GET /api/auth/me answers 200 with
|
|
53
|
+
* {"user":null} when signed out, and POST /api/auth/login with wrong credentials answers 401.
|
|
54
|
+
*/
|
|
55
|
+
export async function probeDeployment(url, fetchImpl = defaultFetch) {
|
|
56
|
+
const startedAt = Date.now();
|
|
57
|
+
const base = url.replace(/\/$/, "");
|
|
58
|
+
const findings = [];
|
|
59
|
+
const get = async (p, init) => {
|
|
60
|
+
try {
|
|
61
|
+
return await fetchImpl(`${base}${p}`, withTimeout(init));
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
findings.push(smokeFinding("smoke/unreachable", `${p} could not be fetched: ${err instanceof Error ? err.message : String(err)}`, p));
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const home = await get("/");
|
|
69
|
+
if (home) {
|
|
70
|
+
await home.body?.cancel().catch(() => { });
|
|
71
|
+
if (home.status >= 500)
|
|
72
|
+
findings.push(smokeFinding("smoke/unreachable", `GET / returned ${home.status}.`, "/:status"));
|
|
73
|
+
for (const h of REQUIRED_HEADERS) {
|
|
74
|
+
if (!home.headers.get(h))
|
|
75
|
+
findings.push(smokeFinding("smoke/header-missing", `The ${h} header is missing, so browsers lose a protection against clickjacking, content sniffing, or script injection.`, h));
|
|
76
|
+
}
|
|
77
|
+
if (base.startsWith("https://") && !home.headers.get("strict-transport-security"))
|
|
78
|
+
findings.push(smokeFinding("smoke/header-missing", "The strict-transport-security header is missing.", "strict-transport-security"));
|
|
79
|
+
}
|
|
80
|
+
const me = await get("/api/auth/me");
|
|
81
|
+
if (me) {
|
|
82
|
+
const body = await me.text().catch(() => "");
|
|
83
|
+
if (me.status !== 200 || !body.includes('"user":null'))
|
|
84
|
+
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"));
|
|
85
|
+
}
|
|
86
|
+
const login = await get("/api/auth/login", {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "content-type": "application/json" },
|
|
89
|
+
body: JSON.stringify({ email: "probe@nerdsystem.invalid", password: "definitely-wrong-password" }),
|
|
90
|
+
});
|
|
91
|
+
if (login) {
|
|
92
|
+
await login.body?.cancel().catch(() => { });
|
|
93
|
+
if (login.status !== 401)
|
|
94
|
+
findings.push(smokeFinding("smoke/auth-login", `Wrong credentials returned ${login.status} instead of 401.`, "/api/auth/login"));
|
|
95
|
+
}
|
|
96
|
+
return probeResult(findings, startedAt);
|
|
97
|
+
}
|
|
98
|
+
/** The result recorded when the probe did not run, so --json consumers see why rather than a missing key. */
|
|
99
|
+
export function skippedProbe(reason) {
|
|
100
|
+
return { name: "smoke", title: "Deployed app probes", status: "skip", summary: reason, findings: [], durationMs: 0, tool: "builtin" };
|
|
101
|
+
}
|
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
|
|
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/dist/util.d.ts
CHANGED
|
@@ -10,22 +10,22 @@ export declare class CliError extends Error {
|
|
|
10
10
|
});
|
|
11
11
|
}
|
|
12
12
|
/** The one sentence every unauthenticated command prints, with both ways in. */
|
|
13
|
-
export declare const NOT_LOGGED_IN = "Not logged in. Run `nerd login --email you@example.com` (
|
|
13
|
+
export declare const NOT_LOGGED_IN = "Not logged in. Run `nerd login --email you@example.com` (the link in the email signs you in, creating your account the first time, and opens a page where you press Approve), or set NERD_TOKEN to an API token for scripts.";
|
|
14
14
|
export declare const log: {
|
|
15
15
|
info: (msg: string) => void;
|
|
16
16
|
step: (msg: string) => void;
|
|
17
17
|
ok: (msg: string) => void;
|
|
18
18
|
warn: (msg: string) => void;
|
|
19
19
|
};
|
|
20
|
-
|
|
21
|
-
export declare function run(cmd: string, args: string[], opts?: {
|
|
22
|
-
cwd?: string;
|
|
23
|
-
env?: NodeJS.ProcessEnv;
|
|
24
|
-
}): Promise<number>;
|
|
25
|
-
export declare function runOrThrow(cmd: string, args: string[], opts?: {
|
|
20
|
+
export interface RunOptions {
|
|
26
21
|
cwd?: string;
|
|
27
22
|
env?: NodeJS.ProcessEnv;
|
|
28
|
-
|
|
23
|
+
/** Send the child's stdout to our stderr, so stdout stays reserved for `--json` output. */
|
|
24
|
+
stdoutToStderr?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Runs a command, streaming output. Resolves with the exit code. */
|
|
27
|
+
export declare function run(cmd: string, args: string[], opts?: RunOptions): Promise<number>;
|
|
28
|
+
export declare function runOrThrow(cmd: string, args: string[], opts?: RunOptions): Promise<void>;
|
|
29
29
|
export declare function printJson(value: unknown): void;
|
|
30
30
|
export declare function table(rows: Record<string, unknown>[], columns: string[]): void;
|
|
31
31
|
/** The dashboard that goes with an API origin (https://api.nerdsystem.com -> https://nerdsystem.com). */
|
package/dist/util.js
CHANGED
|
@@ -15,7 +15,7 @@ export class CliError extends Error {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
/** The one sentence every unauthenticated command prints, with both ways in. */
|
|
18
|
-
export const NOT_LOGGED_IN = "Not logged in. Run `nerd login --email you@example.com` (
|
|
18
|
+
export const NOT_LOGGED_IN = "Not logged in. Run `nerd login --email you@example.com` (the link in the email signs you in, creating your account the first time, and opens a page where you press Approve), or set NERD_TOKEN to an API token for scripts.";
|
|
19
19
|
export const log = {
|
|
20
20
|
info: (msg) => console.error(`${msg}`),
|
|
21
21
|
step: (msg) => console.error(`\x1b[36m›\x1b[0m ${msg}`),
|
|
@@ -25,7 +25,8 @@ export const log = {
|
|
|
25
25
|
/** Runs a command, streaming output. Resolves with the exit code. */
|
|
26
26
|
export function run(cmd, args, opts = {}) {
|
|
27
27
|
return new Promise((resolve, reject) => {
|
|
28
|
-
const child = spawn(cmd, args, { cwd: opts.cwd, env: { ...process.env, ...opts.env }, stdio: "inherit", shell: process.platform === "win32" });
|
|
28
|
+
const child = spawn(cmd, args, { cwd: opts.cwd, env: { ...process.env, ...opts.env }, stdio: ["inherit", opts.stdoutToStderr ? "pipe" : "inherit", "inherit"], shell: process.platform === "win32" });
|
|
29
|
+
child.stdout?.pipe(process.stderr);
|
|
29
30
|
child.on("error", reject);
|
|
30
31
|
child.on("exit", (code) => resolve(code ?? 1));
|
|
31
32
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nerdsystem",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "nerd: deploy
|
|
3
|
+
"version": "0.0.10",
|
|
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,19 +20,30 @@ 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.
|
|
26
37
|
2. `nerd check` (static rules, then typecheck). Fix anything it reports.
|
|
27
38
|
3. `nerd dev` to run locally when you need to try it. The local app talks to the real auth service, so signup and login work exactly as in production.
|
|
28
|
-
4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, and
|
|
39
|
+
4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, prints the URL, and probes the live URL (headers and the sign-in routes). With `--json` the output carries `probes`, the same shape as a check result, with `status: "skip"` and the reason when it did not run; probe findings never fail the deploy.
|
|
29
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.
|
|
30
41
|
|
|
31
42
|
Run `nerd check --json` when you want a machine-readable list of problems. Its summary says how many gates ran and which were skipped (`skipped` in the JSON): semgrep and gitleaks only run locally when installed; git builds always run them in the builder.
|
|
32
43
|
|
|
33
44
|
## Deploying from GitHub
|
|
34
45
|
|
|
35
|
-
Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically
|
|
46
|
+
Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically, and every pull request gets a preview at its own URL, posted as a PR comment. `nerd builds` lists builds; `nerd builds show <id>` prints the log when one fails. A failing build reports the same `nerd check` problems you would see locally, so fix and push again. Every preview gets its own database and bucket, and pending migrations are applied to it on each preview build, so a PR that changes the schema can be tried end to end before it merges.
|
|
36
47
|
|
|
37
48
|
## Where things go
|
|
38
49
|
|
|
@@ -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,
|
|
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 (
|
|
5
|
+
<!-- generated from .claude/skills/nerdsystem/SKILL.md (10dcd79a44bd); edit the skill, then run: nerd agents sync -->
|
|
6
6
|
|
|
7
7
|
# Building on nerdsystem
|
|
8
8
|
|
|
@@ -21,19 +21,30 @@ 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.
|
|
27
38
|
2. `nerd check` (static rules, then typecheck). Fix anything it reports.
|
|
28
39
|
3. `nerd dev` to run locally when you need to try it. The local app talks to the real auth service, so signup and login work exactly as in production.
|
|
29
|
-
4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, and
|
|
40
|
+
4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, prints the URL, and probes the live URL (headers and the sign-in routes). With `--json` the output carries `probes`, the same shape as a check result, with `status: "skip"` and the reason when it did not run; probe findings never fail the deploy.
|
|
30
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.
|
|
31
42
|
|
|
32
43
|
Run `nerd check --json` when you want a machine-readable list of problems. Its summary says how many gates ran and which were skipped (`skipped` in the JSON): semgrep and gitleaks only run locally when installed; git builds always run them in the builder.
|
|
33
44
|
|
|
34
45
|
## Deploying from GitHub
|
|
35
46
|
|
|
36
|
-
Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically
|
|
47
|
+
Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically, and every pull request gets a preview at its own URL, posted as a PR comment. `nerd builds` lists builds; `nerd builds show <id>` prints the log when one fails. A failing build reports the same `nerd check` problems you would see locally, so fix and push again. Every preview gets its own database and bucket, and pending migrations are applied to it on each preview build, so a PR that changes the schema can be tried end to end before it merges.
|
|
37
48
|
|
|
38
49
|
## Where things go
|
|
39
50
|
|
|
@@ -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,
|
|
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 (
|
|
1
|
+
<!-- generated from .claude/skills/nerdsystem/SKILL.md (10dcd79a44bd); edit the skill, then run: nerd agents sync -->
|
|
2
2
|
|
|
3
3
|
# Building on nerdsystem
|
|
4
4
|
|
|
@@ -17,19 +17,30 @@ 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.
|
|
23
34
|
2. `nerd check` (static rules, then typecheck). Fix anything it reports.
|
|
24
35
|
3. `nerd dev` to run locally when you need to try it. The local app talks to the real auth service, so signup and login work exactly as in production.
|
|
25
|
-
4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, and
|
|
36
|
+
4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, prints the URL, and probes the live URL (headers and the sign-in routes). With `--json` the output carries `probes`, the same shape as a check result, with `status: "skip"` and the reason when it did not run; probe findings never fail the deploy.
|
|
26
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.
|
|
27
38
|
|
|
28
39
|
Run `nerd check --json` when you want a machine-readable list of problems. Its summary says how many gates ran and which were skipped (`skipped` in the JSON): semgrep and gitleaks only run locally when installed; git builds always run them in the builder.
|
|
29
40
|
|
|
30
41
|
## Deploying from GitHub
|
|
31
42
|
|
|
32
|
-
Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically
|
|
43
|
+
Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically, and every pull request gets a preview at its own URL, posted as a PR comment. `nerd builds` lists builds; `nerd builds show <id>` prints the log when one fails. A failing build reports the same `nerd check` problems you would see locally, so fix and push again. Every preview gets its own database and bucket, and pending migrations are applied to it on each preview build, so a PR that changes the schema can be tried end to end before it merges.
|
|
33
44
|
|
|
34
45
|
## Where things go
|
|
35
46
|
|
|
@@ -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,
|
|
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
|
|
package/template/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- generated from .claude/skills/nerdsystem/SKILL.md (
|
|
1
|
+
<!-- generated from .claude/skills/nerdsystem/SKILL.md (10dcd79a44bd); edit the skill, then run: nerd agents sync -->
|
|
2
2
|
|
|
3
3
|
# Building on nerdsystem
|
|
4
4
|
|
|
@@ -17,19 +17,30 @@ 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.
|
|
23
34
|
2. `nerd check` (static rules, then typecheck). Fix anything it reports.
|
|
24
35
|
3. `nerd dev` to run locally when you need to try it. The local app talks to the real auth service, so signup and login work exactly as in production.
|
|
25
|
-
4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, and
|
|
36
|
+
4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, prints the URL, and probes the live URL (headers and the sign-in routes). With `--json` the output carries `probes`, the same shape as a check result, with `status: "skip"` and the reason when it did not run; probe findings never fail the deploy.
|
|
26
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.
|
|
27
38
|
|
|
28
39
|
Run `nerd check --json` when you want a machine-readable list of problems. Its summary says how many gates ran and which were skipped (`skipped` in the JSON): semgrep and gitleaks only run locally when installed; git builds always run them in the builder.
|
|
29
40
|
|
|
30
41
|
## Deploying from GitHub
|
|
31
42
|
|
|
32
|
-
Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically
|
|
43
|
+
Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically, and every pull request gets a preview at its own URL, posted as a PR comment. `nerd builds` lists builds; `nerd builds show <id>` prints the log when one fails. A failing build reports the same `nerd check` problems you would see locally, so fix and push again. Every preview gets its own database and bucket, and pending migrations are applied to it on each preview build, so a PR that changes the schema can be tried end to end before it merges.
|
|
33
44
|
|
|
34
45
|
## Where things go
|
|
35
46
|
|
|
@@ -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,
|
|
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
|
|
package/template/GEMINI.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- generated from .claude/skills/nerdsystem/SKILL.md (
|
|
1
|
+
<!-- generated from .claude/skills/nerdsystem/SKILL.md (10dcd79a44bd); edit the skill, then run: nerd agents sync -->
|
|
2
2
|
|
|
3
3
|
# Building on nerdsystem
|
|
4
4
|
|
|
@@ -17,19 +17,30 @@ 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.
|
|
23
34
|
2. `nerd check` (static rules, then typecheck). Fix anything it reports.
|
|
24
35
|
3. `nerd dev` to run locally when you need to try it. The local app talks to the real auth service, so signup and login work exactly as in production.
|
|
25
|
-
4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, and
|
|
36
|
+
4. `nerd deploy` when it is ready. It runs the checks, builds, applies pending migrations, uploads, prints the URL, and probes the live URL (headers and the sign-in routes). With `--json` the output carries `probes`, the same shape as a check result, with `status: "skip"` and the reason when it did not run; probe findings never fail the deploy.
|
|
26
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.
|
|
27
38
|
|
|
28
39
|
Run `nerd check --json` when you want a machine-readable list of problems. Its summary says how many gates ran and which were skipped (`skipped` in the JSON): semgrep and gitleaks only run locally when installed; git builds always run them in the builder.
|
|
29
40
|
|
|
30
41
|
## Deploying from GitHub
|
|
31
42
|
|
|
32
|
-
Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically
|
|
43
|
+
Once the repo is linked (`nerd github connect`, then `nerd github link owner/repo`), every push to the production branch builds and deploys automatically, and every pull request gets a preview at its own URL, posted as a PR comment. `nerd builds` lists builds; `nerd builds show <id>` prints the log when one fails. A failing build reports the same `nerd check` problems you would see locally, so fix and push again. Every preview gets its own database and bucket, and pending migrations are applied to it on each preview build, so a PR that changes the schema can be tried end to end before it merges.
|
|
33
44
|
|
|
34
45
|
## Where things go
|
|
35
46
|
|
|
@@ -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,
|
|
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
|
|