create-kyro 0.5.5 → 0.7.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/dist/index.js CHANGED
@@ -87,6 +87,12 @@ async function promptUser() {
87
87
  value: "kitchen-sink"
88
88
  }
89
89
  ]
90
+ },
91
+ {
92
+ type: "text",
93
+ name: "adminEmail",
94
+ message: "Admin email:",
95
+ initial: (prev, values) => `admin@${values.projectName}.local`
90
96
  }
91
97
  ],
92
98
  {
@@ -151,16 +157,25 @@ ${cyan("?")} ${bold(msg)}`);
151
157
  function generatePackageJson(answers) {
152
158
  const deps = {
153
159
  "astro": "^6.3.1",
160
+ "@astrojs/react": "^5.0.4",
161
+ "@tailwindcss/vite": "^4.0.0",
162
+ "tailwindcss": "^4.0.0",
163
+ "react": "^19.0.0",
164
+ "react-dom": "^19.0.0",
154
165
  "@kyro-cms/core": "latest",
155
166
  "@kyro-cms/admin": "latest"
156
167
  };
157
168
  const devDeps = {
158
- "typescript": "^5.7.3"
169
+ "typescript": "^5.7.3",
170
+ "@types/react": "^19.0.0",
171
+ "@types/react-dom": "^19.0.0"
159
172
  };
160
173
  const scripts = {
161
174
  "dev": "astro dev",
162
175
  "build": "astro build",
163
- "preview": "astro preview"
176
+ "preview": "astro preview",
177
+ "kyro:dev": "kyro dev",
178
+ "kyro:generate": "kyro generate"
164
179
  };
165
180
  if (answers.database === "sqlite") {
166
181
  scripts["db:generate"] = "kyro generate";
@@ -174,7 +189,10 @@ function generatePackageJson(answers) {
174
189
  private: true,
175
190
  scripts,
176
191
  dependencies: deps,
177
- devDependencies: devDeps
192
+ devDependencies: devDeps,
193
+ overrides: {
194
+ "vite": "^7"
195
+ }
178
196
  };
179
197
  }
180
198
  function formatPackageJson(pkg) {
@@ -207,19 +225,19 @@ function generateKyroConfig(answers) {
207
225
  let templateGlobals = "";
208
226
  switch (answers.template) {
209
227
  case "minimal":
210
- templateCollections = "import { minimalCollections } from '@kyro-cms/core/templates';";
228
+ templateCollections = "import { minimalCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';";
211
229
  templateGlobals = "import { coreSettingsGlobals } from '@kyro-cms/core/templates';";
212
230
  break;
213
231
  case "blog":
214
- templateCollections = "import { blogCollections } from '@kyro-cms/core/templates';";
232
+ templateCollections = "import { blogCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';";
215
233
  templateGlobals = "import { allSettingsGlobals } from '@kyro-cms/core/templates';";
216
234
  break;
217
235
  case "ecommerce":
218
- templateCollections = "import { ecommerceCollections } from '@kyro-cms/core/templates';";
236
+ templateCollections = "import { ecommerceCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';";
219
237
  templateGlobals = "import { allSettingsGlobals, ecommerceSettingsGlobals } from '@kyro-cms/core/templates';";
220
238
  break;
221
239
  case "kitchen-sink":
222
- templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections } from '@kyro-cms/core/templates';`;
240
+ templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';`;
223
241
  templateGlobals = "import { allSettingsGlobals, ecommerceSettingsGlobals } from '@kyro-cms/core/templates';";
224
242
  break;
225
243
  }
@@ -227,17 +245,31 @@ function generateKyroConfig(answers) {
227
245
  if (templateGlobals) imports.push(templateGlobals);
228
246
  let collectionsConfig = "";
229
247
  if (answers.template === "minimal") {
230
- collectionsConfig = ` collections: Object.values(minimalCollections),`;
248
+ collectionsConfig = ` collections: [
249
+ ...Object.values(minimalCollections),
250
+ ...Object.values(mediaCollections),
251
+ ...Object.values(authCollections),
252
+ ],`;
231
253
  } else if (answers.template === "blog") {
232
- collectionsConfig = ` collections: Object.values(blogCollections),`;
254
+ collectionsConfig = ` collections: [
255
+ ...Object.values(blogCollections),
256
+ ...Object.values(mediaCollections),
257
+ ...Object.values(authCollections),
258
+ ],`;
233
259
  } else if (answers.template === "ecommerce") {
234
- collectionsConfig = ` collections: Object.values(ecommerceCollections),`;
260
+ collectionsConfig = ` collections: [
261
+ ...Object.values(ecommerceCollections),
262
+ ...Object.values(mediaCollections),
263
+ ...Object.values(authCollections),
264
+ ],`;
235
265
  } else if (answers.template === "kitchen-sink") {
236
266
  collectionsConfig = ` collections: [
237
267
  ...Object.values(minimalCollections),
238
268
  ...Object.values(blogCollections),
239
269
  ...Object.values(ecommerceCollections),
240
270
  ...Object.values(kitchenSinkCollections),
271
+ ...Object.values(mediaCollections),
272
+ ...Object.values(authCollections),
241
273
  ],`;
242
274
  }
243
275
  let globalsConfig = "";
@@ -256,7 +288,9 @@ export default defineConfig({
256
288
  ${adapterLines.join("\n")}
257
289
  ${collectionsConfig}
258
290
  ${globalsConfig}
259
- auth: true,
291
+ auth: {
292
+ secret: process.env.APP_SECRET,
293
+ },
260
294
  });`;
261
295
  }
262
296
 
@@ -265,13 +299,15 @@ function generateAstroConfig(answers) {
265
299
  return `import { defineConfig } from 'astro/config';
266
300
  import { kyro } from '@kyro-cms/core';
267
301
  import { kyroAdmin } from '@kyro-cms/admin';
302
+ import react from '@astrojs/react';
303
+ import tailwind from '@tailwindcss/vite';
268
304
 
269
305
  export default defineConfig({
270
306
  output: 'server',
271
- integrations: [
272
- kyro({ adminPath: '/admin', apiPath: '/api' }),
273
- kyroAdmin({ basePath: '/admin', apiPath: '/api' }),
274
- ],
307
+ integrations: [react(), kyro({ adminPath: '/admin', apiPath: '/api' }), kyroAdmin({ basePath: '/admin', apiPath: '/api' })],
308
+ vite: {
309
+ plugins: [tailwind()],
310
+ },
275
311
  server: {
276
312
  port: 4321,
277
313
  host: true,
@@ -282,19 +318,30 @@ export default defineConfig({
282
318
  // src/generators/files.ts
283
319
  import { writeFileSync, mkdirSync } from "fs";
284
320
  import { join } from "path";
285
- function generateProjectFiles(answers, projectDir) {
321
+ import { randomBytes } from "crypto";
322
+ function generateAppSecret() {
323
+ return randomBytes(32).toString("hex");
324
+ }
325
+ function generateProjectFiles(answers, projectDir, adminCredentials) {
286
326
  const srcDir = join(projectDir, "src");
287
327
  const pagesDir = join(srcDir, "pages");
328
+ const stylesDir = join(srcDir, "styles");
288
329
  const publicDir = join(projectDir, "public");
289
330
  mkdirSync(pagesDir, { recursive: true });
331
+ mkdirSync(stylesDir, { recursive: true });
290
332
  mkdirSync(publicDir, { recursive: true });
333
+ if (answers.database === "sqlite") {
334
+ mkdirSync(join(projectDir, "data"), { recursive: true });
335
+ }
291
336
  const tsconfig = `{
292
337
  "extends": "astro/tsconfigs/strict",
293
338
  "compilerOptions": {
294
339
  "baseUrl": ".",
295
340
  "paths": {
296
341
  "@/*": ["./src/*"]
297
- }
342
+ },
343
+ "jsx": "react-jsx",
344
+ "jsxImportSource": "react"
298
345
  }
299
346
  }`;
300
347
  writeFileSync(join(projectDir, "tsconfig.json"), tsconfig);
@@ -333,17 +380,50 @@ Visit [https://kyro.dev](https://kyro.dev) for full documentation.
333
380
  const envExample = `# Kyro CMS Configuration
334
381
  # Copy this file to .env and fill in your values
335
382
 
383
+ NODE_ENV=development
384
+ APP_URL=http://localhost:4321
385
+
336
386
  ${answers.database === "sqlite" ? "# SQLite (local) - no additional config needed" : answers.database === "postgres" ? "# Database connection (PostgreSQL)\nDATABASE_URL=postgresql://user:password@localhost:5432/kyro_cms" : "# MongoDB connection\nMONGODB_URI=mongodb://localhost:27017/kyro_cms"}
337
387
 
338
- # JWT secret for authentication tokens
339
- JWT_SECRET=change-this-to-a-random-64-character-string
388
+ # App secret (set once; on first run it's stored in the database and can be managed via admin UI)
389
+ APP_SECRET=your-secret-here
340
390
 
341
391
  # Admin credentials (used for first-user bootstrap)
342
392
  # KYRO_ADMIN_EMAIL=admin@example.com
343
393
  # KYRO_ADMIN_PASSWORD=SecurePass123!
344
394
  `;
345
395
  writeFileSync(join(projectDir, ".env.example"), envExample);
396
+ if (adminCredentials) {
397
+ const envFile = `# Kyro CMS Configuration
398
+ # Copy this file to .env and fill in your values
399
+
400
+ NODE_ENV=development
401
+ APP_URL=http://localhost:4321
402
+
403
+ ${answers.database === "sqlite" ? "# SQLite (local) - no additional config needed" : answers.database === "postgres" ? "# Database connection (PostgreSQL)\nDATABASE_URL=postgresql://user:password@localhost:5432/kyro_cms" : "# MongoDB connection\nMONGODB_URI=mongodb://localhost:27017/kyro_cms"}
404
+
405
+ # App secret (auto-generated; stored in database on first run)
406
+ APP_SECRET=${generateAppSecret()}
407
+
408
+ # Admin credentials (used for first-user bootstrap)
409
+ KYRO_ADMIN_EMAIL=${adminCredentials.adminEmail}
410
+ KYRO_ADMIN_PASSWORD=${adminCredentials.adminPassword}
411
+ `;
412
+ writeFileSync(join(projectDir, ".env"), envFile);
413
+ }
414
+ const mainCss = `@import "tailwindcss";
415
+
416
+ @source "./pages/**/*.{astro,html,js,jsx,ts,tsx}";
417
+ @source "./components/**/*.{astro,html,js,jsx,ts,tsx}";
418
+ @source "./layouts/**/*.{astro,html,js,jsx,ts,tsx}";
419
+
420
+ @theme {
421
+ --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
422
+ }
423
+ `;
424
+ writeFileSync(join(stylesDir, "main.css"), mainCss);
346
425
  const indexPage = `---
426
+ import "../styles/main.css";
347
427
  const title = "${answers.projectName}";
348
428
  ---
349
429
  <!DOCTYPE html>
@@ -353,38 +433,52 @@ const title = "${answers.projectName}";
353
433
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
354
434
  <title>{title}</title>
355
435
  </head>
356
- <body>
357
- <main>
358
- <h1>Welcome to {title}</h1>
359
- <p>Your Kyro CMS is ready.</p>
360
- <p><a href="/admin">Go to Admin Dashboard &rarr;</a></p>
361
- </main>
436
+ <body
437
+ class="min-h-screen bg-gradient-to-br from-stone-50 via-white to-stone-100 dark:from-stone-950 dark:via-stone-900 dark:to-stone-950 text-stone-900 dark:text-stone-100 antialiased"
438
+ >
439
+ <div class="relative min-h-screen flex flex-col items-center justify-center px-4">
440
+ <!-- Decorative background blurs -->
441
+ <div class="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
442
+ <div class="absolute -top-40 -right-40 w-96 h-96 rounded-full bg-indigo-100/60 dark:bg-indigo-900/20 blur-3xl"></div>
443
+ <div class="absolute -bottom-40 -left-40 w-96 h-96 rounded-full bg-indigo-100/60 dark:bg-indigo-900/20 blur-3xl"></div>
444
+ </div>
445
+
446
+ <main class="relative text-center max-w-lg">
447
+ <!-- Badge -->
448
+ <div class="mb-8 inline-flex items-center gap-2 px-3 py-1 rounded-full border border-stone-200 dark:border-stone-700 text-xs font-medium text-stone-500 dark:text-stone-400">
449
+ <span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
450
+ Powered by Kyro CMS
451
+ </div>
452
+
453
+ <!-- Title -->
454
+ <h1 class="mb-4 text-5xl sm:text-6xl font-black tracking-tight bg-gradient-to-r from-stone-900 to-stone-500 dark:from-white dark:to-stone-400 bg-clip-text text-transparent">
455
+ {title}
456
+ </h1>
457
+
458
+ <!-- Tagline -->
459
+ <p class="mb-10 text-lg sm:text-xl text-stone-500 dark:text-stone-400 leading-relaxed">
460
+ Your content management system is ready. Start building something amazing.
461
+ </p>
462
+
463
+ <!-- Admin link -->
464
+ <a
465
+ href="/admin"
466
+ class="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-stone-900 dark:bg-white text-white dark:text-stone-900 font-semibold text-sm hover:bg-stone-800 dark:hover:bg-stone-100 transition-all shadow-lg hover:shadow-xl active:scale-[0.97]"
467
+ >
468
+ Go to Admin Dashboard
469
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
470
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M17 8l4 4m0 0l-4 4m4-4H3"/>
471
+ </svg>
472
+ </a>
473
+ </main>
474
+
475
+ <!-- Footer -->
476
+ <footer class="absolute bottom-8 text-xs text-stone-400 dark:text-stone-500">
477
+ &copy; {new Date().getFullYear()} {title}
478
+ </footer>
479
+ </div>
362
480
  </body>
363
481
  </html>
364
-
365
- <style>
366
- main {
367
- max-width: 800px;
368
- margin: 4rem auto;
369
- padding: 2rem;
370
- text-align: center;
371
- font-family: system-ui, sans-serif;
372
- }
373
- h1 {
374
- font-size: 2.5rem;
375
- margin-bottom: 1rem;
376
- }
377
- p {
378
- color: #666;
379
- }
380
- a {
381
- color: #6366f1;
382
- text-decoration: none;
383
- }
384
- a:hover {
385
- text-decoration: underline;
386
- }
387
- </style>
388
482
  `;
389
483
  writeFileSync(join(pagesDir, "index.astro"), indexPage);
390
484
  }
@@ -393,6 +487,16 @@ const title = "${answers.projectName}";
393
487
  import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync } from "fs";
394
488
  import { join as join2 } from "path";
395
489
  import { execSync } from "child_process";
490
+ import { randomBytes as randomBytes2 } from "crypto";
491
+ function generatePassword(length = 24) {
492
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()";
493
+ const bytes = randomBytes2(length);
494
+ let password = "";
495
+ for (let i = 0; i < length; i++) {
496
+ password += chars[bytes[i] % chars.length];
497
+ }
498
+ return password;
499
+ }
396
500
  var VERSION = "0.4.0";
397
501
  async function main() {
398
502
  logger.intro("create-kyro", VERSION);
@@ -402,6 +506,7 @@ async function main() {
402
506
  logger.error(`Directory "${answers.projectName}" already exists.`);
403
507
  process.exit(1);
404
508
  }
509
+ const adminPassword = generatePassword();
405
510
  const steps = [
406
511
  "Creating project directory",
407
512
  "Generating configuration files",
@@ -424,7 +529,7 @@ async function main() {
424
529
  const astroConfig = generateAstroConfig(answers);
425
530
  writeFileSync2(join2(projectDir, "astro.config.mjs"), astroConfig);
426
531
  logger.success("astro.config.mjs generated");
427
- generateProjectFiles(answers, projectDir);
532
+ generateProjectFiles(answers, projectDir, { adminEmail: answers.adminEmail, adminPassword });
428
533
  logger.success("Project files generated");
429
534
  logger.step(3, steps.length, steps[2]);
430
535
  console.log(" Installing dependencies...");
@@ -457,7 +562,14 @@ async function main() {
457
562
  console.log(" Visit http://localhost:4321 to see your app.");
458
563
  console.log(" Visit http://localhost:4321/admin for the admin dashboard.");
459
564
  console.log("");
460
- console.log(" The first user to register will be the super admin.");
565
+ console.log(" \x1B[33m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\x1B[0m");
566
+ console.log(" \x1B[33m Admin Account Created\x1B[0m");
567
+ console.log(" \x1B[33m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\x1B[0m");
568
+ console.log(` Email: \x1B[36m${answers.adminEmail}\x1B[0m`);
569
+ console.log(` Password: \x1B[36m${adminPassword}\x1B[0m`);
570
+ console.log(" \x1B[33m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\x1B[0m");
571
+ console.log(" The admin user will be created automatically on first server start.");
572
+ console.log(" You can change these credentials in the .env file.");
461
573
  console.log("");
462
574
  }
463
575
  main().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kyro",
3
- "version": "0.5.5",
3
+ "version": "0.7.0",
4
4
  "description": "Interactive scaffolding for Kyro CMS projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,25 +29,24 @@ export function generateKyroConfig(answers: Answers): string {
29
29
 
30
30
  switch (answers.template) {
31
31
  case "minimal":
32
- templateCollections =
33
- "import { minimalCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';";
34
- templateGlobals =
35
- "import { coreSettingsGlobals } from '@kyro-cms/core/templates';";
32
+ templateCollections = "import { minimalCollections } from '@kyro-cms/core/templates';";
33
+ templateGlobals = "import { siteSettingsGlobal, seoSettingsGlobal } from '@kyro-cms/core/templates';";
34
+ break;
35
+ case "starter":
36
+ templateCollections = "import { starterCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';";
37
+ templateGlobals = "import { coreSettingsGlobals } from '@kyro-cms/core/templates';";
36
38
  break;
37
39
  case "blog":
38
40
  templateCollections = "import { blogCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';";
39
- templateGlobals =
40
- "import { allSettingsGlobals } from '@kyro-cms/core/templates';";
41
+ templateGlobals = "import { allSettingsGlobals } from '@kyro-cms/core/templates';";
41
42
  break;
42
43
  case "ecommerce":
43
44
  templateCollections = "import { ecommerceCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';";
44
- templateGlobals =
45
- "import { allSettingsGlobals, ecommerceSettingsGlobals } from '@kyro-cms/core/templates';";
45
+ templateGlobals = "import { allSettingsGlobals } from '@kyro-cms/core/templates';";
46
46
  break;
47
47
  case "kitchen-sink":
48
- templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';`;
49
- templateGlobals =
50
- "import { allSettingsGlobals, ecommerceSettingsGlobals } from '@kyro-cms/core/templates';";
48
+ templateCollections = `import { minimalCollections, starterCollections, blogCollections, ecommerceCollections, kitchenSinkCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';`;
49
+ templateGlobals = "import { allSettingsGlobals } from '@kyro-cms/core/templates';";
51
50
  break;
52
51
  }
53
52
 
@@ -58,6 +57,10 @@ export function generateKyroConfig(answers: Answers): string {
58
57
  if (answers.template === "minimal") {
59
58
  collectionsConfig = ` collections: [
60
59
  ...Object.values(minimalCollections),
60
+ ],`;
61
+ } else if (answers.template === "starter") {
62
+ collectionsConfig = ` collections: [
63
+ ...Object.values(starterCollections),
61
64
  ...Object.values(mediaCollections),
62
65
  ...Object.values(authCollections),
63
66
  ],`;
@@ -76,6 +79,7 @@ export function generateKyroConfig(answers: Answers): string {
76
79
  } else if (answers.template === "kitchen-sink") {
77
80
  collectionsConfig = ` collections: [
78
81
  ...Object.values(minimalCollections),
82
+ ...Object.values(starterCollections),
79
83
  ...Object.values(blogCollections),
80
84
  ...Object.values(ecommerceCollections),
81
85
  ...Object.values(kitchenSinkCollections),
@@ -86,13 +90,11 @@ export function generateKyroConfig(answers: Answers): string {
86
90
 
87
91
  let globalsConfig = "";
88
92
  if (answers.template === "minimal") {
93
+ globalsConfig = ` globals: [siteSettingsGlobal, seoSettingsGlobal],`;
94
+ } else if (answers.template === "starter") {
89
95
  globalsConfig = ` globals: coreSettingsGlobals,`;
90
- } else if (answers.template === "blog" || answers.template === "ecommerce") {
91
- globalsConfig = answers.template === "ecommerce"
92
- ? ` globals: [...allSettingsGlobals, ...ecommerceSettingsGlobals],`
93
- : ` globals: allSettingsGlobals,`;
94
- } else if (answers.template === "kitchen-sink") {
95
- globalsConfig = ` globals: [...allSettingsGlobals, ...ecommerceSettingsGlobals],`;
96
+ } else if (answers.template === "blog" || answers.template === "ecommerce" || answers.template === "kitchen-sink") {
97
+ globalsConfig = ` globals: allSettingsGlobals,`;
96
98
  }
97
99
 
98
100
  return `${imports.join("\n")}
@@ -26,13 +26,19 @@ export function generateProjectFiles(
26
26
  mkdirSync(stylesDir, { recursive: true });
27
27
  mkdirSync(publicDir, { recursive: true });
28
28
 
29
+ if (answers.database === "sqlite") {
30
+ mkdirSync(join(projectDir, "data"), { recursive: true });
31
+ }
32
+
29
33
  const tsconfig = `{
30
34
  "extends": "astro/tsconfigs/strict",
31
35
  "compilerOptions": {
32
36
  "baseUrl": ".",
33
37
  "paths": {
34
38
  "@/*": ["./src/*"]
35
- }
39
+ },
40
+ "jsx": "react-jsx",
41
+ "jsxImportSource": "react"
36
42
  }
37
43
  }`;
38
44
 
@@ -77,6 +83,9 @@ Visit [https://kyro.dev](https://kyro.dev) for full documentation.
77
83
  const envExample = `# Kyro CMS Configuration
78
84
  # Copy this file to .env and fill in your values
79
85
 
86
+ NODE_ENV=development
87
+ APP_URL=http://localhost:4321
88
+
80
89
  ${answers.database === "sqlite"
81
90
  ? "# SQLite (local) - no additional config needed"
82
91
  : answers.database === "postgres"
@@ -98,6 +107,9 @@ APP_SECRET=your-secret-here
98
107
  const envFile = `# Kyro CMS Configuration
99
108
  # Copy this file to .env and fill in your values
100
109
 
110
+ NODE_ENV=development
111
+ APP_URL=http://localhost:4321
112
+
101
113
  ${answers.database === "sqlite"
102
114
  ? "# SQLite (local) - no additional config needed"
103
115
  : answers.database === "postgres"
@@ -118,6 +130,10 @@ KYRO_ADMIN_PASSWORD=${adminCredentials.adminPassword}
118
130
 
119
131
  const mainCss = `@import "tailwindcss";
120
132
 
133
+ @source "./pages/**/*.{astro,html,js,jsx,ts,tsx}";
134
+ @source "./components/**/*.{astro,html,js,jsx,ts,tsx}";
135
+ @source "./layouts/**/*.{astro,html,js,jsx,ts,tsx}";
136
+
121
137
  @theme {
122
138
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
123
139
  }
@@ -8,6 +8,7 @@ export interface PackageJson {
8
8
  scripts: Record<string, string>;
9
9
  dependencies: Record<string, string>;
10
10
  devDependencies: Record<string, string>;
11
+ overrides?: Record<string, string>;
11
12
  }
12
13
 
13
14
  export function generatePackageJson(
@@ -18,18 +19,24 @@ export function generatePackageJson(
18
19
  "@astrojs/react": "^5.0.4",
19
20
  "@tailwindcss/vite": "^4.0.0",
20
21
  "tailwindcss": "^4.0.0",
22
+ "react": "^19.0.0",
23
+ "react-dom": "^19.0.0",
21
24
  "@kyro-cms/core": "latest",
22
25
  "@kyro-cms/admin": "latest",
23
26
  };
24
27
 
25
28
  const devDeps: Record<string, string> = {
26
29
  "typescript": "^5.7.3",
30
+ "@types/react": "^19.0.0",
31
+ "@types/react-dom": "^19.0.0",
27
32
  };
28
33
 
29
34
  const scripts: Record<string, string> = {
30
35
  "dev": "astro dev",
31
36
  "build": "astro build",
32
37
  "preview": "astro preview",
38
+ "kyro:dev": "kyro dev",
39
+ "kyro:generate": "kyro generate",
33
40
  };
34
41
 
35
42
  if (answers.database === "sqlite") {
@@ -46,6 +53,9 @@ export function generatePackageJson(
46
53
  scripts,
47
54
  dependencies: deps,
48
55
  devDependencies: devDeps,
56
+ overrides: {
57
+ "vite": "^7",
58
+ },
49
59
  };
50
60
  }
51
61
 
package/src/prompts.ts CHANGED
@@ -4,7 +4,7 @@ import { validateProjectName } from "./validators.js";
4
4
  export interface Answers {
5
5
  projectName: string;
6
6
  database: "sqlite" | "postgres" | "mongodb";
7
- template: "minimal" | "blog" | "ecommerce" | "kitchen-sink";
7
+ template: "minimal" | "starter" | "blog" | "ecommerce" | "kitchen-sink";
8
8
  adminEmail: string;
9
9
  }
10
10
 
@@ -47,29 +47,36 @@ export async function promptUser(): Promise<Answers> {
47
47
  name: "template",
48
48
  message: "Starting template:",
49
49
  hint: " ",
50
- initial: 1,
50
+ initial: 0,
51
51
  choices: [
52
52
  {
53
53
  title: "Minimal",
54
54
  description:
55
- "Basic configuration with one example collection + core settings",
55
+ "Single Posts collection just title & content. Perfect for getting started fast.",
56
56
  value: "minimal",
57
57
  },
58
+ {
59
+ title: "Starter",
60
+ description:
61
+ "Pages, Posts, Categories, Menu + core settings. Great for blogs and small sites.",
62
+ value: "starter",
63
+ },
58
64
  {
59
65
  title: "Blog",
60
- description: "Posts, categories, media library + core settings",
66
+ description:
67
+ "Posts, categories, media library + all settings. Full blog setup.",
61
68
  value: "blog",
62
69
  },
63
70
  {
64
71
  title: "E-commerce",
65
72
  description:
66
- "Products, orders, customers, coupons + core + store/payment settings",
73
+ "Products, orders, customers, coupons + all settings. Online store ready.",
67
74
  value: "ecommerce",
68
75
  },
69
76
  {
70
77
  title: "Kitchen Sink",
71
78
  description:
72
- "Everything: pages, navigation, blog, e-commerce + all settings",
79
+ "Everything: all collections + all settings. Maximum feature set.",
73
80
  value: "kitchen-sink",
74
81
  },
75
82
  ],
@@ -15,7 +15,7 @@ const baseAnswers: Answers = {
15
15
  };
16
16
 
17
17
  const allDatabases = ["sqlite", "postgres", "mongodb"] as const;
18
- const allTemplates = ["minimal", "blog", "ecommerce", "kitchen-sink"] as const;
18
+ const allTemplates = ["minimal", "starter", "blog", "ecommerce", "kitchen-sink"] as const;
19
19
 
20
20
  describe("generators", () => {
21
21
  describe("generateKyroConfig", () => {
@@ -56,8 +56,16 @@ describe("generators", () => {
56
56
  template: "minimal",
57
57
  });
58
58
  expect(minimal).toContain("minimalCollections");
59
- expect(minimal).toContain("mediaCollections");
60
- expect(minimal).toContain("authCollections");
59
+ expect(minimal).not.toContain("mediaCollections");
60
+ expect(minimal).not.toContain("authCollections");
61
+
62
+ const starter = generateKyroConfig({
63
+ ...baseAnswers,
64
+ template: "starter",
65
+ });
66
+ expect(starter).toContain("starterCollections");
67
+ expect(starter).toContain("mediaCollections");
68
+ expect(starter).toContain("authCollections");
61
69
 
62
70
  const blog = generateKyroConfig({ ...baseAnswers, template: "blog" });
63
71
  expect(blog).toContain("blogCollections");
@@ -77,6 +85,7 @@ describe("generators", () => {
77
85
  template: "kitchen-sink",
78
86
  });
79
87
  expect(kitchen).toContain("minimalCollections");
88
+ expect(kitchen).toContain("starterCollections");
80
89
  expect(kitchen).toContain("blogCollections");
81
90
  expect(kitchen).toContain("ecommerceCollections");
82
91
  expect(kitchen).toContain("kitchenSinkCollections");
@@ -86,14 +95,22 @@ describe("generators", () => {
86
95
 
87
96
  it("imports settings globals per template", () => {
88
97
  const minimal = generateKyroConfig({ ...baseAnswers, template: "minimal" });
89
- expect(minimal).toContain("coreSettingsGlobals");
98
+ expect(minimal).toContain("siteSettingsGlobal");
99
+ expect(minimal).toContain("seoSettingsGlobal");
100
+ expect(minimal).not.toContain("coreSettingsGlobals");
101
+
102
+ const starter = generateKyroConfig({ ...baseAnswers, template: "starter" });
103
+ expect(starter).toContain("coreSettingsGlobals");
90
104
 
91
105
  const blog = generateKyroConfig({ ...baseAnswers, template: "blog" });
92
106
  expect(blog).toContain("allSettingsGlobals");
93
107
 
94
108
  const ecommerce = generateKyroConfig({ ...baseAnswers, template: "ecommerce" });
95
109
  expect(ecommerce).toContain("allSettingsGlobals");
96
- expect(ecommerce).toContain("ecommerceSettingsGlobals");
110
+ expect(ecommerce).not.toContain("ecommerceSettingsGlobals");
111
+
112
+ const kitchen = generateKyroConfig({ ...baseAnswers, template: "kitchen-sink" });
113
+ expect(kitchen).toContain("allSettingsGlobals");
97
114
  });
98
115
 
99
116
  it("uses project name in config", () => {
@@ -216,18 +233,30 @@ describe("generators", () => {
216
233
 
217
234
  it("never includes old dependencies", () => {
218
235
  const pkg = generatePackageJson(baseAnswers);
219
- expect(pkg.dependencies["react"]).toBeUndefined();
220
- expect(pkg.dependencies["react-dom"]).toBeUndefined();
236
+ expect(pkg.dependencies["react"]).toBeDefined();
237
+ expect(pkg.dependencies["react-dom"]).toBeDefined();
221
238
  expect(pkg.dependencies["lucide-react"]).toBeUndefined();
222
239
  expect(pkg.dependencies["mysql2"]).toBeUndefined();
223
240
  });
224
241
 
242
+ it("includes react type definitions", () => {
243
+ const pkg = generatePackageJson(baseAnswers);
244
+ expect(pkg.devDependencies["@types/react"]).toBeDefined();
245
+ expect(pkg.devDependencies["@types/react-dom"]).toBeDefined();
246
+ });
247
+
225
248
  it("includes tailwindcss and @tailwindcss/vite", () => {
226
249
  const pkg = generatePackageJson(baseAnswers);
227
250
  expect(pkg.dependencies["tailwindcss"]).toBeDefined();
228
251
  expect(pkg.dependencies["@tailwindcss/vite"]).toBeDefined();
229
252
  });
230
253
 
254
+ it("includes vite override for Astro compatibility", () => {
255
+ const pkg = generatePackageJson(baseAnswers);
256
+ expect(pkg.overrides).toBeDefined();
257
+ expect(pkg.overrides!["vite"]).toBe("^7");
258
+ });
259
+
231
260
  it("never includes manual auth bootstrap script", () => {
232
261
  const pkg = generatePackageJson(baseAnswers);
233
262
  expect(pkg.scripts["db:bootstrap"]).toBeUndefined();
@@ -17,7 +17,7 @@ const baseAnswers: Answers = {
17
17
  };
18
18
 
19
19
  const allDatabases = ["sqlite", "postgres", "mongodb"] as const;
20
- const allTemplates = ["minimal", "blog", "ecommerce", "kitchen-sink"] as const;
20
+ const allTemplates = ["minimal", "starter", "blog", "ecommerce", "kitchen-sink"] as const;
21
21
 
22
22
  describe("file generation", () => {
23
23
  let tmpDir: string;
@@ -102,12 +102,17 @@ describe("file generation", () => {
102
102
  expect(deps).toContain("@kyro-cms/core");
103
103
  expect(deps).toContain("@kyro-cms/admin");
104
104
  expect(deps).toContain("astro");
105
- // No react, no react-dom, no lucide-react
106
- expect(deps).not.toContain("react");
107
- expect(deps).not.toContain("react-dom");
105
+ expect(deps).toContain("react");
106
+ expect(deps).toContain("react-dom");
108
107
  expect(deps).not.toContain("lucide-react");
109
108
  expect(deps).not.toContain("mysql2");
110
109
  });
110
+
111
+ it("includes vite override for Astro compatibility", () => {
112
+ const pkg = JSON.parse(pkgContent);
113
+ expect(pkg.overrides).toBeDefined();
114
+ expect(pkg.overrides["vite"]).toBe("^7");
115
+ });
111
116
  });
112
117
 
113
118
  describe("kyro.config.ts", () => {