nerdsystem 0.0.6 → 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 +11 -0
- package/dist/api.js +3 -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.js +3 -2
- package/dist/commands/db.d.ts +8 -0
- package/dist/commands/db.js +25 -0
- package/dist/commands/deploy.js +2 -2
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/init.js +55 -15
- 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 -262
- package/dist/util.d.ts +10 -1
- package/dist/util.js +9 -1
- package/package.json +1 -1
- package/template/.claude/skills/nerdsystem/SKILL.md +7 -2
- package/template/.cursor/rules/nerdsystem.mdc +8 -3
- package/template/.github/copilot-instructions.md +8 -3
- package/template/AGENTS.md +8 -3
- package/template/GEMINI.md +8 -3
- package/template/package.json +6 -0
package/README.md
CHANGED
|
@@ -11,3 +11,14 @@ 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
|
}
|
|
@@ -38,7 +38,7 @@ export class ApiClient {
|
|
|
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}
|
|
41
|
+
throw new CliError(`${err?.message ?? "request failed"} [${err?.code ?? "error"} ${res.status}]${details}`, 1, { code: err?.code ?? "error", status: res.status });
|
|
42
42
|
}
|
|
43
43
|
return json;
|
|
44
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.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) {
|
|
@@ -147,12 +148,12 @@ export async function check(opts) {
|
|
|
147
148
|
const out = await runVerify(root, { withBuild: !opts["no-build"], stopOnFail: false, quiet: opts.json, onlyRules: opts.rule ? [opts.rule] : undefined });
|
|
148
149
|
const failed = out.results.filter((r) => r.status === "fail").length;
|
|
149
150
|
if (opts.json)
|
|
150
|
-
process.stdout.write(`${JSON.stringify({ ok: out.ok && problems.length === 0, failures: out.failures, stop: out.stop, problems }, null, 2)}\n`);
|
|
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`);
|
|
151
152
|
else {
|
|
152
153
|
for (const p of problems)
|
|
153
154
|
log.warn(p);
|
|
154
155
|
if (out.ok && problems.length === 0)
|
|
155
|
-
log.ok(
|
|
156
|
+
log.ok(gateSummary(out.results));
|
|
156
157
|
const human = out.failures.filter((f) => f.audience === "human" && f.severity === "block");
|
|
157
158
|
if (human.length)
|
|
158
159
|
log.warn(`${human.length} finding(s) need a person, not the agent: ${human.map((f) => f.rule_id).join(", ")}`);
|
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
|
@@ -4,7 +4,7 @@ import { ApiClient } from "../api.js";
|
|
|
4
4
|
import { findProjectRoot, projectPath, readNerdJson } from "../project.js";
|
|
5
5
|
import { CliError, log, printJson, runOrThrow, table, dashboardUrl, ensureDeps } from "../util.js";
|
|
6
6
|
import { runChecks } from "./check.js";
|
|
7
|
-
import { BUILD_GATES, 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,7 +72,7 @@ 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");
|
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/init.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -41,7 +42,7 @@ export async function linkProject(root, args) {
|
|
|
41
42
|
log.ok(`Linked to existing project ${org}/${args.slug}`);
|
|
42
43
|
}
|
|
43
44
|
catch (err) {
|
|
44
|
-
if (!args.create || !(err instanceof CliError) ||
|
|
45
|
+
if (!args.create || !(err instanceof CliError) || err.code !== "not_found")
|
|
45
46
|
throw err;
|
|
46
47
|
log.step(`Creating project ${org}/${args.slug} (provisions a database and a bucket)`);
|
|
47
48
|
project = (await api.post(`/v1/orgs/${org}/projects`, { slug: args.slug, name: args.name })).project;
|
|
@@ -69,6 +70,50 @@ function seedDescription(root, name, description) {
|
|
|
69
70
|
const heading = `# ${name}\n\n${description ? `${description}\n\n` : "(Describe what this app does here; the agent starts from it.)\n\n"}`;
|
|
70
71
|
writeFileSync(p, heading + readFileSync(p, "utf8").replace(/^# This app runs on nerdsystem\n/, "## This app runs on nerdsystem\n"));
|
|
71
72
|
}
|
|
73
|
+
/** Ten lines a person can read before opening the agent. */
|
|
74
|
+
function writeReadme(root, name, description) {
|
|
75
|
+
const lines = [
|
|
76
|
+
`# ${name}`,
|
|
77
|
+
"",
|
|
78
|
+
description ?? "An app built with a coding agent and deployed on nerdsystem.",
|
|
79
|
+
"",
|
|
80
|
+
"- `nerd dev` runs it locally with real sign-in.",
|
|
81
|
+
"- `nerd check` runs every gate a deploy runs (secrets, dependencies, platform rules, types).",
|
|
82
|
+
"- `nerd deploy` checks, builds, migrates, and ships it; the URL is printed at the end.",
|
|
83
|
+
"",
|
|
84
|
+
"Guides for each coding agent: https://nerdsystem.com/guides",
|
|
85
|
+
"",
|
|
86
|
+
];
|
|
87
|
+
writeFileSync(path.join(root, "README.md"), lines.join("\n"));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A repository from the first minute: deploy-on-push and the agent's commits both expect one. Skipped
|
|
91
|
+
* silently when git is missing, and when the folder already sits inside a repository.
|
|
92
|
+
*/
|
|
93
|
+
function initGitRepo(root) {
|
|
94
|
+
try {
|
|
95
|
+
try {
|
|
96
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root, stdio: "ignore" });
|
|
97
|
+
return; // already a repository, or inside one
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// not a repository yet
|
|
101
|
+
}
|
|
102
|
+
execFileSync("git", ["init", "-q"], { cwd: root, stdio: "ignore" });
|
|
103
|
+
execFileSync("git", ["add", "-A"], { cwd: root, stdio: "ignore" });
|
|
104
|
+
const commit = ["commit", "-q", "-m", "Start from the nerdsystem template"];
|
|
105
|
+
try {
|
|
106
|
+
execFileSync("git", commit, { cwd: root, stdio: "ignore" });
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// No identity configured on this machine yet: commit under a placeholder rather than leave the tree uncommitted.
|
|
110
|
+
execFileSync("git", ["-c", "user.name=nerd", "-c", "user.email=nerd@nerdsystem.com", ...commit], { cwd: root, stdio: "ignore" });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// git missing or not configured: fine, the person can init later
|
|
115
|
+
}
|
|
116
|
+
}
|
|
72
117
|
export async function init(dir, args) {
|
|
73
118
|
if (!dir)
|
|
74
119
|
throw new CliError("Usage: nerd init <directory> [--org slug] [--name 'My App'] [--slug my-app]");
|
|
@@ -98,6 +143,7 @@ export async function init(dir, args) {
|
|
|
98
143
|
// The published template carries ".gitignore" as "gitignore" (npm strips the dotted name from packages).
|
|
99
144
|
if (existsSync(path.join(root, "gitignore")) && !existsSync(path.join(root, ".gitignore")))
|
|
100
145
|
renameSync(path.join(root, "gitignore"), path.join(root, ".gitignore"));
|
|
146
|
+
const description = args.describe ?? (await askDescription());
|
|
101
147
|
// Rename the app and point the SDK dependency somewhere installable.
|
|
102
148
|
const pkgPath = path.join(root, "package.json");
|
|
103
149
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
@@ -109,7 +155,12 @@ export async function init(dir, args) {
|
|
|
109
155
|
const p = path.join(root, f);
|
|
110
156
|
writeFileSync(p, readFileSync(p, "utf8").replaceAll("nerd-app", slug));
|
|
111
157
|
}
|
|
112
|
-
|
|
158
|
+
// The visible brand in the client: the display name, unless it would break JSX text.
|
|
159
|
+
const display = /[<>{}]/.test(name) ? slug : name;
|
|
160
|
+
const appTsx = path.join(root, "src/client/App.tsx");
|
|
161
|
+
if (existsSync(appTsx))
|
|
162
|
+
writeFileSync(appTsx, readFileSync(appTsx, "utf8").replaceAll("nerd-app", display));
|
|
163
|
+
writeReadme(root, name, description);
|
|
113
164
|
seedDescription(root, name, description);
|
|
114
165
|
let project;
|
|
115
166
|
try {
|
|
@@ -119,19 +170,8 @@ export async function init(dir, args) {
|
|
|
119
170
|
const reason = err instanceof Error ? err.message : String(err);
|
|
120
171
|
throw new CliError(`The folder is ready but the project was not created: ${reason}\nFix that, then run: cd ${dir} && nerd link --slug ${slug}${args.org ? ` --org ${args.org}` : ""}`);
|
|
121
172
|
}
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
const { execFileSync } = await import("node:child_process");
|
|
125
|
-
if (!existsSync(path.join(root, ".git"))) {
|
|
126
|
-
execFileSync("git", ["init", "-q"], { cwd: root, stdio: "ignore" });
|
|
127
|
-
execFileSync("git", ["add", "-A"], { cwd: root, stdio: "ignore" });
|
|
128
|
-
execFileSync("git", ["commit", "-q", "-m", "Start from the nerdsystem template"], { cwd: root, stdio: "ignore" });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
// git missing or not configured: fine, the person can init later
|
|
133
|
-
}
|
|
173
|
+
initGitRepo(root);
|
|
134
174
|
log.ok(`Ready. Next:\n cd ${dir}\n pnpm install\n nerd dev\n\nThen open the folder in your coding agent and ask it to build the first feature; the nerdsystem skill walks it through.\nDeploys go to ${project.url}`);
|
|
135
175
|
}
|
|
136
176
|
// Kept in sync with packages/sdk/package.json by the release script.
|
|
137
|
-
const SDK_VERSION = "0.0.
|
|
177
|
+
const SDK_VERSION = "0.0.7";
|
package/dist/commands/org.js
CHANGED
|
@@ -76,7 +76,7 @@ export async function logs(opts) {
|
|
|
76
76
|
if (!r.events.length)
|
|
77
77
|
return log.info(`No log events for ${r.scriptName} in the last ${opts.since ?? "30m"}.`);
|
|
78
78
|
for (const e of [...r.events].reverse())
|
|
79
|
-
process.stdout.write(`${e.timestamp} ${e.level.padEnd(5)} ${e.context ? `${e.context} ` : ""}${e.message}\n`);
|
|
79
|
+
process.stdout.write(`${e.timestamp} ${e.level.padEnd(5)} ${e.context && e.context !== e.message ? `${e.context} ` : ""}${e.message}\n`);
|
|
80
80
|
}
|
|
81
81
|
export async function auditExport(opts) {
|
|
82
82
|
const org = orgSlug(opts);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** `nerd delete`: the app goes offline now; the API keeps the data for a grace period and can restore it. */
|
|
2
|
+
export declare function deleteProject(opts: {
|
|
3
|
+
json: boolean;
|
|
4
|
+
yes: boolean;
|
|
5
|
+
org?: string | undefined;
|
|
6
|
+
slug?: string | undefined;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
/** `nerd trash`: deleted projects still inside their grace period. */
|
|
9
|
+
export declare function trashList(opts: {
|
|
10
|
+
json: boolean;
|
|
11
|
+
org?: string | undefined;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
/** `nerd trash restore <slug>`: undo a delete. The app stays offline until the next deploy or rollback. */
|
|
14
|
+
export declare function trashRestore(slug: string | undefined, opts: {
|
|
15
|
+
json: boolean;
|
|
16
|
+
org?: string | undefined;
|
|
17
|
+
}): Promise<void>;
|