Imported from crafter-station/crafter.run (
AGENTS.md). Install upstream withnpx skills add crafter-station/crafter.run. Copyright stays with the author.
AGENTS.md
Commands
- Use Bun for dependency/script commands; this is a Bun workspace with one root
bun.lockandpackageManagerpinned in the rootpackage.json. bun run devstartsapps/webon port 3000 andapps/apion port 3001 through Turbo;bun run dev:webandbun run dev:apirun either app alone.bun run buildis the production smoke test through Turbo. It typechecks and bundlesapps/apiandpackages/cli, but Next does not typecheckapps/webbecauseapps/web/next.config.mjssetstypescript.ignoreBuildErrors: true.bun run db:generategenerates Drizzle migrations;bun run db:migrateapplies them toDATABASE_URL.bun run db:migrate:supabaseis the idempotent one-time board-data importer and requiresSUPABASE_MIGRATION_URLplusSUPABASE_MIGRATION_SERVICE_ROLE_KEYoutside app env validation.bun run lintcurrently fails:apps/web/package.jsoncallseslint ., but ESLint is not installed/configured.bunx tsc -p apps/web/tsconfig.json --noEmit --incremental falsecurrently fails on a pre-existing pagination variant mismatch and migration-script compiler/header typing.bun test apps/web packages/cli packages/db apps/apiruns all current tests; pass a test file and-t '<name>'for one case, for examplebun test apps/api/test/api.test.ts -t 'reports health'. Anything importingapps/web/lib/source.tsonly resolves inside the bundler, so a test that reaches the docs corpus must stub that module withmock.module.
App Shape
- This is a Turborepo. The Next 16 App Router site lives in
apps/web; the Hono service API lives inapps/api; shared packages belong inpackages/only when they have multiple consumers. - Public pages live under
apps/web/app/[lang];apps/web/proxy.tsredirects non-localized paths to/${defaultLocale}and excludes API/static assets. - Supported locales are
en,es,pt,zh, andja. Locale state is duplicated inapps/web/lib/i18n.ts,apps/web/i18n/routing.ts,apps/web/messages/*.json, and many page-levelgenerateStaticParams()implementations. next-intlis wired throughapps/web/next.config.mjsusing./i18n/request.ts; page copy comes fromapps/web/messages/{locale}.jsonunless it is static catalog/team data inapps/web/lib/site.tsorapps/web/lib/team.ts.- SEO routes are centralized in
apps/web/lib/seo.ts; updateindexablePathswhen adding/removing public pages so metadata, sitemap, and alternates stay aligned. - Blog posts are MDX files in
apps/web/content/blog/<slug>.<locale>.mdx, loaded byapps/web/lib/blog.ts(frontmatter is validated with Zod;authorsareusernames fromlib/team.ts).dateis day-granularity, so posts sharing a day sort by the optionalorderfield, highest first, before falling back to slug; keepdateandorderidentical across a slug's locales or the languages order differently. A post only needs the locales it is written in: the index in every locale lists it and links to the best available language, the post page and the.mdtwin exist only where a file does, and hreflang and the sitemap narrow to match./[lang]/blog/rss.xmland/[lang]/blog/sitemap.mdare generated; UI copy lives inapps/web/components/blog/copy.ts, hero copy inpages.blogin the message catalogs. - schema.org output is centralized in
apps/web/lib/structured-data.ts; every page composes from the oneOrganizationnode inapps/web/app/[lang]/layout.tsxrather than restating it,documentedPackagesmaps a docs slug to the npm package that page documents, andsourceVideosmaps a blog slug to the recording a post was written from, which emits theVideoObjectand its chapterClips. - The agent-facing surface is unlocalized and must stay out of
/[lang]:/mcp(read-only MCP server, tools inapps/web/lib/mcp.ts),/agents.md,/openapi.json, and/.well-known/{mcp,ai-plugin}.json. The App Router will not route a dot directory, so the well-known documents live underapps/web/app/well-known/and are rewritten innext.config.mjs./mcpis the only dotless one, so it is named in theapps/web/proxy.tsmatcher exclusion; adding another dotless agent route means adding it there too. - Adding an MCP tool means adding it to the
toolsarray inapps/web/lib/mcp.ts;/agents.md,/openapi.json, and/.well-known/mcp.jsonall read fromdescribeTools()and update themselves. - Shared layout primitives and site components live in
apps/web/components/; shadcn/Radix components live inapps/web/components/uiwith aliases fromapps/web/components.json(@/components,@/lib,@/hooks). - There are two 404s and they are not interchangeable.
app/[lang]/not-found.tsxanswers anotFound()thrown inside a localized route and keeps the header and footer. A URL matching no route never reaches a layout, so it is served byapp/global-not-found.tsx, which owns its own document and therefore re-importsglobals.cssand the fonts, runs without a Clerk provider, and needsexperimental.globalNotFoundinnext.config.mjs. Both rendercomponents/not-found-view.tsx. global-not-found.tsxhas no params, soproxy.tsstamps the request's locale on theLOCALE_HEADERfromapps/web/lib/i18n.tsand the page reads it back, falling through toAccept-Languagethen the default. Anything else that has to render outside[lang]should recover its locale the same way.components/liquid-surface.tsxowns the water simulation; callers supply a painter and get refraction for free (liquid-hero.tsxpaints the brand mark,not-found-surface.tsxpaints the figure). A painter draws only its subject, gets device pixels, and must read fonts and colours off the live document because next/font family names and theme tokens are resolved at runtime. It runs one step per 1/60s regardless of display refresh, pauses through anIntersectionObserverandvisibilitychange, and rebuilds itself onwebglcontextlost.- The surface keeps three rasters at different sizes: the canvas is the CSS box, the painted texture is that box in device pixels, and the wave field is device independent and capped, so the water looks identical on every display. Anything that changes the field's size must also update
gradientScale, which is what keeps refraction the same strength at any resolution. - A painter that picks a layout must branch on
viewportWidthfrom its paint context, never on its ownwidth / dpr: the surfaces sit inside a bordered container and cross a breakpoint a couple of pixels after the matching Tailwind rule does. A painter may return aLiquidFocusto say where its subject landed, and idle drops will rain there. - The 404's backdrop is chosen at runtime by
components/not-found-backdrop.tsx: the black hole on WebGPU, the hero's water on WebGL, and neither under reduced motion, which leaves the DOM figure the page renders regardless. Both backdrops arenext/dynamicon purpose. Next ships thenot-foundboundary with every route's client bundle, so a static import there puts three.js and nine compiled shaders on the blog and the docs. components/black-hole/is vendored fromvercel-labs/vgpuunder the MIT license kept beside it; its README says what was taken and how to mark local edits. Its.wgslfiles are a module graph resolved at build time by theturbopack.rulesentry innext.config.mjs, so adding a shader there means importing it, not inlining a string..theme-scopeinapp/globals.cssis what lets a subtree flip palette on its own. Tailwind substitutes the@themealiases at computed-value time on:root, so a nested.darkchanges the raw tokens and nothing reads them again; pair the two classes or the swap silently does nothing.- The hero's mark and its copy never overlap, at any width:
hero.tsxreserves the mark's bottom band belowlgand a left copy column above it, andliquid-hero.tsxpaints to the same numbers. Changing one means changing the other. Both files also carry a DOM<img>of the mark underneath the water, which is what reduced-motion, no-JavaScript, and no-WebGL visitors see, and what preloads the file for the canvas.
Integrations
- Web env validation is in
apps/web/env.ts; all listed env vars are optional.API_URLdefaults tohttp://localhost:3001. apps/api/src/dev.tsloadsapps/api/.env*, then fills a missingDATABASE_URLfromapps/web/.env*; Drizzle commands also load env fromapps/webviapackages/db/drizzle.config.ts.apps/apiexposes/health,/openapi.json,/.well-known/oauth-protected-resource, and versioned routes under/v1; it requiresDATABASE_URLfor data routes and accepts comma-separated browser origins throughWEB_ORIGINS.- Every 401 from
apps/apicarries aWWW-Authenticateheader pointing at its RFC 9728 metadata, which names Clerk as the authorization server.CLERK_OAUTH_ISSUERoverrides that issuer outside production. packages/cliprovides thecrafterexecutable; run it locally withbun run cli -- <command>. Its OAuth credentials are stored in the operating-system credential store, never project files.- The public CLI's OAuth issuer and client are fixed production values. It must ignore all OAuth environment variables so updating the CLI repairs stale or conflicting local configuration automatically.
- CLI changes must include a Changesets file. Follow
docs/cli-releases.mdfor versioning, the generated release PR, npm publishing, verification, and recovery; do not manually bump or publish normal releases. packages/contractsowns shared Zod API schemas.packages/dbowns the Drizzle schema and the single migration history; do not create app-local migration folders.skills/crafter-ship/SKILL.mdis the portable agent workflow. It delegates all auth and API behavior topackages/cliand must preserve draft-first publishing.skills/video-to-blog/SKILL.mdturns a recorded talk into a series of blog posts. Invoke it for any "write posts from this video" request rather than transcribing by hand: it owns the caption pipeline, the entity-repair step that auto-captions make mandatory, and thesourceVideosregistration that gives a post itsVideoObject. Itsscripts/prepublish.pyis the fastest check that new posts match house style and that thedate/orderpair has not drifted across locales.- Missing
LUMA_API_KEYmakesapps/web/lib/luma.tsreturn empty event lists and log a warning; event pages revalidate every 6 hours. - Project and workshop boards remain in Next route handlers and use
packages/dbover Neon HTTP; new Ships APIs live in Hono. Browser realtime usesNEXT_PUBLIC_PORTAL_KEY, and route handlers publish invalidations withPORTAL_SECRET. - Missing
OPENAI_API_KEYskips AI spam moderation for next-project and Ship submissions; deterministic validation still runs. RESEND_API_KEYis validated but not currently used;/api/contactonly validates email and returns204.
Assets And Config Gotchas
apps/web/scripts/generate-assets.tsis a Bun-only Sharp script with no package script; it regenerates OG images and icons fromapps/web/public/effecto-poster-original.jpg, writing intoapps/web/public/andapps/web/app/.apps/web/next.config.mjssetsimages.unoptimized: true, allows dev origindev.cueva.io, and permanently redirects/vibeto Luma.- Configure separate Vercel projects with Root Directories
apps/webandapps/api. Pointapi.crafter.runat the API project, set the web project'sAPI_URLandNEXT_PUBLIC_API_URLto that origin, and keep app-specific cron configuration inapps/web/vercel.json. opencode.jsoncdefines../crafter.comas the legacy Crafter Station site reference; use it only when current copy/design intent is not clear from this repo.