nerdsystem 0.0.0-stage → 0.0.2

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 (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -2
  3. package/dist/api.d.ts +58 -0
  4. package/dist/api.js +59 -0
  5. package/dist/commands/agents.d.ts +9 -0
  6. package/dist/commands/agents.js +45 -0
  7. package/dist/commands/auth.d.ts +7 -0
  8. package/dist/commands/auth.js +25 -0
  9. package/dist/commands/billing.d.ts +14 -0
  10. package/dist/commands/billing.js +42 -0
  11. package/dist/commands/builds.d.ts +12 -0
  12. package/dist/commands/builds.js +40 -0
  13. package/dist/commands/check.d.ts +20 -0
  14. package/dist/commands/check.js +145 -0
  15. package/dist/commands/db.d.ts +9 -0
  16. package/dist/commands/db.js +32 -0
  17. package/dist/commands/deploy.d.ts +27 -0
  18. package/dist/commands/deploy.js +119 -0
  19. package/dist/commands/dev.d.ts +3 -0
  20. package/dist/commands/dev.js +38 -0
  21. package/dist/commands/doctor.d.ts +4 -0
  22. package/dist/commands/doctor.js +42 -0
  23. package/dist/commands/github.d.ts +17 -0
  24. package/dist/commands/github.js +49 -0
  25. package/dist/commands/init.d.ts +15 -0
  26. package/dist/commands/init.js +107 -0
  27. package/dist/commands/login.d.ts +7 -0
  28. package/dist/commands/login.js +21 -0
  29. package/dist/commands/ops.d.ts +29 -0
  30. package/dist/commands/ops.js +113 -0
  31. package/dist/commands/org.d.ts +47 -0
  32. package/dist/commands/org.js +121 -0
  33. package/dist/commands/verify.d.ts +24 -0
  34. package/dist/commands/verify.js +50 -0
  35. package/dist/config.d.ts +7 -0
  36. package/dist/config.js +22 -0
  37. package/dist/gates/attempts.d.ts +18 -0
  38. package/dist/gates/attempts.js +38 -0
  39. package/dist/gates/code.d.ts +15 -0
  40. package/dist/gates/code.js +179 -0
  41. package/dist/gates/fs.d.ts +7 -0
  42. package/dist/gates/fs.js +35 -0
  43. package/dist/gates/index.d.ts +26 -0
  44. package/dist/gates/index.js +68 -0
  45. package/dist/gates/rules.d.ts +19 -0
  46. package/dist/gates/rules.js +58 -0
  47. package/dist/gates/secrets.d.ts +11 -0
  48. package/dist/gates/secrets.js +98 -0
  49. package/dist/gates/suppressions.d.ts +2 -0
  50. package/dist/gates/suppressions.js +57 -0
  51. package/dist/gates/text.d.ts +31 -0
  52. package/dist/gates/text.js +72 -0
  53. package/dist/gates/tools.d.ts +5 -0
  54. package/dist/gates/tools.js +69 -0
  55. package/dist/gates/types.d.ts +72 -0
  56. package/dist/gates/types.js +18 -0
  57. package/dist/index.d.ts +2 -0
  58. package/dist/index.js +268 -0
  59. package/dist/project.d.ts +11 -0
  60. package/dist/project.js +51 -0
  61. package/dist/util.d.ts +21 -0
  62. package/dist/util.js +39 -0
  63. package/package.json +55 -3
  64. package/rules/semgrep.yml +77 -0
  65. package/template/.claude/settings.json +12 -0
  66. package/template/.claude/skills/nerdsystem/SKILL.md +123 -0
  67. package/template/.cursor/mcp.json +8 -0
  68. package/template/.cursor/rules/nerdsystem.mdc +124 -0
  69. package/template/.gemini/settings.json +8 -0
  70. package/template/.github/copilot-instructions.md +120 -0
  71. package/template/.github/dependabot.yml +14 -0
  72. package/template/.mcp.json +8 -0
  73. package/template/.vscode/mcp.json +9 -0
  74. package/template/AGENTS.md +120 -0
  75. package/template/CLAUDE.md +32 -0
  76. package/template/GEMINI.md +120 -0
  77. package/template/drizzle.config.ts +7 -0
  78. package/template/gitignore +7 -0
  79. package/template/index.html +12 -0
  80. package/template/migrations/0000_init.sql +7 -0
  81. package/template/nerd.json +5 -0
  82. package/template/package.json +33 -0
  83. package/template/scripts/sync-agents.mjs +28 -0
  84. package/template/src/client/App.tsx +182 -0
  85. package/template/src/client/api.ts +42 -0
  86. package/template/src/client/main.tsx +10 -0
  87. package/template/src/client/styles.css +16 -0
  88. package/template/src/client/vite-env.d.ts +1 -0
  89. package/template/src/server/db/index.ts +7 -0
  90. package/template/src/server/db/schema.ts +14 -0
  91. package/template/src/server/index.ts +23 -0
  92. package/template/src/server/routes/todos.ts +57 -0
  93. package/template/tsconfig.client.json +18 -0
  94. package/template/tsconfig.json +4 -0
  95. package/template/tsconfig.worker.json +18 -0
  96. package/template/vite.config.ts +7 -0
  97. package/template/wrangler.jsonc +18 -0
package/dist/index.js ADDED
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { authConfig, authSecret } from "./commands/auth.js";
4
+ import { check } from "./commands/check.js";
5
+ import { dbMigrate, dbMigrations, dbQuery } from "./commands/db.js";
6
+ import { deploy, deployments } from "./commands/deploy.js";
7
+ import { dev } from "./commands/dev.js";
8
+ import { doctor } from "./commands/doctor.js";
9
+ import { cron, health, rollback, secretList, secretRemove, secretSet, transfer, waitlist } from "./commands/ops.js";
10
+ import { verify } from "./commands/verify.js";
11
+ import { agentsSync } from "./commands/agents.js";
12
+ import { billingPortal, billingShow, billingUpgrade } from "./commands/billing.js";
13
+ import { auditExport, clientsCreate, clientsList, clientsRelease, domainsAdd, domainsList, domainsRemove, logs, membersInvite, membersList, membersRemove } from "./commands/org.js";
14
+ import { buildsList, buildsShow, buildsTrigger } from "./commands/builds.js";
15
+ import { githubConnect, githubLink, githubStatus, githubUnlink } from "./commands/github.js";
16
+ import { init, linkProject } from "./commands/init.js";
17
+ import { login, whoami } from "./commands/login.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 --token nsk_... Save an API token (also: NERD_TOKEN, NERD_API_URL)
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 Verify platform rules and typecheck
29
+ nerd verify [--no-build] [--rule id] [--json] Run the security gates; --json gives failures the agent can fix
30
+ nerd deploy [--note "what changed"] Check, build, migrate, upload. Prints the URL
31
+ nerd rollback [deploymentId] Re-publish the previous production bundle
32
+ nerd cron [off|hourly|daily] Scheduled job: the platform calls POST /api/cron
33
+ nerd transfer <org> Move this project to another org you administer
34
+ nerd health Platform component health (operators)
35
+ nerd waitlist [approve <id>] Who is waiting for access while sign-up is invite-only (operators)
36
+ nerd deployments List recent deployments
37
+ nerd db query "<sql>" Run SQL on the production database
38
+ nerd db migrate Apply pending migrations to production
39
+ nerd db migrations List applied migrations
40
+ nerd auth config [--roles a,b] [--default-role r] [--signup on|off]
41
+ nerd auth secret Mint a dev tenant secret into .dev.vars
42
+ nerd secret set NAME [value] App secret for production (c.env.NAME); value from stdin when omitted
43
+ nerd secret list | rm NAME Names only; values are never shown
44
+ nerd github connect Install the GitHub App for this org (prints a link)
45
+ nerd github link owner/repo [--branch main] [--root-dir dir] [--no-previews] [--require-pr]
46
+ nerd github status | unlink
47
+ nerd builds [--limit n] List git-triggered builds
48
+ nerd builds show <id> Build details and log
49
+ nerd builds trigger [--branch b] [--commit sha]
50
+ nerd agents sync Regenerate AGENTS.md, Cursor, Copilot, Gemini files from the skill
51
+ nerd billing [--org slug] Plan, limits, and usage for the org
52
+ nerd billing upgrade --plan pro|studio [--annual] Start checkout
53
+ nerd billing portal Manage cards, invoices, and plan
54
+ nerd members [invite <email> --role r | remove <userId>] Org members and invitations
55
+ nerd clients [create <slug> --name n | release <slug>] Studio: one org per client, billed through yours
56
+ nerd domains [add <hostname> | remove <id>] Custom domains for this project
57
+ nerd logs [--since 30m] [--limit 100] [--q text] [--pr n] Logs from the deployed app
58
+ nerd audit [--out file.csv] Export deployments and builds (Studio)
59
+
60
+ Every command accepts --json for machine-readable output.`;
61
+ const BOOL = { type: "boolean", default: false };
62
+ const STR = { type: "string" };
63
+ async function main(argv) {
64
+ const [cmd, ...rest] = argv;
65
+ if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
66
+ console.log(HELP);
67
+ return;
68
+ }
69
+ const parse = (options, allowPositionals = true) => parseArgs({ args: rest, options: { json: BOOL, ...options }, allowPositionals, strict: cmd !== "dev" });
70
+ switch (cmd) {
71
+ case "login": {
72
+ const { values } = parse({ token: STR, "api-url": STR });
73
+ return login(values);
74
+ }
75
+ case "doctor": {
76
+ const { values } = parse({});
77
+ return doctor({ json: Boolean(values.json) });
78
+ }
79
+ case "whoami": {
80
+ const { values } = parse({});
81
+ return whoami({ json: Boolean(values.json) });
82
+ }
83
+ case "init": {
84
+ const { values, positionals } = parse({ org: STR, name: STR, slug: STR, describe: STR, "no-create": BOOL });
85
+ return init(positionals[0], values);
86
+ }
87
+ case "link": {
88
+ const { values } = parse({ org: STR, slug: STR, name: STR, "no-create": BOOL });
89
+ if (!values.slug)
90
+ throw new CliError("Usage: nerd link --slug <project> [--org <org>] [--name 'My App']");
91
+ const root = (() => {
92
+ try {
93
+ return findProjectRoot();
94
+ }
95
+ catch {
96
+ return process.cwd();
97
+ }
98
+ })();
99
+ await linkProject(root, { ...(values.org ? { org: values.org } : {}), slug: values.slug, name: values.name ?? values.slug, create: !values["no-create"] });
100
+ return;
101
+ }
102
+ case "dev":
103
+ return dev(rest);
104
+ case "check": {
105
+ const { values } = parse({ "no-typecheck": BOOL, "no-remote": BOOL });
106
+ return check(values);
107
+ }
108
+ case "verify": {
109
+ const { values } = parse({ "no-build": BOOL, rule: STR });
110
+ return verify({ json: Boolean(values.json), "no-build": Boolean(values["no-build"]), ...(values.rule ? { rule: values.rule } : {}) });
111
+ }
112
+ case "deploy": {
113
+ const { values } = parse({ "skip-check": BOOL, "skip-build": BOOL, note: STR });
114
+ return deploy(values);
115
+ }
116
+ case "rollback": {
117
+ const { values, positionals } = parse({ note: STR });
118
+ return rollback(positionals[0], { json: Boolean(values.json), ...(values.note ? { note: values.note } : {}) });
119
+ }
120
+ case "cron": {
121
+ const { values, positionals } = parse({});
122
+ return cron(positionals[0], { json: Boolean(values.json) });
123
+ }
124
+ case "transfer": {
125
+ const { values, positionals } = parse({});
126
+ return transfer(positionals[0], { json: Boolean(values.json) });
127
+ }
128
+ case "health": {
129
+ const { values } = parse({});
130
+ return health({ json: Boolean(values.json) });
131
+ }
132
+ case "waitlist": {
133
+ const [sub, ...subRest] = rest;
134
+ const { values, positionals } = parseArgs({ args: sub === "approve" ? subRest : rest, options: { json: BOOL }, allowPositionals: true });
135
+ return waitlist({ json: Boolean(values.json), ...(sub === "approve" ? { approve: positionals[0] } : {}) });
136
+ }
137
+ case "deployments": {
138
+ const { values } = parse({ limit: { type: "string", default: "10" } });
139
+ return deployments(values);
140
+ }
141
+ case "db": {
142
+ const [sub, ...subRest] = rest;
143
+ const { values, positionals } = parseArgs({ args: subRest, options: { json: BOOL }, allowPositionals: true });
144
+ if (sub === "query")
145
+ return dbQuery(positionals[0], { json: Boolean(values.json) });
146
+ if (sub === "migrate")
147
+ return dbMigrate({ json: Boolean(values.json) });
148
+ if (sub === "migrations")
149
+ return dbMigrations({ json: Boolean(values.json) });
150
+ throw new CliError("Usage: nerd db query|migrate|migrations");
151
+ }
152
+ case "auth": {
153
+ const [sub, ...subRest] = rest;
154
+ const { values } = parseArgs({ args: subRest, options: { json: BOOL, roles: STR, "default-role": STR, signup: STR }, allowPositionals: false });
155
+ if (sub === "config")
156
+ return authConfig(values);
157
+ if (sub === "secret")
158
+ return authSecret();
159
+ throw new CliError("Usage: nerd auth config|secret");
160
+ }
161
+ case "billing": {
162
+ const [sub, ...subRest] = rest;
163
+ const isSub = sub === "upgrade" || sub === "portal" || sub === "show";
164
+ const { values } = parseArgs({ args: isSub ? subRest : rest, options: { json: BOOL, org: STR, plan: STR, annual: BOOL }, allowPositionals: false });
165
+ const o = { json: Boolean(values.json), ...(values.org ? { org: values.org } : {}) };
166
+ if (sub === "upgrade")
167
+ return billingUpgrade({ ...o, ...(values.plan ? { plan: values.plan } : {}), annual: Boolean(values.annual) });
168
+ if (sub === "portal")
169
+ return billingPortal(o);
170
+ return billingShow(o);
171
+ }
172
+ case "members": {
173
+ const [sub, ...subRest] = rest;
174
+ const isSub = sub === "invite" || sub === "remove" || sub === "list";
175
+ const { values, positionals } = parseArgs({ args: isSub ? subRest : rest, options: { json: BOOL, org: STR, role: STR }, allowPositionals: true });
176
+ const o = { json: Boolean(values.json), ...(values.org ? { org: values.org } : {}) };
177
+ if (sub === "invite")
178
+ return membersInvite(positionals[0], { ...o, ...(values.role ? { role: values.role } : {}) });
179
+ if (sub === "remove")
180
+ return membersRemove(positionals[0], o);
181
+ return membersList(o);
182
+ }
183
+ case "clients": {
184
+ const [sub, ...subRest] = rest;
185
+ const isSub = sub === "create" || sub === "release" || sub === "list";
186
+ const { values, positionals } = parseArgs({ args: isSub ? subRest : rest, options: { json: BOOL, org: STR, name: STR }, allowPositionals: true });
187
+ const o = { json: Boolean(values.json), ...(values.org ? { org: values.org } : {}) };
188
+ if (sub === "create")
189
+ return clientsCreate(positionals[0], { ...o, ...(values.name ? { name: values.name } : {}) });
190
+ if (sub === "release")
191
+ return clientsRelease(positionals[0], o);
192
+ return clientsList(o);
193
+ }
194
+ case "secret": {
195
+ const [sub, ...subRest] = rest;
196
+ const { values, positionals } = parseArgs({ args: subRest, options: { json: BOOL }, allowPositionals: true });
197
+ const json = Boolean(values.json);
198
+ if (sub === "set")
199
+ return secretSet(positionals[0], positionals[1], { json });
200
+ if (sub === "rm" || sub === "remove" || sub === "delete")
201
+ return secretRemove(positionals[0], { json });
202
+ if (sub === "list" || sub === "ls" || sub === undefined)
203
+ return secretList({ json });
204
+ throw new CliError("Usage: nerd secret set NAME [value] | list | rm NAME");
205
+ }
206
+ case "domains": {
207
+ const [sub, ...subRest] = rest;
208
+ const isSub = sub === "add" || sub === "remove" || sub === "list";
209
+ const { values, positionals } = parseArgs({ args: isSub ? subRest : rest, options: { json: BOOL }, allowPositionals: true });
210
+ const o = { json: Boolean(values.json) };
211
+ if (sub === "add")
212
+ return domainsAdd(positionals[0], o);
213
+ if (sub === "remove")
214
+ return domainsRemove(positionals[0], o);
215
+ return domainsList(o);
216
+ }
217
+ case "logs": {
218
+ const { values } = parse({ since: STR, limit: STR, q: STR, pr: STR });
219
+ 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 } : {}) });
220
+ }
221
+ case "audit": {
222
+ const { values } = parse({ org: STR, out: STR });
223
+ return auditExport({ json: Boolean(values.json), ...(values.org ? { org: values.org } : {}), ...(values.out ? { out: values.out } : {}) });
224
+ }
225
+ case "agents": {
226
+ const [sub, ...subRest] = rest;
227
+ const { values } = parseArgs({ args: subRest, options: { json: BOOL }, allowPositionals: false });
228
+ if (sub === "sync")
229
+ return agentsSync({ json: Boolean(values.json) });
230
+ throw new CliError("Usage: nerd agents sync");
231
+ }
232
+ case "github": {
233
+ const [sub, ...subRest] = rest;
234
+ const { values, positionals } = parseArgs({ args: subRest, options: { json: BOOL, branch: STR, "root-dir": STR, "no-previews": BOOL, "require-pr": BOOL, installation: STR }, allowPositionals: true });
235
+ const json = Boolean(values.json);
236
+ if (sub === "connect")
237
+ return githubConnect({ json });
238
+ if (sub === "link")
239
+ return githubLink(positionals[0], { json, "no-previews": Boolean(values["no-previews"]), "require-pr": Boolean(values["require-pr"]), ...(values.branch ? { branch: values.branch } : {}), ...(values["root-dir"] ? { "root-dir": values["root-dir"] } : {}), ...(values.installation ? { installation: values.installation } : {}) });
240
+ if (sub === "unlink")
241
+ return githubUnlink({ json });
242
+ if (sub === "status" || sub === undefined)
243
+ return githubStatus({ json });
244
+ throw new CliError("Usage: nerd github connect|link|unlink|status");
245
+ }
246
+ case "builds": {
247
+ const [sub, ...subRest] = rest;
248
+ const isSub = sub === "show" || sub === "trigger" || sub === "list";
249
+ const { values, positionals } = parseArgs({ args: isSub ? subRest : rest, options: { json: BOOL, limit: { type: "string", default: "10" }, branch: STR, commit: STR }, allowPositionals: true });
250
+ const json = Boolean(values.json);
251
+ if (sub === "show")
252
+ return buildsShow(positionals[0], { json });
253
+ if (sub === "trigger")
254
+ return buildsTrigger({ json, ...(values.branch ? { branch: values.branch } : {}), ...(values.commit ? { commit: values.commit } : {}) });
255
+ return buildsList({ json, limit: values.limit });
256
+ }
257
+ default:
258
+ throw new CliError(`Unknown command "${cmd}". Run \`nerd help\`.`);
259
+ }
260
+ }
261
+ main(process.argv.slice(2)).catch((err) => {
262
+ if (err instanceof CliError) {
263
+ log.warn(err.message);
264
+ process.exit(err.exitCode);
265
+ }
266
+ console.error(err);
267
+ process.exit(1);
268
+ });
@@ -0,0 +1,11 @@
1
+ export interface NerdJson {
2
+ org: string;
3
+ project: string;
4
+ }
5
+ export declare function findProjectRoot(start?: string): string;
6
+ export declare function readNerdJson(root: string): NerdJson;
7
+ export declare function writeNerdJson(root: string, value: NerdJson): void;
8
+ export declare function projectPath(p: NerdJson, suffix?: string): string;
9
+ /** Reads .dev.vars as a simple key=value map. Values may be single- or double-quoted. */
10
+ export declare function readDevVars(root: string): Record<string, string>;
11
+ export declare function writeDevVars(root: string, vars: Record<string, string>): void;
@@ -0,0 +1,51 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { CliError } from "./util.js";
4
+ export function findProjectRoot(start = process.cwd()) {
5
+ let dir = start;
6
+ for (;;) {
7
+ if (existsSync(path.join(dir, "nerd.json")))
8
+ return dir;
9
+ const parent = path.dirname(dir);
10
+ if (parent === dir)
11
+ throw new CliError("No nerd.json found. Run this inside a nerdsystem app, or create one with `nerd init <dir>`.");
12
+ dir = parent;
13
+ }
14
+ }
15
+ export function readNerdJson(root) {
16
+ const raw = JSON.parse(readFileSync(path.join(root, "nerd.json"), "utf8"));
17
+ if (!raw.org || !raw.project)
18
+ throw new CliError("nerd.json is missing org or project. Run `nerd link`.");
19
+ return { org: raw.org, project: raw.project };
20
+ }
21
+ export function writeNerdJson(root, value) {
22
+ const existing = existsSync(path.join(root, "nerd.json")) ? JSON.parse(readFileSync(path.join(root, "nerd.json"), "utf8")) : {};
23
+ writeFileSync(path.join(root, "nerd.json"), `${JSON.stringify({ ...existing, ...value }, null, 2)}\n`);
24
+ }
25
+ export function projectPath(p, suffix = "") {
26
+ return `/v1/orgs/${p.org}/projects/${p.project}${suffix}`;
27
+ }
28
+ /** Reads .dev.vars as a simple key=value map. Values may be single- or double-quoted. */
29
+ export function readDevVars(root) {
30
+ const file = path.join(root, ".dev.vars");
31
+ if (!existsSync(file))
32
+ return {};
33
+ const out = {};
34
+ for (const line of readFileSync(file, "utf8").split("\n")) {
35
+ const m = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/.exec(line);
36
+ if (!m)
37
+ continue;
38
+ let v = m[2];
39
+ if ((v.startsWith("'") && v.endsWith("'")) || (v.startsWith('"') && v.endsWith('"')))
40
+ v = v.slice(1, -1);
41
+ out[m[1]] = v;
42
+ }
43
+ return out;
44
+ }
45
+ export function writeDevVars(root, vars) {
46
+ const merged = { ...readDevVars(root), ...vars };
47
+ const body = Object.entries(merged)
48
+ .map(([k, v]) => `${k}='${v.replace(/'/g, "'\\''")}'`)
49
+ .join("\n");
50
+ writeFileSync(path.join(root, ".dev.vars"), `${body}\n`, { mode: 0o600 });
51
+ }
package/dist/util.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ export declare class CliError extends Error {
2
+ readonly exitCode: number;
3
+ constructor(message: string, exitCode?: number);
4
+ }
5
+ export declare const log: {
6
+ info: (msg: string) => void;
7
+ step: (msg: string) => void;
8
+ ok: (msg: string) => void;
9
+ warn: (msg: string) => void;
10
+ };
11
+ /** Runs a command, streaming output. Resolves with the exit code. */
12
+ export declare function run(cmd: string, args: string[], opts?: {
13
+ cwd?: string;
14
+ env?: NodeJS.ProcessEnv;
15
+ }): Promise<number>;
16
+ export declare function runOrThrow(cmd: string, args: string[], opts?: {
17
+ cwd?: string;
18
+ env?: NodeJS.ProcessEnv;
19
+ }): Promise<void>;
20
+ export declare function printJson(value: unknown): void;
21
+ export declare function table(rows: Record<string, unknown>[], columns: string[]): void;
package/dist/util.js ADDED
@@ -0,0 +1,39 @@
1
+ import { spawn } from "node:child_process";
2
+ export class CliError extends Error {
3
+ exitCode;
4
+ constructor(message, exitCode = 1) {
5
+ super(message);
6
+ this.exitCode = exitCode;
7
+ }
8
+ }
9
+ export const log = {
10
+ info: (msg) => console.error(`${msg}`),
11
+ step: (msg) => console.error(`\x1b[36m›\x1b[0m ${msg}`),
12
+ ok: (msg) => console.error(`\x1b[32m✓\x1b[0m ${msg}`),
13
+ warn: (msg) => console.error(`\x1b[33m!\x1b[0m ${msg}`),
14
+ };
15
+ /** Runs a command, streaming output. Resolves with the exit code. */
16
+ export function run(cmd, args, opts = {}) {
17
+ return new Promise((resolve, reject) => {
18
+ const child = spawn(cmd, args, { cwd: opts.cwd, env: { ...process.env, ...opts.env }, stdio: "inherit", shell: process.platform === "win32" });
19
+ child.on("error", reject);
20
+ child.on("exit", (code) => resolve(code ?? 1));
21
+ });
22
+ }
23
+ export async function runOrThrow(cmd, args, opts = {}) {
24
+ const code = await run(cmd, args, opts);
25
+ if (code !== 0)
26
+ throw new CliError(`${cmd} ${args.join(" ")} exited with ${code}`, code);
27
+ }
28
+ export function printJson(value) {
29
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
30
+ }
31
+ export function table(rows, columns) {
32
+ if (rows.length === 0)
33
+ return;
34
+ const widths = columns.map((c) => Math.max(c.length, ...rows.map((r) => String(r[c] ?? "").length)));
35
+ const line = (cells) => cells.map((v, i) => v.padEnd(widths[i])).join(" ");
36
+ process.stdout.write(`${line(columns)}\n`);
37
+ for (const r of rows)
38
+ process.stdout.write(`${line(columns.map((c) => String(r[c] ?? "")))}\n`);
39
+ }
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "nerdsystem",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.2",
4
+ "description": "nerd: deploy apps to nerdsystem from the terminal or a coding agent.",
5
+ "type": "module",
6
+ "bin": {
7
+ "nerd": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "rules",
12
+ "template",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=22"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.0.0",
21
+ "typescript": "^5.9.3",
22
+ "vitest": "^4.1.0"
23
+ },
24
+ "exports": {
25
+ ".": "./dist/index.js",
26
+ "./gates": "./dist/gates/index.js",
27
+ "./gates/text": "./dist/gates/text.js",
28
+ "./gates/rules": "./dist/gates/rules.js",
29
+ "./project": "./dist/project.js",
30
+ "./api": "./dist/api.js",
31
+ "./commands/check": "./dist/commands/check.js",
32
+ "./commands/deploy": "./dist/commands/deploy.js",
33
+ "./commands/verify": "./dist/commands/verify.js"
34
+ },
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/doobe01/nerdsystem-cloudflare",
39
+ "directory": "packages/cli"
40
+ },
41
+ "homepage": "https://nerdsystem.com",
42
+ "keywords": [
43
+ "cloudflare",
44
+ "deploy",
45
+ "cli",
46
+ "claude-code",
47
+ "vibe-coding"
48
+ ],
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "scripts": {
53
+ "build": "tsc -p tsconfig.build.json && chmod +x dist/index.js",
54
+ "typecheck": "tsc --noEmit",
55
+ "sync-template": "node scripts/sync-template.mjs",
56
+ "test": "vitest run"
57
+ }
6
58
  }
@@ -0,0 +1,77 @@
1
+ # nerdsystem Semgrep rules. Run by the builder (and by `nerd verify` when semgrep is installed).
2
+ # Severity ERROR blocks a deploy; WARNING is advisory.
3
+ rules:
4
+ - id: nerd-raw-sql-interpolation
5
+ languages: [typescript, javascript]
6
+ severity: ERROR
7
+ message: SQL built with string interpolation. Use Drizzle's query builder or sql`` with parameters.
8
+ patterns:
9
+ - pattern-either:
10
+ - pattern: $DB.prepare(`...${$X}...`)
11
+ - pattern: $DB.exec(`...${$X}...`)
12
+ - pattern: sql.raw(`...${$X}...`)
13
+ - id: nerd-eval
14
+ languages: [typescript, javascript]
15
+ severity: ERROR
16
+ message: eval and new Function execute arbitrary code; not allowed in hosted apps.
17
+ pattern-either:
18
+ - pattern: eval(...)
19
+ - pattern: new Function(...)
20
+ - id: nerd-dangerously-set-inner-html
21
+ languages: [typescript, javascript]
22
+ severity: WARNING
23
+ message: dangerouslySetInnerHTML with a non-literal value can inject scripts. Sanitize or avoid.
24
+ patterns:
25
+ - pattern: dangerouslySetInnerHTML={{ __html: $X }}
26
+ - pattern-not: dangerouslySetInnerHTML={{ __html: "..." }}
27
+ - id: nerd-innerhtml-assignment
28
+ languages: [typescript, javascript]
29
+ severity: WARNING
30
+ message: Assigning to innerHTML from a variable can inject scripts.
31
+ patterns:
32
+ - pattern: $EL.innerHTML = $X
33
+ - pattern-not: $EL.innerHTML = "..."
34
+ - id: nerd-hand-rolled-password-check
35
+ languages: [typescript, javascript]
36
+ severity: ERROR
37
+ message: Comparing passwords in app code. Authentication is provided by the platform; do not store or compare passwords.
38
+ pattern-either:
39
+ - pattern: $A.password === $B
40
+ - pattern: $A.password == $B
41
+ - pattern: bcrypt.compare(...)
42
+ - pattern: bcrypt.hash(...)
43
+ - id: nerd-jwt-in-app
44
+ languages: [typescript, javascript]
45
+ severity: WARNING
46
+ message: Signing or verifying tokens in app code. Sessions are handled by the SDK; use c.var.user.
47
+ pattern-either:
48
+ - pattern: jwt.sign(...)
49
+ - pattern: jwt.verify(...)
50
+ - pattern: sign($PAYLOAD, $SECRET, ...)
51
+ - id: nerd-open-redirect
52
+ languages: [typescript, javascript]
53
+ severity: WARNING
54
+ message: Redirecting to a value from the request can send users to attacker sites. Allow-list destinations.
55
+ patterns:
56
+ - pattern-either:
57
+ - pattern: c.redirect(c.req.query(...))
58
+ - pattern: c.redirect($REQ.query.$X)
59
+ - id: nerd-fetch-to-user-url
60
+ languages: [typescript, javascript]
61
+ severity: WARNING
62
+ message: Fetching a URL taken from the request can reach internal services (SSRF). Validate the host.
63
+ patterns:
64
+ - pattern-either:
65
+ - pattern: fetch(c.req.query(...), ...)
66
+ - pattern: fetch((await c.req.json()).$X, ...)
67
+ - id: nerd-math-random-for-secrets
68
+ languages: [typescript, javascript]
69
+ severity: WARNING
70
+ message: Math.random is not a secure source for tokens or ids used in security decisions. Use crypto.randomUUID or getRandomValues.
71
+ patterns:
72
+ - pattern-inside: |
73
+ $TOKEN = ...
74
+ - pattern: Math.random()
75
+ - metavariable-regex:
76
+ metavariable: $TOKEN
77
+ regex: (?i).*(token|secret|nonce|otp|code|key).*
@@ -0,0 +1,12 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(nerd check:*)",
5
+ "Bash(nerd deployments:*)",
6
+ "Bash(nerd db migrations:*)",
7
+ "Bash(nerd auth config)",
8
+ "Bash(pnpm db:generate)",
9
+ "Bash(pnpm typecheck)"
10
+ ]
11
+ }
12
+ }