nerdsystem 0.0.5 → 0.0.7

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.
Files changed (46) hide show
  1. package/README.md +12 -1
  2. package/dist/api.js +4 -3
  3. package/dist/cli.d.ts +7 -0
  4. package/dist/cli.js +311 -0
  5. package/dist/commands/admin.d.ts +10 -0
  6. package/dist/commands/admin.js +36 -0
  7. package/dist/commands/check.d.ts +6 -1
  8. package/dist/commands/check.js +28 -10
  9. package/dist/commands/db.d.ts +8 -0
  10. package/dist/commands/db.js +25 -0
  11. package/dist/commands/deploy.js +5 -3
  12. package/dist/commands/dev.js +2 -1
  13. package/dist/commands/doctor.js +1 -1
  14. package/dist/commands/github.d.ts +1 -0
  15. package/dist/commands/github.js +2 -2
  16. package/dist/commands/init.js +76 -6
  17. package/dist/commands/login.js +1 -1
  18. package/dist/commands/ops.d.ts +1 -0
  19. package/dist/commands/ops.js +3 -3
  20. package/dist/commands/org.js +1 -1
  21. package/dist/commands/trash.d.ts +17 -0
  22. package/dist/commands/trash.js +84 -0
  23. package/dist/commands/verify.js +3 -3
  24. package/dist/gates/code.d.ts +4 -1
  25. package/dist/gates/code.js +8 -5
  26. package/dist/gates/fs.js +1 -1
  27. package/dist/gates/index.d.ts +2 -1
  28. package/dist/gates/index.js +1 -0
  29. package/dist/gates/rules.js +6 -3
  30. package/dist/gates/secrets.d.ts +5 -0
  31. package/dist/gates/secrets.js +50 -10
  32. package/dist/gates/text.d.ts +39 -0
  33. package/dist/gates/text.js +82 -2
  34. package/dist/gates/types.d.ts +12 -0
  35. package/dist/gates/types.js +18 -0
  36. package/dist/index.js +2 -267
  37. package/dist/util.d.ts +14 -1
  38. package/dist/util.js +20 -1
  39. package/package.json +1 -1
  40. package/template/.claude/skills/nerdsystem/SKILL.md +12 -5
  41. package/template/.cursor/rules/nerdsystem.mdc +13 -6
  42. package/template/.github/copilot-instructions.md +13 -6
  43. package/template/AGENTS.md +13 -6
  44. package/template/GEMINI.md +13 -6
  45. package/template/package.json +6 -0
  46. package/template/src/client/auth.tsx +1 -1
package/README.md CHANGED
@@ -6,8 +6,19 @@
6
6
  npx nerdsystem init my-app
7
7
  cd my-app && pnpm install
8
8
  nerd dev # run locally with real sign-in
9
- nerd verify # secrets, dependencies, policy, PII, types
9
+ nerd check # every gate a deploy runs: secrets, dependencies, policy, PII, types (alias: nerd verify)
10
10
  nerd deploy # checks, builds, migrates, ships
11
11
  ```
12
12
 
13
13
  Every command takes `--json` for agents. `nerd help` lists them all.
14
+
15
+ More you will reach for:
16
+
17
+ ```sh
18
+ nerd db export # SQL dump of the production database to <slug>-<date>.sql (--out file.sql)
19
+ nerd delete --yes # take the app offline; data kept a week in the trash
20
+ nerd trash # deleted projects you can still restore
21
+ nerd trash restore <slug> # bring one back, then nerd deploy or roll back
22
+ ```
23
+
24
+ `nerd check` prints how many gates ran and which were skipped (semgrep and gitleaks are optional locally; git builds run them in the builder). Platform operators also have `nerd admin --help`.
package/dist/api.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { loadConfig } from "./config.js";
2
- import { CliError } from "./util.js";
2
+ import { CliError, NOT_LOGGED_IN } from "./util.js";
3
3
  export class ApiClient {
4
4
  apiUrl;
5
5
  token;
@@ -10,7 +10,7 @@ export class ApiClient {
10
10
  static fromConfig(opts = {}) {
11
11
  const cfg = loadConfig();
12
12
  if (opts.requireAuth !== false && !cfg.token) {
13
- throw new CliError("Not logged in. Run `nerd login` (or set NERD_TOKEN).");
13
+ throw new CliError(NOT_LOGGED_IN);
14
14
  }
15
15
  return new ApiClient(cfg.apiUrl, cfg.token);
16
16
  }
@@ -37,7 +37,8 @@ export class ApiClient {
37
37
  if (!res.ok) {
38
38
  const err = json?.error;
39
39
  const details = err?.details ? `\n${JSON.stringify(err.details, null, 2)}` : "";
40
- throw new CliError(`${err?.code ?? "error"} (${res.status}): ${err?.message ?? "request failed"}${details}`);
40
+ // The message leads; the code and status follow for people who need them.
41
+ throw new CliError(`${err?.message ?? "request failed"} [${err?.code ?? "error"} ${res.status}]${details}`, 1, { code: err?.code ?? "error", status: res.status });
41
42
  }
42
43
  return json;
43
44
  }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export declare function main(argv: string[]): Promise<void>;
2
+ /**
3
+ * Runs one command and turns any failure into a non-zero exit code. `process.exitCode` is set
4
+ * rather than calling `process.exit()`, so piped stdout (including `--json` output) is flushed
5
+ * before the process ends. Returns the exit code for tests.
6
+ */
7
+ export declare function runCli(argv: string[]): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,311 @@
1
+ import { parseArgs } from "node:util";
2
+ import { authConfig, authSecret } from "./commands/auth.js";
3
+ import { check } from "./commands/check.js";
4
+ import { dbExport, dbMigrate, dbMigrations, dbQuery } from "./commands/db.js";
5
+ import { deploy, deployments } from "./commands/deploy.js";
6
+ import { dev } from "./commands/dev.js";
7
+ import { doctor } from "./commands/doctor.js";
8
+ import { cron, health, rollback, secretList, secretRemove, secretSet, transfer, waitlist } from "./commands/ops.js";
9
+ import { adminHelp, adminSuspend, adminUnsuspend } from "./commands/admin.js";
10
+ import { agentsSync } from "./commands/agents.js";
11
+ import { billingPortal, billingShow, billingUpgrade } from "./commands/billing.js";
12
+ import { auditExport, clientsCreate, clientsList, clientsRelease, domainsAdd, domainsList, domainsRemove, logs, membersInvite, membersList, membersRemove } from "./commands/org.js";
13
+ import { buildsList, buildsShow, buildsTrigger } from "./commands/builds.js";
14
+ import { githubConnect, githubLink, githubStatus, githubUnlink } from "./commands/github.js";
15
+ import { init, linkProject } from "./commands/init.js";
16
+ import { login, whoami } from "./commands/login.js";
17
+ import { deleteProject, trashList, trashRestore } from "./commands/trash.js";
18
+ import { findProjectRoot } from "./project.js";
19
+ import { CliError, log } from "./util.js";
20
+ const HELP = `nerd — deploy apps to nerdsystem
21
+
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
+ nerd whoami Show the current user and orgs
24
+ nerd doctor Check Node, pnpm, git, login, and the project
25
+ nerd init <dir> [--org] [--name] [--describe "what it does"] Create an app and a project for it
26
+ nerd link --slug <project> [--org] Link this directory to an existing or new project
27
+ nerd dev [vite args] Run locally (writes .dev.vars, applies local migrations)
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
30
+ nerd rollback [deploymentId] Re-publish the previous production bundle
31
+ nerd cron [off|hourly|daily] Scheduled job: the platform calls POST /api/cron
32
+ nerd transfer <org> Move this project to another org you administer
33
+ nerd delete [--slug p] [--org o] --yes Take the app offline; data kept a week in the trash
34
+ nerd trash [--org o] Deleted projects you can still restore
35
+ nerd trash restore <slug> [--org o] Bring a deleted project back (then deploy or roll back)
36
+ nerd health Platform component health (operators)
37
+ nerd waitlist [approve <id>] Who is waiting for access while sign-up is invite-only (operators)
38
+ nerd deployments List recent deployments
39
+ nerd db query "<sql>" Run SQL on the production database
40
+ nerd db migrate Apply pending migrations to production
41
+ nerd db migrations List applied migrations
42
+ nerd db export [--out file.sql] Download a SQL dump of the production database
43
+ nerd auth config [--roles a,b] [--default-role r] [--signup on|off]
44
+ nerd auth secret Mint a dev tenant secret into .dev.vars
45
+ nerd secret set NAME [value] App secret for production (c.env.NAME); value from stdin when omitted
46
+ nerd secret list | rm NAME Names only; values are never shown
47
+ nerd github connect Install the GitHub App for this org (prints a link)
48
+ nerd github link owner/repo [--branch main] [--root-dir dir] [--no-previews] [--require-pr] [--agent @claude]
49
+ nerd github status | unlink
50
+ nerd builds [--limit n] List git-triggered builds
51
+ nerd builds show <id> Build details and log
52
+ nerd builds trigger [--branch b] [--commit sha]
53
+ nerd agents sync Regenerate AGENTS.md, Cursor, Copilot, Gemini files from the skill
54
+ nerd billing [--org slug] Plan, limits, and usage for the org
55
+ nerd billing upgrade --plan pro|studio [--annual] Start checkout
56
+ nerd billing portal Manage cards, invoices, and plan
57
+ nerd members [invite <email> --role r | remove <userId>] Org members and invitations
58
+ nerd clients [create <slug> --name n | release <slug>] Studio: one org per client, billed through yours
59
+ nerd domains [add <hostname> | remove <id>] Custom domains for this project
60
+ nerd logs [--since 30m] [--limit 100] [--q text] [--pr n] Logs from the deployed app
61
+ nerd audit [--out file.csv] Export deployments and builds (Studio)
62
+
63
+ Every command accepts --json for machine-readable output.
64
+ Operator: nerd admin --help`;
65
+ const BOOL = { type: "boolean", default: false };
66
+ const STR = { type: "string" };
67
+ export async function main(argv) {
68
+ const [cmd, ...rest] = argv;
69
+ if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
70
+ console.log(HELP);
71
+ return;
72
+ }
73
+ const parse = (options, allowPositionals = true) => parseArgs({ args: rest, options: { json: BOOL, ...options }, allowPositionals, strict: cmd !== "dev" });
74
+ switch (cmd) {
75
+ case "login": {
76
+ const { values } = parse({ token: STR, "api-url": STR, email: STR });
77
+ return login(values);
78
+ }
79
+ case "doctor": {
80
+ const { values } = parse({});
81
+ return doctor({ json: Boolean(values.json) });
82
+ }
83
+ case "whoami": {
84
+ const { values } = parse({});
85
+ return whoami({ json: Boolean(values.json) });
86
+ }
87
+ case "init": {
88
+ const { values, positionals } = parse({ org: STR, name: STR, slug: STR, describe: STR, "no-create": BOOL });
89
+ return init(positionals[0], values);
90
+ }
91
+ case "link": {
92
+ const { values } = parse({ org: STR, slug: STR, name: STR, "no-create": BOOL });
93
+ if (!values.slug)
94
+ throw new CliError("Usage: nerd link --slug <project> [--org <org>] [--name 'My App']");
95
+ const root = (() => {
96
+ try {
97
+ return findProjectRoot();
98
+ }
99
+ catch {
100
+ return process.cwd();
101
+ }
102
+ })();
103
+ await linkProject(root, { ...(values.org ? { org: values.org } : {}), slug: values.slug, name: values.name ?? values.slug, create: !values["no-create"] });
104
+ return;
105
+ }
106
+ case "dev":
107
+ return dev(rest);
108
+ case "check":
109
+ case "verify": {
110
+ const { values } = parse({ "no-build": BOOL, "no-remote": BOOL, "no-typecheck": BOOL, rule: STR });
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
+ }
113
+ case "deploy": {
114
+ const { values } = parse({ "skip-check": BOOL, "skip-build": BOOL, note: STR });
115
+ return deploy(values);
116
+ }
117
+ case "rollback": {
118
+ const { values, positionals } = parse({ note: STR });
119
+ return rollback(positionals[0], { json: Boolean(values.json), ...(values.note ? { note: values.note } : {}) });
120
+ }
121
+ case "cron": {
122
+ const { values, positionals } = parse({});
123
+ return cron(positionals[0], { json: Boolean(values.json) });
124
+ }
125
+ case "transfer": {
126
+ const { values, positionals } = parse({});
127
+ return transfer(positionals[0], { json: Boolean(values.json) });
128
+ }
129
+ case "delete": {
130
+ const { values } = parse({ org: STR, slug: STR, yes: BOOL, force: BOOL }, false);
131
+ return deleteProject({ json: Boolean(values.json), yes: Boolean(values.yes || values.force), ...(values.org ? { org: values.org } : {}), ...(values.slug ? { slug: values.slug } : {}) });
132
+ }
133
+ case "trash": {
134
+ const [sub, ...subRest] = rest;
135
+ const isSub = sub === "restore" || sub === "list";
136
+ const { values, positionals } = parseArgs({ args: isSub ? subRest : rest, options: { json: BOOL, org: STR }, allowPositionals: true });
137
+ const o = { json: Boolean(values.json), ...(values.org ? { org: values.org } : {}) };
138
+ if (sub === "restore")
139
+ return trashRestore(positionals[0], o);
140
+ return trashList(o);
141
+ }
142
+ case "health": {
143
+ const { values } = parse({});
144
+ return health({ json: Boolean(values.json) });
145
+ }
146
+ case "waitlist": {
147
+ const [sub, ...subRest] = rest;
148
+ const { values, positionals } = parseArgs({ args: sub === "approve" ? subRest : rest, options: { json: BOOL }, allowPositionals: true });
149
+ return waitlist({ json: Boolean(values.json), ...(sub === "approve" ? { approve: positionals[0] } : {}) });
150
+ }
151
+ case "deployments": {
152
+ const { values } = parse({ limit: { type: "string", default: "10" } });
153
+ return deployments(values);
154
+ }
155
+ case "db": {
156
+ const [sub, ...subRest] = rest;
157
+ const { values, positionals } = parseArgs({ args: subRest, options: { json: BOOL, out: STR }, allowPositionals: true });
158
+ if (sub === "query")
159
+ return dbQuery(positionals[0], { json: Boolean(values.json) });
160
+ if (sub === "migrate")
161
+ return dbMigrate({ json: Boolean(values.json) });
162
+ if (sub === "migrations")
163
+ return dbMigrations({ json: Boolean(values.json) });
164
+ if (sub === "export")
165
+ return dbExport({ json: Boolean(values.json), ...(values.out ? { out: values.out } : {}) });
166
+ throw new CliError("Usage: nerd db query|migrate|migrations|export [--out file.sql]");
167
+ }
168
+ case "auth": {
169
+ const [sub, ...subRest] = rest;
170
+ const { values } = parseArgs({ args: subRest, options: { json: BOOL, roles: STR, "default-role": STR, signup: STR }, allowPositionals: false });
171
+ if (sub === "config")
172
+ return authConfig(values);
173
+ if (sub === "secret")
174
+ return authSecret();
175
+ throw new CliError("Usage: nerd auth config|secret");
176
+ }
177
+ case "billing": {
178
+ const [sub, ...subRest] = rest;
179
+ const isSub = sub === "upgrade" || sub === "portal" || sub === "show";
180
+ const { values } = parseArgs({ args: isSub ? subRest : rest, options: { json: BOOL, org: STR, plan: STR, annual: BOOL }, allowPositionals: false });
181
+ const o = { json: Boolean(values.json), ...(values.org ? { org: values.org } : {}) };
182
+ if (sub === "upgrade")
183
+ return billingUpgrade({ ...o, ...(values.plan ? { plan: values.plan } : {}), annual: Boolean(values.annual) });
184
+ if (sub === "portal")
185
+ return billingPortal(o);
186
+ return billingShow(o);
187
+ }
188
+ case "members": {
189
+ const [sub, ...subRest] = rest;
190
+ const isSub = sub === "invite" || sub === "remove" || sub === "list";
191
+ const { values, positionals } = parseArgs({ args: isSub ? subRest : rest, options: { json: BOOL, org: STR, role: STR }, allowPositionals: true });
192
+ const o = { json: Boolean(values.json), ...(values.org ? { org: values.org } : {}) };
193
+ if (sub === "invite")
194
+ return membersInvite(positionals[0], { ...o, ...(values.role ? { role: values.role } : {}) });
195
+ if (sub === "remove")
196
+ return membersRemove(positionals[0], o);
197
+ return membersList(o);
198
+ }
199
+ case "clients": {
200
+ const [sub, ...subRest] = rest;
201
+ const isSub = sub === "create" || sub === "release" || sub === "list";
202
+ const { values, positionals } = parseArgs({ args: isSub ? subRest : rest, options: { json: BOOL, org: STR, name: STR }, allowPositionals: true });
203
+ const o = { json: Boolean(values.json), ...(values.org ? { org: values.org } : {}) };
204
+ if (sub === "create")
205
+ return clientsCreate(positionals[0], { ...o, ...(values.name ? { name: values.name } : {}) });
206
+ if (sub === "release")
207
+ return clientsRelease(positionals[0], o);
208
+ return clientsList(o);
209
+ }
210
+ case "secret": {
211
+ const [sub, ...subRest] = rest;
212
+ const { values, positionals } = parseArgs({ args: subRest, options: { json: BOOL, previews: BOOL }, allowPositionals: true });
213
+ const json = Boolean(values.json);
214
+ if (sub === "set")
215
+ return secretSet(positionals[0], positionals[1], { json, previews: Boolean(values.previews) });
216
+ if (sub === "rm" || sub === "remove" || sub === "delete")
217
+ return secretRemove(positionals[0], { json });
218
+ if (sub === "list" || sub === "ls" || sub === undefined)
219
+ return secretList({ json });
220
+ throw new CliError("Usage: nerd secret set NAME [value] [--previews] | list | rm NAME");
221
+ }
222
+ case "domains": {
223
+ const [sub, ...subRest] = rest;
224
+ const isSub = sub === "add" || sub === "remove" || sub === "list";
225
+ const { values, positionals } = parseArgs({ args: isSub ? subRest : rest, options: { json: BOOL }, allowPositionals: true });
226
+ const o = { json: Boolean(values.json) };
227
+ if (sub === "add")
228
+ return domainsAdd(positionals[0], o);
229
+ if (sub === "remove")
230
+ return domainsRemove(positionals[0], o);
231
+ return domainsList(o);
232
+ }
233
+ case "logs": {
234
+ const { values } = parse({ since: STR, limit: STR, q: STR, pr: STR });
235
+ return logs({ json: Boolean(values.json), ...(values.since ? { since: values.since } : {}), ...(values.limit ? { limit: values.limit } : {}), ...(values.q ? { q: values.q } : {}), ...(values.pr ? { pr: values.pr } : {}) });
236
+ }
237
+ case "audit": {
238
+ const { values } = parse({ org: STR, out: STR });
239
+ return auditExport({ json: Boolean(values.json), ...(values.org ? { org: values.org } : {}), ...(values.out ? { out: values.out } : {}) });
240
+ }
241
+ case "admin": {
242
+ const [sub, ...subRest] = rest;
243
+ if (!sub || sub === "--help" || sub === "-h" || sub === "help")
244
+ return adminHelp();
245
+ const { values } = parseArgs({ args: subRest, options: { json: BOOL, org: STR, reason: STR }, allowPositionals: false });
246
+ const o = { json: Boolean(values.json), ...(values.org ? { org: values.org } : {}) };
247
+ if (sub === "suspend")
248
+ return adminSuspend({ ...o, ...(values.reason ? { reason: values.reason } : {}) });
249
+ if (sub === "unsuspend")
250
+ return adminUnsuspend(o);
251
+ throw new CliError("Usage: nerd admin suspend|unsuspend (see `nerd admin --help`)");
252
+ }
253
+ case "agents": {
254
+ const [sub, ...subRest] = rest;
255
+ const { values } = parseArgs({ args: subRest, options: { json: BOOL }, allowPositionals: false });
256
+ if (sub === "sync")
257
+ return agentsSync({ json: Boolean(values.json) });
258
+ throw new CliError("Usage: nerd agents sync");
259
+ }
260
+ case "github": {
261
+ const [sub, ...subRest] = rest;
262
+ const { values, positionals } = parseArgs({ args: subRest, options: { json: BOOL, branch: STR, "root-dir": STR, "no-previews": BOOL, "require-pr": BOOL, installation: STR, agent: STR }, allowPositionals: true });
263
+ const json = Boolean(values.json);
264
+ if (sub === "connect")
265
+ return githubConnect({ json });
266
+ if (sub === "link")
267
+ return githubLink(positionals[0], { json, "no-previews": Boolean(values["no-previews"]), "require-pr": Boolean(values["require-pr"]), ...(values.agent ? { agent: String(values.agent) } : {}), ...(values.branch ? { branch: values.branch } : {}), ...(values["root-dir"] ? { "root-dir": values["root-dir"] } : {}), ...(values.installation ? { installation: values.installation } : {}) });
268
+ if (sub === "unlink")
269
+ return githubUnlink({ json });
270
+ if (sub === "status" || sub === undefined)
271
+ return githubStatus({ json });
272
+ throw new CliError("Usage: nerd github connect|link|unlink|status");
273
+ }
274
+ case "builds": {
275
+ const [sub, ...subRest] = rest;
276
+ const isSub = sub === "show" || sub === "trigger" || sub === "list";
277
+ const { values, positionals } = parseArgs({ args: isSub ? subRest : rest, options: { json: BOOL, limit: { type: "string", default: "10" }, branch: STR, commit: STR }, allowPositionals: true });
278
+ const json = Boolean(values.json);
279
+ if (sub === "show")
280
+ return buildsShow(positionals[0], { json });
281
+ if (sub === "trigger")
282
+ return buildsTrigger({ json, ...(values.branch ? { branch: values.branch } : {}), ...(values.commit ? { commit: values.commit } : {}) });
283
+ return buildsList({ json, limit: values.limit });
284
+ }
285
+ default:
286
+ throw new CliError(`Unknown command "${cmd}". Run \`nerd help\`.`);
287
+ }
288
+ }
289
+ /**
290
+ * Runs one command and turns any failure into a non-zero exit code. `process.exitCode` is set
291
+ * rather than calling `process.exit()`, so piped stdout (including `--json` output) is flushed
292
+ * before the process ends. Returns the exit code for tests.
293
+ */
294
+ export async function runCli(argv) {
295
+ let code = 0;
296
+ try {
297
+ await main(argv);
298
+ }
299
+ catch (err) {
300
+ if (err instanceof CliError) {
301
+ log.warn(err.message);
302
+ code = err.exitCode > 0 ? err.exitCode : 1;
303
+ }
304
+ else {
305
+ console.error(err);
306
+ code = 1;
307
+ }
308
+ }
309
+ process.exitCode = code;
310
+ return code;
311
+ }
@@ -0,0 +1,10 @@
1
+ export declare function adminHelp(): void;
2
+ export declare function adminSuspend(opts: {
3
+ json: boolean;
4
+ org?: string | undefined;
5
+ reason?: string | undefined;
6
+ }): Promise<void>;
7
+ export declare function adminUnsuspend(opts: {
8
+ json: boolean;
9
+ org?: string | undefined;
10
+ }): Promise<void>;
@@ -0,0 +1,36 @@
1
+ import { ApiClient } from "../api.js";
2
+ import { CliError, log, printJson } from "../util.js";
3
+ const ADMIN_HELP = `nerd admin — platform operator commands (anyone else gets "not found")
4
+
5
+ nerd admin suspend --org <slug> --reason "<text>" Suspend an org: apps offline, users and tokens locked
6
+ nerd admin unsuspend --org <slug> Lift the suspension`;
7
+ export function adminHelp() {
8
+ console.log(ADMIN_HELP);
9
+ }
10
+ const OPERATOR_ONLY = "Only the platform operator can do this.";
11
+ async function operatorPost(path, body) {
12
+ try {
13
+ return await ApiClient.fromConfig().post(path, body);
14
+ }
15
+ catch (err) {
16
+ if (err instanceof CliError && err.status === 404)
17
+ throw new CliError(OPERATOR_ONLY);
18
+ throw err;
19
+ }
20
+ }
21
+ export async function adminSuspend(opts) {
22
+ if (!opts.org || !opts.reason)
23
+ throw new CliError('Usage: nerd admin suspend --org <slug> --reason "<why>"');
24
+ const r = await operatorPost("/v1/admin/suspend", { orgSlug: opts.org, reason: opts.reason });
25
+ if (opts.json)
26
+ return printJson(r);
27
+ log.ok(`Suspended ${r.org} at ${r.suspendedAt}: ${r.projectsTrashed} project(s) trashed, ${r.usersSuspended} user(s) suspended, ${r.tokensRevoked} token(s) revoked.`);
28
+ }
29
+ export async function adminUnsuspend(opts) {
30
+ if (!opts.org)
31
+ throw new CliError("Usage: nerd admin unsuspend --org <slug>");
32
+ const r = await operatorPost("/v1/admin/unsuspend", { orgSlug: opts.org });
33
+ if (opts.json)
34
+ return printJson(r);
35
+ log.ok(`Unsuspended ${r.org}: ${r.usersRestored} user(s) restored. Trashed projects stay in the trash until restored with \`nerd trash restore\`.`);
36
+ }
@@ -13,8 +13,13 @@ export declare function runChecks(root: string, opts: {
13
13
  remote: boolean;
14
14
  typecheck: boolean;
15
15
  }): Promise<CheckResult>;
16
+ /**
17
+ * `nerd check` (and its alias `nerd verify`): every gate a deploy runs, plus a check that the linked
18
+ * project still exists. One command, one output shape, so agents learn one thing.
19
+ */
16
20
  export declare function check(opts: {
17
21
  json: boolean;
18
- "no-typecheck": boolean;
22
+ "no-build": boolean;
19
23
  "no-remote": boolean;
24
+ rule?: string | undefined;
20
25
  }): Promise<void>;
@@ -4,6 +4,7 @@ import { ApiClient } from "../api.js";
4
4
  import { findProjectRoot, projectPath, readNerdJson } from "../project.js";
5
5
  import { CliError, log, run } from "../util.js";
6
6
  import { staleAgentFiles } from "./agents.js";
7
+ import { gateSummary, skippedGates } from "../gates/types.js";
7
8
  const ALLOWED_BINDINGS = new Set(["DB", "STORAGE", "ASSETS"]);
8
9
  /** Removes line and block comments and trailing commas from JSONC without touching string contents. */
9
10
  export function stripJsonComments(input) {
@@ -126,20 +127,37 @@ export async function runChecks(root, opts) {
126
127
  }
127
128
  return { ok: problems.length === 0, problems };
128
129
  }
130
+ /**
131
+ * `nerd check` (and its alias `nerd verify`): every gate a deploy runs, plus a check that the linked
132
+ * project still exists. One command, one output shape, so agents learn one thing.
133
+ */
129
134
  export async function check(opts) {
130
135
  const root = findProjectRoot();
131
- const result = await runChecks(root, { remote: !opts["no-remote"], typecheck: !opts["no-typecheck"] });
132
- if (opts.json) {
133
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
134
- }
135
- else if (result.ok) {
136
- log.ok("All checks passed");
136
+ const problems = [];
137
+ if (!opts["no-remote"] && !opts.rule) {
138
+ const nerd = readNerdJson(root);
139
+ try {
140
+ await ApiClient.fromConfig().get(projectPath(nerd));
141
+ }
142
+ catch (err) {
143
+ problems.push(`The linked project ${nerd.org}/${nerd.project} is not reachable: ${err.message}`);
144
+ }
137
145
  }
146
+ // Loaded here, not at the top: the gates import staticChecks from this file.
147
+ const { runVerify } = await import("./verify.js");
148
+ const out = await runVerify(root, { withBuild: !opts["no-build"], stopOnFail: false, quiet: opts.json, onlyRules: opts.rule ? [opts.rule] : undefined });
149
+ const failed = out.results.filter((r) => r.status === "fail").length;
150
+ if (opts.json)
151
+ process.stdout.write(`${JSON.stringify({ ok: out.ok && problems.length === 0, failures: out.failures, stop: out.stop, problems, skipped: skippedGates(out.results) }, null, 2)}\n`);
138
152
  else {
139
- for (const p of result.problems)
153
+ for (const p of problems)
140
154
  log.warn(p);
155
+ if (out.ok && problems.length === 0)
156
+ log.ok(gateSummary(out.results));
157
+ const human = out.failures.filter((f) => f.audience === "human" && f.severity === "block");
158
+ if (human.length)
159
+ log.warn(`${human.length} finding(s) need a person, not the agent: ${human.map((f) => f.rule_id).join(", ")}`);
141
160
  }
142
- const blocking = result.problems.filter((p) => !p.endsWith("(warning)") && !p.startsWith("Agent instruction files"));
143
- if (blocking.length)
144
- throw new CliError(`${blocking.length} problem(s) found`, 2);
161
+ if (failed || problems.length)
162
+ throw new CliError(failed ? `${failed} gate(s) failed. Fix what is listed above, then run nerd check again.` : problems[0], 2);
145
163
  }
@@ -7,3 +7,11 @@ export declare function dbMigrate(opts: {
7
7
  export declare function dbMigrations(opts: {
8
8
  json: boolean;
9
9
  }): Promise<void>;
10
+ /**
11
+ * `nerd db export`: asks the API for a SQL dump of the production database and downloads it. With
12
+ * --json the response is printed and nothing is downloaded, so scripts can fetch the URL themselves.
13
+ */
14
+ export declare function dbExport(opts: {
15
+ json: boolean;
16
+ out?: string | undefined;
17
+ }): Promise<void>;
@@ -30,3 +30,28 @@ export async function dbMigrations(opts) {
30
30
  return printJson(r);
31
31
  table(r.migrations, ["name", "appliedAt"]);
32
32
  }
33
+ /**
34
+ * `nerd db export`: asks the API for a SQL dump of the production database and downloads it. With
35
+ * --json the response is printed and nothing is downloaded, so scripts can fetch the URL themselves.
36
+ */
37
+ export async function dbExport(opts) {
38
+ const nerd = readNerdJson(findProjectRoot());
39
+ const r = await ApiClient.fromConfig().post(projectPath(nerd, "/db/export"), {});
40
+ if (opts.json)
41
+ return printJson(r);
42
+ if (r.url.startsWith("fake://")) {
43
+ log.info(`Export ready at ${r.url} (dev mode, nothing downloaded). ${r.note}`);
44
+ return;
45
+ }
46
+ const { writeFileSync } = await import("node:fs");
47
+ const path = await import("node:path");
48
+ const out = path.resolve(opts.out ?? `${nerd.project}-${new Date().toISOString().slice(0, 10)}.sql`);
49
+ const res = await fetch(r.url).catch((err) => {
50
+ throw new CliError(`Could not download the export: ${err.message}`);
51
+ });
52
+ if (!res.ok)
53
+ throw new CliError(`Download failed (${res.status}). The link may have expired${r.expiresAt ? ` at ${r.expiresAt}` : ""}; run \`nerd db export\` again.`);
54
+ writeFileSync(out, Buffer.from(await res.arrayBuffer()));
55
+ log.ok(`Wrote ${out}`);
56
+ log.info(r.note);
57
+ }
@@ -2,9 +2,9 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { ApiClient } from "../api.js";
4
4
  import { findProjectRoot, projectPath, readNerdJson } from "../project.js";
5
- import { CliError, log, printJson, runOrThrow, table } from "../util.js";
5
+ import { CliError, log, printJson, runOrThrow, table, dashboardUrl, ensureDeps } from "../util.js";
6
6
  import { runChecks } from "./check.js";
7
- import { BUILD_GATES, knownSecretsFrom, runGates, SOURCE_GATES } from "../gates/index.js";
7
+ import { BUILD_GATES, gateSummary, knownSecretsFrom, runGates, SOURCE_GATES } 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) {
@@ -72,10 +72,11 @@ export async function deploy(opts) {
72
72
  const gates = await runGates(SOURCE_GATES, { root, knownSecrets, approvedSuppressions: await approvedSuppressions(root), log: (l) => log.info(l) }, { stopOnFail: true });
73
73
  if (!gates.ok)
74
74
  throw new CliError(`Blocked by: ${gates.failed.map((g) => g.title).join(", ")}. Fix the findings above, then deploy again.`, 2);
75
- log.ok("Gates passed");
75
+ log.ok(gateSummary(gates.results));
76
76
  }
77
77
  if (!opts["skip-build"]) {
78
78
  log.step("Building with vite");
79
+ ensureDeps(root);
79
80
  await runOrThrow("npx", ["vite", "build"], { cwd: root });
80
81
  }
81
82
  const build = readBuildOutput(root);
@@ -109,6 +110,7 @@ export async function deploy(opts) {
109
110
  if (opts.json)
110
111
  return printJson({ deployment, url: project.url, migrations: mig });
111
112
  log.ok(`Deployed ${deployment.id} (${deployment.status}) → ${project.url}`);
113
+ 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`);
112
114
  }
113
115
  export async function deployments(opts) {
114
116
  const nerd = readNerdJson(findProjectRoot());
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { ApiClient } from "../api.js";
4
4
  import { findProjectRoot, projectPath, readDevVars, readNerdJson, writeDevVars } from "../project.js";
5
- import { log, run, runOrThrow } from "../util.js";
5
+ import { log, run, runOrThrow, ensureDeps } from "../util.js";
6
6
  /** Makes sure .dev.vars carries the platform bindings for local dev, minting a dev tenant secret once. */
7
7
  export async function ensureDevVars(root) {
8
8
  const nerd = readNerdJson(root);
@@ -33,6 +33,7 @@ export async function dev(extra) {
33
33
  await runOrThrow("npx", ["wrangler", "d1", "migrations", "apply", "DB", "--local"], { cwd: root });
34
34
  }
35
35
  log.step("Starting vite dev");
36
+ ensureDeps(root);
36
37
  const code = await run("npx", ["vite", ...extra], { cwd: root });
37
38
  process.exitCode = code;
38
39
  }
@@ -30,7 +30,7 @@ export async function doctor(opts) {
30
30
  }
31
31
  for (const tool of ["gitleaks", "semgrep", "osv-scanner"]) {
32
32
  const ok = await hasTool(tool);
33
- checks.push({ name: `${tool} (optional)`, ok, detail: ok ? "installed" : "not installed; the builder runs it for you", fix: `Optional for \`nerd verify\` locally.` });
33
+ checks.push({ name: `${tool} (optional)`, ok, detail: ok ? "installed" : "not installed; git builds run it in the builder, but `nerd check` and `nerd deploy` from this machine skip it", fix: `Optional for \`nerd verify\` locally.` });
34
34
  }
35
35
  if (opts.json)
36
36
  return printJson({ ok: checks.filter((c) => !c.name.includes("optional")).every((c) => c.ok), checks });
@@ -7,6 +7,7 @@ export declare function githubLink(fullName: string | undefined, opts: {
7
7
  "root-dir"?: string;
8
8
  "no-previews": boolean;
9
9
  "require-pr": boolean;
10
+ agent?: string;
10
11
  installation?: string;
11
12
  }): Promise<void>;
12
13
  export declare function githubUnlink(opts: {
@@ -10,9 +10,9 @@ export async function githubConnect(opts) {
10
10
  }
11
11
  export async function githubLink(fullName, opts) {
12
12
  if (!fullName)
13
- throw new CliError("Usage: nerd github link owner/repo [--branch main] [--root-dir apps/web] [--no-previews] [--require-pr]");
13
+ throw new CliError("Usage: nerd github link owner/repo [--branch main] [--root-dir apps/web] [--no-previews] [--require-pr] [--agent @claude]");
14
14
  const nerd = readNerdJson(findProjectRoot());
15
- const body = { fullName, previews: !opts["no-previews"], rootDir: opts["root-dir"] ?? "", requirePr: opts["require-pr"] };
15
+ const body = { fullName, previews: !opts["no-previews"], rootDir: opts["root-dir"] ?? "", requirePr: opts["require-pr"], ...(opts.agent ? { agentMention: opts.agent } : {}) };
16
16
  if (opts.branch)
17
17
  body.productionBranch = opts.branch;
18
18
  if (opts.installation)