create-kyro 0.3.1 → 0.4.1

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
@@ -52,11 +52,6 @@ async function promptUser() {
52
52
  description: "Recommended for production. Robust and scalable.",
53
53
  value: "postgres"
54
54
  },
55
- {
56
- title: "MySQL",
57
- description: "Popular choice for web applications.",
58
- value: "mysql"
59
- },
60
55
  {
61
56
  title: "MongoDB",
62
57
  description: "Best for flexible, document-based schemas.",
@@ -64,58 +59,6 @@ async function promptUser() {
64
59
  }
65
60
  ]
66
61
  },
67
- {
68
- type: "select",
69
- name: "styling",
70
- message: "Styling:",
71
- hint: " ",
72
- choices: [
73
- {
74
- title: "Tailwind CSS",
75
- description: "Utility-first CSS framework, excellent DX",
76
- value: "tailwind"
77
- },
78
- {
79
- title: "CSS Modules",
80
- description: "Scoped CSS, no extra dependencies",
81
- value: "cssmodules"
82
- },
83
- {
84
- title: "Styled Components",
85
- description: "CSS-in-JS with tagged template literals",
86
- value: "styled"
87
- },
88
- {
89
- title: "None",
90
- description: "Bring your own styling solution",
91
- value: "none"
92
- }
93
- ]
94
- },
95
- {
96
- type: "toggle",
97
- name: "auth",
98
- message: "Add authentication (JWT)?",
99
- initial: true,
100
- active: "Yes",
101
- inactive: "No"
102
- },
103
- {
104
- type: "toggle",
105
- name: "versioning",
106
- message: "Add versioning/drafts?",
107
- initial: true,
108
- active: "Yes",
109
- inactive: "No"
110
- },
111
- {
112
- type: "toggle",
113
- name: "admin",
114
- message: "Include admin dashboard?",
115
- initial: true,
116
- active: "Yes",
117
- inactive: "No"
118
- },
119
62
  {
120
63
  type: "select",
121
64
  name: "template",
@@ -205,44 +148,20 @@ ${cyan("?")} ${bold(msg)}`);
205
148
  };
206
149
 
207
150
  // src/generators/packagejson.ts
208
- function generatePackageJson(answers, projectDir) {
151
+ function generatePackageJson(answers) {
209
152
  const deps = {
153
+ "astro": "^6.3.1",
210
154
  "@kyro-cms/core": "latest",
211
- astro: "^5.4.0"
155
+ "@kyro-cms/admin": "latest"
212
156
  };
213
157
  const devDeps = {
214
- typescript: "^5.7.3"
158
+ "typescript": "^5.7.3"
215
159
  };
216
- if (answers.styling === "tailwind") {
217
- deps["@astrojs/react"] = "^4.2.0";
218
- deps["react"] = "^19.0.0";
219
- deps["react-dom"] = "^19.0.0";
220
- deps["tailwindcss"] = "^4.0.0";
221
- deps["@tailwindcss/vite"] = "^4.0.0";
222
- devDeps["@types/react"] = "^19.0.0";
223
- devDeps["@types/react-dom"] = "^19.0.0";
224
- }
225
- if (answers.database === "postgres") {
226
- deps["pg"] = "^8.13.1";
227
- deps["@types/pg"] = "^8.11.0";
228
- } else if (answers.database === "mysql") {
229
- deps["mysql2"] = "^3.12.0";
230
- } else if (answers.database === "mongodb") {
231
- deps["mongodb"] = "^6.12.0";
232
- }
233
- if (answers.admin) {
234
- deps["@kyro-cms/admin"] = "latest";
235
- deps["@astrojs/node"] = "^9.5.5";
236
- deps["lucide-react"] = "^0.475.0";
237
- }
238
160
  const scripts = {
239
- dev: "astro dev",
240
- build: "astro build",
241
- preview: "astro preview"
161
+ "dev": "astro dev",
162
+ "build": "astro build",
163
+ "preview": "astro preview"
242
164
  };
243
- if (answers.auth) {
244
- scripts["db:bootstrap"] = "kyro auth bootstrap";
245
- }
246
165
  if (answers.database === "sqlite") {
247
166
  scripts["db:generate"] = "kyro generate";
248
167
  scripts["db:push"] = "kyro push";
@@ -265,58 +184,47 @@ function formatPackageJson(pkg) {
265
184
  // src/generators/config.ts
266
185
  function generateKyroConfig(answers) {
267
186
  const imports = ["import { defineConfig } from '@kyro-cms/core';"];
268
- const adapterLines = [];
269
187
  if (answers.database === "sqlite") {
270
188
  imports.push("import { createLocalAdapter } from '@kyro-cms/core';");
271
- adapterLines.push(` adapter: createLocalAdapter({ path: './data.db' }),`);
272
189
  } else if (answers.database === "postgres") {
273
190
  imports.push("import { createDrizzleAdapter } from '@kyro-cms/core';");
274
- adapterLines.push(` adapter: createDrizzleAdapter({`);
275
- adapterLines.push(` connectionString: process.env.DATABASE_URL,`);
276
- adapterLines.push(` }),`);
277
- } else if (answers.database === "mysql") {
278
- imports.push("import { createDrizzleAdapter } from '@kyro-cms/core';");
191
+ } else if (answers.database === "mongodb") {
192
+ imports.push("import { createMongoDBAdapter } from '@kyro-cms/core';");
193
+ }
194
+ const adapterLines = [];
195
+ if (answers.database === "sqlite") {
196
+ adapterLines.push(` adapter: createLocalAdapter({ path: './data.db' }),`);
197
+ } else if (answers.database === "postgres") {
279
198
  adapterLines.push(` adapter: createDrizzleAdapter({`);
280
199
  adapterLines.push(` connectionString: process.env.DATABASE_URL,`);
281
200
  adapterLines.push(` }),`);
282
201
  } else if (answers.database === "mongodb") {
283
- imports.push("import { createMongoDBAdapter } from '@kyro-cms/core';");
284
202
  adapterLines.push(` adapter: createMongoDBAdapter({`);
285
203
  adapterLines.push(` connectionString: process.env.MONGODB_URI,`);
286
204
  adapterLines.push(` }),`);
287
205
  }
288
- const apiConfig = [
289
- " rest: true,",
290
- " graphql: true,",
291
- " trpc: true,",
292
- " websocket: true,"
293
- ];
294
- const features = [];
295
- if (answers.auth) {
296
- features.push(" auth: true,");
297
- }
298
- if (answers.versioning) {
299
- features.push(" versioning: true,");
300
- }
301
206
  let templateCollections = "";
302
207
  let templateGlobals = "";
303
208
  switch (answers.template) {
304
209
  case "minimal":
305
- templateCollections = "import { minimalCollections } from '@kyro-cms/core';";
210
+ templateCollections = "import { minimalCollections } from '@kyro-cms/core/templates';";
211
+ templateGlobals = "import { coreSettingsGlobals } from '@kyro-cms/core/templates';";
306
212
  break;
307
213
  case "blog":
308
- templateCollections = "import { blogCollections } from '@kyro-cms/core';";
214
+ templateCollections = "import { blogCollections } from '@kyro-cms/core/templates';";
215
+ templateGlobals = "import { allSettingsGlobals } from '@kyro-cms/core/templates';";
309
216
  break;
310
217
  case "ecommerce":
311
- templateCollections = "import { ecommerceCollections } from '@kyro-cms/core';";
218
+ templateCollections = "import { ecommerceCollections } from '@kyro-cms/core/templates';";
219
+ templateGlobals = "import { allSettingsGlobals, ecommerceSettingsGlobals } from '@kyro-cms/core/templates';";
312
220
  break;
313
221
  case "kitchen-sink":
314
- templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections } from '@kyro-cms/core';`;
222
+ templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections } from '@kyro-cms/core/templates';`;
223
+ templateGlobals = "import { allSettingsGlobals, ecommerceSettingsGlobals } from '@kyro-cms/core/templates';";
315
224
  break;
316
225
  }
317
- if (templateCollections) {
318
- imports.push(templateCollections);
319
- }
226
+ if (templateCollections) imports.push(templateCollections);
227
+ if (templateGlobals) imports.push(templateGlobals);
320
228
  let collectionsConfig = "";
321
229
  if (answers.template === "minimal") {
322
230
  collectionsConfig = ` collections: Object.values(minimalCollections),`;
@@ -332,89 +240,43 @@ function generateKyroConfig(answers) {
332
240
  ...Object.values(kitchenSinkCollections),
333
241
  ],`;
334
242
  }
335
- const config = `${imports.join("\n")}
243
+ let globalsConfig = "";
244
+ if (answers.template === "minimal") {
245
+ globalsConfig = ` globals: coreSettingsGlobals,`;
246
+ } else if (answers.template === "blog" || answers.template === "ecommerce") {
247
+ globalsConfig = answers.template === "ecommerce" ? ` globals: [...allSettingsGlobals, ...ecommerceSettingsGlobals],` : ` globals: allSettingsGlobals,`;
248
+ } else if (answers.template === "kitchen-sink") {
249
+ globalsConfig = ` globals: [...allSettingsGlobals, ...ecommerceSettingsGlobals],`;
250
+ }
251
+ return `${imports.join("\n")}
336
252
 
337
253
  export default defineConfig({
338
254
  name: '${answers.projectName}',
339
- prefix: '/api',${adapterLines.length > 0 ? "\n" + adapterLines.join("\n") : ""}
340
- ${collectionsConfig ? collectionsConfig : ""}${features.length > 0 ? "\n" + features.join("\n") : ""}
341
-
342
- api: {
343
- ${apiConfig.join("\n")}
344
- },
255
+ prefix: '/api',
256
+ ${adapterLines.join("\n")}
257
+ ${collectionsConfig}
258
+ ${globalsConfig}
259
+ auth: true,
345
260
  });`;
346
- return config;
347
261
  }
348
262
 
349
263
  // src/generators/astro.ts
350
264
  function generateAstroConfig(answers) {
351
- const integrations = [];
352
- const vitePlugins = [];
353
- const vitePluginsLength = vitePlugins.length;
354
- if (answers.styling === "tailwind") {
355
- integrations.push(" react(),");
356
- vitePlugins.push(" tailwindcss(),");
357
- }
358
- const adapter = answers.admin ? `
359
- adapter: node({
360
- mode: 'standalone'
361
- }),` : "";
362
- const nativeExternals = `
363
- ssr: {
364
- external: [
365
- 'better-sqlite3',
366
- 'sharp',
367
- 'ssh2',
368
- 'cpu-features',
369
- 'ssh2-sftp-client',
370
- 'ioredis',
371
- 'nodemailer',
372
- 'jsonwebtoken',
373
- '@mapbox/node-pre-gyp',
374
- 'mock-aws-s3',
375
- 'aws-sdk',
376
- 'nock',
377
- ],
378
- },
379
- optimizeDeps: {
380
- exclude: [
381
- 'better-sqlite3',
382
- 'sharp',
383
- 'ssh2',
384
- 'cpu-features',
385
- 'ssh2-sftp-client',
386
- 'ioredis',
387
- 'nodemailer',
388
- 'jsonwebtoken',
389
- '@mapbox/node-pre-gyp',
390
- 'mock-aws-s3',
391
- 'aws-sdk',
392
- 'nock',
393
- ],
394
- },`;
395
- const config = `import { defineConfig } from 'astro/config';
396
- import node from '@astrojs/node';
397
- ${answers.styling === "tailwind" ? "import react from '@astrojs/react';\nimport tailwindcss from '@tailwindcss/vite';" : ""}
265
+ return `import { defineConfig } from 'astro/config';
266
+ import kyro from '@kyro-cms/core';
267
+ import { kyroAdmin } from '@kyro-cms/admin';
398
268
 
399
269
  export default defineConfig({
400
- output: 'server',${adapter}
401
-
270
+ output: 'server',
402
271
  integrations: [
403
- ${integrations.join("\n")}
404
- ],${vitePluginsLength > 0 ? `
405
- vite: {
406
- plugins: [
407
- ${vitePlugins.join("\n")}
408
- ],${nativeExternals}
409
- },` : `
410
- vite: {${nativeExternals}
411
- },`}
272
+ kyro({ adminPath: '/admin', apiPath: '/api' }),
273
+ kyroAdmin({ basePath: '/admin', apiPath: '/api' }),
274
+ ],
412
275
  server: {
413
276
  port: 4321,
414
277
  host: true,
415
278
  },
416
279
  });`;
417
- return config;
418
280
  }
419
281
 
420
282
  // src/generators/files.ts
@@ -427,7 +289,11 @@ function generateProjectFiles(answers, projectDir) {
427
289
  mkdirSync(pagesDir, { recursive: true });
428
290
  mkdirSync(publicDir, { recursive: true });
429
291
  if (answers.database === "sqlite") {
430
- mkdirSync(join(projectDir, "data"), { recursive: true });
292
+ envDbSection = "# SQLite (local) - no additional config needed";
293
+ } else if (answers.database === "postgres") {
294
+ envDbSection = "# Database connection (PostgreSQL)\nDATABASE_URL=postgresql://user:password@localhost:5432/kyro_cms";
295
+ } else {
296
+ envDbSection = "# MongoDB connection\nMONGODB_URI=mongodb://localhost:27017/kyro_cms";
431
297
  }
432
298
  const tsconfig = `{
433
299
  "extends": "astro/tsconfigs/strict",
@@ -464,74 +330,26 @@ npm run dev
464
330
 
465
331
  Visit [http://localhost:4321/admin](http://localhost:4321/admin) to access the admin.
466
332
 
467
- ${answers.auth ? `## Creating Your Admin User
468
-
469
- Before logging into the admin, you need to create an admin user. Run:
470
-
471
- \`\`\`bash
472
- npm run db:bootstrap
473
- \`\`\`
474
-
475
- Or set environment variables to auto-bootstrap on startup:
476
-
477
- \`\`\`bash
478
- # .env
479
- KYRO_ADMIN_EMAIL=admin@example.com
480
- KYRO_ADMIN_PASSWORD=SecurePass123!
481
- \`\`\`
482
-
483
- Then restart the dev server.
484
- ` : ""}
333
+ The first user to register will automatically be granted super admin privileges.
485
334
 
486
335
  ## Documentation
487
336
 
488
- Visit [https://kyro.cms](https://kyro.cms) for full documentation.
337
+ Visit [https://kyro.dev](https://kyro.dev) for full documentation.
489
338
  `;
490
339
  writeFileSync(join(projectDir, "README.md"), readme);
491
340
  const envExample = `# Kyro CMS Configuration
341
+ # Copy this file to .env and fill in your values
492
342
 
493
- ${answers.database === "sqlite" ? "# SQLite (local) - no additional config needed" : answers.database === "postgres" || answers.database === "mysql" ? "# Database connection (PostgreSQL/MySQL)\nDATABASE_URL=postgresql://user:password@localhost:5432/kyro_cms\nDATABASE_SSL=false" : "# MongoDB connection\nMONGODB_URI=mongodb://localhost:27017/kyro_cms"}
343
+ ${answers.database === "sqlite" ? "# SQLite (local) - no additional config needed" : answers.database === "postgres" || answers.database === "mysql" ? "# Database connection (PostgreSQL/MySQL)\nDATABASE_URL=postgresql://user:password@localhost:5432/kyro_cms" : "# MongoDB connection\nMONGODB_URI=mongodb://localhost:27017/kyro_cms"}
494
344
 
495
- ${answers.auth ? `# Authentication (uses SQLite at ./data/auth.db - no Redis needed)
496
- JWT_SECRET=change-this-to-a-random-32-character-string
497
- JWT_EXPIRES_IN=24h
345
+ # JWT secret for authentication tokens
346
+ JWT_SECRET=change-this-to-a-random-64-character-string
498
347
 
499
- # Registration control (set to false to disable public registration after first user)
500
- KYRO_ALLOW_REGISTRATION=true
501
-
502
- # Optional: Custom auth database path (default: ./data/auth.db)
503
- # KYRO_AUTH_DB_PATH=./data/auth.db
504
-
505
- # Optional: SMTP for emails
506
- # SMTP_HOST=smtp.example.com
507
- # SMTP_PORT=587
508
- # SMTP_SECURE=false
509
- # SMTP_USER=your-email@example.com
510
- # SMTP_PASS=your-password
511
- # SMTP_FROM=noreply@example.com` : ""}
348
+ # Admin credentials (used for first-user bootstrap)
349
+ # KYRO_ADMIN_EMAIL=admin@example.com
350
+ # KYRO_ADMIN_PASSWORD=SecurePass123!
512
351
  `;
513
352
  writeFileSync(join(projectDir, ".env.example"), envExample);
514
- const spec = `# ${answers.projectName}
515
-
516
- ## Overview
517
-
518
- This project uses Kyro CMS - an Astro-native headless CMS.
519
-
520
- ## Configuration
521
-
522
- - **Database**: ${answers.database === "sqlite" ? "SQLite (local-first)" : answers.database}
523
- - **APIs**: REST, GraphQL, tRPC, WebSocket
524
- - **Styling**: ${answers.styling}
525
- - **Auth**: ${answers.auth ? "Enabled" : "Disabled"}
526
- - **Versioning**: ${answers.versioning ? "Enabled" : "Disabled"}
527
- - **Admin**: ${answers.admin ? "Included" : "Not included"}
528
- - **Template**: ${answers.template}
529
-
530
- ## Collections
531
-
532
- ${answers.template === "minimal" ? "- Posts" : answers.template === "blog" ? "- Posts\n- Categories\n- Media" : "- Products\n- Categories\n- Customers\n- Orders\n- Coupons"}
533
- `;
534
- writeFileSync(join(projectDir, "SPEC.md"), spec);
535
353
  const indexPage = `---
536
354
  const title = "${answers.projectName}";
537
355
  ---
@@ -540,13 +358,13 @@ const title = "${answers.projectName}";
540
358
  <head>
541
359
  <meta charset="UTF-8" />
542
360
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
543
- <title>${answers.projectName}</title>
361
+ <title>{title}</title>
544
362
  </head>
545
363
  <body>
546
364
  <main>
547
- <h1>Welcome to ${answers.projectName}</h1>
365
+ <h1>Welcome to {title}</h1>
548
366
  <p>Your Kyro CMS is ready.</p>
549
- ${answers.admin ? '<p><a href="/admin">Go to Admin Dashboard \u2192</a></p>' : ""}
367
+ <p><a href="/admin">Go to Admin Dashboard &rarr;</a></p>
550
368
  </main>
551
369
  </body>
552
370
  </html>
@@ -576,428 +394,13 @@ const title = "${answers.projectName}";
576
394
  </style>
577
395
  `;
578
396
  writeFileSync(join(pagesDir, "index.astro"), indexPage);
579
- if (answers.admin) {
580
- const adminDir = join(pagesDir, "admin");
581
- mkdirSync(adminDir, { recursive: true });
582
- const adminIndex = `---
583
- import { Admin } from '@kyro-cms/admin';
584
- import config from '../../../kyro.config';
585
- ---
586
- <!DOCTYPE html>
587
- <html lang="en">
588
- <head>
589
- <meta charset="UTF-8" />
590
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
591
- <title>Admin - ${answers.projectName}</title>
592
- </head>
593
- <body>
594
- <Admin client:load config={config} />
595
- </body>
596
- </html>
597
- `;
598
- writeFileSync(join(adminDir, "index.astro"), adminIndex);
599
- }
600
- if (answers.auth) {
601
- const authApiDir = join(pagesDir, "api", "auth");
602
- mkdirSync(authApiDir, { recursive: true });
603
- writeFileSync(
604
- join(authApiDir, "login.ts"),
605
- generateLoginEndpoint(answers.database)
606
- );
607
- writeFileSync(
608
- join(authApiDir, "register.ts"),
609
- generateRegisterEndpoint(answers.database)
610
- );
611
- writeFileSync(
612
- join(authApiDir, "logout.ts"),
613
- generateLogoutEndpoint(answers.database)
614
- );
615
- writeFileSync(join(authApiDir, "me.ts"), generateMeEndpoint());
616
- writeFileSync(
617
- join(authApiDir, "users.ts"),
618
- generateUsersEndpoint(answers.database)
619
- );
620
- writeFileSync(join(srcDir, "middleware.ts"), generateMiddleware());
621
- }
622
- }
623
- function generateLoginEndpoint(database) {
624
- return `import type { APIRoute } from "astro";
625
- import { SQLiteAuthAdapter } from "@kyro-cms/core";
626
- import jwt from "jsonwebtoken";
627
-
628
- const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
629
- const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "24h";
630
-
631
- async function getAuthApi() {
632
- return new SQLiteAuthAdapter({ path: "./data/auth.db" });
633
- }
634
-
635
- export const POST: APIRoute = async ({ request }) => {
636
- try {
637
- const body = (await request.json()) as {
638
- email?: string;
639
- password?: string;
640
- };
641
- const { email, password } = body;
642
-
643
- if (!email || !password) {
644
- return new Response(
645
- JSON.stringify({ error: "Email and password required" }),
646
- { status: 400, headers: { "Content-Type": "application/json" } },
647
- );
648
- }
649
-
650
- const adapter = await getAuthApi();
651
- await adapter.connect();
652
-
653
- const user = await adapter.findUserByEmail(email);
654
- if (!user || !user.passwordHash) {
655
- await adapter.disconnect();
656
- return new Response(JSON.stringify({ error: "Invalid credentials" }), {
657
- status: 401,
658
- headers: { "Content-Type": "application/json" },
659
- });
660
- }
661
-
662
- const valid = await adapter.verifyPassword(password, user.passwordHash);
663
- if (!valid) {
664
- await adapter.disconnect();
665
- return new Response(JSON.stringify({ error: "Invalid credentials" }), {
666
- status: 401,
667
- headers: { "Content-Type": "application/json" },
668
- });
669
- }
670
-
671
- const session = await adapter.createSession(user.id, {
672
- ipAddress: request.headers.get("x-forwarded-for") || "unknown",
673
- userAgent: request.headers.get("user-agent") || "",
674
- });
675
-
676
- const token = jwt.sign(
677
- {
678
- sub: user.id,
679
- email: user.email,
680
- role: user.role,
681
- tenantId: user.tenantId,
682
- },
683
- JWT_SECRET,
684
- { expiresIn: JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] },
685
- );
686
-
687
- await adapter.disconnect();
688
-
689
- const { passwordHash, ...safeUser } = user;
690
-
691
- return new Response(
692
- JSON.stringify({
693
- success: true,
694
- user: safeUser,
695
- token,
696
- refreshToken: session.refreshToken,
697
- }),
698
- {
699
- status: 200,
700
- headers: { "Content-Type": "application/json" },
701
- },
702
- );
703
- } catch (error) {
704
- console.error("Login error:", error);
705
- return new Response(JSON.stringify({ error: "Login failed" }), {
706
- status: 500,
707
- headers: { "Content-Type": "application/json" },
708
- });
709
- }
710
- };
711
- `;
712
- }
713
- function generateRegisterEndpoint(database) {
714
- return `import type { APIRoute } from "astro";
715
- import { SQLiteAuthAdapter } from "@kyro-cms/core";
716
- import jwt from "jsonwebtoken";
717
-
718
- const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
719
- const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "24h";
720
- const ALLOW_REGISTRATION = process.env.KYRO_ALLOW_REGISTRATION !== "false";
721
-
722
- async function getAuthApi() {
723
- return new SQLiteAuthAdapter({ path: "./data/auth.db" });
724
- }
725
-
726
- export const POST: APIRoute = async ({ request }) => {
727
- try {
728
- const body = (await request.json()) as {
729
- email?: string;
730
- password?: string;
731
- confirmPassword?: string;
732
- };
733
- const { email, password, confirmPassword } = body;
734
-
735
- if (!email || !password) {
736
- return new Response(
737
- JSON.stringify({ error: "Email and password required" }),
738
- { status: 400, headers: { "Content-Type": "application/json" } },
739
- );
740
- }
741
-
742
- if (password !== confirmPassword) {
743
- return new Response(
744
- JSON.stringify({ error: "Passwords do not match" }),
745
- { status: 400, headers: { "Content-Type": "application/json" } },
746
- );
747
- }
748
-
749
- if (password.length < 8) {
750
- return new Response(
751
- JSON.stringify({ error: "Password must be at least 8 characters" }),
752
- { status: 400, headers: { "Content-Type": "application/json" } },
753
- );
754
- }
755
-
756
- const adapter = await getAuthApi();
757
- try {
758
- await adapter.connect();
759
- } catch {
760
- return new Response(
761
- JSON.stringify({ error: "Unable to connect to auth storage." }),
762
- { status: 500, headers: { "Content-Type": "application/json" } },
763
- );
764
- }
765
-
766
- const existingUser = await adapter.findUserByEmail(email);
767
- if (existingUser) {
768
- await adapter.disconnect();
769
- return new Response(
770
- JSON.stringify({ error: "Email already registered" }),
771
- { status: 409, headers: { "Content-Type": "application/json" } },
772
- );
773
- }
774
-
775
- const isFirstUser = !(await adapter.hasAnyUsers());
776
-
777
- if (!isFirstUser && !ALLOW_REGISTRATION) {
778
- await adapter.disconnect();
779
- return new Response(
780
- JSON.stringify({ error: "Registration is disabled" }),
781
- { status: 403, headers: { "Content-Type": "application/json" } },
782
- );
783
- }
784
-
785
- const passwordHash = await adapter.hashPassword(password);
786
- const user = await adapter.createUser({
787
- email,
788
- passwordHash,
789
- role: isFirstUser ? "super_admin" : "editor",
790
- });
791
-
792
- if (isFirstUser) {
793
- await adapter.updateUser(user.id, { emailVerified: true });
794
- }
795
-
796
- const session = await adapter.createSession(user.id, {
797
- ipAddress: request.headers.get("x-forwarded-for") || "unknown",
798
- userAgent: request.headers.get("user-agent") || "",
799
- });
800
-
801
- const token = jwt.sign(
802
- {
803
- sub: user.id,
804
- email: user.email,
805
- role: user.role,
806
- tenantId: user.tenantId,
807
- },
808
- JWT_SECRET,
809
- { expiresIn: JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] },
810
- );
811
-
812
- await adapter.disconnect();
813
-
814
- const { passwordHash: _, ...safeUser } = user;
815
-
816
- return new Response(
817
- JSON.stringify({
818
- success: true,
819
- isFirstUser,
820
- user: safeUser,
821
- token,
822
- refreshToken: session.refreshToken,
823
- }),
824
- {
825
- status: 201,
826
- headers: { "Content-Type": "application/json" },
827
- },
828
- );
829
- } catch (error) {
830
- console.error("Registration error:", error);
831
- return new Response(JSON.stringify({ error: "Registration failed" }), {
832
- status: 500,
833
- headers: { "Content-Type": "application/json" },
834
- });
835
- }
836
- };
837
- `;
838
- }
839
- function generateLogoutEndpoint(database) {
840
- return `import type { APIRoute } from "astro";
841
- import { SQLiteAuthAdapter } from "@kyro-cms/core";
842
-
843
- async function getAuthApi() {
844
- return new SQLiteAuthAdapter({ path: "./data/auth.db" });
845
- }
846
-
847
- export const POST: APIRoute = async ({ request }) => {
848
- try {
849
- const authHeader = request.headers.get("authorization");
850
- const token = authHeader?.startsWith("Bearer ")
851
- ? authHeader.slice(7)
852
- : null;
853
-
854
- if (token) {
855
- const adapter = await getAuthApi();
856
- await adapter.connect();
857
- await adapter.deleteSession(token);
858
- await adapter.disconnect();
859
- }
860
-
861
- return new Response(
862
- JSON.stringify({ success: true }),
863
- { status: 200, headers: { "Content-Type": "application/json" } },
864
- );
865
- } catch {
866
- return new Response(
867
- JSON.stringify({ success: true }),
868
- { status: 200, headers: { "Content-Type": "application/json" } },
869
- );
870
- }
871
- };
872
- `;
873
- }
874
- function generateMeEndpoint() {
875
- return `import type { APIRoute } from "astro";
876
- import jwt from "jsonwebtoken";
877
-
878
- const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
879
-
880
- export const GET: APIRoute = async ({ request }) => {
881
- const authHeader = request.headers.get("authorization");
882
- const token = authHeader?.startsWith("Bearer ")
883
- ? authHeader.slice(7)
884
- : null;
885
-
886
- if (!token) {
887
- return new Response(
888
- JSON.stringify({ error: "Not authenticated" }),
889
- { status: 401, headers: { "Content-Type": "application/json" } },
890
- );
891
- }
892
-
893
- try {
894
- const payload = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
895
- return new Response(
896
- JSON.stringify({
897
- id: payload.sub,
898
- email: payload.email,
899
- role: payload.role,
900
- }),
901
- { status: 200, headers: { "Content-Type": "application/json" } },
902
- );
903
- } catch {
904
- return new Response(
905
- JSON.stringify({ error: "Invalid token" }),
906
- { status: 401, headers: { "Content-Type": "application/json" } },
907
- );
908
- }
909
- };
910
- `;
911
- }
912
- function generateUsersEndpoint(database) {
913
- return `import type { APIRoute } from "astro";
914
- import { SQLiteAuthAdapter } from "@kyro-cms/core";
915
-
916
- async function getAuthApi() {
917
- return new SQLiteAuthAdapter({ path: "./data/auth.db" });
918
- }
919
-
920
- export const GET: APIRoute = async () => {
921
- try {
922
- const adapter = await getAuthApi();
923
- await adapter.connect();
924
-
925
- const hasUsers = await adapter.hasAnyUsers();
926
-
927
- await adapter.disconnect();
928
-
929
- return new Response(JSON.stringify({ hasUsers }), {
930
- status: 200,
931
- headers: { "Content-Type": "application/json" },
932
- });
933
- } catch {
934
- return new Response(JSON.stringify({ hasUsers: false }), {
935
- status: 200,
936
- headers: { "Content-Type": "application/json" },
937
- });
938
- }
939
- };
940
- `;
941
- }
942
- function generateMiddleware() {
943
- return `import type { MiddlewareHandler } from "astro";
944
- import jwt from "jsonwebtoken";
945
-
946
- const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
947
-
948
- const PUBLIC_PATHS = [
949
- "/api/auth/login",
950
- "/api/auth/logout",
951
- "/api/auth/register",
952
- "/api/auth/me",
953
- "/api/auth/users",
954
- "/api/health",
955
- "/favicon.svg",
956
- ];
957
-
958
- const PUBLIC_PREFIXES = ["/api/auth/", "/admin"];
959
-
960
- export const onRequest: MiddlewareHandler = async ({ request, url }, next) => {
961
- const pathname = new URL(url).pathname;
962
-
963
- if (PUBLIC_PATHS.includes(pathname) || PUBLIC_PATHS.includes(pathname.replace(/\\/$/, ""))) {
964
- return next();
965
- }
966
-
967
- for (const prefix of PUBLIC_PREFIXES) {
968
- if (pathname.startsWith(prefix)) {
969
- return next();
970
- }
971
- }
972
-
973
- const authHeader = request.headers.get("authorization");
974
- const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
975
-
976
- if (!token) {
977
- return new Response(
978
- JSON.stringify({ error: "Authentication required" }),
979
- { status: 401, headers: { "Content-Type": "application/json" } },
980
- );
981
- }
982
-
983
- try {
984
- jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
985
- return next();
986
- } catch {
987
- return new Response(
988
- JSON.stringify({ error: "Invalid or expired token" }),
989
- { status: 401, headers: { "Content-Type": "application/json" } },
990
- );
991
- }
992
- };
993
- `;
994
397
  }
995
398
 
996
399
  // src/index.ts
997
400
  import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync } from "fs";
998
401
  import { join as join2 } from "path";
999
402
  import { execSync } from "child_process";
1000
- var VERSION = "0.1.1";
403
+ var VERSION = "0.4.0";
1001
404
  async function main() {
1002
405
  logger.intro("create-kyro", VERSION);
1003
406
  const answers = await promptUser();
@@ -1016,7 +419,7 @@ async function main() {
1016
419
  mkdirSync2(projectDir, { recursive: true });
1017
420
  logger.success("Project directory created");
1018
421
  logger.step(2, steps.length, steps[1]);
1019
- const pkg = generatePackageJson(answers, projectDir);
422
+ const pkg = generatePackageJson(answers);
1020
423
  writeFileSync2(
1021
424
  join2(projectDir, "package.json"),
1022
425
  formatPackageJson(pkg)
@@ -1059,9 +462,9 @@ async function main() {
1059
462
  console.log(` ${logger ? "\x1B[36m" : ""}npm run dev${logger ? "\x1B[0m" : ""}`);
1060
463
  console.log("");
1061
464
  console.log(" Visit http://localhost:4321 to see your app.");
1062
- if (answers.admin) {
1063
- console.log(" Visit http://localhost:4321/admin for the admin dashboard.");
1064
- }
465
+ console.log(" Visit http://localhost:4321/admin for the admin dashboard.");
466
+ console.log("");
467
+ console.log(" The first user to register will be the super admin.");
1065
468
  console.log("");
1066
469
  }
1067
470
  main().catch((error) => {