@zenness/cli
v0.3.2
Published
The zenness CLI — scaffold and run full-stack projects built on Vite + Nitro and Astro.
Maintainers
Readme
@zenness/cli
The zenness CLI. Scaffolds and runs full-stack projects built on Vite + Nitro (backend) and Astro (frontend) — on one origin, with no CORS to configure and one thing to deploy.
npm create zn # scaffold a new project
npx @zenness/cli init my-app # the same thing, spelled directlyFlags need a -- in front of them through npm create, which parses the line
first and keeps every flag it finds — npm create zn -- ./app --shape backend
--yes. pnpm, yarn and bun forward flags either way, and accept the separator
too, so that one spelling works everywhere; without it, npm hands the scaffolder
the values with no flags attached, and it will say so rather than build the
wrong project.
The first form goes through a shim that forwards to zn init here. There are
three of them, because the registry resolves each spelling to a different package
name — create-zn for npm create zn, create-zenness for
npm create zenness, and @zenness/create for the bare scope
npm create @zenness. All three call runCreate() exported from this package,
so there is one install flow, not three.
Installed into a project, the binary is zn:
npm i -D @zenness/cli
npx zn devCommands
| Command | What it does |
| --------------- | ------------------------------------------------- |
| zn init [dir] | Interactive install flow. Aliased as zn create. |
| zn dev | Starts the right dev server for this project |
| zn build | Builds for production: Astro, then Nitro |
| zn preview | Serves the production build |
dev, build and preview prefer the project's own package.json script when
one exists, so a project that customises dev keeps its behaviour. What the
project is comes from the answers zn init recorded in its package.json
("zenness": { "shape": [...] }); a project assembled by hand is still
recognised from its config files.
The parts
zn init asks which parts you want, and they compose:
| Part | What it adds |
| -------------- | ------------------- |
| web-frontend | Astro pages |
| backend | A Nitro API on Vite |
Either part on its own is a project: Astro alone is a static site with no server to run, and Nitro alone is a headless API. Together they are one project, not two in a folder — see Connections below.
Features
Six things a project decides once, at the start. All are off by default, and each arrives as ordinary files in your project rather than a dependency to configure.
| Feature | What you get | Needs |
| -------- | -------------------------------------------------------------------------------------- | ------------------ |
| rss | /rss.xml from your content collection, linked from every page for autodiscovery | web frontend |
| seo | Open Graph and Twitter tags, JSON-LD, and robots.txt | web frontend |
| search | Pagefind, indexed during npm run build — no service to run, no index to keep in sync | web frontend |
| auth | Register, log in, log out and "who am I" — scrypt passwords, signed session cookies | backend + database |
| media | Uploads keyed by content hash, with a table recording where each asset is used | backend + database |
| forms | A validating endpoint that sends mail (SMTP or Resend), with a honeypot | backend |
zn init ./app --features rss,seo,search,auth,media,forms --database sqlite --email smtp --yesThe database is db0 through Nitro's
useDatabase(), with .sql migrations in server/migrations/ applied in
filename order at boot. db0 rather than an ORM's own driver for one concrete
reason: its connectors cover SQLite, Postgres, libSQL and Cloudflare D1, so
choosing a database does not cost you the deploy presets. --database sqlite
uses better-sqlite3, which is native and cannot run on Workers; the installer
says so when you pick that combination.
auth is deliberately small: a sessions table, scrypt from node:crypto, and
signed cookies, readable end to end. No OAuth, no MFA, no password reset, no
rate limiting. Those are real requirements, and this scaffolds only the part
that is boilerplate.
Selling from your content
--commerce shopify adds an editorial section that merchandises real products,
and answers the question a CMS and a store on either side of a redirect cannot:
which article sold what.
zn init ./shop --commerce shopify --store your-store.myshopify.com --journal journal --yes| Route or file | What it does |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| /journal (or /blog, /guides) | A content collection whose frontmatter carries Shopify product handles |
| src/components/ShopTheLook.astro | Renders those products with the page, price and stock filled in at request time |
| server/routes/api/products/[handle].ts | The volatile half — price and availability, cached for a minute |
| server/routes/api/shopify/webhook.ts | Verifies the HMAC before parsing, answers immediately, deduplicates retries |
| server/routes/api/attribution/* | A first-party session, a record of what it read, and a report that publishes its own coverage next to every revenue figure |
The split is the point: an article is copy and images and a list of handles, none
of which changes on its own, so it caches hard; a price changes without anyone
editing anything, so it never enters the cached page. Attribution is first-party
throughout — a cookie this server sets, a call to this server, and the join
arriving on a webhook. --no-attribution keeps the storefront without the
measurement. Nothing here touches checkout, payment, tax or fraud; Shopify does
those.
Non-interactive use
Every prompt has a matching flag, so the same flow works in CI:
zn init ./app --preset vercel --yes
zn init ./site --shape web-frontend --css tailwind --sitemap --site https://example.com --yes
zn init ./api --shape backend --database postgres --features auth,forms --yes
zn init ./app --tools prettier,biome --ui react,svelte --no-install --no-git --yes--yes accepts defaults for anything not passed. Flags: --shape, --css,
--processor, --ui, --tools, --preset, --sitemap, --site,
--commerce, --store, --journal, --attribution, --features,
--database, --email, --install/--no-install, --git/--no-git. Two
more work on every command: --no-color (as does NO_COLOR) and --debug
(as does ZN_DEBUG).
--shape, --ui, --tools and --features are the multi-select prompts and
take a comma-separated list; none answers one with nothing (--ui none), which
is how a script opts out explicitly rather than inheriting whatever the default
becomes later. --shape defaults to web-frontend,backend.
A flag that cannot be honoured is refused rather than quietly downgraded:
--commerce shopify --shape web-frontend stops and says why, as do
--features auth --shape web-frontend and --features media --database none.
So does a flag that does not exist — --shpe backend used to scaffold the
default shape in silence.
The run narrates itself through Consola:
resolved answers first, then each thing that gets wired up, so CI logs say the
same as an interactive terminal. Prompts, the spinner and the closing summary
stay with @clack/prompts — the two never interleave.
What every project gets
Beyond Astro, Nitro and Partytown, three libraries are installed and already
used, so each has a working example in the project rather than a docs tab:
Consola (one tagged logger, request-lifecycle
logging, a permanently-present debug line behind NITRO_LOG_LEVEL),
ofetch (the homepage's API call), and
magic-regexp (a slug pattern that
validates a dynamic route's param). Deleting the example takes the dependency
with it. Zod ships inside Astro as astro/zod.
--css adds Tailwind, Bootstrap, shadcn/ui or Ant Design; --processor adds
Sass or PostCSS; --tools adds Prettier, Autoprefixer, Stylelint or Biome — and
runs them over the new project once its dependencies are installed, so
npm run format:check is green in a project you have not touched yet.
--ui adds Astro's React, Vue and/or Svelte integrations, one example component
each under src/components/, and — for React — the jsx/jsxImportSource
compiler options a .tsx component needs under Astro's own tsconfig. The
components are not imported anywhere: which page wants an island is your call,
not the installer's.
--preset writes the Nitro deploy target: Vercel, Firebase App Hosting,
Cloudflare Workers or Zephyr, with the host's environment variables named (and
left empty) in .env.
Questions that stop applying are not asked. A headless backend is never asked about CSS frameworks, and a project with no Nitro in it is never asked for a deploy preset.
Connections
Templates are bundled into dist/templates at build time, so zn init writes
from local disk — it never fetches a tarball, and works offline. Their source is
templates/ at the repo root.
One template per part, always. There is no template for a combination. A
selection copies each part's template in turn, and the files every template
carries (package.json, .env, .gitignore, README.md) are combined rather
than overwritten, so the last one copied cannot take the others' dependencies
with it.
What a combination needs because the parts are together is a connection,
applied after the copy by src/generators/connect.ts. Connections are indexed
by the set of parts each one needs, and every connection whose parts are all
present applies:
| Connection | What it supplies |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| web-frontend + backend | the dev proxy, Nitro's publicAssets, the merged scripts, the second tsconfig, scripts/dev.mjs, and one ZN_API_PORT that both halves read |
Indexing by sets rather than by combinations is the whole trick: a scaffolder keyed on combinations needs 2ⁿ templates, and this needs n templates plus however many connections genuinely have something to say. The powerset is traversed, never enumerated. Adding a part costs one template plus the edges you actually care about.
Three rules keep that predictable as the list grows. Smaller sets apply first, so a connection over three parts can build on the pairs inside it; ties go in declaration order; and where two write the same value the later one wins — which is why each connection declares what it writes, so a conflict is something you can see before it happens. The rule is also monotone: a connection names parts that must be present, never absent. Adding a part to a selection can only add connections, never retract one.
The prose problem
Almost every scaffolder generates correct configuration and leaves lying documentation next to it — the README that describes a project you did not scaffold. Each template here is written to be scaffolded alone, so the frontend's home page says there is no server of your own to run. True, until a backend is scaffolded beside it.
So the sentences that stop being true are values a connection overwrites. They sit in named regions:
<!-- zn:region lede -->
Astro renders these pages, and there is no server of your own to run.
<!-- zn:endregion -->The markers are whole lines, commented in whatever syntax surrounds them, so a
template carrying them is still a working file that renders, typechecks and
reads normally on its own — which matters, because these templates are meant to
be scaffolded and read on their own too. A connection replaces the region's body;
anything left unclaimed loses its markers and keeps its content. The replacement
copy lives in templates/connections/<parts>/prose/, as markdown, so prose
diffs as prose.
Versions
Every version the installer can write lives in one object
(src/versions.ts), so what a new project gets is one file to read and one file
to change. Nitro is pinned exactly and deliberately — v3 is a moving beta that
has already renamed public APIs between releases, and the templates are written
against this exact build. Everything else is a caret range.
A weekly job diffs that file against the registry, and a nightly canary scaffolds every shape with nothing pinned and builds it, so a breaking release turns up here before it turns up in your install.
Releases are published from GitHub Actions with npm provenance, so every version on this page carries an attestation of the workflow, repository and commit that built it.
Links
MIT.
