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.
- package/README.md +12 -1
- package/dist/api.js +4 -3
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +311 -0
- package/dist/commands/admin.d.ts +10 -0
- package/dist/commands/admin.js +36 -0
- package/dist/commands/check.d.ts +6 -1
- package/dist/commands/check.js +28 -10
- package/dist/commands/db.d.ts +8 -0
- package/dist/commands/db.js +25 -0
- package/dist/commands/deploy.js +5 -3
- package/dist/commands/dev.js +2 -1
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/github.d.ts +1 -0
- package/dist/commands/github.js +2 -2
- package/dist/commands/init.js +76 -6
- package/dist/commands/login.js +1 -1
- package/dist/commands/ops.d.ts +1 -0
- package/dist/commands/ops.js +3 -3
- package/dist/commands/org.js +1 -1
- package/dist/commands/trash.d.ts +17 -0
- package/dist/commands/trash.js +84 -0
- package/dist/commands/verify.js +3 -3
- package/dist/gates/code.d.ts +4 -1
- package/dist/gates/code.js +8 -5
- package/dist/gates/fs.js +1 -1
- package/dist/gates/index.d.ts +2 -1
- package/dist/gates/index.js +1 -0
- package/dist/gates/rules.js +6 -3
- package/dist/gates/secrets.d.ts +5 -0
- package/dist/gates/secrets.js +50 -10
- package/dist/gates/text.d.ts +39 -0
- package/dist/gates/text.js +82 -2
- package/dist/gates/types.d.ts +12 -0
- package/dist/gates/types.js +18 -0
- package/dist/index.js +2 -267
- package/dist/util.d.ts +14 -1
- package/dist/util.js +20 -1
- package/package.json +1 -1
- package/template/.claude/skills/nerdsystem/SKILL.md +12 -5
- package/template/.cursor/rules/nerdsystem.mdc +13 -6
- package/template/.github/copilot-instructions.md +13 -6
- package/template/AGENTS.md +13 -6
- package/template/GEMINI.md +13 -6
- package/template/package.json +6 -0
- 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
|
|
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(
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/commands/check.d.ts
CHANGED
|
@@ -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-
|
|
22
|
+
"no-build": boolean;
|
|
19
23
|
"no-remote": boolean;
|
|
24
|
+
rule?: string | undefined;
|
|
20
25
|
}): Promise<void>;
|
package/dist/commands/check.js
CHANGED
|
@@ -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
|
|
132
|
-
if (opts.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
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
|
-
|
|
143
|
-
|
|
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
|
}
|
package/dist/commands/db.d.ts
CHANGED
|
@@ -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>;
|
package/dist/commands/db.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -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(
|
|
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());
|
package/dist/commands/dev.js
CHANGED
|
@@ -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
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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
|
|
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 });
|
package/dist/commands/github.js
CHANGED
|
@@ -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)
|