import * as fs from "node:fs" import * as path from "node:path" import spawn from "cross-spawn" import { randomBytes } from "node:crypto" import { getTemplate, pickTemplateForPrompt, STUB_CLIENT_TSX, STUB_SERVER_TS, TEMPLATES, Template, } from "./templates.js" // Run git with argv (no shell), throwing on failure so callers can try/catch // the same way they did around execSync. cross-spawn resolves git.exe on // Windows; argv form avoids shell-quoting the args entirely. function runGit(args: string[], cwd: string): void { const r = spawn.sync("git", args, { cwd, stdio: "ignore" }) if (r.error) throw r.error if (r.status !== 0) throw new Error(`git ${args[0]} exited with code ${r.status}`) } const POND_VERSION = JSON.parse(fs.readFileSync(path.resolve(import.meta.dirname, "../package.json"), "utf-8")) .version as string const BASE_ENV_TEMPLATE = `# Server-only environment variables # POND_SESSION_SECRET={{SESSION_SECRET}} # OPENAI_API_KEY=sk-... # ANTHROPIC_API_KEY=sk-... # HERMES_BASE_URL=http://127.0.0.1:8642 # GOOGLE_CLIENT_ID= # GOOGLE_CLIENT_SECRET= # GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google/callback # GITHUB_CLIENT_ID= # GITHUB_CLIENT_SECRET= # GITHUB_REDIRECT_URI=http://localhost:3000/auth/github/callback # EMAIL_FROM= # RESEND_API_KEY= ` const GITIGNORE = `node_modules .pond .env.pond.server ` const CAPSULE_CONTRACT = `## Pond capsule contract A pond capsule is one server file + one client file + a shared dir. ### server/index.ts \`\`\`ts import { capsule, mutation, query, table, string, number, boolean } from "pond/server" export default capsule({ schema: { items: table({ body: string(), done: boolean() }), }, queries: { items: query((ctx) => ctx.db.items.orderBy("createdAt", "desc").all()), }, mutations: { addItem: mutation((ctx, body: string) => ctx.db.items.insert({ body, done: false })), toggleItem: mutation((ctx, id: string) => { const row = ctx.db.items.get(id) if (row) ctx.db.items.update(id, { done: !row.done }) }), }, }) \`\`\` Every table auto-gets \`id\` (uuid), \`createdAt\`, \`updatedAt\`. Column helpers: \`string()\`, \`number()\`, \`boolean()\`. SQLite under the hood — booleans round-trip as 0/1. \`ctx.db.\` exposes \`.all() / .get(id) / .where(col, val) / .orderBy(col, dir) / .limit(n) / .insert({...}) / .update(id, {...}) / .delete(id)\`. Queries and mutations both take \`(ctx, ...args)\` — the args after \`ctx\` are the wire payload. Queries re-run reactively on the client whenever a mutation completes. \`ctx.ai\`, \`ctx.blob\`, \`ctx.shopify\` and per-route \`rateLimit\` are first-class — see https://pond.run/docs/api-reference.md. ### client/index.tsx Preact + Tailwind classes (use \`class\` not \`className\`). Use \`useQuery(name, ...args)\` and \`useMutation(name)\` from \`"pond/client"\`. Queries can take args — pass them after the name and they're spread into the server handler. \`\`\`tsx import { useMutation, useQuery } from "pond/client" export function App({ itemId }: { itemId: string }) { const { data, isLoading } = useQuery("items") // no-arg: GET const { data: item } = useQuery("itemById", itemId) // arg'd: POST { args: [itemId] } const [addItem] = useMutation<[body: string], void>("addItem") return
} \`\`\` ### Running it - \`npm install\` then \`npm run dev\` — local dev with hot reload at http://localhost:3000 - \`npm run deploy\` — anonymous deploy. You get a URL + a one-time claim token. ### Rules of thumb - Keep everything in \`server/index.ts\` and \`client/index.tsx\`. Add files under \`shared/\` only if both sides import them. - No separate API layer. Define mutations/queries on the server; call them by name from the client. - No \`fetch\` from the client to your own server — use \`useQuery\` / \`useMutation\`. - Tailwind classes are available out of the box (Tailwind v3 via CDN); no config needed. Use raw Tailwind utilities — there are **no preset \`.btn\` / \`.card\` / \`.input\` classes** and you should not invent any. - **Design intentionally — do not produce the AI default.** A pond capsule that looks like every other Claude/Cursor demo is a bug. Specifically avoid: - \`bg-zinc-950\` plus \`bg-zinc-900 border border-zinc-800 rounded-lg\` card stacks. Pick \`bg-black\` or a non-zinc neutral, and use \`divide-y\` / \`border-y\` rows or borderless lists instead of card-per-item. - The white-filled pill button (\`bg-zinc-100 text-zinc-950 rounded-lg px-5 py-3 font-semibold\`). Prefer wireframe buttons (\`border border-white px-4 py-2 font-medium\`) or a real brand color — never the universal AI-default white pill. - One font-family everywhere. Sprinkle \`font-mono\` on timestamps, indices, source tags, IDs — anything machine-flavored. - Timid \`text-2xl\` page titles. Page titles are display type: \`text-4xl\` / \`text-5xl\` \`font-bold tracking-tight\` minimum. - \`rounded-lg\` on everything. Square corners (no \`rounded-*\` except round avatars) read as intentional; universally rounded reads as defaulted. - Each capsule should have **one signature move** — a single deliberate choice that makes it not look templated. Examples: numbered mono indices on list rows, oversized \`tabular-nums\` numbers, a serif body type, a bottom-anchored composer, a colored source tag. ` function agentsMdContent(prompt: string, templateName: string): string { const isStub = templateName === "stub" const starter = isStub ? `\`server/index.ts\` and \`client/index.tsx\` are empty stubs. Replace them entirely with an implementation that matches the description above.` : `You're starting from the **${templateName}** template — read \`server/index.ts\` and \`client/index.tsx\` to see what's already there, then edit them in place to match the description.` // Stub mode = headless `pond new --generate`. The agent runs in -p mode, // can't open a browser, and any background `npm run dev` it spawns leaks // after exit. So in this mode we *forbid* verification and tell it to stop // as soon as the two files compile in its head. Without this constraint // claude burns 5–10 minutes looping on `curl localhost:3000` after writing // ~1KB of server code. const closing = isStub ? `Your job: write a complete implementation in \`server/index.ts\` and \`client/index.tsx\`. That is the entire job. **HARD RULES — do not violate:** - Do NOT run \`npm install\`. - Do NOT run \`npm run dev\`, \`pond dev\`, or start a server in any form (foreground OR background). - Do NOT \`curl\` / fetch / hit \`localhost:3000\` to "verify" — the dev server is not running and will not be running. - Do NOT loop trying to test the app. After both files are written and look correct on a read-through, **stop**. The human will run \`npm install && npm run dev\` themselves. - Exit as soon as both files contain a working implementation. You are running headlessly via \`claude -p\`; there is no human to ask, no browser to open, and no useful output beyond the two files.` : `Your job: implement the description. Add tables, queries, mutations, and UI to satisfy what the user asked for. When you're done, run \`npm install && npm run dev\` and verify the app works in a browser.` return `# Build instructions The user ran \`pond new\` with this description: > ${prompt.replace(/\n/g, "\n> ")} ${starter} ${closing} ### Canonical references If you need more detail than the contract below, fetch: - https://pond.run/llms-full.txt — full server + client API in one file - https://pond.run/docs/api-reference.md — server (\`pond/server\`) - https://pond.run/docs/client-reference.md — client (\`pond/client\`) ${CAPSULE_CONTRACT} ### When you're done Delete this file (\`AGENTS.md\`) and the \`.claude/\` / \`.cursor/\` directories — they were scaffolding for the build, not part of the app. ` } const CURSOR_RULES_HEADER = `--- description: Pond capsule contract — server + client API alwaysApply: true --- ` export interface CopyTemplateOptions { name: string templateName: string initGit: boolean prompt?: string // When true, write blank-canvas stubs instead of a template. Used by // `pond new --generate` so the agent has a clean slate to design from. useStub?: boolean } export interface CopyTemplateResult { // The template that was used. Null when `useStub: true`. template: Template | null dir: string } export async function copyTemplate(opts: CopyTemplateOptions): Promise export async function copyTemplate( name: string, templateName: string, initGit: boolean, prompt?: string, ): Promise export async function copyTemplate( nameOrOpts: string | CopyTemplateOptions, templateName?: string, initGit?: boolean, prompt?: string, ): Promise { const o: CopyTemplateOptions = typeof nameOrOpts === "string" ? { name: nameOrOpts, templateName: templateName ?? "todo", initGit: Boolean(initGit), prompt } : nameOrOpts // Stub mode short-circuits the template lookup — we write a minimal // capsule + placeholder UI and let the agent design from scratch. const chosen: Template | null = o.useStub ? null : o.prompt && (o.templateName === "todo" || !o.templateName) ? pickTemplateForPrompt(o.prompt) : (getTemplate(o.templateName) ?? null) if (!o.useStub && !chosen) { console.error(`Unknown template: ${o.templateName}. Try one of: ${TEMPLATES.map((t) => t.name).join(", ")}`) process.exit(1) } const dir = path.resolve(process.cwd(), o.name) if (fs.existsSync(dir)) { console.error(`Directory ${o.name} already exists`) process.exit(1) } fs.mkdirSync(dir, { recursive: true }) fs.mkdirSync(path.join(dir, "server"), { recursive: true }) fs.mkdirSync(path.join(dir, "client"), { recursive: true }) fs.mkdirSync(path.join(dir, "shared"), { recursive: true }) fs.writeFileSync( path.join(dir, "package.json"), JSON.stringify( { name: path.basename(dir), private: true, type: "module", scripts: { dev: "pond dev", start: "pond start", deploy: "pond deploy", inspect: "pond inspect", logs: "pond logs", "db:list": "pond db list", "db:dump": "pond db dump", }, devDependencies: { // The runtime is published as `pondsh`; alias it to `pond` so the // `pond/server` + `pond/client` import paths resolve and `npm install` // succeeds. (`pond` on npm is an unrelated package.) pond: `npm:pondsh@^${POND_VERSION}`, }, }, null, 2, ), ) fs.writeFileSync(path.join(dir, "server", "index.ts"), chosen ? chosen.serverTs : STUB_SERVER_TS) fs.writeFileSync(path.join(dir, "client", "index.tsx"), chosen ? chosen.clientTsx : STUB_CLIENT_TSX) fs.writeFileSync(path.join(dir, "shared", ".gitkeep"), "") const envContents = BASE_ENV_TEMPLATE.replace("{{SESSION_SECRET}}", randomBytes(32).toString("hex")) + (chosen?.envExtra ?? "") fs.writeFileSync(path.join(dir, ".env.pond.server"), envContents, { mode: 0o600 }) fs.writeFileSync(path.join(dir, ".gitignore"), GITIGNORE) // Always scaffold .cursor/rules + .claude/CLAUDE.md with the capsule contract // so any agent opening the folder picks up the rules. The AGENTS.md is only // written when a prompt was passed (i.e. the user wants a build kicked off). fs.mkdirSync(path.join(dir, ".cursor", "rules"), { recursive: true }) fs.writeFileSync(path.join(dir, ".cursor", "rules", "pond.mdc"), CURSOR_RULES_HEADER + CAPSULE_CONTRACT) fs.mkdirSync(path.join(dir, ".claude"), { recursive: true }) if (o.prompt) { const agents = agentsMdContent(o.prompt, chosen?.name ?? "stub") fs.writeFileSync(path.join(dir, "AGENTS.md"), agents) fs.writeFileSync(path.join(dir, ".claude", "CLAUDE.md"), agents) } else { fs.writeFileSync(path.join(dir, ".claude", "CLAUDE.md"), `# Pond capsule\n\n${CAPSULE_CONTRACT}`) } if (o.initGit) { try { runGit(["init"], dir) runGit(["add", "-A"], dir) runGit(["-c", "user.email=pond@local", "-c", "user.name=pond", "commit", "-m", "init"], dir) } catch (err) { const msg = (err as Error).message.split("\n")[0] console.warn( `pond: skipping initial git commit (${msg}). Files are scaffolded; ` + "run `git init && git add -A && git commit -m init` manually if you want a repo.", ) } } return { template: chosen, dir } }