@pyreon/create-zero is the interactive project scaffolder for Pyreon Zero. One command walks you through template choice, rendering mode, deployment adapter, feature selection, backend integrations, AI tooling, and framework-migration compat — then writes a ready-to-run project. Every prompt also has a flag, so the whole flow is scriptable for CI, templates repos, and --yes one-liners.
Installation
You don't install it — you invoke it through your package manager's create command. It ships under two bin aliases (create-pyreon-app is canonical, create-zero is the back-compat alias) but both run the same scaffolder.
npm create @pyreon/zero@latest my-appbun create @pyreon/zero my-apppnpm create @pyreon/zero my-appyarn create @pyreon/zero my-appThen start the dev server:
cd my-app
npm install
npm run devcd my-app
bun install
bun run devcd my-app
pnpm install
pnpm run devcd my-app
yarn
yarn devThe scaffolder never installs dependencies itself — it prints cd, install, and dev as next steps and stops. (It accepts --install / --no-install as no-ops, since people pass them out of habit from create-vite / create-next-app.)
Why a scaffolder?
A Pyreon Zero project has several genuinely-computed pieces — the dependencies map depends on which features you pick, the vite.config.ts plugins array depends on rendering mode + adapter + compat shim, and the deploy artefacts depend on the target platform. Wiring all of that by hand means cross-referencing docs and keeping versions in sync. The scaffolder does it in one pass:
Feature → dependency resolution — pick
queryand it adds@pyreon/query+@tanstack/query-core; pickformsand it adds@pyreon/form+@pyreon/validation+zod. Every@pyreon/*dep is pinned to the same monorepo version as the scaffolder itself, so the set is always internally consistent.Mode + adapter →
vite.config.ts— the generated config imports the matching*Adapter()factory and setszero({ mode, ssr })for you.Platform → deploy artefacts —
vercel.json,wrangler.toml,Dockerfile, etc. land alongside your code.AI tooling — generates
CLAUDE.md,.mcp.json, Cursor rules, and more from one shared "Pyreon principles" body so the guidance stays consistent across editors.
Quick Start
The fastest non-interactive paths, by template:
# Standard app (store + query + forms), SSR streaming, Vercel
npm create @pyreon/zero@latest my-app -- --preset standard --yes
# SSG markdown blog on Cloudflare Pages
npm create @pyreon/zero@latest my-blog -- --template blog --adapter cloudflare --yes
# SaaS-shape dashboard with Supabase + Resend
npm create @pyreon/zero@latest my-saas -- --template dashboard \
--integrations supabase,email --yes
# Bun-workspaces monorepo
npm create @pyreon/zero@latest my-mono -- --template monorepo --preset standard --yesThe interactive flow
Run without --yes (or with any prompt's flag omitted) and you get an ordered series of prompts. Every prompt is skipped when its flag is supplied — flags and prompts are interchangeable.
Project name — the positional argument, or a text prompt. The target directory must not already exist (it bails if it does). Required up front when using
--yes.Template —
app/blog/dashboard/monorepo.Rendering mode —
ssr-stream/ssr-string/ssg/isr/spa. Skipped for templates that force a mode (blog→ SSG,dashboard→ SSR streaming).Deployment target — filtered to the adapters the chosen template supports.
Feature preset — pick a preset, or
Customto drop into a grouped multiselect of all 22 features (8 categories).Package imports —
@pyreon/metasingle barrel, or individual packages.Backend integrations — Supabase, Email (Resend), or none.
AI tooling — multi-select of rule files (MCP,
CLAUDE.md, Cursor, Copilot,AGENTS.md).Compat mode — native Pyreon, or a React / Vue / Solid / Preact migration shim.
Lint — include
@pyreon/lint(the prompt advertises "59 Pyreon-specific rules").Typed routes —
<Link href>autocompletes real route paths and rejects typos at compile time (default on).
Cancelling at any prompt (Ctrl-C) aborts cleanly.
Templates
Four curated starting points, selected by --template or interactively. Each declares its own default rendering mode, default feature set, supported adapters, and (for dashboard) preselected integrations.
| Template | Default mode | Mode forced? | Default features | What you get |
|---|---|---|---|---|
app | SSR streaming | no | store + query + forms | Full-featured starter — counter, posts list + detail, an (admin) route group, _layout / _error / _loading, and api/ route handlers. |
blog | Static (SSG) | yes | (none) | Markdown-style TSX posts in src/content/posts/, an RSS feed at /api/rss, SEO-ready. Static-first — node / bun adapters are excluded. |
dashboard | SSR streaming | yes | store + query + forms + table | SaaS shape — marketing landing → auth-gated app/* routes (dashboard, users, invoices, settings) → an invoice PDF/email export demo built on @pyreon/document-primitives. Server-required, so static is excluded. Preselects both integrations. |
monorepo | SSR streaming | no | store + query + forms | Bun workspaces shell — apps/web/ (a full app-shaped project) + packages/ui/ + packages/types/. |
npm create @pyreon/zero@latest my-app -- --template dashboard
npm create @pyreon/zero@latest my-blog -- --template blogGenerated structure (flat templates)
The app template scaffolds the following. blog and dashboard share the same shell (entry points, config, public/) with different src/routes/ content.
my-app/
├── package.json # generated — deps computed from your selections
├── vite.config.ts # generated — plugins computed from mode + adapter + compat
├── tsconfig.json # strict, jsxImportSource: @pyreon/core, moduleResolution: bundler
├── index.html # #app mount point + theme-flash-prevention script
├── env.d.ts # ambient types for virtual:zero/* modules
├── .gitignore
├── README.md
├── public/
│ └── favicon.svg
└── src/
├── entry-client.ts # startClient({ routes }) from @pyreon/zero/client
├── entry-server.ts # createServer({ ... }) with the SSR mode baked in
├── global.css
└── routes/ # file-based routing
├── index.tsx
├── about.tsx
├── counter.tsx
├── _layout.tsx
├── _error.tsx
├── _loading.tsx
├── (admin)/dashboard.tsx
├── api/health.ts
├── api/posts.ts
└── posts/
├── index.tsx
└── [id].tsxThere is no pyreon.config.ts — all configuration lives in vite.config.ts via the zero({ ... }) plugin and in tsconfig.json. Selecting the store feature adds src/stores/app.ts (and swaps in a store-aware _layout.tsx); selecting feature adds src/features/posts.ts + a posts/new.tsx route. Integration and AI selections add their own files (see below).
Monorepo layout
--template monorepo scaffolds the web app first (running the full flat pipeline against apps/web/ with the app shape), then wraps it in a Bun workspaces root:
my-mono/
├── package.json # workspaces: ["apps/*", "packages/*"] + proxy scripts
├── tsconfig.json # root TS settings
├── README.md
├── .gitignore
├── apps/
│ └── web/ # full app-shaped project
│ ├── package.json # name: "web" + workspace deps on the shared packages
│ └── … # everything the `app` template produces
└── packages/
├── ui/ # @my-mono/ui — shared component stub
│ ├── package.json # deps: { "@my-mono/types": "workspace:^" }
│ ├── tsconfig.json
│ └── src/index.ts # Button + ButtonProps stub
└── types/ # @my-mono/types — framework-agnostic shared types
├── package.json
├── tsconfig.json
└── src/index.ts # ButtonVariant + User stubsThe @<projectName>/ scope is derived from the project name automatically — no extra prompt. The root package.json carries no dependencies; it's a dispatcher whose dev / build / preview scripts proxy to the web app via bun run --filter='web' …, and typecheck runs --filter='*'. All your feature / adapter / integration / AI / lint choices apply to the inner web app (which is always app-shaped today).
Rendering modes
Set with --mode (ignored when the template forces a mode). The mode maps directly to the generated vite.config.ts and entry-server.ts.
| Mode | zero({ ... }) | When to use |
|---|---|---|
ssr-stream | mode: 'ssr', ssr: { mode: 'stream' } | Recommended. Progressive HTML with Suspense — best TTFB. |
ssr-string | mode: 'ssr' | Buffered HTML — simpler, slower first byte. |
ssg | mode: 'ssg' | Pre-rendered at build time. No runtime server. |
isr | mode: 'isr' | Static-first with periodic revalidation (a server-side SWR cache). |
spa | mode: 'spa' | Client-only — no server rendering. |
At the server level the ssr.mode field only has stream / string; ssg, isr, and spa all fall back to string (SSG renders once at build time; ISR caches complete HTML documents, and a stream can't be cached-then-replayed; SPA never SSRs). See Zero → Rendering modes for the full semantics.
Deployment adapters
Pick a target during the prompt or pass --adapter. The prompt is filtered to the adapters the chosen template supports. Each adapter writes its platform deploy artefacts and (where applicable) injects the matching *Adapter() factory into the generated vite.config.ts.
| Adapter | Vite factory | Files written | Env keys |
|---|---|---|---|
vercel | vercelAdapter() | vercel.json | — |
cloudflare | cloudflareAdapter() | wrangler.toml (the adapter writes the authoritative dist/_routes.json at build time) | — |
netlify | netlifyAdapter() | netlify.toml (render-mode-aware — SSR modes publish dist/publish + wire the ssr function from dist/netlify/functions; SSG/SPA publish dist) | — |
node | nodeAdapter() | Dockerfile, .dockerignore | PORT |
bun | bunAdapter() | Dockerfile (Bun-based), .dockerignore | PORT |
static | (none) | (none — dist/ after build is the whole site) | — |
npm create @pyreon/zero@latest my-app -- --adapter cloudflareFeatures
22 Pyreon fundamentals can be enabled per project. The interactive flow offers a preset shortcut first; choosing Custom drops you into a multiselect grouped into 8 categories for discoverability.
Presets
Four atomic shortcuts via --preset. Each is a (features, mode, adapter) triple:
| Preset | Feature set | Mode | Adapter |
|---|---|---|---|
minimal | (none) | spa | static |
standard | store + query + forms | ssr-stream | vercel |
dashboard | standard + table + charts | ssr-stream | vercel |
full | every feature (22) | ssr-stream | vercel |
npm create @pyreon/zero@latest my-app -- --preset standard --yes
npm create @pyreon/zero@latest my-app -- --preset full --yesFeature categories
When you pick Custom, the multiselect groups features for browsing:
| Category | Features |
|---|---|
| State management | store, state-tree, storage, url-state |
| Forms + validation | forms, feature |
| Data fetching + transforms | query, rx |
| UI primitives + animation | styler, elements, animations, coolgrid |
| Collections + tables | table, virtual |
| Interactive widgets | charts, code, flow, toast |
| Internationalization + accessibility | i18n, hotkeys, permissions |
| Utility hooks | hooks |
Each feature pulls its package(s) into dependencies. Some pull peers — query adds @tanstack/query-core, forms adds @pyreon/validation + zod, and the feature (CRUD) bundle pulls store + query + forms + their peers.
Atomic add / remove
--with-<feature> adds one feature; --no-<feature> removes one. They compose on top of whatever the base set is (preset, explicit --features, or template default under --yes):
# Standard preset, plus i18n, minus forms
npm create @pyreon/zero@latest my-app -- --preset standard --with-i18n --no-forms --yes--no-X always wins over --with-X for the same feature (set-subtraction semantics). An unknown feature in --with- / --no- is a hard error that prints the known list.
Explicit feature list
--features <csv> is an explicit set that overrides --preset entirely (only --with- / --no- still compose on top):
npm create @pyreon/zero@latest my-app -- --features store,query,i18n --with-toast --yesResolution order
When several feature sources are combined, the highest-priority source wins as the base, then --with-X / --no-X overlay on top:
--features <csv>— explicit list, ignores preset + template default--preset <id>— preset's feature set as the base (skips the feature prompts)--yes(no preset / features) — the chosen template's default featuresInteractive — preset prompt →
Custom→ grouped multiselect
Package imports
The --packages flag (prompt: "Package imports") chooses how features reach your code:
| Value | Behaviour |
|---|---|
meta | Adds @pyreon/meta — a single barrel re-exporting everything, tree-shaken at build. Simpler imports. |
individual | Installs only the packages your selected features need — smaller node_modules. |
npm create @pyreon/zero@latest my-app -- --packages individual --yesEither way, the specific feature packages are still added to dependencies (templates import them by their real package name, e.g. @pyreon/query); meta just adds the barrel on top for ergonomic imports.
Backend integrations
Two scaffolders that write plain files into your project — no Pyreon-side wrapper packages and no version coupling. You own the code and update it independently of Pyreon releases. Select with --integrations (csv) or the multiselect.
| Integration | Files written | Dependencies | Env keys |
|---|---|---|---|
supabase | src/lib/supabase.ts (+ src/lib/auth.ts, src/lib/db.ts on the dashboard template) | @supabase/supabase-js | SUPABASE_URL, SUPABASE_ANON_KEY |
email | src/lib/email.ts, src/emails/welcome.tsx, src/routes/api/email/welcome.ts | resend, @pyreon/document-primitives, @pyreon/document, @pyreon/connector-document | RESEND_API_KEY, EMAIL_FROM |
Each integration appends its env keys to a generated .env.example. On the dashboard template, Supabase's auth.ts / db.ts replace the template's in-memory stubs (same contract). The dashboard template preselects both integrations; on app / blog they're off by default but selectable.
npm create @pyreon/zero@latest my-saas -- --template dashboard --integrations supabase,email --yesThe email integration showcases the document-primitives angle: the same <DocDocument> / <DocSection> / <DocText> tree renders in the browser and exports to email HTML via @pyreon/document-primitives — one author surface for many output formats.
AI tooling
Generate per-tool rule files so AI assistants understand Pyreon's conventions. All share a canonical "Pyreon principles" body, so the guidance stays consistent across editors. Select with --ai (csv) or the multiselect.
| Option | File written | Default |
|---|---|---|
mcp | .mcp.json (also adds @pyreon/mcp as a dev dep) | ✓ |
claude | CLAUDE.md (includes a bun run doctor line) | ✓ |
cursor | .cursor/rules/pyreon.md | — |
copilot | .github/copilot-instructions.md | — |
agents | AGENTS.md | — |
npm create @pyreon/zero@latest my-app -- --ai mcp,claude,cursor --yesCompat mode
Migrating from another framework? --compat (or the "Migrating from another framework?" prompt) configures @pyreon/vite-plugin with the matching shim and adds the matching compat package.
| Value | Package added | Lets you write |
|---|---|---|
none | — | Native Pyreon (recommended) |
react | @pyreon/react-compat | useState, useEffect, … |
vue | @pyreon/vue-compat | ref, computed, watch, … |
solid | @pyreon/solid-compat | createSignal, createEffect, … |
preact | @pyreon/preact-compat | useState, signals, … |
npm create @pyreon/zero@latest my-app -- --compat react --yesLint
--lint / --no-lint (prompt: "Include @pyreon/lint?", default yes) toggles @pyreon/lint. When enabled, the scaffolder adds the dev dep, writes a .pyreonlintrc.json with the recommended preset, and adds a lint script (pyreon-lint .).
npm create @pyreon/zero@latest my-app -- --no-lint --yesGenerated package.json scripts
Every scaffolded project gets these scripts (lint adds one more when enabled):
| Script | Command | Purpose |
|---|---|---|
dev | zero dev | Dev server with HMR |
build | zero build | Production build |
preview | zero preview | Preview the production build locally |
doctor | zero doctor | Project health audit (React-pattern + anti-pattern detection) |
doctor:fix | zero doctor --fix | Auto-fix what the audit can |
doctor:ci | zero doctor --ci | Exit non-zero on findings (CI gate) |
lint | pyreon-lint . | Only present when @pyreon/lint is enabled |
The dev dependencies always include @pyreon/vite-plugin, @pyreon/zero-cli (the zero CLI), typescript, and vite. See Zero CLI for the zero command surface.
CLI flags
Every prompt maps to a flag, so the entire flow is scriptable. Run --help (or -h) for the built-in usage text.
| Flag | Values |
|---|---|
[name] | Positional project name (required with --yes) |
--template <id> | app / blog / dashboard / monorepo |
--mode <id> | ssr-stream / ssr-string / ssg / spa / isr |
--typed-routes / --no-typed-routes | Typed routes (<Link href> autocomplete + typo rejection); default on |
--adapter <id> | vercel / cloudflare / netlify / node / bun / static |
--preset <id> | minimal / standard / dashboard / full (composes with --with-X / --no-X) |
--features <csv> | e.g. store,query,forms — overrides --preset entirely |
--with-<feature> | atomic add, e.g. --with-store --with-i18n |
--no-<feature> | atomic remove, e.g. --no-forms |
--integrations <csv> | supabase,email |
--ai <csv> | mcp,claude,cursor,copilot,agents |
--compat <id> | none / react / vue / solid / preact |
--packages <id> | meta (single barrel) / individual (selected packages only) |
--lint / --no-lint | toggle @pyreon/lint |
--yes | skip every prompt that has a usable default |
--install / --no-install | accepted no-ops (the scaffolder never installs) |
--help, -h | print usage and exit |
--packages also accepts the alias --package-strategy (--pm is rejected: the package manager is whichever one ran create). --mode that a mode-forcing template (blog, dashboard) cannot honor, and --adapter static with an SSR or ISR mode, are refused rather than silently overridden. An unknown flag, an invalid enum value, or an extra positional argument fails fast with an actionable message and a non-zero exit code.
Worked examples
# Standard app, defaults for everything else
npm create @pyreon/zero@latest my-app -- --yes
# Dashboard + Supabase + Resend + full AI tooling, on Vercel
npm create @pyreon/zero@latest my-saas -- --template dashboard \
--adapter vercel \
--integrations supabase,email \
--ai mcp,claude,cursor \
--yes
# Standard preset, plus i18n, minus forms
npm create @pyreon/zero@latest my-app -- --preset standard --with-i18n --no-forms --yes
# Blog on Cloudflare Pages
npm create @pyreon/zero@latest my-blog -- --template blog --adapter cloudflare --yes
# Monorepo with the full preset, individual package imports
npm create @pyreon/zero@latest my-mono -- --template monorepo \
--preset full --packages individual --yes
# React migration target, no lint
npm create @pyreon/zero@latest my-app -- --compat react --no-lint --yesAfter scaffolding
cd my-app
bun install # or npm install / pnpm install / yarn
bun run dev # zero dev — HMR dev serverThe generated project is a complete Pyreon Zero app: file-based routing, SSR/SSG/SPA per your mode, @pyreon/vite-plugin wired up, strict TypeScript with jsxImportSource: @pyreon/core, and your selected features, adapter, integrations, and AI rule files all in place. From here, see:
Zero — the meta-framework the scaffolder targets (routing, rendering modes, adapters, server actions).
Zero CLI — the
zerocommand your scripts call (dev/build/preview/doctor).SSG — static generation (the
blogtemplate's mode).Deployment — per-platform deploy notes for each adapter.
Create Multiplatform — the scaffolder for iOS + Android + web targets, when you need native.
Reference
| Flag | Default (interactive) | Notes |
|---|---|---|
--template | prompt (app under --yes) | Drives mode/adapter/feature/integration defaults |
--mode | template default | Skipped when the template forces a mode |
--adapter | template default | Prompt filtered to template-supported adapters |
--preset | prompt (Custom) | minimal / standard / dashboard / full |
--features | template default | csv; overrides --preset; not validated |
--with-<feature> | — | composes on top of the base set; errors on unknown |
--no-<feature> | — | composes on top; wins over --with- for the same feature |
--integrations | template default ([], both for dashboard) | supabase / email |
--ai | mcp,claude | mcp / claude / cursor / copilot / agents |
--compat | none | react / vue / solid / preact shim |
--packages | prompt | meta / individual (alias: --package-strategy) |
--lint | true | --no-lint to opt out |
--yes | — | requires a positional name |