create-kyro 0.5.5 → 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/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";
@@ -207,19 +222,19 @@ function generateKyroConfig(answers) {
207
222
  let templateGlobals = "";
208
223
  switch (answers.template) {
209
224
  case "minimal":
210
- templateCollections = "import { minimalCollections } from '@kyro-cms/core/templates';";
225
+ templateCollections = "import { minimalCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';";
211
226
  templateGlobals = "import { coreSettingsGlobals } from '@kyro-cms/core/templates';";
212
227
  break;
213
228
  case "blog":
214
- templateCollections = "import { blogCollections } from '@kyro-cms/core/templates';";
229
+ templateCollections = "import { blogCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';";
215
230
  templateGlobals = "import { allSettingsGlobals } from '@kyro-cms/core/templates';";
216
231
  break;
217
232
  case "ecommerce":
218
- templateCollections = "import { ecommerceCollections } from '@kyro-cms/core/templates';";
233
+ templateCollections = "import { ecommerceCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';";
219
234
  templateGlobals = "import { allSettingsGlobals, ecommerceSettingsGlobals } from '@kyro-cms/core/templates';";
220
235
  break;
221
236
  case "kitchen-sink":
222
- templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections } from '@kyro-cms/core/templates';`;
237
+ templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';`;
223
238
  templateGlobals = "import { allSettingsGlobals, ecommerceSettingsGlobals } from '@kyro-cms/core/templates';";
224
239
  break;
225
240
  }
@@ -227,17 +242,31 @@ function generateKyroConfig(answers) {
227
242
  if (templateGlobals) imports.push(templateGlobals);
228
243
  let collectionsConfig = "";
229
244
  if (answers.template === "minimal") {
230
- collectionsConfig = ` collections: Object.values(minimalCollections),`;
245
+ collectionsConfig = ` collections: [
246
+ ...Object.values(minimalCollections),
247
+ ...Object.values(mediaCollections),
248
+ ...Object.values(authCollections),
249
+ ],`;
231
250
  } else if (answers.template === "blog") {
232
- collectionsConfig = ` collections: Object.values(blogCollections),`;
251
+ collectionsConfig = ` collections: [
252
+ ...Object.values(blogCollections),
253
+ ...Object.values(mediaCollections),
254
+ ...Object.values(authCollections),
255
+ ],`;
233
256
  } else if (answers.template === "ecommerce") {
234
- collectionsConfig = ` collections: Object.values(ecommerceCollections),`;
257
+ collectionsConfig = ` collections: [
258
+ ...Object.values(ecommerceCollections),
259
+ ...Object.values(mediaCollections),
260
+ ...Object.values(authCollections),
261
+ ],`;
235
262
  } else if (answers.template === "kitchen-sink") {
236
263
  collectionsConfig = ` collections: [
237
264
  ...Object.values(minimalCollections),
238
265
  ...Object.values(blogCollections),
239
266
  ...Object.values(ecommerceCollections),
240
267
  ...Object.values(kitchenSinkCollections),
268
+ ...Object.values(mediaCollections),
269
+ ...Object.values(authCollections),
241
270
  ],`;
242
271
  }
243
272
  let globalsConfig = "";
@@ -256,7 +285,9 @@ export default defineConfig({
256
285
  ${adapterLines.join("\n")}
257
286
  ${collectionsConfig}
258
287
  ${globalsConfig}
259
- auth: true,
288
+ auth: {
289
+ secret: process.env.APP_SECRET,
290
+ },
260
291
  });`;
261
292
  }
262
293
 
@@ -265,13 +296,15 @@ function generateAstroConfig(answers) {
265
296
  return `import { defineConfig } from 'astro/config';
266
297
  import { kyro } from '@kyro-cms/core';
267
298
  import { kyroAdmin } from '@kyro-cms/admin';
299
+ import react from '@astrojs/react';
300
+ import tailwind from '@tailwindcss/vite';
268
301
 
269
302
  export default defineConfig({
270
303
  output: 'server',
271
- integrations: [
272
- kyro({ adminPath: '/admin', apiPath: '/api' }),
273
- kyroAdmin({ basePath: '/admin', apiPath: '/api' }),
274
- ],
304
+ integrations: [react(), kyro({ adminPath: '/admin', apiPath: '/api' }), kyroAdmin({ basePath: '/admin', apiPath: '/api' })],
305
+ vite: {
306
+ plugins: [tailwind()],
307
+ },
275
308
  server: {
276
309
  port: 4321,
277
310
  host: true,
@@ -282,19 +315,30 @@ export default defineConfig({
282
315
  // src/generators/files.ts
283
316
  import { writeFileSync, mkdirSync } from "fs";
284
317
  import { join } from "path";
285
- function generateProjectFiles(answers, projectDir) {
318
+ import { randomBytes } from "crypto";
319
+ function generateAppSecret() {
320
+ return randomBytes(32).toString("hex");
321
+ }
322
+ function generateProjectFiles(answers, projectDir, adminCredentials) {
286
323
  const srcDir = join(projectDir, "src");
287
324
  const pagesDir = join(srcDir, "pages");
325
+ const stylesDir = join(srcDir, "styles");
288
326
  const publicDir = join(projectDir, "public");
289
327
  mkdirSync(pagesDir, { recursive: true });
328
+ mkdirSync(stylesDir, { recursive: true });
290
329
  mkdirSync(publicDir, { recursive: true });
330
+ if (answers.database === "sqlite") {
331
+ mkdirSync(join(projectDir, "data"), { recursive: true });
332
+ }
291
333
  const tsconfig = `{
292
334
  "extends": "astro/tsconfigs/strict",
293
335
  "compilerOptions": {
294
336
  "baseUrl": ".",
295
337
  "paths": {
296
338
  "@/*": ["./src/*"]
297
- }
339
+ },
340
+ "jsx": "react-jsx",
341
+ "jsxImportSource": "react"
298
342
  }
299
343
  }`;
300
344
  writeFileSync(join(projectDir, "tsconfig.json"), tsconfig);
@@ -333,17 +377,50 @@ Visit [https://kyro.dev](https://kyro.dev) for full documentation.
333
377
  const envExample = `# Kyro CMS Configuration
334
378
  # Copy this file to .env and fill in your values
335
379
 
380
+ NODE_ENV=development
381
+ APP_URL=http://localhost:4321
382
+
336
383
  ${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
384
 
338
- # JWT secret for authentication tokens
339
- JWT_SECRET=change-this-to-a-random-64-character-string
385
+ # App secret (set once; on first run it's stored in the database and can be managed via admin UI)
386
+ APP_SECRET=your-secret-here
340
387
 
341
388
  # Admin credentials (used for first-user bootstrap)
342
389
  # KYRO_ADMIN_EMAIL=admin@example.com
343
390
  # KYRO_ADMIN_PASSWORD=SecurePass123!
344
391
  `;
345
392
  writeFileSync(join(projectDir, ".env.example"), envExample);
393
+ if (adminCredentials) {
394
+ const envFile = `# Kyro CMS Configuration
395
+ # Copy this file to .env and fill in your values
396
+
397
+ NODE_ENV=development
398
+ APP_URL=http://localhost:4321
399
+
400
+ ${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"}
401
+
402
+ # App secret (auto-generated; stored in database on first run)
403
+ APP_SECRET=${generateAppSecret()}
404
+
405
+ # Admin credentials (used for first-user bootstrap)
406
+ KYRO_ADMIN_EMAIL=${adminCredentials.adminEmail}
407
+ KYRO_ADMIN_PASSWORD=${adminCredentials.adminPassword}
408
+ `;
409
+ writeFileSync(join(projectDir, ".env"), envFile);
410
+ }
411
+ const mainCss = `@import "tailwindcss";
412
+
413
+ @source "./pages/**/*.{astro,html,js,jsx,ts,tsx}";
414
+ @source "./components/**/*.{astro,html,js,jsx,ts,tsx}";
415
+ @source "./layouts/**/*.{astro,html,js,jsx,ts,tsx}";
416
+
417
+ @theme {
418
+ --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
419
+ }
420
+ `;
421
+ writeFileSync(join(stylesDir, "main.css"), mainCss);
346
422
  const indexPage = `---
423
+ import "../styles/main.css";
347
424
  const title = "${answers.projectName}";
348
425
  ---
349
426
  <!DOCTYPE html>
@@ -353,38 +430,52 @@ const title = "${answers.projectName}";
353
430
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
354
431
  <title>{title}</title>
355
432
  </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>
433
+ <body
434
+ 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"
435
+ >
436
+ <div class="relative min-h-screen flex flex-col items-center justify-center px-4">
437
+ <!-- Decorative background blurs -->
438
+ <div class="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
439
+ <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>
440
+ <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>
441
+ </div>
442
+
443
+ <main class="relative text-center max-w-lg">
444
+ <!-- Badge -->
445
+ <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">
446
+ <span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
447
+ Powered by Kyro CMS
448
+ </div>
449
+
450
+ <!-- Title -->
451
+ <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">
452
+ {title}
453
+ </h1>
454
+
455
+ <!-- Tagline -->
456
+ <p class="mb-10 text-lg sm:text-xl text-stone-500 dark:text-stone-400 leading-relaxed">
457
+ Your content management system is ready. Start building something amazing.
458
+ </p>
459
+
460
+ <!-- Admin link -->
461
+ <a
462
+ href="/admin"
463
+ 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]"
464
+ >
465
+ Go to Admin Dashboard
466
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
467
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M17 8l4 4m0 0l-4 4m4-4H3"/>
468
+ </svg>
469
+ </a>
470
+ </main>
471
+
472
+ <!-- Footer -->
473
+ <footer class="absolute bottom-8 text-xs text-stone-400 dark:text-stone-500">
474
+ &copy; {new Date().getFullYear()} {title}
475
+ </footer>
476
+ </div>
362
477
  </body>
363
478
  </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
479
  `;
389
480
  writeFileSync(join(pagesDir, "index.astro"), indexPage);
390
481
  }
@@ -393,6 +484,16 @@ const title = "${answers.projectName}";
393
484
  import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync } from "fs";
394
485
  import { join as join2 } from "path";
395
486
  import { execSync } from "child_process";
487
+ import { randomBytes as randomBytes2 } from "crypto";
488
+ function generatePassword(length = 24) {
489
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()";
490
+ const bytes = randomBytes2(length);
491
+ let password = "";
492
+ for (let i = 0; i < length; i++) {
493
+ password += chars[bytes[i] % chars.length];
494
+ }
495
+ return password;
496
+ }
396
497
  var VERSION = "0.4.0";
397
498
  async function main() {
398
499
  logger.intro("create-kyro", VERSION);
@@ -402,6 +503,7 @@ async function main() {
402
503
  logger.error(`Directory "${answers.projectName}" already exists.`);
403
504
  process.exit(1);
404
505
  }
506
+ const adminPassword = generatePassword();
405
507
  const steps = [
406
508
  "Creating project directory",
407
509
  "Generating configuration files",
@@ -424,7 +526,7 @@ async function main() {
424
526
  const astroConfig = generateAstroConfig(answers);
425
527
  writeFileSync2(join2(projectDir, "astro.config.mjs"), astroConfig);
426
528
  logger.success("astro.config.mjs generated");
427
- generateProjectFiles(answers, projectDir);
529
+ generateProjectFiles(answers, projectDir, { adminEmail: answers.adminEmail, adminPassword });
428
530
  logger.success("Project files generated");
429
531
  logger.step(3, steps.length, steps[2]);
430
532
  console.log(" Installing dependencies...");
@@ -457,7 +559,14 @@ async function main() {
457
559
  console.log(" Visit http://localhost:4321 to see your app.");
458
560
  console.log(" Visit http://localhost:4321/admin for the admin dashboard.");
459
561
  console.log("");
460
- console.log(" The first user to register will be the super admin.");
562
+ 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");
563
+ console.log(" \x1B[33m Admin Account Created\x1B[0m");
564
+ 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");
565
+ console.log(` Email: \x1B[36m${answers.adminEmail}\x1B[0m`);
566
+ console.log(` Password: \x1B[36m${adminPassword}\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(" The admin user will be created automatically on first server start.");
569
+ console.log(" You can change these credentials in the .env file.");
461
570
  console.log("");
462
571
  }
463
572
  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.6.0",
4
4
  "description": "Interactive scaffolding for Kyro CMS projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
  }
@@ -18,18 +18,24 @@ export function generatePackageJson(
18
18
  "@astrojs/react": "^5.0.4",
19
19
  "@tailwindcss/vite": "^4.0.0",
20
20
  "tailwindcss": "^4.0.0",
21
+ "react": "^19.0.0",
22
+ "react-dom": "^19.0.0",
21
23
  "@kyro-cms/core": "latest",
22
24
  "@kyro-cms/admin": "latest",
23
25
  };
24
26
 
25
27
  const devDeps: Record<string, string> = {
26
28
  "typescript": "^5.7.3",
29
+ "@types/react": "^19.0.0",
30
+ "@types/react-dom": "^19.0.0",
27
31
  };
28
32
 
29
33
  const scripts: Record<string, string> = {
30
34
  "dev": "astro dev",
31
35
  "build": "astro build",
32
36
  "preview": "astro preview",
37
+ "kyro:dev": "kyro dev",
38
+ "kyro:generate": "kyro generate",
33
39
  };
34
40
 
35
41
  if (answers.database === "sqlite") {
@@ -216,12 +216,18 @@ describe("generators", () => {
216
216
 
217
217
  it("never includes old dependencies", () => {
218
218
  const pkg = generatePackageJson(baseAnswers);
219
- expect(pkg.dependencies["react"]).toBeUndefined();
220
- expect(pkg.dependencies["react-dom"]).toBeUndefined();
219
+ expect(pkg.dependencies["react"]).toBeDefined();
220
+ expect(pkg.dependencies["react-dom"]).toBeDefined();
221
221
  expect(pkg.dependencies["lucide-react"]).toBeUndefined();
222
222
  expect(pkg.dependencies["mysql2"]).toBeUndefined();
223
223
  });
224
224
 
225
+ it("includes react type definitions", () => {
226
+ const pkg = generatePackageJson(baseAnswers);
227
+ expect(pkg.devDependencies["@types/react"]).toBeDefined();
228
+ expect(pkg.devDependencies["@types/react-dom"]).toBeDefined();
229
+ });
230
+
225
231
  it("includes tailwindcss and @tailwindcss/vite", () => {
226
232
  const pkg = generatePackageJson(baseAnswers);
227
233
  expect(pkg.dependencies["tailwindcss"]).toBeDefined();
@@ -102,9 +102,8 @@ 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
  });