create-kywi-app 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,14 +14,17 @@ independently of the CLI's prompting/arg-parsing.
14
14
  ## Usage
15
15
 
16
16
  ```bash
17
- npx create-kywi-app [project-name] [options]
17
+ npx create-kywi-app [project-name] [options] # scaffold a new project (default)
18
+ npx create-kywi-app agents [--force] # add/refresh agent guidance in an
19
+ # existing project (run from its root)
18
20
 
19
21
  Options:
20
22
  --yes, -y Use defaults, skip prompts
21
23
  --mode <mode> coupled | headless | decoupled (default: coupled)
22
24
  --db <provider> postgresql | mysql (default: postgresql)
23
25
  --auth <list> comma-separated: credentials,google,github (default: credentials)
24
- --help, -h Show this help
26
+ --force, -f agents: overwrite existing guidance files (default: skip them)
27
+ --help, -h Show this help
25
28
  ```
26
29
 
27
30
  With no flags and a TTY, it prompts interactively (project name, DB provider,
@@ -116,6 +119,35 @@ The generated project's own `README.md` walks through `createdb`, copying
116
119
  `decoupled` deployment it also shows the `@kywi-software/sdk` usage snippet (see
117
120
  `packages/sdk/README.md` in this monorepo for the current, verified SDK API).
118
121
 
122
+ ## Adding (or refreshing) agent guidance in an existing project
123
+
124
+ New scaffolds get the agent-guidance files above automatically. To add them to a
125
+ project that already exists — or to pull the **latest** guidance into one that has
126
+ older copies — run the `agents` subcommand **from the project's root**:
127
+
128
+ ```bash
129
+ npx create-kywi-app@latest agents # add any missing guidance files
130
+ npx create-kywi-app@latest agents --force # overwrite them with the latest
131
+ ```
132
+
133
+ It writes the same five files (`AGENTS.md`, `CLAUDE.md`, and the three
134
+ `.claude/skills/<slug>/SKILL.md`). What it does:
135
+
136
+ - **Detects the project** — requires a `kywi.config.ts` in the current directory
137
+ (otherwise it exits with an error), reads the project name from `package.json`
138
+ (falling back to the directory name) and the deployment mode from the config.
139
+ - **Writes the real `AGENTS.md` header** — the "where things live" section is
140
+ generated from the landmarks it actually finds on disk (e.g. `lib/site.ts`,
141
+ `lib/modules.tsx`, the public `app/(site)/[[...slug]]/page.tsx`), so a
142
+ hand-built app that predates part of the scaffold gets an honest header rather
143
+ than one that asserts files it doesn't have.
144
+ - **Never clobbers by default** — existing files are skipped (a customized
145
+ `AGENTS.md` is left untouched); pass `--force` to overwrite. It prints a
146
+ created/updated/skipped summary either way.
147
+
148
+ Use `@latest` so an existing project picks up the newest guidance regardless of
149
+ which `create-kywi-app` version originally scaffolded it.
150
+
119
151
  ## Installing the generated packages
120
152
 
121
153
  All `@kywi-software/*` packages (`core`, `cli`, `sdk`, `mcp`, `js`) are
@@ -12,12 +12,12 @@
12
12
  */
13
13
  import { mkdir, writeFile, readdir } from 'node:fs/promises'
14
14
  import { existsSync } from 'node:fs'
15
- import { dirname, join, resolve } from 'node:path'
15
+ import { dirname, join, resolve, basename } from 'node:path'
16
16
  import { createInterface } from 'node:readline/promises'
17
17
  import { stdin, stdout, argv, exit } from 'node:process'
18
18
  import { fileURLToPath } from 'node:url'
19
19
  import { readFileSync } from 'node:fs'
20
- import { buildFileSet } from '../lib/templates.mjs'
20
+ import { buildFileSet, guidanceFileSet, detectAgentsLandmarks } from '../lib/templates.mjs'
21
21
 
22
22
  const MODES = ['coupled', 'headless', 'decoupled']
23
23
  const DB_PROVIDERS = ['postgresql', 'mysql']
@@ -38,12 +38,13 @@ function readKywiVersion() {
38
38
  // ── Arg parsing ───────────────────────────────────────────────────────────────
39
39
 
40
40
  function parseArgs(args) {
41
- const opts = { name: undefined, yes: false, mode: undefined, db: undefined, auth: undefined, help: false }
42
- const positional = []
41
+ const opts = { name: undefined, yes: false, mode: undefined, db: undefined, auth: undefined, help: false, force: false, positional: [] }
42
+ const positional = opts.positional
43
43
  for (let i = 0; i < args.length; i++) {
44
44
  const arg = args[i]
45
45
  if (arg === '--yes' || arg === '-y') opts.yes = true
46
46
  else if (arg === '--help' || arg === '-h') opts.help = true
47
+ else if (arg === '--force' || arg === '-f') opts.force = true
47
48
  else if (arg === '--mode') opts.mode = args[++i]
48
49
  else if (arg.startsWith('--mode=')) opts.mode = arg.slice(7)
49
50
  else if (arg === '--db') opts.db = args[++i]
@@ -61,19 +62,27 @@ function printHelp() {
61
62
  create-kywi-app — scaffold a new Kywi CMS project
62
63
 
63
64
  Usage:
64
- create-kywi-app [project-name] [options]
65
+ create-kywi-app [project-name] [options] Scaffold a new project (default)
66
+ create-kywi-app agents [--force] Install/refresh the agent-guidance
67
+ files (AGENTS.md, CLAUDE.md,
68
+ .claude/skills/) in the CURRENT
69
+ Kywi project — run it from the
70
+ project root
65
71
 
66
72
  Options:
67
73
  --yes, -y Use defaults, skip prompts
68
74
  --mode <mode> coupled | headless | decoupled (default: coupled)
69
75
  --db <provider> postgresql | mysql (default: postgresql)
70
76
  --auth <list> comma-separated: credentials,google,github (default: credentials)
77
+ --force, -f agents: overwrite existing guidance files (default: skip them)
71
78
  --help, -h Show this help
72
79
 
73
80
  Examples:
74
81
  npx create-kywi-app my-site
75
82
  npx create-kywi-app my-site --yes
76
83
  npx create-kywi-app blog --mode headless --auth credentials,google
84
+ npx create-kywi-app@latest agents # add guidance to an existing project
85
+ npx create-kywi-app@latest agents --force # refresh it to the latest version
77
86
  `)
78
87
  }
79
88
 
@@ -146,6 +155,107 @@ async function isNonEmptyDir(dir) {
146
155
  return entries.length > 0
147
156
  }
148
157
 
158
+ // ── `agents` subcommand ─────────────────────────────────────────────────────────
159
+
160
+ const DETECTABLE_MODES = ['coupled', 'headless', 'decoupled']
161
+
162
+ /** Read the project name from ./package.json, falling back to the directory name. */
163
+ function readProjectName(dir) {
164
+ try {
165
+ const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
166
+ if (typeof pkg.name === 'string' && pkg.name.trim()) return pkg.name.trim()
167
+ } catch {
168
+ /* no/invalid package.json — fall through to the directory basename */
169
+ }
170
+ return basename(dir) || 'kywi-app'
171
+ }
172
+
173
+ /**
174
+ * Detect the deployment mode from kywi.config.ts (`mode: 'coupled'|'headless'|
175
+ * 'decoupled'`). Returns coupled with detected:false when absent/unmatched, so the
176
+ * caller can print a note.
177
+ */
178
+ function detectMode(dir) {
179
+ try {
180
+ const cfg = readFileSync(join(dir, 'kywi.config.ts'), 'utf8')
181
+ const m = cfg.match(/mode:\s*['"](coupled|headless|decoupled)['"]/)
182
+ if (m && DETECTABLE_MODES.includes(m[1])) return { mode: m[1], detected: true }
183
+ } catch {
184
+ /* unreadable config — handled by the caller's kywi.config.ts existence gate */
185
+ }
186
+ return { mode: 'coupled', detected: false }
187
+ }
188
+
189
+ /**
190
+ * `create-kywi-app agents [--force]` — install/refresh the agent-guidance files
191
+ * that a fresh scaffold ships (AGENTS.md, CLAUDE.md, the three .claude/skills/*)
192
+ * into an EXISTING Kywi project. The AGENTS.md header is generated from landmarks
193
+ * detected on disk, so it describes the real project rather than asserting scaffold
194
+ * structure. Without --force, existing target files are skipped (never clobbered);
195
+ * with --force, they are overwritten.
196
+ */
197
+ async function runAgentsCommand(opts) {
198
+ const cwd = process.cwd()
199
+
200
+ // 1. Detect a Kywi project.
201
+ if (!existsSync(join(cwd, 'kywi.config.ts'))) {
202
+ stdout.write(
203
+ `\n✖ No kywi.config.ts here — run \`create-kywi-app agents\` inside a Kywi project (its root).\n`,
204
+ )
205
+ return 1
206
+ }
207
+
208
+ const projectName = readProjectName(cwd)
209
+ const { mode, detected } = detectMode(cwd)
210
+
211
+ stdout.write(`\nRefreshing agent guidance for ${projectName} (${mode} mode)…\n`)
212
+ if (!detected) {
213
+ stdout.write(` note: no deployment mode found in kywi.config.ts — assuming \`coupled\`.\n`)
214
+ }
215
+
216
+ // The guidance files depend only on projectName + mode (+ detected landmarks);
217
+ // the other answers fields are irrelevant here but kept shape-complete.
218
+ const answers = {
219
+ projectName,
220
+ mode,
221
+ dbProvider: 'postgresql',
222
+ authProviders: ['credentials'],
223
+ kywiVersion: KYWI_VERSION,
224
+ }
225
+ const landmarks = detectAgentsLandmarks(cwd)
226
+ const files = guidanceFileSet(answers, landmarks)
227
+
228
+ // 4. Write with safe semantics: without --force, skip existing files.
229
+ let created = 0
230
+ let updated = 0
231
+ let skipped = 0
232
+ for (const [rel, content] of Object.entries(files)) {
233
+ const abs = join(cwd, rel)
234
+ const exists = existsSync(abs)
235
+ if (exists && !opts.force) {
236
+ stdout.write(` skipped (exists): ${rel}\n`)
237
+ skipped++
238
+ continue
239
+ }
240
+ await mkdir(dirname(abs), { recursive: true })
241
+ await writeFile(abs, content, 'utf8')
242
+ if (exists) {
243
+ stdout.write(` updated: ${rel}\n`)
244
+ updated++
245
+ } else {
246
+ stdout.write(` created: ${rel}\n`)
247
+ created++
248
+ }
249
+ }
250
+
251
+ stdout.write(`\n✔ ${created} created, ${updated} updated, ${skipped} skipped.\n`)
252
+ if (skipped > 0) {
253
+ stdout.write(` Re-run with --force to overwrite the skipped file(s).\n`)
254
+ }
255
+ stdout.write(`\nAgents will pick these up automatically; see AGENTS.md.\n`)
256
+ return 0
257
+ }
258
+
149
259
  // ── Main ──────────────────────────────────────────────────────────────────────
150
260
 
151
261
  async function main() {
@@ -155,6 +265,12 @@ async function main() {
155
265
  return 0
156
266
  }
157
267
 
268
+ // Subcommand: `create-kywi-app agents [--force]` refreshes the agent-guidance
269
+ // files in an existing project instead of scaffolding a new one.
270
+ if (opts.positional[0] === 'agents') {
271
+ return runAgentsCommand(opts)
272
+ }
273
+
158
274
  let answers
159
275
  try {
160
276
  answers = await resolveAnswers(opts)
package/lib/templates.mjs CHANGED
@@ -32,7 +32,7 @@
32
32
  * ships session fixes without the app hand-maintaining crypto.
33
33
  */
34
34
 
35
- import { readFileSync } from 'node:fs'
35
+ import { readFileSync, existsSync } from 'node:fs'
36
36
  import { dirname, join } from 'node:path'
37
37
  import { fileURLToPath } from 'node:url'
38
38
 
@@ -1990,7 +1990,7 @@ const _skillDocCache = new Map()
1990
1990
  * @param {string} asset
1991
1991
  * @returns {string}
1992
1992
  */
1993
- function skillDoc(asset) {
1993
+ export function skillDoc(asset) {
1994
1994
  if (!_skillDocCache.has(asset)) {
1995
1995
  const assetPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'assets', asset)
1996
1996
  _skillDocCache.set(asset, readFileSync(assetPath, 'utf8'))
@@ -1999,51 +1999,159 @@ function skillDoc(asset) {
1999
1999
  }
2000
2000
 
2001
2001
  /**
2002
- * AGENTS.md a short, app-specific header that orients an agent in THIS generated
2003
- * app, followed by Kywi's canonical patterns doc verbatim. Mode-aware: only
2004
- * coupled apps have public-render wiring (lib/site.ts), so the header reflects it.
2002
+ * Landmark its relative path in a Kywi project. A "landmark" is a file whose
2003
+ * presence changes how the AGENTS.md header should describe the app. The scaffold
2004
+ * derives its set statically from the mode ({@link scaffoldLandmarks}); the
2005
+ * `agents` refresh command detects them on disk ({@link detectAgentsLandmarks}),
2006
+ * so the header reflects the REAL project instead of asserting scaffold structure.
2007
+ * One source of truth for the path strings, shared by both.
2008
+ * @type {Record<'adminHost'|'moduleMap'|'middleware'|'libSite'|'sitePage'|'headlessPage', string>}
2009
+ */
2010
+ export const AGENTS_LANDMARK_PATHS = {
2011
+ adminHost: 'app/admin/[[...admin]]/page.tsx',
2012
+ moduleMap: 'lib/modules.tsx',
2013
+ middleware: 'middleware.ts',
2014
+ libSite: 'lib/site.ts',
2015
+ sitePage: 'app/(site)/[[...slug]]/page.tsx',
2016
+ headlessPage: 'app/page.tsx',
2017
+ }
2018
+
2019
+ /**
2020
+ * @typedef {Object} AgentsLandmarks
2021
+ * @property {boolean} scaffolded Generating for a fresh scaffold (true) vs
2022
+ * refreshing into an existing project (false). Controls only the intro line's
2023
+ * "scaffolded by create-kywi-app" claim.
2024
+ * @property {boolean} adminHost app/admin/[[...admin]]/page.tsx present
2025
+ * @property {boolean} moduleMap lib/modules.tsx present
2026
+ * @property {boolean} middleware middleware.ts present
2027
+ * @property {boolean} libSite lib/site.ts present (the scaffold's public-render helpers)
2028
+ * @property {boolean} sitePage app/(site)/[[...slug]]/page.tsx present (renders public pages)
2029
+ * @property {boolean} headlessPage app/page.tsx present (the headless/decoupled 404 root)
2030
+ */
2031
+
2032
+ /**
2033
+ * The landmark set a FRESH scaffold of the given mode has — derived statically
2034
+ * from the mode, never from the filesystem, so buildFileSet emits AGENTS.md that
2035
+ * is byte-identical to the previous mode-branching implementation.
2036
+ * @param {'coupled'|'headless'|'decoupled'} mode
2037
+ * @returns {AgentsLandmarks}
2038
+ */
2039
+ export function scaffoldLandmarks(mode) {
2040
+ const coupled = mode === 'coupled'
2041
+ return {
2042
+ scaffolded: true,
2043
+ adminHost: true,
2044
+ moduleMap: true,
2045
+ middleware: true,
2046
+ libSite: coupled,
2047
+ sitePage: coupled,
2048
+ headlessPage: !coupled,
2049
+ }
2050
+ }
2051
+
2052
+ /**
2053
+ * Detect the AGENTS.md landmarks that actually exist in an EXISTING project, so
2054
+ * `create-kywi-app agents` writes a header that matches the app on disk (a
2055
+ * hand-built app may render the public site from the page directly and predate
2056
+ * the scaffold's lib/site.ts helper or lib/modules.tsx map).
2057
+ * @param {string} projectDir absolute path to the project root
2058
+ * @returns {AgentsLandmarks}
2059
+ */
2060
+ export function detectAgentsLandmarks(projectDir) {
2061
+ const has = (rel) => existsSync(join(projectDir, rel))
2062
+ return {
2063
+ scaffolded: false,
2064
+ adminHost: has(AGENTS_LANDMARK_PATHS.adminHost),
2065
+ moduleMap: has(AGENTS_LANDMARK_PATHS.moduleMap),
2066
+ middleware: has(AGENTS_LANDMARK_PATHS.middleware),
2067
+ libSite: has(AGENTS_LANDMARK_PATHS.libSite),
2068
+ sitePage: has(AGENTS_LANDMARK_PATHS.sitePage),
2069
+ headlessPage: has(AGENTS_LANDMARK_PATHS.headlessPage),
2070
+ }
2071
+ }
2072
+
2073
+ /**
2074
+ * AGENTS.md — a short, app-specific header that orients an agent in THIS app,
2075
+ * followed by Kywi's canonical patterns doc verbatim. Landmark-aware: each "Where
2076
+ * things live" bullet is emitted only for a landmark that is actually present, and
2077
+ * absent public-render (lib/site.ts) or module-map (lib/modules.tsx) wiring becomes
2078
+ * an honest "this app predates the scaffold's …" line instead of a bullet that
2079
+ * asserts a file the project does not have. With no landmarks passed it defaults to
2080
+ * the fresh-scaffold set for the mode, so the scaffold output is unchanged.
2005
2081
  * @param {Answers} a
2082
+ * @param {AgentsLandmarks} [landmarks]
2006
2083
  */
2007
- function agentsMd(a) {
2008
- const coupled = a.mode === 'coupled'
2009
- const modeSentence = coupled
2010
- ? 'This app renders the public site AND serves the admin + API.'
2011
- : a.mode === 'headless'
2012
- ? 'This app serves the admin + API only (`GET /` returns 404); there is no public rendering here.'
2013
- : 'This app serves the admin + API only; a separate frontend consumes the API via `@kywi-software/sdk`. There is no public rendering here.'
2014
- const surfaceLine = coupled
2015
- ? `- \`lib/site.ts\` — public-render helpers (path/locale resolution, feeds,
2084
+ export function agentsMd(a, landmarks = scaffoldLandmarks(a.mode)) {
2085
+ const L = landmarks
2086
+ const modeSentence =
2087
+ a.mode === 'coupled'
2088
+ ? 'This app renders the public site AND serves the admin + API.'
2089
+ : a.mode === 'headless'
2090
+ ? 'This app serves the admin + API only (`GET /` returns 404); there is no public rendering here.'
2091
+ : 'This app serves the admin + API only; a separate frontend consumes the API via `@kywi-software/sdk`. There is no public rendering here.'
2092
+
2093
+ // Existing projects aren't necessarily scaffolded — don't assert they were.
2094
+ const intro = L.scaffolded
2095
+ ? `This is a **Kywi CMS** project scaffolded by \`create-kywi-app\` (\`${a.mode}\` mode).`
2096
+ : `This is a **Kywi CMS** project (\`${a.mode}\` mode).`
2097
+
2098
+ // "Where things live" — one bullet per PRESENT landmark, in a fixed order.
2099
+ const bullets = []
2100
+ bullets.push(`- \`kywi.config.ts\` — project config: sites, themes, content types, auth
2101
+ providers, deployment mode, and \`admin.features\`. Edit it, then re-run
2102
+ \`pnpm migrate\`.`)
2103
+ if (L.adminHost) {
2104
+ bullets.push(`- \`app/admin/[[...admin]]/page.tsx\` — mounts Kywi's **full admin**
2105
+ (\`KywiAdminApp\`) at **\`/admin\`**. Every surface — content, media, feeds,
2106
+ forms, audiences, settings, … — is already there; never hand-build admin pages.`)
2107
+ }
2108
+ if (L.moduleMap) {
2109
+ bullets.push(`- \`lib/modules.tsx\` — the custom-module map (\`defineModule\` renderers), shared
2110
+ by the admin editor and the public site.`)
2111
+ } else {
2112
+ bullets.push(`- No \`lib/modules.tsx\` custom-module map — this app predates the scaffold's
2113
+ \`defineModule\` module map (shared by the admin editor and the public site); see
2114
+ the patterns doc below for the intended shape.`)
2115
+ }
2116
+ // Public-render surface: the scaffold's lib/site.ts helper, else a page that
2117
+ // renders publicly without it (honest note), else no public rendering at all.
2118
+ if (L.libSite) {
2119
+ bullets.push(`- \`lib/site.ts\` — public-render helpers (path/locale resolution, feeds,
2016
2120
  components, personalization) used by \`app/(site)/[[...slug]]/page.tsx\`, which
2017
- renders every published page at its slug.`
2018
- : a.mode === 'headless'
2019
- ? `- No public rendering in this mode — \`app/page.tsx\` returns 404. Content is
2020
- served over the REST API at \`/api/v1\`.`
2021
- : `- No public rendering in this mode — \`app/page.tsx\` returns 404. Build a
2121
+ renders every published page at its slug.`)
2122
+ } else if (L.sitePage) {
2123
+ bullets.push(`- \`app/(site)/[[...slug]]/page.tsx\` renders every published page at its
2124
+ slug. This app predates the scaffold's \`lib/site.ts\` public-render helpers
2125
+ (path/locale resolution, feeds, components, personalization); see the patterns
2126
+ doc below for the intended shape.`)
2127
+ } else {
2128
+ bullets.push(
2129
+ a.mode === 'decoupled'
2130
+ ? `- No public rendering in this mode — \`app/page.tsx\` returns 404. Build a
2022
2131
  separate frontend against the REST API at \`/api/v1\` with \`@kywi-software/sdk\`.`
2132
+ : `- No public rendering in this mode — \`app/page.tsx\` returns 404. Content is
2133
+ served over the REST API at \`/api/v1\`.`,
2134
+ )
2135
+ }
2136
+ if (L.middleware) {
2137
+ bullets.push(`- \`middleware.ts\` — auth gate + session refresh, thin wiring over
2138
+ \`@kywi-software/core/host\`.`)
2139
+ }
2140
+ bullets.push(`- \`.claude/skills/kywi-content-model/SKILL.md\` — content-model planning
2141
+ skill, loaded automatically before building anything.`)
2142
+ bullets.push(`- \`.claude/skills/kywi-collections/SKILL.md\` — collections skill, loaded
2143
+ automatically when adding any collection.`)
2144
+ bullets.push(`- \`.claude/skills/kywi-personalization/SKILL.md\` — personalization skill,
2145
+ loaded automatically when the owner wants personalization or A/B testing.`)
2146
+
2023
2147
  const header = `# Agent guide — ${a.projectName}
2024
2148
 
2025
- This is a **Kywi CMS** project scaffolded by \`create-kywi-app\` (\`${a.mode}\` mode).
2149
+ ${intro}
2026
2150
  ${modeSentence}
2027
2151
 
2028
2152
  ## Where things live
2029
2153
 
2030
- - \`kywi.config.ts\` — project config: sites, themes, content types, auth
2031
- providers, deployment mode, and \`admin.features\`. Edit it, then re-run
2032
- \`pnpm migrate\`.
2033
- - \`app/admin/[[...admin]]/page.tsx\` — mounts Kywi's **full admin**
2034
- (\`KywiAdminApp\`) at **\`/admin\`**. Every surface — content, media, feeds,
2035
- forms, audiences, settings, … — is already there; never hand-build admin pages.
2036
- - \`lib/modules.tsx\` — the custom-module map (\`defineModule\` renderers), shared
2037
- by the admin editor and the public site.
2038
- ${surfaceLine}
2039
- - \`middleware.ts\` — auth gate + session refresh, thin wiring over
2040
- \`@kywi-software/core/host\`.
2041
- - \`.claude/skills/kywi-content-model/SKILL.md\` — content-model planning
2042
- skill, loaded automatically before building anything.
2043
- - \`.claude/skills/kywi-collections/SKILL.md\` — collections skill, loaded
2044
- automatically when adding any collection.
2045
- - \`.claude/skills/kywi-personalization/SKILL.md\` — personalization skill,
2046
- loaded automatically when the owner wants personalization or A/B testing.
2154
+ ${bullets.join('\n')}
2047
2155
 
2048
2156
  ## Running it
2049
2157
 
@@ -2070,7 +2178,7 @@ CMS instead of hardcoding it.**
2070
2178
  * CLAUDE.md — a thin pointer so a Claude Code session reads AGENTS.md first.
2071
2179
  * @param {Answers} a
2072
2180
  */
2073
- function claudeMd(a) {
2181
+ export function claudeMd(a) {
2074
2182
  return `# ${a.projectName}
2075
2183
 
2076
2184
  This is a **Kywi CMS** project. **Read \`AGENTS.md\` before building anything** — it
@@ -2082,6 +2190,30 @@ Before building out a new content area, use the \`kywi-content-model\` skill (de
2082
2190
  `
2083
2191
  }
2084
2192
 
2193
+ /**
2194
+ * The agent-guidance files every Kywi app should carry: AGENTS.md, an app-specific
2195
+ * header + Kywi's canonical patterns doc; CLAUDE.md, a thin pointer into it; and
2196
+ * the three Claude Code project skills, verbatim assets. ONE source of truth for
2197
+ * "which files are the agent guidance", shared by {@link buildFileSet} (fresh
2198
+ * scaffold, {@link scaffoldLandmarks}) and the `agents` refresh command
2199
+ * ({@link detectAgentsLandmarks}) — so both emit identical content for identical
2200
+ * inputs, and adding/renaming a skill in {@link SKILLS} updates both at once.
2201
+ * @param {Answers} a
2202
+ * @param {AgentsLandmarks} [landmarks]
2203
+ * @returns {Record<string, string>}
2204
+ */
2205
+ export function guidanceFileSet(a, landmarks = scaffoldLandmarks(a.mode)) {
2206
+ /** @type {Record<string, string>} */
2207
+ const files = {
2208
+ 'AGENTS.md': agentsMd(a, landmarks),
2209
+ 'CLAUDE.md': claudeMd(a),
2210
+ }
2211
+ for (const { slug, asset } of SKILLS) {
2212
+ files[`.claude/skills/${slug}/SKILL.md`] = skillDoc(asset)
2213
+ }
2214
+ return files
2215
+ }
2216
+
2085
2217
  /**
2086
2218
  * Build the complete map of relative-path → file-content for a project.
2087
2219
  * @param {Answers} answers
@@ -2098,10 +2230,12 @@ export function buildFileSet(answers) {
2098
2230
  '.env.example': envExample(),
2099
2231
  '.gitignore': gitignore(),
2100
2232
  'README.md': readme(answers),
2101
- // agent guidance (every mode): app-specific header + Kywi's canonical patterns
2102
- // doc verbatim. CLAUDE.md is a thin pointer into AGENTS.md.
2103
- 'AGENTS.md': agentsMd(answers),
2104
- 'CLAUDE.md': claudeMd(answers),
2233
+ // agent guidance (every mode): AGENTS.md (app-specific header + Kywi's
2234
+ // canonical patterns doc verbatim), CLAUDE.md (thin pointer into it), and the
2235
+ // three Claude Code project skills. Landmarks are derived statically from the
2236
+ // mode so this stays byte-identical to the pre-landmark implementation; the
2237
+ // same guidanceFileSet powers `create-kywi-app agents` for existing projects.
2238
+ ...guidanceFileSet(answers, scaffoldLandmarks(answers.mode)),
2105
2239
  // server runtime + config
2106
2240
  'lib/kywi.ts': libKywi(),
2107
2241
  'lib/config.ts': libConfig(),
@@ -2126,15 +2260,6 @@ export function buildFileSet(answers) {
2126
2260
  'app/robots.txt/route.ts': axRootRoute('robots.txt'),
2127
2261
  }
2128
2262
 
2129
- // Claude Code project skills (every mode): content-model-first discipline,
2130
- // collections, and personalization — Claude Code loads each automatically at
2131
- // the moment it's relevant (see SKILLS above for when-to-use). Verbatim
2132
- // assets, same readFileSync-from-import.meta.url + cache mechanism as
2133
- // AGENTS.md above.
2134
- for (const { slug, asset } of SKILLS) {
2135
- files[`.claude/skills/${slug}/SKILL.md`] = skillDoc(asset)
2136
- }
2137
-
2138
2263
  if (answers.mode === 'coupled') {
2139
2264
  // Public site: an optional catch-all renders "/" (home) and every published
2140
2265
  // page at its slug. More-specific /admin and /api routes take precedence.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kywi-app",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Scaffold a new Kywi CMS project — npx create-kywi-app my-site",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Kywi-Software/kywi-cms#readme",