create-kyro 0.3.0 → 0.4.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
@@ -64,58 +64,6 @@ async function promptUser() {
64
64
  }
65
65
  ]
66
66
  },
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
67
  {
120
68
  type: "select",
121
69
  name: "template",
@@ -205,44 +153,25 @@ ${cyan("?")} ${bold(msg)}`);
205
153
  };
206
154
 
207
155
  // src/generators/packagejson.ts
208
- function generatePackageJson(answers, projectDir) {
156
+ function generatePackageJson(answers) {
209
157
  const deps = {
158
+ "astro": "^6.3.1",
210
159
  "@kyro-cms/core": "latest",
211
- astro: "^5.4.0"
160
+ "@kyro-cms/admin": "latest"
212
161
  };
213
162
  const devDeps = {
214
- typescript: "^5.7.3"
163
+ "typescript": "^5.7.3"
215
164
  };
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
165
  if (answers.database === "postgres") {
226
166
  deps["pg"] = "^8.13.1";
227
- deps["@types/pg"] = "^8.11.0";
228
167
  } else if (answers.database === "mysql") {
229
168
  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
169
  }
238
170
  const scripts = {
239
- dev: "astro dev",
240
- build: "astro build",
241
- preview: "astro preview"
171
+ "dev": "astro dev",
172
+ "build": "astro build",
173
+ "preview": "astro preview"
242
174
  };
243
- if (answers.auth) {
244
- scripts["db:bootstrap"] = "kyro auth bootstrap";
245
- }
246
175
  if (answers.database === "sqlite") {
247
176
  scripts["db:generate"] = "kyro generate";
248
177
  scripts["db:push"] = "kyro push";
@@ -265,58 +194,49 @@ function formatPackageJson(pkg) {
265
194
  // src/generators/config.ts
266
195
  function generateKyroConfig(answers) {
267
196
  const imports = ["import { defineConfig } from '@kyro-cms/core';"];
268
- const adapterLines = [];
269
197
  if (answers.database === "sqlite") {
270
198
  imports.push("import { createLocalAdapter } from '@kyro-cms/core';");
271
- adapterLines.push(` adapter: createLocalAdapter({ path: './data.db' }),`);
272
199
  } else if (answers.database === "postgres") {
273
200
  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
201
  } else if (answers.database === "mysql") {
278
202
  imports.push("import { createDrizzleAdapter } from '@kyro-cms/core';");
203
+ } else if (answers.database === "mongodb") {
204
+ imports.push("import { createMongoDBAdapter } from '@kyro-cms/core';");
205
+ }
206
+ const adapterLines = [];
207
+ if (answers.database === "sqlite") {
208
+ adapterLines.push(` adapter: createLocalAdapter({ path: './data.db' }),`);
209
+ } else if (answers.database === "postgres" || answers.database === "mysql") {
279
210
  adapterLines.push(` adapter: createDrizzleAdapter({`);
280
211
  adapterLines.push(` connectionString: process.env.DATABASE_URL,`);
281
212
  adapterLines.push(` }),`);
282
213
  } else if (answers.database === "mongodb") {
283
- imports.push("import { createMongoDBAdapter } from '@kyro-cms/core';");
284
214
  adapterLines.push(` adapter: createMongoDBAdapter({`);
285
215
  adapterLines.push(` connectionString: process.env.MONGODB_URI,`);
286
216
  adapterLines.push(` }),`);
287
217
  }
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
218
  let templateCollections = "";
302
219
  let templateGlobals = "";
303
220
  switch (answers.template) {
304
221
  case "minimal":
305
- templateCollections = "import { minimalCollections } from '@kyro-cms/core';";
222
+ templateCollections = "import { minimalCollections } from '@kyro-cms/core/templates';";
223
+ templateGlobals = "import { coreSettingsGlobals } from '@kyro-cms/core/templates';";
306
224
  break;
307
225
  case "blog":
308
- templateCollections = "import { blogCollections } from '@kyro-cms/core';";
226
+ templateCollections = "import { blogCollections } from '@kyro-cms/core/templates';";
227
+ templateGlobals = "import { allSettingsGlobals } from '@kyro-cms/core/templates';";
309
228
  break;
310
229
  case "ecommerce":
311
- templateCollections = "import { ecommerceCollections } from '@kyro-cms/core';";
230
+ templateCollections = "import { ecommerceCollections } from '@kyro-cms/core/templates';";
231
+ templateGlobals = "import { allSettingsGlobals, ecommerceSettingsGlobals } from '@kyro-cms/core/templates';";
312
232
  break;
313
233
  case "kitchen-sink":
314
- templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections } from '@kyro-cms/core';`;
234
+ templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections } from '@kyro-cms/core/templates';`;
235
+ templateGlobals = "import { allSettingsGlobals, ecommerceSettingsGlobals } from '@kyro-cms/core/templates';";
315
236
  break;
316
237
  }
317
- if (templateCollections) {
318
- imports.push(templateCollections);
319
- }
238
+ if (templateCollections) imports.push(templateCollections);
239
+ if (templateGlobals) imports.push(templateGlobals);
320
240
  let collectionsConfig = "";
321
241
  if (answers.template === "minimal") {
322
242
  collectionsConfig = ` collections: Object.values(minimalCollections),`;
@@ -332,89 +252,43 @@ function generateKyroConfig(answers) {
332
252
  ...Object.values(kitchenSinkCollections),
333
253
  ],`;
334
254
  }
335
- const config = `${imports.join("\n")}
255
+ let globalsConfig = "";
256
+ if (answers.template === "minimal") {
257
+ globalsConfig = ` globals: coreSettingsGlobals,`;
258
+ } else if (answers.template === "blog" || answers.template === "ecommerce") {
259
+ globalsConfig = answers.template === "ecommerce" ? ` globals: [...allSettingsGlobals, ...ecommerceSettingsGlobals],` : ` globals: allSettingsGlobals,`;
260
+ } else if (answers.template === "kitchen-sink") {
261
+ globalsConfig = ` globals: [...allSettingsGlobals, ...ecommerceSettingsGlobals],`;
262
+ }
263
+ return `${imports.join("\n")}
336
264
 
337
265
  export default defineConfig({
338
266
  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
- },
267
+ prefix: '/api',
268
+ ${adapterLines.join("\n")}
269
+ ${collectionsConfig}
270
+ ${globalsConfig}
271
+ auth: true,
345
272
  });`;
346
- return config;
347
273
  }
348
274
 
349
275
  // src/generators/astro.ts
350
276
  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';" : ""}
277
+ return `import { defineConfig } from 'astro/config';
278
+ import kyro from '@kyro-cms/core';
279
+ import { kyroAdmin } from '@kyro-cms/admin';
398
280
 
399
281
  export default defineConfig({
400
- output: 'server',${adapter}
401
-
282
+ output: 'server',
402
283
  integrations: [
403
- ${integrations.join("\n")}
404
- ],${vitePluginsLength > 0 ? `
405
- vite: {
406
- plugins: [
407
- ${vitePlugins.join("\n")}
408
- ],${nativeExternals}
409
- },` : `
410
- vite: {${nativeExternals}
411
- },`}
284
+ kyro({ adminPath: '/admin', apiPath: '/api' }),
285
+ kyroAdmin({ basePath: '/admin', apiPath: '/api' }),
286
+ ],
412
287
  server: {
413
288
  port: 4321,
414
289
  host: true,
415
290
  },
416
291
  });`;
417
- return config;
418
292
  }
419
293
 
420
294
  // src/generators/files.ts
@@ -464,74 +338,26 @@ npm run dev
464
338
 
465
339
  Visit [http://localhost:4321/admin](http://localhost:4321/admin) to access the admin.
466
340
 
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
- ` : ""}
341
+ The first user to register will automatically be granted super admin privileges.
485
342
 
486
343
  ## Documentation
487
344
 
488
- Visit [https://kyro.cms](https://kyro.cms) for full documentation.
345
+ Visit [https://kyro.dev](https://kyro.dev) for full documentation.
489
346
  `;
490
347
  writeFileSync(join(projectDir, "README.md"), readme);
491
348
  const envExample = `# Kyro CMS Configuration
349
+ # Copy this file to .env and fill in your values
492
350
 
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"}
494
-
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
498
-
499
- # Registration control (set to false to disable public registration after first user)
500
- KYRO_ALLOW_REGISTRATION=true
351
+ ${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"}
501
352
 
502
- # Optional: Custom auth database path (default: ./data/auth.db)
503
- # KYRO_AUTH_DB_PATH=./data/auth.db
353
+ # JWT secret for authentication tokens
354
+ JWT_SECRET=change-this-to-a-random-64-character-string
504
355
 
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` : ""}
356
+ # Admin credentials (used for first-user bootstrap)
357
+ # KYRO_ADMIN_EMAIL=admin@example.com
358
+ # KYRO_ADMIN_PASSWORD=SecurePass123!
512
359
  `;
513
360
  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
361
  const indexPage = `---
536
362
  const title = "${answers.projectName}";
537
363
  ---
@@ -540,13 +366,13 @@ const title = "${answers.projectName}";
540
366
  <head>
541
367
  <meta charset="UTF-8" />
542
368
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
543
- <title>${answers.projectName}</title>
369
+ <title>{title}</title>
544
370
  </head>
545
371
  <body>
546
372
  <main>
547
- <h1>Welcome to ${answers.projectName}</h1>
373
+ <h1>Welcome to {title}</h1>
548
374
  <p>Your Kyro CMS is ready.</p>
549
- ${answers.admin ? '<p><a href="/admin">Go to Admin Dashboard \u2192</a></p>' : ""}
375
+ <p><a href="/admin">Go to Admin Dashboard &rarr;</a></p>
550
376
  </main>
551
377
  </body>
552
378
  </html>
@@ -576,428 +402,13 @@ const title = "${answers.projectName}";
576
402
  </style>
577
403
  `;
578
404
  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
405
  }
995
406
 
996
407
  // src/index.ts
997
408
  import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync } from "fs";
998
409
  import { join as join2 } from "path";
999
410
  import { execSync } from "child_process";
1000
- var VERSION = "0.1.1";
411
+ var VERSION = "0.4.0";
1001
412
  async function main() {
1002
413
  logger.intro("create-kyro", VERSION);
1003
414
  const answers = await promptUser();
@@ -1016,7 +427,7 @@ async function main() {
1016
427
  mkdirSync2(projectDir, { recursive: true });
1017
428
  logger.success("Project directory created");
1018
429
  logger.step(2, steps.length, steps[1]);
1019
- const pkg = generatePackageJson(answers, projectDir);
430
+ const pkg = generatePackageJson(answers);
1020
431
  writeFileSync2(
1021
432
  join2(projectDir, "package.json"),
1022
433
  formatPackageJson(pkg)
@@ -1059,9 +470,9 @@ async function main() {
1059
470
  console.log(` ${logger ? "\x1B[36m" : ""}npm run dev${logger ? "\x1B[0m" : ""}`);
1060
471
  console.log("");
1061
472
  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
- }
473
+ console.log(" Visit http://localhost:4321/admin for the admin dashboard.");
474
+ console.log("");
475
+ console.log(" The first user to register will be the super admin.");
1065
476
  console.log("");
1066
477
  }
1067
478
  main().catch((error) => {