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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nerdsystem
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# nerdsystem
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`nerd`: deploy apps built with coding agents to [nerdsystem](https://nerdsystem.com), with sign-in, roles, a database, file storage, previews per pull request, and security gates on every deploy.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx nerdsystem init my-app
|
|
7
|
+
cd my-app && pnpm install
|
|
8
|
+
nerd dev # run locally with real sign-in
|
|
9
|
+
nerd verify # secrets, dependencies, policy, PII, types
|
|
10
|
+
nerd deploy # checks, builds, migrates, ships
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Every command takes `--json` for agents. `nerd help` lists them all.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface ApiErrorBody {
|
|
2
|
+
error: {
|
|
3
|
+
code: string;
|
|
4
|
+
message: string;
|
|
5
|
+
details?: unknown;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export declare class ApiClient {
|
|
9
|
+
readonly apiUrl: string;
|
|
10
|
+
private readonly token;
|
|
11
|
+
constructor(apiUrl: string, token: string | undefined);
|
|
12
|
+
static fromConfig(opts?: {
|
|
13
|
+
requireAuth?: boolean;
|
|
14
|
+
}): ApiClient;
|
|
15
|
+
request<T>(method: string, path: string, body?: unknown | FormData): Promise<T>;
|
|
16
|
+
get<T>(path: string): Promise<T>;
|
|
17
|
+
post<T>(path: string, body?: unknown): Promise<T>;
|
|
18
|
+
put<T>(path: string, body: unknown): Promise<T>;
|
|
19
|
+
patch<T>(path: string, body: unknown): Promise<T>;
|
|
20
|
+
delete<T>(path: string): Promise<T>;
|
|
21
|
+
}
|
|
22
|
+
export interface Me {
|
|
23
|
+
user: {
|
|
24
|
+
id: string;
|
|
25
|
+
email: string;
|
|
26
|
+
name: string;
|
|
27
|
+
};
|
|
28
|
+
orgs: {
|
|
29
|
+
id: string;
|
|
30
|
+
slug: string;
|
|
31
|
+
name: string;
|
|
32
|
+
role: string;
|
|
33
|
+
}[];
|
|
34
|
+
}
|
|
35
|
+
export interface Project {
|
|
36
|
+
id: string;
|
|
37
|
+
orgId: string;
|
|
38
|
+
slug: string;
|
|
39
|
+
name: string;
|
|
40
|
+
scriptName: string;
|
|
41
|
+
url: string;
|
|
42
|
+
}
|
|
43
|
+
export interface Deployment {
|
|
44
|
+
id: string;
|
|
45
|
+
status: "uploading" | "live" | "failed";
|
|
46
|
+
moduleCount: number;
|
|
47
|
+
assetCount: number;
|
|
48
|
+
bundleBytes: number;
|
|
49
|
+
error: string | null;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
finishedAt: string | null;
|
|
52
|
+
}
|
|
53
|
+
export interface TenantConfig {
|
|
54
|
+
projectId: string;
|
|
55
|
+
roles: string[];
|
|
56
|
+
defaultRole: string;
|
|
57
|
+
allowSignup: boolean;
|
|
58
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { loadConfig } from "./config.js";
|
|
2
|
+
import { CliError } from "./util.js";
|
|
3
|
+
export class ApiClient {
|
|
4
|
+
apiUrl;
|
|
5
|
+
token;
|
|
6
|
+
constructor(apiUrl, token) {
|
|
7
|
+
this.apiUrl = apiUrl;
|
|
8
|
+
this.token = token;
|
|
9
|
+
}
|
|
10
|
+
static fromConfig(opts = {}) {
|
|
11
|
+
const cfg = loadConfig();
|
|
12
|
+
if (opts.requireAuth !== false && !cfg.token) {
|
|
13
|
+
throw new CliError("Not logged in. Run `nerd login --token nsk_...` (or set NERD_TOKEN).");
|
|
14
|
+
}
|
|
15
|
+
return new ApiClient(cfg.apiUrl, cfg.token);
|
|
16
|
+
}
|
|
17
|
+
async request(method, path, body) {
|
|
18
|
+
const isForm = body instanceof FormData;
|
|
19
|
+
const res = await fetch(`${this.apiUrl}${path}`, {
|
|
20
|
+
method,
|
|
21
|
+
headers: {
|
|
22
|
+
...(this.token ? { authorization: `Bearer ${this.token}` } : {}),
|
|
23
|
+
...(body !== undefined && !isForm ? { "content-type": "application/json" } : {}),
|
|
24
|
+
},
|
|
25
|
+
body: body === undefined ? null : isForm ? body : JSON.stringify(body),
|
|
26
|
+
}).catch((err) => {
|
|
27
|
+
throw new CliError(`Could not reach ${this.apiUrl}: ${err.message}`);
|
|
28
|
+
});
|
|
29
|
+
const text = await res.text();
|
|
30
|
+
let json = null;
|
|
31
|
+
try {
|
|
32
|
+
json = text ? JSON.parse(text) : null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new CliError(`API returned non-JSON (${res.status}): ${text.slice(0, 200)}`);
|
|
36
|
+
}
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
const err = json?.error;
|
|
39
|
+
const details = err?.details ? `\n${JSON.stringify(err.details, null, 2)}` : "";
|
|
40
|
+
throw new CliError(`${err?.code ?? "error"} (${res.status}): ${err?.message ?? "request failed"}${details}`);
|
|
41
|
+
}
|
|
42
|
+
return json;
|
|
43
|
+
}
|
|
44
|
+
get(path) {
|
|
45
|
+
return this.request("GET", path);
|
|
46
|
+
}
|
|
47
|
+
post(path, body) {
|
|
48
|
+
return this.request("POST", path, body);
|
|
49
|
+
}
|
|
50
|
+
put(path, body) {
|
|
51
|
+
return this.request("PUT", path, body);
|
|
52
|
+
}
|
|
53
|
+
patch(path, body) {
|
|
54
|
+
return this.request("PATCH", path, body);
|
|
55
|
+
}
|
|
56
|
+
delete(path) {
|
|
57
|
+
return this.request("DELETE", path);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function renderAgentFiles(root: string): {
|
|
2
|
+
files: Record<string, string>;
|
|
3
|
+
hash: string;
|
|
4
|
+
};
|
|
5
|
+
/** Returns the agent files that are missing or generated from an older skill. */
|
|
6
|
+
export declare function staleAgentFiles(root: string): string[];
|
|
7
|
+
export declare function agentsSync(opts: {
|
|
8
|
+
json: boolean;
|
|
9
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { findProjectRoot } from "../project.js";
|
|
5
|
+
import { CliError, log } from "../util.js";
|
|
6
|
+
/** The agent files generated from the skill, and the front matter each needs. */
|
|
7
|
+
const TARGETS = {
|
|
8
|
+
"AGENTS.md": (s, b) => `${s}\n\n${b}`,
|
|
9
|
+
"GEMINI.md": (s, b) => `${s}\n\n${b}`,
|
|
10
|
+
".github/copilot-instructions.md": (s, b) => `${s}\n\n${b}`,
|
|
11
|
+
".cursor/rules/nerdsystem.mdc": (s, b) => `---\ndescription: How to build, run, and deploy this app on nerdsystem. Applies to server, database, auth, and deploy work.\nalwaysApply: true\n---\n${s}\n\n${b}`,
|
|
12
|
+
};
|
|
13
|
+
export function renderAgentFiles(root) {
|
|
14
|
+
const skillPath = path.join(root, ".claude/skills/nerdsystem/SKILL.md");
|
|
15
|
+
if (!existsSync(skillPath))
|
|
16
|
+
throw new CliError("No .claude/skills/nerdsystem/SKILL.md in this project.");
|
|
17
|
+
const body = readFileSync(skillPath, "utf8").replace(/^---[\s\S]*?---\s*/, "");
|
|
18
|
+
const hash = createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
19
|
+
const stamp = `<!-- generated from .claude/skills/nerdsystem/SKILL.md (${hash}); edit the skill, then run: nerd agents sync -->`;
|
|
20
|
+
const files = {};
|
|
21
|
+
for (const [file, render] of Object.entries(TARGETS))
|
|
22
|
+
files[file] = render(stamp, body);
|
|
23
|
+
return { files, hash };
|
|
24
|
+
}
|
|
25
|
+
/** Returns the agent files that are missing or generated from an older skill. */
|
|
26
|
+
export function staleAgentFiles(root) {
|
|
27
|
+
const { files, hash } = renderAgentFiles(root);
|
|
28
|
+
return Object.keys(files).filter((f) => {
|
|
29
|
+
const p = path.join(root, f);
|
|
30
|
+
return !existsSync(p) || !readFileSync(p, "utf8").includes(`(${hash})`);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export async function agentsSync(opts) {
|
|
34
|
+
const root = findProjectRoot();
|
|
35
|
+
const { files, hash } = renderAgentFiles(root);
|
|
36
|
+
for (const [f, content] of Object.entries(files)) {
|
|
37
|
+
const p = path.join(root, f);
|
|
38
|
+
mkdirSync(path.dirname(p), { recursive: true });
|
|
39
|
+
writeFileSync(p, content);
|
|
40
|
+
}
|
|
41
|
+
if (opts.json)
|
|
42
|
+
process.stdout.write(`${JSON.stringify({ synced: Object.keys(files), hash })}\n`);
|
|
43
|
+
else
|
|
44
|
+
log.ok(`Synced ${Object.keys(files).length} agent files from the skill (${hash}): ${Object.keys(files).join(", ")}`);
|
|
45
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ApiClient } from "../api.js";
|
|
2
|
+
import { findProjectRoot, projectPath, readNerdJson } from "../project.js";
|
|
3
|
+
import { log, printJson } from "../util.js";
|
|
4
|
+
import { ensureDevVars } from "./dev.js";
|
|
5
|
+
export async function authConfig(opts) {
|
|
6
|
+
const nerd = readNerdJson(findProjectRoot());
|
|
7
|
+
const api = ApiClient.fromConfig();
|
|
8
|
+
const patch = {};
|
|
9
|
+
if (opts.roles)
|
|
10
|
+
patch.roles = opts.roles.split(",").map((r) => r.trim()).filter(Boolean);
|
|
11
|
+
if (opts["default-role"])
|
|
12
|
+
patch.defaultRole = opts["default-role"];
|
|
13
|
+
if (opts.signup)
|
|
14
|
+
patch.allowSignup = opts.signup === "on" || opts.signup === "true";
|
|
15
|
+
const r = Object.keys(patch).length
|
|
16
|
+
? await api.patch(projectPath(nerd, "/auth"), patch)
|
|
17
|
+
: await api.get(projectPath(nerd, "/auth"));
|
|
18
|
+
if (opts.json)
|
|
19
|
+
return printJson(r);
|
|
20
|
+
log.info(`roles: ${r.auth.roles.join(", ")}\ndefault role: ${r.auth.defaultRole}\nsignup: ${r.auth.allowSignup ? "on" : "off"}`);
|
|
21
|
+
}
|
|
22
|
+
export async function authSecret() {
|
|
23
|
+
await ensureDevVars(findProjectRoot());
|
|
24
|
+
log.ok("Development tenant secret is in .dev.vars");
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function billingShow(opts: {
|
|
2
|
+
json: boolean;
|
|
3
|
+
org?: string;
|
|
4
|
+
}): Promise<void>;
|
|
5
|
+
export declare function billingUpgrade(opts: {
|
|
6
|
+
json: boolean;
|
|
7
|
+
org?: string;
|
|
8
|
+
plan?: string;
|
|
9
|
+
annual?: boolean;
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
export declare function billingPortal(opts: {
|
|
12
|
+
json: boolean;
|
|
13
|
+
org?: string;
|
|
14
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ApiClient } from "../api.js";
|
|
2
|
+
import { findProjectRoot, readNerdJson } from "../project.js";
|
|
3
|
+
import { CliError, log, printJson } from "../util.js";
|
|
4
|
+
function orgSlug(opts) {
|
|
5
|
+
if (opts.org)
|
|
6
|
+
return opts.org;
|
|
7
|
+
return readNerdJson(findProjectRoot()).org;
|
|
8
|
+
}
|
|
9
|
+
const n = (v) => (v < 0 ? "unlimited" : String(v));
|
|
10
|
+
export async function billingShow(opts) {
|
|
11
|
+
const org = orgSlug(opts);
|
|
12
|
+
const b = await ApiClient.fromConfig().get(`/v1/orgs/${org}/billing`);
|
|
13
|
+
if (opts.json)
|
|
14
|
+
return printJson(b);
|
|
15
|
+
log.info(`${org}: ${b.limits.name} plan${b.subscriptionStatus ? ` (${b.subscriptionStatus})` : ""}`);
|
|
16
|
+
log.info(` projects ${b.usage.projects} of ${n(b.limits.projects)}`);
|
|
17
|
+
log.info(` active users ${b.usage.monthlyActiveUsers} of ${b.limits.monthlyActiveUsers} this month`);
|
|
18
|
+
log.info(` build minutes ${b.usage.buildMinutesThisMonth} of ${b.limits.buildMinutesPerMonth} this month`);
|
|
19
|
+
log.info(` members ${b.usage.members} of ${n(b.limits.members)}`);
|
|
20
|
+
for (const w of b.warnings)
|
|
21
|
+
log.warn(w);
|
|
22
|
+
if (b.billedThrough)
|
|
23
|
+
log.info(` billed through ${b.billedThrough} (a Studio client org)`);
|
|
24
|
+
if (b.effectivePlan === "free" && !b.billedThrough)
|
|
25
|
+
log.info(`\nUpgrade: nerd billing upgrade --plan pro ($${b.plans.pro?.priceUsdMonth}/mo) or --plan studio ($${b.plans.studio?.priceUsdMonth}/mo, agencies: one org per client)`);
|
|
26
|
+
}
|
|
27
|
+
export async function billingUpgrade(opts) {
|
|
28
|
+
const org = orgSlug(opts);
|
|
29
|
+
if (opts.plan !== "pro" && opts.plan !== "studio")
|
|
30
|
+
throw new CliError("Usage: nerd billing upgrade --plan pro|studio [--annual]");
|
|
31
|
+
const r = await ApiClient.fromConfig().post(`/v1/orgs/${org}/billing/checkout`, { plan: opts.plan, interval: opts.annual ? "annual" : "monthly", returnPath: `/app/${org}/billing` });
|
|
32
|
+
if (opts.json)
|
|
33
|
+
return printJson(r);
|
|
34
|
+
log.ok(`Open this link to complete checkout:\n\n ${r.url}`);
|
|
35
|
+
}
|
|
36
|
+
export async function billingPortal(opts) {
|
|
37
|
+
const org = orgSlug(opts);
|
|
38
|
+
const r = await ApiClient.fromConfig().post(`/v1/orgs/${org}/billing/portal`, { returnPath: `/app/${org}/billing` });
|
|
39
|
+
if (opts.json)
|
|
40
|
+
return printJson(r);
|
|
41
|
+
log.ok(`Manage cards, invoices, and your plan here:\n\n ${r.url}`);
|
|
42
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function buildsList(opts: {
|
|
2
|
+
json: boolean;
|
|
3
|
+
limit: string;
|
|
4
|
+
}): Promise<void>;
|
|
5
|
+
export declare function buildsShow(id: string | undefined, opts: {
|
|
6
|
+
json: boolean;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
export declare function buildsTrigger(opts: {
|
|
9
|
+
json: boolean;
|
|
10
|
+
branch?: string;
|
|
11
|
+
commit?: string;
|
|
12
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ApiClient } from "../api.js";
|
|
2
|
+
import { findProjectRoot, projectPath, readNerdJson } from "../project.js";
|
|
3
|
+
import { CliError, log, printJson, table } from "../util.js";
|
|
4
|
+
export async function buildsList(opts) {
|
|
5
|
+
const nerd = readNerdJson(findProjectRoot());
|
|
6
|
+
const r = await ApiClient.fromConfig().get(projectPath(nerd, `/builds?limit=${Number(opts.limit) || 10}`));
|
|
7
|
+
if (opts.json)
|
|
8
|
+
return printJson(r);
|
|
9
|
+
table(r.builds.map((b) => ({ id: b.id, status: b.status, env: b.environment, trigger: b.trigger, ref: b.prNumber ? `PR #${b.prNumber}` : b.branch, commit: b.commitSha.slice(0, 7), created: b.createdAt, error: (b.error ?? "").split("\n")[0] })), ["id", "status", "env", "trigger", "ref", "commit", "created", "error"]);
|
|
10
|
+
}
|
|
11
|
+
export async function buildsShow(id, opts) {
|
|
12
|
+
if (!id)
|
|
13
|
+
throw new CliError("Usage: nerd builds show <buildId>");
|
|
14
|
+
const nerd = readNerdJson(findProjectRoot());
|
|
15
|
+
const r = await ApiClient.fromConfig().get(projectPath(nerd, `/builds/${id}`));
|
|
16
|
+
if (opts.json)
|
|
17
|
+
return printJson(r);
|
|
18
|
+
const b = r.build;
|
|
19
|
+
log.info(`${b.id} ${b.status} ${b.environment} ${b.trigger} ${b.commitSha.slice(0, 7)} ${b.url}`);
|
|
20
|
+
if (b.error)
|
|
21
|
+
log.warn(b.error);
|
|
22
|
+
for (const c of b.checks ?? []) {
|
|
23
|
+
log.info(` ${c.status === "pass" ? "✓" : c.status === "fail" ? "✗" : c.status === "warn" ? "!" : "–"} ${c.title}: ${c.summary}`);
|
|
24
|
+
for (const f of c.findings.slice(0, 10))
|
|
25
|
+
log.info(` ${f.severity}: ${f.file ? `${f.file}${f.line ? `:${f.line}` : ""} ` : ""}${f.message}`);
|
|
26
|
+
}
|
|
27
|
+
process.stdout.write(r.log.endsWith("\n") || !r.log ? r.log : `${r.log}\n`);
|
|
28
|
+
}
|
|
29
|
+
export async function buildsTrigger(opts) {
|
|
30
|
+
const nerd = readNerdJson(findProjectRoot());
|
|
31
|
+
const body = {};
|
|
32
|
+
if (opts.branch)
|
|
33
|
+
body.branch = opts.branch;
|
|
34
|
+
if (opts.commit)
|
|
35
|
+
body.commitSha = opts.commit;
|
|
36
|
+
const r = await ApiClient.fromConfig().post(projectPath(nerd, "/builds"), body);
|
|
37
|
+
if (opts.json)
|
|
38
|
+
return printJson(r);
|
|
39
|
+
log.ok(`Queued build ${r.build.id} of ${r.build.branch} → ${r.build.url}\n Follow it with: nerd builds show ${r.build.id}`);
|
|
40
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface CheckResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
problems: string[];
|
|
4
|
+
}
|
|
5
|
+
/** Removes line and block comments and trailing commas from JSONC without touching string contents. */
|
|
6
|
+
export declare function stripJsonComments(input: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Static rules the platform relies on. These mirror the rules in the template's CLAUDE.md so an
|
|
9
|
+
* agent gets a concrete failure instead of a silent misdeploy.
|
|
10
|
+
*/
|
|
11
|
+
export declare function staticChecks(root: string): string[];
|
|
12
|
+
export declare function runChecks(root: string, opts: {
|
|
13
|
+
remote: boolean;
|
|
14
|
+
typecheck: boolean;
|
|
15
|
+
}): Promise<CheckResult>;
|
|
16
|
+
export declare function check(opts: {
|
|
17
|
+
json: boolean;
|
|
18
|
+
"no-typecheck": boolean;
|
|
19
|
+
"no-remote": boolean;
|
|
20
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ApiClient } from "../api.js";
|
|
4
|
+
import { findProjectRoot, projectPath, readNerdJson } from "../project.js";
|
|
5
|
+
import { CliError, log, run } from "../util.js";
|
|
6
|
+
import { staleAgentFiles } from "./agents.js";
|
|
7
|
+
const ALLOWED_BINDINGS = new Set(["DB", "STORAGE", "ASSETS"]);
|
|
8
|
+
/** Removes line and block comments and trailing commas from JSONC without touching string contents. */
|
|
9
|
+
export function stripJsonComments(input) {
|
|
10
|
+
let out = "";
|
|
11
|
+
let i = 0;
|
|
12
|
+
while (i < input.length) {
|
|
13
|
+
const ch = input[i];
|
|
14
|
+
const next = input[i + 1];
|
|
15
|
+
if (ch === '"') {
|
|
16
|
+
let j = i + 1;
|
|
17
|
+
while (j < input.length && input[j] !== '"')
|
|
18
|
+
j += input[j] === "\\" ? 2 : 1;
|
|
19
|
+
out += input.slice(i, j + 1);
|
|
20
|
+
i = j + 1;
|
|
21
|
+
}
|
|
22
|
+
else if (ch === "/" && next === "/") {
|
|
23
|
+
while (i < input.length && input[i] !== "\n")
|
|
24
|
+
i++;
|
|
25
|
+
}
|
|
26
|
+
else if (ch === "/" && next === "*") {
|
|
27
|
+
const end = input.indexOf("*/", i + 2);
|
|
28
|
+
i = end === -1 ? input.length : end + 2;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
out += ch;
|
|
32
|
+
i++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out.replace(/,(\s*[}\]])/g, "$1");
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Static rules the platform relies on. These mirror the rules in the template's CLAUDE.md so an
|
|
39
|
+
* agent gets a concrete failure instead of a silent misdeploy.
|
|
40
|
+
*/
|
|
41
|
+
export function staticChecks(root) {
|
|
42
|
+
const problems = [];
|
|
43
|
+
const entry = path.join(root, "src/server/index.ts");
|
|
44
|
+
if (!existsSync(entry)) {
|
|
45
|
+
problems.push("src/server/index.ts is missing; it must be the Worker entry.");
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const src = readFileSync(entry, "utf8");
|
|
49
|
+
if (!/from\s+["']@nerdsystem\/sdk["']/.test(src))
|
|
50
|
+
problems.push("src/server/index.ts must import from @nerdsystem/sdk.");
|
|
51
|
+
if (!/createNerdApp\s*</.test(src) && !/createNerdApp\s*\(/.test(src))
|
|
52
|
+
problems.push("src/server/index.ts must create the app with createNerdApp().");
|
|
53
|
+
if (/new\s+Hono\s*[<(]/.test(src))
|
|
54
|
+
problems.push("src/server/index.ts must not create a bare `new Hono()` root; use createNerdApp(). Sub-routers in other files are fine.");
|
|
55
|
+
if (!/export\s+default\s+app/.test(src))
|
|
56
|
+
problems.push("src/server/index.ts must `export default app`.");
|
|
57
|
+
}
|
|
58
|
+
const wranglerPath = ["wrangler.jsonc", "wrangler.json"].map((f) => path.join(root, f)).find(existsSync);
|
|
59
|
+
if (!wranglerPath) {
|
|
60
|
+
problems.push("wrangler.jsonc is missing.");
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
let cfg;
|
|
64
|
+
try {
|
|
65
|
+
cfg = JSON.parse(stripJsonComments(readFileSync(wranglerPath, "utf8")));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
problems.push(`${path.basename(wranglerPath)} is not valid JSON.`);
|
|
69
|
+
cfg = {};
|
|
70
|
+
}
|
|
71
|
+
const bindingNames = [];
|
|
72
|
+
for (const key of ["d1_databases", "r2_buckets", "kv_namespaces", "services", "queues", "vectorize", "hyperdrive", "ai", "browser"]) {
|
|
73
|
+
const v = cfg[key];
|
|
74
|
+
if (Array.isArray(v)) {
|
|
75
|
+
for (const b of v)
|
|
76
|
+
if (b.binding)
|
|
77
|
+
bindingNames.push(b.binding);
|
|
78
|
+
}
|
|
79
|
+
else if (v && typeof v === "object" && "binding" in v) {
|
|
80
|
+
bindingNames.push(String(v.binding));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (cfg.durable_objects)
|
|
84
|
+
problems.push("wrangler.jsonc: Durable Objects are not supported on nerdsystem yet.");
|
|
85
|
+
for (const b of bindingNames)
|
|
86
|
+
if (!ALLOWED_BINDINGS.has(b))
|
|
87
|
+
problems.push(`wrangler.jsonc: binding "${b}" is not provisioned by the platform. Allowed: ${[...ALLOWED_BINDINGS].join(", ")}.`);
|
|
88
|
+
const assets = cfg.assets;
|
|
89
|
+
if (!assets || !Array.isArray(assets.run_worker_first) || !assets.run_worker_first.includes("/api/*")) {
|
|
90
|
+
problems.push('wrangler.jsonc: assets.run_worker_first must include "/api/*" so API routes reach the Worker.');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
|
|
94
|
+
for (const [name, cmd] of Object.entries(pkg.scripts ?? {})) {
|
|
95
|
+
if (/wrangler\s+(deploy|publish|secret|d1\s+migrations\s+apply(?!.*--local))/.test(cmd))
|
|
96
|
+
problems.push(`package.json script "${name}" runs wrangler against Cloudflare directly. Use nerd commands.`);
|
|
97
|
+
}
|
|
98
|
+
if (!pkg.dependencies?.["@nerdsystem/sdk"])
|
|
99
|
+
problems.push("package.json must depend on @nerdsystem/sdk.");
|
|
100
|
+
if (!existsSync(path.join(root, ".github/dependabot.yml"))) {
|
|
101
|
+
problems.push("No .github/dependabot.yml: add it so dependency updates arrive as pull requests that get built and checked (warning).");
|
|
102
|
+
}
|
|
103
|
+
if (existsSync(path.join(root, ".claude/skills/nerdsystem/SKILL.md"))) {
|
|
104
|
+
const stale = staleAgentFiles(root);
|
|
105
|
+
if (stale.length)
|
|
106
|
+
problems.push(`Agent instruction files are out of date with the skill: ${stale.join(", ")}. Run \`nerd agents sync\`.`);
|
|
107
|
+
}
|
|
108
|
+
return problems;
|
|
109
|
+
}
|
|
110
|
+
export async function runChecks(root, opts) {
|
|
111
|
+
const problems = staticChecks(root);
|
|
112
|
+
if (opts.remote) {
|
|
113
|
+
const nerd = readNerdJson(root);
|
|
114
|
+
try {
|
|
115
|
+
await ApiClient.fromConfig().get(projectPath(nerd));
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
problems.push(`Remote project ${nerd.org}/${nerd.project} is not reachable: ${err.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (opts.typecheck && problems.every((p) => p.endsWith("(warning)") || p.startsWith("Agent instruction files"))) {
|
|
122
|
+
log.step("Typechecking");
|
|
123
|
+
const code = await run("npx", ["tsc", "-b", "--pretty"], { cwd: root });
|
|
124
|
+
if (code !== 0)
|
|
125
|
+
problems.push("Typecheck failed (see errors above).");
|
|
126
|
+
}
|
|
127
|
+
return { ok: problems.length === 0, problems };
|
|
128
|
+
}
|
|
129
|
+
export async function check(opts) {
|
|
130
|
+
const root = findProjectRoot();
|
|
131
|
+
const result = await runChecks(root, { remote: !opts["no-remote"], typecheck: !opts["no-typecheck"] });
|
|
132
|
+
if (opts.json) {
|
|
133
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
134
|
+
}
|
|
135
|
+
else if (result.ok) {
|
|
136
|
+
log.ok("All checks passed");
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
for (const p of result.problems)
|
|
140
|
+
log.warn(p);
|
|
141
|
+
}
|
|
142
|
+
const blocking = result.problems.filter((p) => !p.endsWith("(warning)") && !p.startsWith("Agent instruction files"));
|
|
143
|
+
if (blocking.length)
|
|
144
|
+
throw new CliError(`${blocking.length} problem(s) found`, 2);
|
|
145
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function dbQuery(sql: string | undefined, opts: {
|
|
2
|
+
json: boolean;
|
|
3
|
+
}): Promise<void>;
|
|
4
|
+
export declare function dbMigrate(opts: {
|
|
5
|
+
json: boolean;
|
|
6
|
+
}): Promise<void>;
|
|
7
|
+
export declare function dbMigrations(opts: {
|
|
8
|
+
json: boolean;
|
|
9
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ApiClient } from "../api.js";
|
|
2
|
+
import { findProjectRoot, projectPath, readNerdJson } from "../project.js";
|
|
3
|
+
import { CliError, log, printJson, table } from "../util.js";
|
|
4
|
+
import { applyRemoteMigrations } from "./deploy.js";
|
|
5
|
+
export async function dbQuery(sql, opts) {
|
|
6
|
+
if (!sql)
|
|
7
|
+
throw new CliError('Usage: nerd db query "select * from todos limit 5"');
|
|
8
|
+
const nerd = readNerdJson(findProjectRoot());
|
|
9
|
+
const r = await ApiClient.fromConfig().post(projectPath(nerd, "/db/query"), { sql });
|
|
10
|
+
if (opts.json)
|
|
11
|
+
return printJson(r);
|
|
12
|
+
for (const res of r.results) {
|
|
13
|
+
if (res.results.length)
|
|
14
|
+
table(res.results, Object.keys(res.results[0]));
|
|
15
|
+
else
|
|
16
|
+
log.info(` ok (${JSON.stringify(res.meta)})`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function dbMigrate(opts) {
|
|
20
|
+
const root = findProjectRoot();
|
|
21
|
+
const r = await applyRemoteMigrations(root, ApiClient.fromConfig());
|
|
22
|
+
if (opts.json)
|
|
23
|
+
return printJson(r);
|
|
24
|
+
log.ok(`Applied: ${r.applied.join(", ") || "none"}; already applied: ${r.skipped.length}`);
|
|
25
|
+
}
|
|
26
|
+
export async function dbMigrations(opts) {
|
|
27
|
+
const nerd = readNerdJson(findProjectRoot());
|
|
28
|
+
const r = await ApiClient.fromConfig().get(projectPath(nerd, "/db/migrations"));
|
|
29
|
+
if (opts.json)
|
|
30
|
+
return printJson(r);
|
|
31
|
+
table(r.migrations, ["name", "appliedAt"]);
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ApiClient } from "../api.js";
|
|
2
|
+
interface BuildOutput {
|
|
3
|
+
workerDir: string;
|
|
4
|
+
main: string;
|
|
5
|
+
modules: string[];
|
|
6
|
+
assetsDir: string | undefined;
|
|
7
|
+
assets: string[];
|
|
8
|
+
compatibilityDate: string | undefined;
|
|
9
|
+
compatibilityFlags: string[];
|
|
10
|
+
}
|
|
11
|
+
/** Locates the Vite build output: dist/<worker>/wrangler.json names the entry and the assets dir. */
|
|
12
|
+
export declare function readBuildOutput(root: string): BuildOutput;
|
|
13
|
+
export declare function applyRemoteMigrations(root: string, api: ApiClient): Promise<{
|
|
14
|
+
applied: string[];
|
|
15
|
+
skipped: string[];
|
|
16
|
+
}>;
|
|
17
|
+
export declare function deploy(opts: {
|
|
18
|
+
json: boolean;
|
|
19
|
+
"skip-check": boolean;
|
|
20
|
+
"skip-build": boolean;
|
|
21
|
+
note?: string | undefined;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
export declare function deployments(opts: {
|
|
24
|
+
json: boolean;
|
|
25
|
+
limit: string;
|
|
26
|
+
}): Promise<void>;
|
|
27
|
+
export {};
|