create-kywi-app
v0.24.0
Published
Scaffold a new Kywi CMS project — npx create-kywi-app my-site
Readme
create-kywi-app
Scaffold a new Kywi CMS project.
npx create-kywi-app my-siteDependency-free by design (bin/create-kywi-app.mjs uses only Node built-ins),
so npx create-kywi-app runs with nothing pre-installed. The file contents it
writes live in lib/templates.mjs (buildFileSet(answers)), unit-tested
independently of the CLI's prompting/arg-parsing.
Usage
npx create-kywi-app [project-name] [options] # scaffold a new project (default)
npx create-kywi-app agents [--force] # add/refresh agent guidance in an
# existing project (run from its root)
Options:
--yes, -y Use defaults, skip prompts
--mode <mode> coupled | headless | decoupled (default: coupled)
--db <provider> postgresql | mysql (default: postgresql)
--auth <list> comma-separated: credentials,google,github (default: credentials)
--force, -f agents: overwrite existing guidance files (default: skip them)
--help, -h Show this helpWith no flags and a TTY, it prompts interactively (project name, DB provider,
auth providers, deployment mode). Pass --yes (or run non-interactively, e.g.
in CI) to skip straight to defaults + flags.
npx create-kywi-app my-site --yes
npx create-kywi-app blog --mode headless --auth credentials,googleDeployment modes
Set via --mode (and written into the generated kywi.config.ts's
mode field):
coupled— the app renders the public site AND serves the API + admin.GET /renders content from the DB-backed scope.headless— API + admin only.GET /returns 404.decoupled— API + admin serve here; a separate frontend consumes@kywi-software/sdkagainst this app's/api/v1.GET /returns 404.
What gets generated
A generated project boots and works with zero manual edits:
pnpm install && pnpm migrate && pnpm seed && pnpm dev gives you a working
/admin — the full Kywi admin (dashboard, site tree, media, users, forms,
audiences, settings, and every other core surface, sign in and create +
publish a page) — and, in coupled mode, a public site that renders published
pages at their slug.
buildFileSet() writes (lib/templates.mjs):
package.json scripts (dev/build/start/migrate/seed) + deps incl. drizzle-kit + tsx
kywi.config.ts defineKywiConfig/Site/Theme + a default "Page" content type
tsconfig.json
next.config.mjs the FULL config to consume core (transpilePackages, extensionAlias,
serverExternalPackages) — every line is load-bearing
.env.example DATABASE_URL + AUTH_SECRET
.gitignore
README.md project-specific quick start (createdb, migrate, seed, dev, /admin, admin.features)
AGENTS.md guidance for AI agents working on this site: an app-specific
header (where things live, the /admin URL, the CLI, the three
scaffolded Claude Code skills) + Kywi's official building
patterns (docs/agents/AGENT-PATTERNS.md, verbatim)
CLAUDE.md thin pointer that sends a Claude Code session to AGENTS.md first,
plus a nudge toward the kywi-content-model skill (and the other
two scaffolded skills)
.claude/skills/kywi-content-model/SKILL.md Claude Code project skill: design a CONTENT-MODEL.md and get
owner sign-off before creating types or entering content — loaded
automatically by Claude Code, a pre-build checklist for other agents
.claude/skills/kywi-collections/SKILL.md Claude Code project skill: build any collection (testimonials,
logos, posts, products, …) as folder + feed + Feed Display — loaded
automatically when adding a collection
.claude/skills/kywi-personalization/SKILL.md Claude Code project skill: confirm the use case, then
build audiences, page variants, or A/B experiments — loaded
automatically when the owner wants personalization or A/B testing
lib/kywi.ts the ONE wiring point: kywi.config.ts → core's render path (stamped)
kywi.layers.ts GENERATED layer registry: imports every site/theme layer so core can
resolve overrides theme → site → core at build time (never edit; upgrade regenerates) (stamped)
sites/default/index.ts this site's layer: re-exports lib/modules.tsx as `modules` (add `templates` to override)
sites/default/themes/default/index.tsx the default theme's layer: `templates.nav` → components/site-nav.tsx (coupled only)
middleware.ts re-exports core's middleware (auth gate, session refresh, cookie→bearer bridge) (stamped)
app/api/v1/[...kywi]/route.ts the versioned API — a thin delegate to core's `createApiRoute` (cookie lifting lives in core) (stamped)
app/llms.txt/route.ts etc. the root AX routes — thin delegates to core's `createAxRoute` (stamped: all 4 of
llms.txt, llms-full.txt, sitemap.xml, robots.txt)
app/layout.tsx root <html>/<body>
app/icon.svg favicon
app/admin/[[...admin]]/page.tsx mounts the FULL core admin (KywiAdminApp, every surface) at /admin (stamped)coupled additionally gets app/(site)/layout.tsx + app/(site)/[[...slug]]/page.tsx
(the public site — renders "/" and every published page at its slug, both stamped),
plus app/(site)/site.css and components/site-nav.tsx (project-owned, unstamped).
headless and decoupled instead get app/page.tsx (returns 404 — no public
rendering, unstamped).
All eleven of the files marked (stamped) above are thin, version-stamped
delegates: the two app/(site) route files, middleware.ts, lib/kywi.ts,
kywi.layers.ts, the API route, the four AX root routes and the admin catch-all
page. Each opens with a // kywi-render v2 (create-kywi-app <version>)
sha256:<12 hex> comment — the hash is over the file's own body, so
create-kywi-app upgrade (see below) can tell an untouched generated file
(safe to replace) from one you've edited (refused) without a diff against
every historical emission. The
public render path itself — content and locale resolution, SEO metadata, JSON-LD,
feed and nav hydration, personalization, A/B assignment and the front-of-site
editor — is shipped as @kywi-software/core/next, so pnpm up
@kywi-software/core upgrades it. Before this, ~1,370 lines of it were copied
into every project, where no upgrade could ever reach them again. What a project
owns: kywi.config.ts, app/(site)/site.css, lib/modules.tsx,
components/site-nav.tsx and the layer files under sites/**.
The layer chain. Every named artifact core renders — a module renderer, the nav,
the front-edit overlay, the personalization runtime, the /kywi.js loader — is resolved
per request as theme → site → core, first hit wins, from the registry kywi.layers.ts
imports. To override one, export it by name from sites/<site>/index.ts (modules or
templates) or from sites/<site>/themes/<theme>/index.tsx; never edit core or the
generated files. Core validates the registry at startup and fails loudly when a configured
site or theme is missing from it, when two themes of one site disagree on region names, or
when a layer overrides a module name neither core nor kywi.config.ts defines.
Note what's not in this list: there is no components/admin-shell.tsx, no
per-route app/admin/content/**, no lib/admin-auth.ts. Since 0.2.0 the whole
admin is one component (KywiAdminApp) shipped by @kywi-software/core — the
generated project mounts it with the single catch-all page above instead of
hand-rolling admin pages. See docs/hosting.md §3
for exactly what that mount does, and
docs/admin-configuration.md for
disabling surfaces you don't want (per deployment via admin.features in
kywi.config.ts, or per site at runtime via the superAdmin "Admin Features"
surface — no redeploy).
The security-critical session plumbing (JWT verify, the httpOnly cookie contract,
the cookie→bearer bridge) is NOT copied into every project. It lives once in
@kywi-software/core/host (+ /host-client); the generated middleware.ts and
API route are thin framework wiring over those primitives, so a kywi upgrade
ships session fixes without the app hand-maintaining crypto. See
docs/hosting.md for the full host-app contract.
The generated project's own README.md walks through createdb, copying
.env.example → .env, pnpm migrate, pnpm seed, and pnpm dev. For a
decoupled deployment it also shows the @kywi-software/sdk usage snippet (see
packages/sdk/README.md in this monorepo for the current, verified SDK API).
Adding (or refreshing) agent guidance in an existing project
New scaffolds get the agent-guidance files above automatically. To add them to a
project that already exists — or to pull the latest guidance into one that has
older copies — run the agents subcommand from the project's root:
npx create-kywi-app@latest agents # add any missing guidance files
npx create-kywi-app@latest agents --force # overwrite them with the latestIt writes the same five files (AGENTS.md, CLAUDE.md, and the three
.claude/skills/<slug>/SKILL.md). What it does:
- Detects the project — requires a
kywi.config.tsin the current directory (otherwise it exits with an error), reads the project name frompackage.json(falling back to the directory name) and the deployment mode from the config. - Writes the real
AGENTS.mdheader — the "where things live" section is generated from the landmarks it actually finds on disk (e.g.lib/modules.tsx, the publicapp/(site)/[[...slug]]/page.tsx, and alib/site.ts— which only a project scaffolded before the layered render path still has), so a hand-built or older app gets an honest header rather than one that asserts files it doesn't have, or claims a thin delegate where it holds its own copy. - Never clobbers by default — existing files are skipped (a customized
AGENTS.mdis left untouched); pass--forceto overwrite. It prints a created/updated/skipped summary either way.
Use @latest so an existing project picks up the newest guidance regardless of
which create-kywi-app version originally scaffolded it.
Upgrading a project
Every generated render-path file is version-stamped (see the eleven (stamped)
files above), so create-kywi-app upgrade can prove which ones are still exactly
what it emitted and safe to replace, versus which ones carry your own edits and
must not be touched. Run it from the project's root:
npx create-kywi-app@latest upgrade --dry-run # see the full report, write nothing
npx create-kywi-app@latest upgrade # apply itWhat it does, one line per file in this order — landmarks, then layer files, then retired pre-0.19 glue:
created— a landmark file the project is missing, or a layer file (sites/<id>/index.ts,sites/<id>/themes/<theme>/index.tsx) a site declared inkywi.config.tshas none of yet.replaced— a stamped landmark whose hash proves it is still untouched; swapped for the current emission.current— a stamped landmark that already matches the current emission byte for byte.kept— a layer file undersites/**that already exists. Layer files are created once and never compared or overwritten again — from the moment they exist they are the project's.REFUSED (edited)/REFUSED (unstamped)— a landmark whose hash doesn't match (someone's work is in it) or that carries no stamp at all (predates the layered render path, or was hand-written). The exception: a project scaffolded by 0.19.0 has six landmarks (the API route, the four AX root routes, the admin catch-all page) that 0.19.0 emitted with no stamp at all —upgraderecognises those six by their exact 0.19.0 bytes andreplaces them like any other untouched file, so a 0.19.0 project upgrades with a plainupgrade, no--forceneeded.REFUSED (legacy)— one of the six retired pre-0.19 files (lib/site.ts,lib/config.ts,lib/kywi-js-loader.ts,components/kywi-js-loader.tsx,components/personalization-runtime.tsx,app/(site)/kywi-front-edit.tsx) is still present; their contents now live in@kywi-software/core/next, and leaving a stale copy shadows the real thing.--forceturns this intoremovedinstead.backed up: <path> → .kywi-upgrade/<timestamp>/<path>— printed above anyreplaced/removedline that--forceproduced from a refusal: the original is copied there before it's touched.warn: <path> was ejected from @kywi-software/core <v>; installed is <v>— advisory only, for a filecreate-kywi-app eject(below) put undersites/**whose stamped core version no longer matches the installed one.note: …— advisory hints: staleAGENTS.mdguidance (create-kywi-app agents --forcerefreshes it), or an installed@kywi-software/coreolder than the generated files target.
Without --force, an edited or unstamped file (and the six retired files) is
refused, not touched — the command reports it and leaves it exactly as it
is. Re-run with --force to replace/remove refused files too; each one is
backed up under .kywi-upgrade/<timestamp>/ first, so nothing is discarded.
--dry-run runs the identical plan and prints the identical report, but writes
nothing.
Exit codes: 0 — nothing was refused (with --force, every refusable file
was replaced/removed instead). 2 — one or more files are still refused
because --force wasn't passed — a CI job should treat this as "the project
isn't caught up yet." 1 — no kywi.config.ts here, or it couldn't be scanned
(the layer registry is generated from it, so the command would rather refuse to
write anything than emit one that fails assertLayerContracts at boot).
What it never touches: kywi.config.ts, package.json, lib/modules.tsx,
components/site-nav.tsx, app/(site)/site.css, app/layout.tsx, and any
sites/** file that already exists.
Where your edits belong now, once a file is refused or you're migrating off a pre-0.19 project:
- module renderers →
sites/<site>/index.ts - nav / front-edit / personalization runtime / JS loader →
create-kywi-app eject <artifact>(below) - styles →
app/(site)/site.css
Ejecting a core artifact
Four of core's render-path client components ship as readable .tsx sources
inside @kywi-software/core's eject/ folder, one per name:
| artifact | source file |
| -------------------------- | ------------------------------- |
| nav | site-nav.tsx |
| frontEdit | front-edit.tsx |
| personalizationRuntime | personalization-runtime.tsx |
| jsLoader | kywi-js-loader.tsx |
npx create-kywi-app@latest eject <artifact> [--site <id>] [--theme <name>] [--layer site|theme] [--force]It copies the named artifact into the project as a layer override you own —
sites/<site>/themes/<theme>/<file>.tsx by default (--layer theme; --site
defaults to the first site kywi.config.ts declares, --theme to that site's
own configured theme), or sites/<site>/<file>.tsx with --layer site
(--theme is meaningless there — a site layer isn't per-theme). The copy opens with two stamp
lines: 'use client' first (a directive after any other line is inert, so this
has to stay first), then // kywi-eject <artifact> (@kywi-software/core
<version>) — the provenance create-kywi-app upgrade reads to warn when
core's copy has since moved on.
When the target layer's index file still has the shape create-kywi-app
generates, eject also wires the copy into that layer's templates
export automatically (dropping any import that pointed the same entry at its
previous component, so a re-eject with --force doesn't stack imports). When
the index has been hand-edited into some other shape, it refuses to touch that
file and prints the two lines to add by hand instead — an import line and a
<artifact>: <ExportName>, entry for inside templates.
--force here means something different than in upgrade: it overwrites an
existing ejected file at the target path (without it, eject refuses to
clobber one). Needs node_modules installed, and an installed
@kywi-software/core >= 0.20.0 (the first release that ships eject/).
Ejecting is a deliberate, one-artifact opt-out of core upgrades: from that
moment the file is the project's own, pnpm up @kywi-software/core can no
longer reach it, and create-kywi-app upgrade only warns — via the warn:
line above — when the installed core has moved past the version it was copied
from. Reach for it when you need to change the behaviour of one of these four
components; everything else (module renderers, styles) has a lighter-weight
home — see "Where your edits belong now" above.
Installing the generated packages
All @kywi-software/* packages (core, cli, sdk, mcp, js) are
published to npm at the same version as create-kywi-app. The package.json
this tool writes pins "@kywi-software/core" and "@kywi-software/cli" to
^<that version> (see buildFileSet() in lib/templates.mjs), so a plain
cd my-site && pnpm install # (or npm install / yarn)resolves them straight from the registry — no local build, packing, or override step required.
Working against a local monorepo checkout instead
If you're developing @kywi-software/* itself and want a scaffolded app to
pick up unreleased local changes rather than the published npm packages,
build and pack the packages you changed, then repoint those two deps at the
tarballs in the generated package.json (mirrors a real npm install —
dist-only, deduped React — better than a file: link to the source directory,
which risks a duplicate React copy):
pnpm --filter @kywi-software/core --filter @kywi-software/cli build
( cd packages/core && pnpm pack --pack-destination /tmp/kywi-pkgs )
( cd packages/cli && pnpm pack --pack-destination /tmp/kywi-pkgs )
node packages/create-kywi-app/bin/create-kywi-app.mjs my-site --yes
cd my-site
# Repoint "@kywi-software/core"/"@kywi-software/cli" in package.json at
# file:/tmp/kywi-pkgs/kywi-software-{core,cli}-<version>.tgz, then:
pnpm install && createdb my_site && cp .env.example .env
# (set DATABASE_URL + AUTH_SECRET in .env)
pnpm migrate && pnpm seed && pnpm devRe-pack after any change to core/cli and re-run pnpm install.
Testing
pnpm --filter create-kywi-app test__tests__/templates.test.mjs asserts on buildFileSet()'s output directly —
no filesystem or network access required.
