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.
- package/LICENSE +21 -0
- package/README.md +12 -2
- package/dist/api.d.ts +58 -0
- package/dist/api.js +59 -0
- package/dist/commands/agents.d.ts +9 -0
- package/dist/commands/agents.js +45 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/auth.js +25 -0
- package/dist/commands/billing.d.ts +14 -0
- package/dist/commands/billing.js +42 -0
- package/dist/commands/builds.d.ts +12 -0
- package/dist/commands/builds.js +40 -0
- package/dist/commands/check.d.ts +20 -0
- package/dist/commands/check.js +145 -0
- package/dist/commands/db.d.ts +9 -0
- package/dist/commands/db.js +32 -0
- package/dist/commands/deploy.d.ts +27 -0
- package/dist/commands/deploy.js +119 -0
- package/dist/commands/dev.d.ts +3 -0
- package/dist/commands/dev.js +38 -0
- package/dist/commands/doctor.d.ts +4 -0
- package/dist/commands/doctor.js +42 -0
- package/dist/commands/github.d.ts +17 -0
- package/dist/commands/github.js +49 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +107 -0
- package/dist/commands/login.d.ts +7 -0
- package/dist/commands/login.js +21 -0
- package/dist/commands/ops.d.ts +29 -0
- package/dist/commands/ops.js +113 -0
- package/dist/commands/org.d.ts +47 -0
- package/dist/commands/org.js +121 -0
- package/dist/commands/verify.d.ts +24 -0
- package/dist/commands/verify.js +50 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +22 -0
- package/dist/gates/attempts.d.ts +18 -0
- package/dist/gates/attempts.js +38 -0
- package/dist/gates/code.d.ts +15 -0
- package/dist/gates/code.js +179 -0
- package/dist/gates/fs.d.ts +7 -0
- package/dist/gates/fs.js +35 -0
- package/dist/gates/index.d.ts +26 -0
- package/dist/gates/index.js +68 -0
- package/dist/gates/rules.d.ts +19 -0
- package/dist/gates/rules.js +58 -0
- package/dist/gates/secrets.d.ts +11 -0
- package/dist/gates/secrets.js +98 -0
- package/dist/gates/suppressions.d.ts +2 -0
- package/dist/gates/suppressions.js +57 -0
- package/dist/gates/text.d.ts +31 -0
- package/dist/gates/text.js +72 -0
- package/dist/gates/tools.d.ts +5 -0
- package/dist/gates/tools.js +69 -0
- package/dist/gates/types.d.ts +72 -0
- package/dist/gates/types.js +18 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +268 -0
- package/dist/project.d.ts +11 -0
- package/dist/project.js +51 -0
- package/dist/util.d.ts +21 -0
- package/dist/util.js +39 -0
- package/package.json +55 -3
- package/rules/semgrep.yml +77 -0
- package/template/.claude/settings.json +12 -0
- package/template/.claude/skills/nerdsystem/SKILL.md +123 -0
- package/template/.cursor/mcp.json +8 -0
- package/template/.cursor/rules/nerdsystem.mdc +124 -0
- package/template/.gemini/settings.json +8 -0
- package/template/.github/copilot-instructions.md +120 -0
- package/template/.github/dependabot.yml +14 -0
- package/template/.mcp.json +8 -0
- package/template/.vscode/mcp.json +9 -0
- package/template/AGENTS.md +120 -0
- package/template/CLAUDE.md +32 -0
- package/template/GEMINI.md +120 -0
- package/template/drizzle.config.ts +7 -0
- package/template/gitignore +7 -0
- package/template/index.html +12 -0
- package/template/migrations/0000_init.sql +7 -0
- package/template/nerd.json +5 -0
- package/template/package.json +33 -0
- package/template/scripts/sync-agents.mjs +28 -0
- package/template/src/client/App.tsx +182 -0
- package/template/src/client/api.ts +42 -0
- package/template/src/client/main.tsx +10 -0
- package/template/src/client/styles.css +16 -0
- package/template/src/client/vite-env.d.ts +1 -0
- package/template/src/server/db/index.ts +7 -0
- package/template/src/server/db/schema.ts +14 -0
- package/template/src/server/index.ts +23 -0
- package/template/src/server/routes/todos.ts +57 -0
- package/template/tsconfig.client.json +18 -0
- package/template/tsconfig.json +4 -0
- package/template/tsconfig.worker.json +18 -0
- package/template/vite.config.ts +7 -0
- 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;
|
package/dist/project.js
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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).*
|