create-kywi-app 0.4.0 → 0.6.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 +34 -2
- package/bin/create-kywi-app.mjs +121 -5
- package/lib/templates.mjs +350 -63
- package/package.json +1 -1
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
|
-
--
|
|
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
|
package/bin/create-kywi-app.mjs
CHANGED
|
@@ -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
|
|
|
@@ -383,12 +383,13 @@ import type {
|
|
|
383
383
|
ModuleNode,
|
|
384
384
|
FeedItemsResolver,
|
|
385
385
|
ComponentResolver,
|
|
386
|
+
SectionComponentResolver,
|
|
386
387
|
PersonalizationState,
|
|
387
388
|
} from '@kywi-software/core/layout'
|
|
388
389
|
import { isLayoutSection, applyPageVariant } from '@kywi-software/core/layout'
|
|
389
390
|
import { resolveContentByPath, normalizePath } from '@kywi-software/core/nav'
|
|
390
391
|
import { getFeedBySlug, getComponentById, resolveLocaleFromRequest } from '@kywi-software/core'
|
|
391
|
-
import { resolveComponentDefinition } from '@kywi-software/core/admin/server'
|
|
392
|
+
import { resolveComponentDefinition, resolveSectionComponentDefinition } from '@kywi-software/core/admin/server'
|
|
392
393
|
import {
|
|
393
394
|
evaluateActiveAudiences,
|
|
394
395
|
getSelfIdWidgetConfig,
|
|
@@ -590,6 +591,41 @@ export async function buildComponentResolver(
|
|
|
590
591
|
return (componentId) => resolveComponentDefinition(map.get(componentId) ?? null)
|
|
591
592
|
}
|
|
592
593
|
|
|
594
|
+
/** Yield every section-level component reference (\`section.componentId\`) in the
|
|
595
|
+
* layout, descending into variantContainer default + variant sections (#69). */
|
|
596
|
+
function* sectionComponentIds(regions: Record<string, RegionNode[]>): Generator<string> {
|
|
597
|
+
for (const nodes of Object.values(regions)) {
|
|
598
|
+
for (const node of nodes) {
|
|
599
|
+
if (isLayoutSection(node)) {
|
|
600
|
+
if (node.componentId) yield node.componentId
|
|
601
|
+
} else {
|
|
602
|
+
for (const s of node.defaultSections) if (s.componentId) yield s.componentId
|
|
603
|
+
for (const v of node.variants) for (const s of v.sections) if (s.componentId) yield s.componentId
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Prefetch every connected *section* component (\`section.componentId\`) referenced
|
|
611
|
+
* by the layout and return a synchronous {@link SectionComponentResolver} over the
|
|
612
|
+
* snapshot — parallel to {@link buildComponentResolver} but for reusable sections
|
|
613
|
+
* (#69). Returns undefined when the layout references none (skip the prop).
|
|
614
|
+
*/
|
|
615
|
+
export async function buildSectionComponentResolver(
|
|
616
|
+
layout: LayoutDocument,
|
|
617
|
+
runtime: KywiRuntime,
|
|
618
|
+
): Promise<SectionComponentResolver | undefined> {
|
|
619
|
+
const ids = new Set<string>(sectionComponentIds(layout.regions))
|
|
620
|
+
if (ids.size === 0) return undefined
|
|
621
|
+
const { db, siteId } = runtime
|
|
622
|
+
const entries = await Promise.all(
|
|
623
|
+
[...ids].map(async (id) => [id, await getComponentById(db, id, siteId)] as const),
|
|
624
|
+
)
|
|
625
|
+
const map = new Map(entries)
|
|
626
|
+
return (componentId) => resolveSectionComponentDefinition(map.get(componentId) ?? null)
|
|
627
|
+
}
|
|
628
|
+
|
|
593
629
|
// ─── Personalization + experiments (#50) ─────────────────────────────────────
|
|
594
630
|
|
|
595
631
|
/** Server-resolved personalization for one public request. */
|
|
@@ -1117,6 +1153,7 @@ import {
|
|
|
1117
1153
|
clientRuntimeEnabled,
|
|
1118
1154
|
buildFeedResolver,
|
|
1119
1155
|
buildComponentResolver,
|
|
1156
|
+
buildSectionComponentResolver,
|
|
1120
1157
|
localeAlternates,
|
|
1121
1158
|
mediaUrl,
|
|
1122
1159
|
requestBaseUrl,
|
|
@@ -1255,6 +1292,7 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
|
|
|
1255
1292
|
const personalized = personalizeLayout(layout, perso.audienceId)
|
|
1256
1293
|
const hydrated = await hydrateLayoutFeeds(personalized, buildFeedResolver(runtime))
|
|
1257
1294
|
const componentResolver = await buildComponentResolver(hydrated, runtime)
|
|
1295
|
+
const sectionComponentResolver = await buildSectionComponentResolver(hydrated, runtime)
|
|
1258
1296
|
content = (
|
|
1259
1297
|
<article className="page page--layout" data-kywi-content-id={contentId}>
|
|
1260
1298
|
{head}
|
|
@@ -1263,6 +1301,7 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
|
|
|
1263
1301
|
personalization={perso.personalization}
|
|
1264
1302
|
moduleComponents={moduleComponents}
|
|
1265
1303
|
{...(componentResolver ? { componentResolver } : {})}
|
|
1304
|
+
{...(sectionComponentResolver ? { sectionComponentResolver } : {})}
|
|
1266
1305
|
>
|
|
1267
1306
|
{Object.keys(hydrated.regions).map((name) => (
|
|
1268
1307
|
<KywiRegion key={name} name={name} />
|
|
@@ -1298,9 +1337,12 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
|
|
|
1298
1337
|
canEdit={perms.canEdit}
|
|
1299
1338
|
canPublish={perms.canPublish}
|
|
1300
1339
|
contentId={contentId}
|
|
1340
|
+
contentType={contentType}
|
|
1301
1341
|
pageTitle={title}
|
|
1302
1342
|
pageStatus={String(node['status'] ?? '')}
|
|
1343
|
+
initialLayout={layout ?? { regions: { main: [] } }}
|
|
1303
1344
|
adminHref={\`/admin/content/\${contentType}/\${contentId}\`}
|
|
1345
|
+
moduleComponents={moduleComponents}
|
|
1304
1346
|
>
|
|
1305
1347
|
{content}
|
|
1306
1348
|
</KywiFrontEdit>
|
|
@@ -1315,56 +1357,167 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
|
|
|
1315
1357
|
/**
|
|
1316
1358
|
* Front-of-site edit overlay (client). Mounted by the public page ONLY for an
|
|
1317
1359
|
* authenticated admin who opened the page with ?kywi-edit=1 (the page verifies
|
|
1318
|
-
* the session cookie server-side first).
|
|
1319
|
-
*
|
|
1320
|
-
*
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1360
|
+
* the session cookie server-side first).
|
|
1361
|
+
*
|
|
1362
|
+
* Two modes, same component:
|
|
1363
|
+
* - **Browse** — core's slim `KywiEditToolbar` over the live page, with the
|
|
1364
|
+
* editable regions outlined via the `data-kywi-*` protocol.
|
|
1365
|
+
* - **Edit** (`?kywi-edit=1`, or the toolbar's Edit toggle) — the FULL
|
|
1366
|
+
* `OverlayShell` layout editor, the same one the admin app mounts, rendered
|
|
1367
|
+
* from the same core module registry + custom renderers. Save PUTs the layout
|
|
1368
|
+
* document; Publish PUTs the layout then flips status — exactly the API the
|
|
1369
|
+
* admin editor uses.
|
|
1370
|
+
*
|
|
1371
|
+
* The OverlayShell is lazy-loaded (`next/dynamic`, client-only): its weight
|
|
1372
|
+
* (dnd-kit, canvas, side panels) is only fetched when an editor actually enters
|
|
1373
|
+
* edit mode, so the public browse bundle stays light.
|
|
1323
1374
|
*/
|
|
1324
1375
|
function frontEditOverlay() {
|
|
1325
1376
|
return `'use client'
|
|
1326
1377
|
import React from 'react'
|
|
1378
|
+
import dynamic from 'next/dynamic'
|
|
1327
1379
|
import { KywiEditToolbar, useKywiEditMode } from '@kywi-software/core/scope-client'
|
|
1328
1380
|
import { adminFetch } from '@kywi-software/core/host-client'
|
|
1381
|
+
import {
|
|
1382
|
+
createModuleRegistry,
|
|
1383
|
+
createThemeRegistry,
|
|
1384
|
+
BUILT_IN_MODULE_COMPONENTS,
|
|
1385
|
+
type LayoutDocument,
|
|
1386
|
+
type ModuleComponentMap,
|
|
1387
|
+
} from '@kywi-software/core/layout'
|
|
1388
|
+
import type { SaveAction } from '@kywi-software/core/admin'
|
|
1389
|
+
|
|
1390
|
+
// Lazy-load the full layout editor AND its stylesheet: the shell bundle (dnd-kit,
|
|
1391
|
+
// canvas, panels) and the ~4.7k-line admin design system are pulled INSIDE this
|
|
1392
|
+
// factory, so they enter the module graph only when an editor opens the overlay —
|
|
1393
|
+
// never in the public browse bundle a visitor downloads.
|
|
1394
|
+
const OverlayShell = dynamic(
|
|
1395
|
+
async () => {
|
|
1396
|
+
await import('@kywi-software/core/admin/styles.css')
|
|
1397
|
+
const mod = await import('@kywi-software/core/admin')
|
|
1398
|
+
return mod.OverlayShell
|
|
1399
|
+
},
|
|
1400
|
+
{ ssr: false },
|
|
1401
|
+
)
|
|
1329
1402
|
|
|
1330
1403
|
export interface KywiFrontEditProps {
|
|
1331
1404
|
canEdit: boolean
|
|
1332
1405
|
canPublish: boolean
|
|
1333
1406
|
contentId: string
|
|
1407
|
+
contentType: string
|
|
1334
1408
|
pageTitle: string
|
|
1335
1409
|
pageStatus: string
|
|
1410
|
+
/** The page's saved layout document (empty regions when it has none yet). */
|
|
1411
|
+
initialLayout: LayoutDocument
|
|
1336
1412
|
/** Deep link to this page in the full admin editor. */
|
|
1337
1413
|
adminHref: string
|
|
1414
|
+
/** Custom (defineModule) renderers, shared with the public layout + admin (#48). */
|
|
1415
|
+
moduleComponents?: ModuleComponentMap
|
|
1338
1416
|
children: React.ReactNode
|
|
1339
1417
|
}
|
|
1340
1418
|
|
|
1341
1419
|
/**
|
|
1342
1420
|
* Wraps the public page with the front-of-site edit affordance. The page only
|
|
1343
1421
|
* renders this for a signed-in admin who asked to edit (?kywi-edit=1), so the
|
|
1344
|
-
*
|
|
1422
|
+
* permission gate (canEdit) is always satisfied here.
|
|
1345
1423
|
*/
|
|
1346
1424
|
export function KywiFrontEdit({
|
|
1347
1425
|
canEdit,
|
|
1348
1426
|
canPublish,
|
|
1349
1427
|
contentId,
|
|
1428
|
+
contentType,
|
|
1350
1429
|
pageTitle,
|
|
1351
1430
|
pageStatus,
|
|
1431
|
+
initialLayout,
|
|
1352
1432
|
adminHref,
|
|
1433
|
+
moduleComponents = {},
|
|
1353
1434
|
children,
|
|
1354
1435
|
}: KywiFrontEditProps) {
|
|
1355
1436
|
const edit = useKywiEditMode({ canEdit, canPublish })
|
|
1356
1437
|
|
|
1357
1438
|
// ?kywi-edit=1 means "enter edit mode now" — flip it on once after mount.
|
|
1358
|
-
const { startEdit } = edit
|
|
1439
|
+
const { startEdit, endEdit } = edit
|
|
1359
1440
|
React.useEffect(() => {
|
|
1360
1441
|
startEdit()
|
|
1361
1442
|
}, [startEdit])
|
|
1362
1443
|
|
|
1363
|
-
|
|
1444
|
+
// Registries + renderers for the editor: built from core (no module list is
|
|
1445
|
+
// re-declared here) and merged with this app's custom module renderers from
|
|
1446
|
+
// lib/modules — the SAME source the admin app and public renderer use (#48).
|
|
1447
|
+
const moduleRegistry = React.useMemo(() => createModuleRegistry([]), [])
|
|
1448
|
+
const themeRegistry = React.useMemo(() => createThemeRegistry(), [])
|
|
1449
|
+
const editorComponents = React.useMemo(
|
|
1450
|
+
() => ({ ...BUILT_IN_MODULE_COMPONENTS, ...moduleComponents }),
|
|
1451
|
+
[moduleComponents],
|
|
1452
|
+
)
|
|
1453
|
+
|
|
1454
|
+
// Persist the edited layout to the same content API the admin editor uses.
|
|
1455
|
+
const persistLayout = React.useCallback(
|
|
1456
|
+
async (next: LayoutDocument) => {
|
|
1457
|
+
const res = await fetch(\`/api/v1/content/\${contentType}/\${contentId}/layout\`, {
|
|
1458
|
+
method: 'PUT',
|
|
1459
|
+
credentials: 'same-origin',
|
|
1460
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1461
|
+
body: JSON.stringify(next),
|
|
1462
|
+
})
|
|
1463
|
+
return res.ok
|
|
1464
|
+
},
|
|
1465
|
+
[contentType, contentId],
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1468
|
+
const handleSave = React.useCallback(
|
|
1469
|
+
async (next: LayoutDocument, _action: SaveAction) => {
|
|
1470
|
+
await persistLayout(next)
|
|
1471
|
+
endEdit()
|
|
1472
|
+
},
|
|
1473
|
+
[persistLayout, endEdit],
|
|
1474
|
+
)
|
|
1475
|
+
|
|
1476
|
+
const handlePublish = React.useCallback(
|
|
1477
|
+
async (next: LayoutDocument, _action: SaveAction) => {
|
|
1478
|
+
const ok = await persistLayout(next)
|
|
1479
|
+
if (ok) {
|
|
1480
|
+
// Best-effort status flip; the layout itself is already persisted above.
|
|
1481
|
+
await fetch(\`/api/v1/content/\${contentType}/\${contentId}\`, {
|
|
1482
|
+
method: 'PUT',
|
|
1483
|
+
credentials: 'same-origin',
|
|
1484
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1485
|
+
body: JSON.stringify({ status: 'published' }),
|
|
1486
|
+
}).catch(() => undefined)
|
|
1487
|
+
}
|
|
1488
|
+
endEdit()
|
|
1489
|
+
},
|
|
1490
|
+
[persistLayout, contentType, contentId, endEdit],
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
// Browse-mode one-click publish from the toolbar (no editor needed).
|
|
1494
|
+
const handleToolbarPublish = React.useCallback(async () => {
|
|
1364
1495
|
const res = await adminFetch(\`/api/v1/content/by-id/\${contentId}/publish\`, { method: 'POST' })
|
|
1365
1496
|
if (res.ok) window.location.reload()
|
|
1366
1497
|
}, [contentId])
|
|
1367
1498
|
|
|
1499
|
+
// Edit mode: the full layout editor, in place, over the live page.
|
|
1500
|
+
if (edit.isEditMode && edit.canEdit) {
|
|
1501
|
+
return (
|
|
1502
|
+
<div className="kywi-admin-shell kywi-frontend-edit">
|
|
1503
|
+
<OverlayShell
|
|
1504
|
+
editMode={edit}
|
|
1505
|
+
initialLayout={initialLayout}
|
|
1506
|
+
contentId={contentId}
|
|
1507
|
+
contentType={contentType}
|
|
1508
|
+
pageTitle={pageTitle}
|
|
1509
|
+
themeName="default"
|
|
1510
|
+
themeRegistry={themeRegistry}
|
|
1511
|
+
moduleRegistry={moduleRegistry}
|
|
1512
|
+
moduleComponents={editorComponents}
|
|
1513
|
+
onSave={handleSave}
|
|
1514
|
+
onPublish={handlePublish}
|
|
1515
|
+
/>
|
|
1516
|
+
</div>
|
|
1517
|
+
)
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// Browse mode: slim toolbar + editable-region outlines over the live page.
|
|
1368
1521
|
return (
|
|
1369
1522
|
<>
|
|
1370
1523
|
<KywiEditToolbar
|
|
@@ -1372,7 +1525,7 @@ export function KywiFrontEdit({
|
|
|
1372
1525
|
canEdit={edit.canEdit}
|
|
1373
1526
|
canPublish={edit.canPublish}
|
|
1374
1527
|
onToggleEdit={edit.toggleEdit}
|
|
1375
|
-
onPublish={
|
|
1528
|
+
onPublish={handleToolbarPublish}
|
|
1376
1529
|
pageTitle={pageTitle}
|
|
1377
1530
|
pageStatus={pageStatus}
|
|
1378
1531
|
adminHref={adminHref}
|
|
@@ -1800,7 +1953,16 @@ their Body rich text. Reach for the Layout tab when a page needs sections,
|
|
|
1800
1953
|
columns, or modules; use the Body for simple prose.
|
|
1801
1954
|
|
|
1802
1955
|
**Front-of-site editor.** Signed in as an admin, append \`?kywi-edit=1\` to any
|
|
1803
|
-
public page to get
|
|
1956
|
+
public page to edit it in place. You get the full **Layout editor** (the same
|
|
1957
|
+
drag-and-drop canvas, module palette and props panel as the admin's Layout tab),
|
|
1958
|
+
mounted right over the live page — add sections and modules, then **Save** (PUTs
|
|
1959
|
+
the layout) or **Publish** (saves + publishes). Exit the editor for the slim
|
|
1960
|
+
browse toolbar with the editable-region outlines. It all lives in
|
|
1961
|
+
\`app/(site)/kywi-front-edit.tsx\`; the editor bundle (and the admin stylesheet) is
|
|
1962
|
+
lazy-loaded, so pages your visitors see never carry its weight. Note: custom
|
|
1963
|
+
\`defineModule\` types still RENDER on the canvas, but they don't appear in the
|
|
1964
|
+
front-of-site editor's insert palette (it uses the built-in module set) — add
|
|
1965
|
+
them from the admin's Layout tab instead.
|
|
1804
1966
|
|
|
1805
1967
|
## Personalization, A/B testing & self-ID
|
|
1806
1968
|
|
|
@@ -1919,7 +2081,7 @@ lib/config.ts single import path for kywi.config.ts
|
|
|
1919
2081
|
app/api/v1/[...kywi]/route.ts the versioned API (delegates to core)
|
|
1920
2082
|
app/admin/[[...admin]]/page.tsx mounts the FULL core admin (all surfaces) at /admin
|
|
1921
2083
|
lib/modules.tsx custom (defineModule) module renderers (admin + public)
|
|
1922
|
-
app/{llms,robots,sitemap,…} root AX routes (llms.txt, robots.txt, sitemap.xml, …)${a.mode === 'coupled' ? '\napp/(site)/layout.tsx public shell: theme tokens + your header/footer\napp/(site)/site.css your site chrome styles (edit freely)\napp/(site)/[[...slug]]/page.tsx renders published pages (layout + SEO + JSON-LD + i18n)\nlib/site.ts public-render helpers: path/locale resolution, feeds, personalization\ncomponents/personalization-runtime.tsx optional client runtime (self-ID widget, live re-eval)\napp/(site)/kywi-front-edit.tsx front-of-site
|
|
2084
|
+
app/{llms,robots,sitemap,…} root AX routes (llms.txt, robots.txt, sitemap.xml, …)${a.mode === 'coupled' ? '\napp/(site)/layout.tsx public shell: theme tokens + your header/footer\napp/(site)/site.css your site chrome styles (edit freely)\napp/(site)/[[...slug]]/page.tsx renders published pages (layout + SEO + JSON-LD + i18n)\nlib/site.ts public-render helpers: path/locale resolution, feeds, personalization\ncomponents/personalization-runtime.tsx optional client runtime (self-ID widget, live re-eval)\napp/(site)/kywi-front-edit.tsx front-of-site editor: browse toolbar + in-place Layout editor (?kywi-edit=1, lazy-loaded)' : '\napp/page.tsx returns 404 (no public rendering in this mode)'}
|
|
1923
2085
|
\`\`\`
|
|
1924
2086
|
`
|
|
1925
2087
|
}
|
|
@@ -1990,7 +2152,7 @@ const _skillDocCache = new Map()
|
|
|
1990
2152
|
* @param {string} asset
|
|
1991
2153
|
* @returns {string}
|
|
1992
2154
|
*/
|
|
1993
|
-
function skillDoc(asset) {
|
|
2155
|
+
export function skillDoc(asset) {
|
|
1994
2156
|
if (!_skillDocCache.has(asset)) {
|
|
1995
2157
|
const assetPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'assets', asset)
|
|
1996
2158
|
_skillDocCache.set(asset, readFileSync(assetPath, 'utf8'))
|
|
@@ -1999,51 +2161,159 @@ function skillDoc(asset) {
|
|
|
1999
2161
|
}
|
|
2000
2162
|
|
|
2001
2163
|
/**
|
|
2002
|
-
*
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
2164
|
+
* Landmark → its relative path in a Kywi project. A "landmark" is a file whose
|
|
2165
|
+
* presence changes how the AGENTS.md header should describe the app. The scaffold
|
|
2166
|
+
* derives its set statically from the mode ({@link scaffoldLandmarks}); the
|
|
2167
|
+
* `agents` refresh command detects them on disk ({@link detectAgentsLandmarks}),
|
|
2168
|
+
* so the header reflects the REAL project instead of asserting scaffold structure.
|
|
2169
|
+
* One source of truth for the path strings, shared by both.
|
|
2170
|
+
* @type {Record<'adminHost'|'moduleMap'|'middleware'|'libSite'|'sitePage'|'headlessPage', string>}
|
|
2171
|
+
*/
|
|
2172
|
+
export const AGENTS_LANDMARK_PATHS = {
|
|
2173
|
+
adminHost: 'app/admin/[[...admin]]/page.tsx',
|
|
2174
|
+
moduleMap: 'lib/modules.tsx',
|
|
2175
|
+
middleware: 'middleware.ts',
|
|
2176
|
+
libSite: 'lib/site.ts',
|
|
2177
|
+
sitePage: 'app/(site)/[[...slug]]/page.tsx',
|
|
2178
|
+
headlessPage: 'app/page.tsx',
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
/**
|
|
2182
|
+
* @typedef {Object} AgentsLandmarks
|
|
2183
|
+
* @property {boolean} scaffolded Generating for a fresh scaffold (true) vs
|
|
2184
|
+
* refreshing into an existing project (false). Controls only the intro line's
|
|
2185
|
+
* "scaffolded by create-kywi-app" claim.
|
|
2186
|
+
* @property {boolean} adminHost app/admin/[[...admin]]/page.tsx present
|
|
2187
|
+
* @property {boolean} moduleMap lib/modules.tsx present
|
|
2188
|
+
* @property {boolean} middleware middleware.ts present
|
|
2189
|
+
* @property {boolean} libSite lib/site.ts present (the scaffold's public-render helpers)
|
|
2190
|
+
* @property {boolean} sitePage app/(site)/[[...slug]]/page.tsx present (renders public pages)
|
|
2191
|
+
* @property {boolean} headlessPage app/page.tsx present (the headless/decoupled 404 root)
|
|
2192
|
+
*/
|
|
2193
|
+
|
|
2194
|
+
/**
|
|
2195
|
+
* The landmark set a FRESH scaffold of the given mode has — derived statically
|
|
2196
|
+
* from the mode, never from the filesystem, so buildFileSet emits AGENTS.md that
|
|
2197
|
+
* is byte-identical to the previous mode-branching implementation.
|
|
2198
|
+
* @param {'coupled'|'headless'|'decoupled'} mode
|
|
2199
|
+
* @returns {AgentsLandmarks}
|
|
2200
|
+
*/
|
|
2201
|
+
export function scaffoldLandmarks(mode) {
|
|
2202
|
+
const coupled = mode === 'coupled'
|
|
2203
|
+
return {
|
|
2204
|
+
scaffolded: true,
|
|
2205
|
+
adminHost: true,
|
|
2206
|
+
moduleMap: true,
|
|
2207
|
+
middleware: true,
|
|
2208
|
+
libSite: coupled,
|
|
2209
|
+
sitePage: coupled,
|
|
2210
|
+
headlessPage: !coupled,
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
/**
|
|
2215
|
+
* Detect the AGENTS.md landmarks that actually exist in an EXISTING project, so
|
|
2216
|
+
* `create-kywi-app agents` writes a header that matches the app on disk (a
|
|
2217
|
+
* hand-built app may render the public site from the page directly and predate
|
|
2218
|
+
* the scaffold's lib/site.ts helper or lib/modules.tsx map).
|
|
2219
|
+
* @param {string} projectDir absolute path to the project root
|
|
2220
|
+
* @returns {AgentsLandmarks}
|
|
2221
|
+
*/
|
|
2222
|
+
export function detectAgentsLandmarks(projectDir) {
|
|
2223
|
+
const has = (rel) => existsSync(join(projectDir, rel))
|
|
2224
|
+
return {
|
|
2225
|
+
scaffolded: false,
|
|
2226
|
+
adminHost: has(AGENTS_LANDMARK_PATHS.adminHost),
|
|
2227
|
+
moduleMap: has(AGENTS_LANDMARK_PATHS.moduleMap),
|
|
2228
|
+
middleware: has(AGENTS_LANDMARK_PATHS.middleware),
|
|
2229
|
+
libSite: has(AGENTS_LANDMARK_PATHS.libSite),
|
|
2230
|
+
sitePage: has(AGENTS_LANDMARK_PATHS.sitePage),
|
|
2231
|
+
headlessPage: has(AGENTS_LANDMARK_PATHS.headlessPage),
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
/**
|
|
2236
|
+
* AGENTS.md — a short, app-specific header that orients an agent in THIS app,
|
|
2237
|
+
* followed by Kywi's canonical patterns doc verbatim. Landmark-aware: each "Where
|
|
2238
|
+
* things live" bullet is emitted only for a landmark that is actually present, and
|
|
2239
|
+
* absent public-render (lib/site.ts) or module-map (lib/modules.tsx) wiring becomes
|
|
2240
|
+
* an honest "this app predates the scaffold's …" line instead of a bullet that
|
|
2241
|
+
* asserts a file the project does not have. With no landmarks passed it defaults to
|
|
2242
|
+
* the fresh-scaffold set for the mode, so the scaffold output is unchanged.
|
|
2005
2243
|
* @param {Answers} a
|
|
2244
|
+
* @param {AgentsLandmarks} [landmarks]
|
|
2006
2245
|
*/
|
|
2007
|
-
function agentsMd(a) {
|
|
2008
|
-
const
|
|
2009
|
-
const modeSentence =
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2246
|
+
export function agentsMd(a, landmarks = scaffoldLandmarks(a.mode)) {
|
|
2247
|
+
const L = landmarks
|
|
2248
|
+
const modeSentence =
|
|
2249
|
+
a.mode === 'coupled'
|
|
2250
|
+
? 'This app renders the public site AND serves the admin + API.'
|
|
2251
|
+
: a.mode === 'headless'
|
|
2252
|
+
? 'This app serves the admin + API only (`GET /` returns 404); there is no public rendering here.'
|
|
2253
|
+
: 'This app serves the admin + API only; a separate frontend consumes the API via `@kywi-software/sdk`. There is no public rendering here.'
|
|
2254
|
+
|
|
2255
|
+
// Existing projects aren't necessarily scaffolded — don't assert they were.
|
|
2256
|
+
const intro = L.scaffolded
|
|
2257
|
+
? `This is a **Kywi CMS** project scaffolded by \`create-kywi-app\` (\`${a.mode}\` mode).`
|
|
2258
|
+
: `This is a **Kywi CMS** project (\`${a.mode}\` mode).`
|
|
2259
|
+
|
|
2260
|
+
// "Where things live" — one bullet per PRESENT landmark, in a fixed order.
|
|
2261
|
+
const bullets = []
|
|
2262
|
+
bullets.push(`- \`kywi.config.ts\` — project config: sites, themes, content types, auth
|
|
2263
|
+
providers, deployment mode, and \`admin.features\`. Edit it, then re-run
|
|
2264
|
+
\`pnpm migrate\`.`)
|
|
2265
|
+
if (L.adminHost) {
|
|
2266
|
+
bullets.push(`- \`app/admin/[[...admin]]/page.tsx\` — mounts Kywi's **full admin**
|
|
2267
|
+
(\`KywiAdminApp\`) at **\`/admin\`**. Every surface — content, media, feeds,
|
|
2268
|
+
forms, audiences, settings, … — is already there; never hand-build admin pages.`)
|
|
2269
|
+
}
|
|
2270
|
+
if (L.moduleMap) {
|
|
2271
|
+
bullets.push(`- \`lib/modules.tsx\` — the custom-module map (\`defineModule\` renderers), shared
|
|
2272
|
+
by the admin editor and the public site.`)
|
|
2273
|
+
} else {
|
|
2274
|
+
bullets.push(`- No \`lib/modules.tsx\` custom-module map — this app predates the scaffold's
|
|
2275
|
+
\`defineModule\` module map (shared by the admin editor and the public site); see
|
|
2276
|
+
the patterns doc below for the intended shape.`)
|
|
2277
|
+
}
|
|
2278
|
+
// Public-render surface: the scaffold's lib/site.ts helper, else a page that
|
|
2279
|
+
// renders publicly without it (honest note), else no public rendering at all.
|
|
2280
|
+
if (L.libSite) {
|
|
2281
|
+
bullets.push(`- \`lib/site.ts\` — public-render helpers (path/locale resolution, feeds,
|
|
2016
2282
|
components, personalization) used by \`app/(site)/[[...slug]]/page.tsx\`, which
|
|
2017
|
-
renders every published page at its slug.`
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2283
|
+
renders every published page at its slug.`)
|
|
2284
|
+
} else if (L.sitePage) {
|
|
2285
|
+
bullets.push(`- \`app/(site)/[[...slug]]/page.tsx\` — renders every published page at its
|
|
2286
|
+
slug. This app predates the scaffold's \`lib/site.ts\` public-render helpers
|
|
2287
|
+
(path/locale resolution, feeds, components, personalization); see the patterns
|
|
2288
|
+
doc below for the intended shape.`)
|
|
2289
|
+
} else {
|
|
2290
|
+
bullets.push(
|
|
2291
|
+
a.mode === 'decoupled'
|
|
2292
|
+
? `- No public rendering in this mode — \`app/page.tsx\` returns 404. Build a
|
|
2022
2293
|
separate frontend against the REST API at \`/api/v1\` with \`@kywi-software/sdk\`.`
|
|
2294
|
+
: `- No public rendering in this mode — \`app/page.tsx\` returns 404. Content is
|
|
2295
|
+
served over the REST API at \`/api/v1\`.`,
|
|
2296
|
+
)
|
|
2297
|
+
}
|
|
2298
|
+
if (L.middleware) {
|
|
2299
|
+
bullets.push(`- \`middleware.ts\` — auth gate + session refresh, thin wiring over
|
|
2300
|
+
\`@kywi-software/core/host\`.`)
|
|
2301
|
+
}
|
|
2302
|
+
bullets.push(`- \`.claude/skills/kywi-content-model/SKILL.md\` — content-model planning
|
|
2303
|
+
skill, loaded automatically before building anything.`)
|
|
2304
|
+
bullets.push(`- \`.claude/skills/kywi-collections/SKILL.md\` — collections skill, loaded
|
|
2305
|
+
automatically when adding any collection.`)
|
|
2306
|
+
bullets.push(`- \`.claude/skills/kywi-personalization/SKILL.md\` — personalization skill,
|
|
2307
|
+
loaded automatically when the owner wants personalization or A/B testing.`)
|
|
2308
|
+
|
|
2023
2309
|
const header = `# Agent guide — ${a.projectName}
|
|
2024
2310
|
|
|
2025
|
-
|
|
2311
|
+
${intro}
|
|
2026
2312
|
${modeSentence}
|
|
2027
2313
|
|
|
2028
2314
|
## Where things live
|
|
2029
2315
|
|
|
2030
|
-
|
|
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.
|
|
2316
|
+
${bullets.join('\n')}
|
|
2047
2317
|
|
|
2048
2318
|
## Running it
|
|
2049
2319
|
|
|
@@ -2070,7 +2340,7 @@ CMS instead of hardcoding it.**
|
|
|
2070
2340
|
* CLAUDE.md — a thin pointer so a Claude Code session reads AGENTS.md first.
|
|
2071
2341
|
* @param {Answers} a
|
|
2072
2342
|
*/
|
|
2073
|
-
function claudeMd(a) {
|
|
2343
|
+
export function claudeMd(a) {
|
|
2074
2344
|
return `# ${a.projectName}
|
|
2075
2345
|
|
|
2076
2346
|
This is a **Kywi CMS** project. **Read \`AGENTS.md\` before building anything** — it
|
|
@@ -2082,6 +2352,30 @@ Before building out a new content area, use the \`kywi-content-model\` skill (de
|
|
|
2082
2352
|
`
|
|
2083
2353
|
}
|
|
2084
2354
|
|
|
2355
|
+
/**
|
|
2356
|
+
* The agent-guidance files every Kywi app should carry: AGENTS.md, an app-specific
|
|
2357
|
+
* header + Kywi's canonical patterns doc; CLAUDE.md, a thin pointer into it; and
|
|
2358
|
+
* the three Claude Code project skills, verbatim assets. ONE source of truth for
|
|
2359
|
+
* "which files are the agent guidance", shared by {@link buildFileSet} (fresh
|
|
2360
|
+
* scaffold, {@link scaffoldLandmarks}) and the `agents` refresh command
|
|
2361
|
+
* ({@link detectAgentsLandmarks}) — so both emit identical content for identical
|
|
2362
|
+
* inputs, and adding/renaming a skill in {@link SKILLS} updates both at once.
|
|
2363
|
+
* @param {Answers} a
|
|
2364
|
+
* @param {AgentsLandmarks} [landmarks]
|
|
2365
|
+
* @returns {Record<string, string>}
|
|
2366
|
+
*/
|
|
2367
|
+
export function guidanceFileSet(a, landmarks = scaffoldLandmarks(a.mode)) {
|
|
2368
|
+
/** @type {Record<string, string>} */
|
|
2369
|
+
const files = {
|
|
2370
|
+
'AGENTS.md': agentsMd(a, landmarks),
|
|
2371
|
+
'CLAUDE.md': claudeMd(a),
|
|
2372
|
+
}
|
|
2373
|
+
for (const { slug, asset } of SKILLS) {
|
|
2374
|
+
files[`.claude/skills/${slug}/SKILL.md`] = skillDoc(asset)
|
|
2375
|
+
}
|
|
2376
|
+
return files
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2085
2379
|
/**
|
|
2086
2380
|
* Build the complete map of relative-path → file-content for a project.
|
|
2087
2381
|
* @param {Answers} answers
|
|
@@ -2098,10 +2392,12 @@ export function buildFileSet(answers) {
|
|
|
2098
2392
|
'.env.example': envExample(),
|
|
2099
2393
|
'.gitignore': gitignore(),
|
|
2100
2394
|
'README.md': readme(answers),
|
|
2101
|
-
// agent guidance (every mode): app-specific header + Kywi's
|
|
2102
|
-
// doc verbatim
|
|
2103
|
-
|
|
2104
|
-
|
|
2395
|
+
// agent guidance (every mode): AGENTS.md (app-specific header + Kywi's
|
|
2396
|
+
// canonical patterns doc verbatim), CLAUDE.md (thin pointer into it), and the
|
|
2397
|
+
// three Claude Code project skills. Landmarks are derived statically from the
|
|
2398
|
+
// mode so this stays byte-identical to the pre-landmark implementation; the
|
|
2399
|
+
// same guidanceFileSet powers `create-kywi-app agents` for existing projects.
|
|
2400
|
+
...guidanceFileSet(answers, scaffoldLandmarks(answers.mode)),
|
|
2105
2401
|
// server runtime + config
|
|
2106
2402
|
'lib/kywi.ts': libKywi(),
|
|
2107
2403
|
'lib/config.ts': libConfig(),
|
|
@@ -2126,15 +2422,6 @@ export function buildFileSet(answers) {
|
|
|
2126
2422
|
'app/robots.txt/route.ts': axRootRoute('robots.txt'),
|
|
2127
2423
|
}
|
|
2128
2424
|
|
|
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
2425
|
if (answers.mode === 'coupled') {
|
|
2139
2426
|
// Public site: an optional catch-all renders "/" (home) and every published
|
|
2140
2427
|
// page at its slug. More-specific /admin and /api routes take precedence.
|
package/package.json
CHANGED