create-kyro 0.1.2 → 0.1.4

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
@@ -86,12 +86,14 @@ async function promptUser() {
86
86
  {
87
87
  title: "tRPC",
88
88
  description: "End-to-end typesafe APIs, great for TypeScript",
89
- value: "trpc"
89
+ value: "trpc",
90
+ selected: true
90
91
  },
91
92
  {
92
93
  title: "WebSocket",
93
94
  description: "Real-time bidirectional communication",
94
- value: "websocket"
95
+ value: "websocket",
96
+ selected: true
95
97
  }
96
98
  ]
97
99
  },
@@ -127,6 +129,7 @@ async function promptUser() {
127
129
  type: "toggle",
128
130
  name: "auth",
129
131
  message: "Add authentication (JWT)?",
132
+ initial: true,
130
133
  active: "Yes",
131
134
  inactive: "No"
132
135
  },
@@ -134,6 +137,7 @@ async function promptUser() {
134
137
  type: "toggle",
135
138
  name: "versioning",
136
139
  message: "Add versioning/drafts?",
140
+ initial: true,
137
141
  active: "Yes",
138
142
  inactive: "No"
139
143
  },
@@ -150,6 +154,7 @@ async function promptUser() {
150
154
  name: "template",
151
155
  message: "Starting template:",
152
156
  hint: " ",
157
+ initial: 1,
153
158
  choices: [
154
159
  {
155
160
  title: "Minimal",
@@ -236,10 +241,10 @@ ${cyan("?")} ${bold(msg)}`);
236
241
  function generatePackageJson(answers, projectDir) {
237
242
  const deps = {
238
243
  "@kyro-cms/core": "latest",
239
- "astro": "^5.4.0"
244
+ astro: "^5.4.0"
240
245
  };
241
246
  const devDeps = {
242
- "typescript": "^5.7.3"
247
+ typescript: "^5.7.3"
243
248
  };
244
249
  if (answers.styling === "tailwind") {
245
250
  deps["@astrojs/react"] = "^4.2.0";
@@ -260,13 +265,17 @@ function generatePackageJson(answers, projectDir) {
260
265
  }
261
266
  if (answers.admin) {
262
267
  deps["@kyro-cms/admin"] = "latest";
268
+ deps["@astrojs/node"] = "^9.5.5";
263
269
  deps["lucide-react"] = "^0.475.0";
264
270
  }
265
271
  const scripts = {
266
- "dev": "astro dev",
267
- "build": "astro build",
268
- "preview": "astro preview"
272
+ dev: "astro dev",
273
+ build: "astro build",
274
+ preview: "astro preview"
269
275
  };
276
+ if (answers.auth) {
277
+ scripts["db:bootstrap"] = "kyro auth bootstrap";
278
+ }
270
279
  if (answers.database === "sqlite") {
271
280
  scripts["db:generate"] = "kyro generate";
272
281
  scripts["db:push"] = "kyro push";
@@ -288,26 +297,24 @@ function formatPackageJson(pkg) {
288
297
 
289
298
  // src/generators/config.ts
290
299
  function generateKyroConfig(answers) {
291
- const imports = [
292
- "import { defineConfig, createTemplateConfig } from '@kyro-cms/core';"
293
- ];
300
+ const imports = ["import { defineConfig } from '@kyro-cms/core';"];
294
301
  const adapterLines = [];
295
302
  if (answers.database === "sqlite") {
296
- imports.push("import { localAdapter } from '@kyro-cms/core';");
297
- adapterLines.push(` adapter: localAdapter({ path: './data.db' }),`);
303
+ imports.push("import { createLocalAdapter } from '@kyro-cms/core';");
304
+ adapterLines.push(` adapter: createLocalAdapter({ path: './data.db' }),`);
298
305
  } else if (answers.database === "postgres") {
299
- imports.push("import { drizzleAdapter } from '@kyro-cms/core';");
300
- adapterLines.push(` adapter: drizzleAdapter({`);
306
+ imports.push("import { createDrizzleAdapter } from '@kyro-cms/core';");
307
+ adapterLines.push(` adapter: createDrizzleAdapter({`);
301
308
  adapterLines.push(` connectionString: process.env.DATABASE_URL,`);
302
309
  adapterLines.push(` }),`);
303
310
  } else if (answers.database === "mysql") {
304
- imports.push("import { drizzleAdapter } from '@kyro-cms/core';");
305
- adapterLines.push(` adapter: drizzleAdapter({`);
311
+ imports.push("import { createDrizzleAdapter } from '@kyro-cms/core';");
312
+ adapterLines.push(` adapter: createDrizzleAdapter({`);
306
313
  adapterLines.push(` connectionString: process.env.DATABASE_URL,`);
307
314
  adapterLines.push(` }),`);
308
315
  } else if (answers.database === "mongodb") {
309
- imports.push("import { mongoAdapter } from '@kyro-cms/core';");
310
- adapterLines.push(` adapter: mongoAdapter({`);
316
+ imports.push("import { createMongoDBAdapter } from '@kyro-cms/core';");
317
+ adapterLines.push(` adapter: createMongoDBAdapter({`);
311
318
  adapterLines.push(` connectionString: process.env.MONGODB_URI,`);
312
319
  adapterLines.push(` }),`);
313
320
  }
@@ -331,39 +338,46 @@ function generateKyroConfig(answers) {
331
338
  if (answers.versioning) {
332
339
  features.push(" versioning: true,");
333
340
  }
334
- let templateCollections = [];
335
- let templateGlobals = [];
341
+ let templateCollections = "";
342
+ let templateGlobals = "";
336
343
  switch (answers.template) {
337
344
  case "minimal":
338
- templateCollections.push("...minimalCollections,");
345
+ templateCollections = "import { minimalCollections } from '@kyro-cms/core';";
339
346
  break;
340
347
  case "blog":
341
- templateCollections.push("...blogCollections,");
348
+ templateCollections = "import { blogCollections } from '@kyro-cms/core';";
342
349
  break;
343
350
  case "ecommerce":
344
- templateCollections.push("...ecommerceCollections,");
351
+ templateCollections = "import { ecommerceCollections } from '@kyro-cms/core';";
345
352
  break;
346
353
  case "kitchen-sink":
347
- templateCollections.push(
348
- "...minimalCollections,",
349
- "...blogCollections,",
350
- "...ecommerceCollections,",
351
- "...kitchenSinkCollections,"
352
- );
354
+ templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections } from '@kyro-cms/core';`;
353
355
  break;
354
356
  }
355
- const collectionsBlock = templateCollections.length > 0 ? ` collections: [
356
- ${templateCollections.join("\n ")}
357
- ],` : "";
358
- const globalsBlock = templateGlobals.length > 0 ? ` globals: [
359
- ${templateGlobals.join("\n ")}
360
- ],` : "";
357
+ if (templateCollections) {
358
+ imports.push(templateCollections);
359
+ }
360
+ let collectionsConfig = "";
361
+ if (answers.template === "minimal") {
362
+ collectionsConfig = ` collections: Object.values(minimalCollections),`;
363
+ } else if (answers.template === "blog") {
364
+ collectionsConfig = ` collections: Object.values(blogCollections),`;
365
+ } else if (answers.template === "ecommerce") {
366
+ collectionsConfig = ` collections: Object.values(ecommerceCollections),`;
367
+ } else if (answers.template === "kitchen-sink") {
368
+ collectionsConfig = ` collections: [
369
+ ...Object.values(minimalCollections),
370
+ ...Object.values(blogCollections),
371
+ ...Object.values(ecommerceCollections),
372
+ ...Object.values(kitchenSinkCollections),
373
+ ],`;
374
+ }
361
375
  const config = `${imports.join("\n")}
362
376
 
363
377
  export default defineConfig({
364
378
  name: '${answers.projectName}',
365
379
  prefix: '/api',${adapterLines.length > 0 ? "\n" + adapterLines.join("\n") : ""}
366
- ${collectionsBlock ? "\n" + collectionsBlock : ""}${globalsBlock ? "\n" + globalsBlock : ""}${features.length > 0 ? "\n" + features.join("\n") : ""}
380
+ ${collectionsConfig ? collectionsConfig : ""}${features.length > 0 ? "\n" + features.join("\n") : ""}
367
381
 
368
382
  api: {
369
383
  ${apiConfig.join("\n")}
@@ -385,6 +399,7 @@ function generateAstroConfig(answers) {
385
399
  mode: 'standalone'
386
400
  }),` : "";
387
401
  const config = `import { defineConfig } from 'astro/config';
402
+ import node from '@astrojs/node';
388
403
  ${answers.styling === "tailwind" ? "import react from '@astrojs/react';\nimport tailwindcss from '@tailwindcss/vite';" : ""}
389
404
 
390
405
  export default defineConfig({
@@ -442,18 +457,67 @@ data/
442
457
 
443
458
  A Kyro CMS project.
444
459
 
445
- ## Getting Started
460
+ ## Quick Start
446
461
 
447
462
  \`\`\`bash
448
463
  npm install
449
464
  npm run dev
450
465
  \`\`\`
451
466
 
467
+ ## Admin Dashboard
468
+
469
+ Visit [http://localhost:4321/admin](http://localhost:4321/admin) to access the admin.
470
+
471
+ ${answers.auth ? `## Creating Your Admin User
472
+
473
+ Before logging into the admin, you need to create an admin user. Run:
474
+
475
+ \`\`\`bash
476
+ npm run db:bootstrap
477
+ \`\`\`
478
+
479
+ Or set environment variables to auto-bootstrap on startup:
480
+
481
+ \`\`\`bash
482
+ # .env
483
+ KYRO_ADMIN_EMAIL=admin@example.com
484
+ KYRO_ADMIN_PASSWORD=SecurePass123!
485
+ \`\`\`
486
+
487
+ Then restart the dev server.
488
+ ` : ""}
489
+
452
490
  ## Documentation
453
491
 
454
492
  Visit [https://kyro.cms](https://kyro.cms) for full documentation.
455
493
  `;
456
494
  writeFileSync(join(projectDir, "README.md"), readme);
495
+ const envExample = `# Kyro CMS Configuration
496
+
497
+ ${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"}
498
+
499
+ ${answers.auth ? `# Authentication (required for auth)
500
+ JWT_SECRET=change-this-to-a-random-32-character-string
501
+ JWT_EXPIRES_IN=24h
502
+
503
+ # Bootstrap admin user (creates on first run)
504
+ KYRO_ADMIN_EMAIL=admin@example.com
505
+ KYRO_ADMIN_PASSWORD=SecurePass123!
506
+ KYRO_ADMIN_ROLE=super_admin
507
+
508
+ # Optional: Redis for sessions (recommended for production)
509
+ # REDIS_URL=redis://localhost:6379
510
+ # REDIS_TLS=false
511
+
512
+ # Optional: SMTP for emails
513
+ # SMTP_HOST=smtp.example.com
514
+ # SMTP_PORT=587
515
+ # SMTP_SECURE=false
516
+ # SMTP_USER=your-email@example.com
517
+ # SMTP_PASS=your-password
518
+ # SMTP_FROM=noreply@example.com` : ""}
519
+ `;
520
+ writeFileSync(join(projectDir, ".env.example"), envExample);
457
521
  const spec = `# ${answers.projectName}
458
522
 
459
523
  ## Overview
@@ -520,11 +584,11 @@ const title = "${answers.projectName}";
520
584
  `;
521
585
  writeFileSync(join(pagesDir, "index.astro"), indexPage);
522
586
  if (answers.admin) {
523
- const adminDir = join(srcDir, "admin");
587
+ const adminDir = join(pagesDir, "admin");
524
588
  mkdirSync(adminDir, { recursive: true });
525
589
  const adminIndex = `---
526
590
  import { Admin } from '@kyro-cms/admin';
527
- import config from '../kyro.config';
591
+ import config from '../../../kyro.config';
528
592
  ---
529
593
  <!DOCTYPE html>
530
594
  <html lang="en">
@@ -540,6 +604,448 @@ import config from '../kyro.config';
540
604
  `;
541
605
  writeFileSync(join(adminDir, "index.astro"), adminIndex);
542
606
  }
607
+ if (answers.auth) {
608
+ const authApiDir = join(pagesDir, "api", "auth");
609
+ mkdirSync(authApiDir, { recursive: true });
610
+ writeFileSync(
611
+ join(authApiDir, "login.ts"),
612
+ generateLoginEndpoint(answers.database)
613
+ );
614
+ writeFileSync(
615
+ join(authApiDir, "register.ts"),
616
+ generateRegisterEndpoint(answers.database)
617
+ );
618
+ writeFileSync(
619
+ join(authApiDir, "logout.ts"),
620
+ generateLogoutEndpoint(answers.database)
621
+ );
622
+ writeFileSync(join(authApiDir, "me.ts"), generateMeEndpoint());
623
+ writeFileSync(
624
+ join(authApiDir, "users.ts"),
625
+ generateUsersEndpoint(answers.database)
626
+ );
627
+ writeFileSync(join(srcDir, "middleware.ts"), generateMiddleware());
628
+ }
629
+ }
630
+ function generateLoginEndpoint(database) {
631
+ const adapterImport = database === "sqlite" ? `import { SQLiteAuthAdapter } from "@kyro-cms/core";` : `import { RedisAuthAdapter } from "@kyro-cms/core";`;
632
+ const adapterInit = database === "sqlite" ? ` return new SQLiteAuthAdapter({ path: "./data.db" });` : ` return new RedisAuthAdapter({
633
+ url: process.env.REDIS_URL || "redis://localhost:6379",
634
+ tls: process.env.REDIS_TLS === "true",
635
+ });`;
636
+ return `import type { APIRoute } from "astro";
637
+ ${adapterImport}
638
+ import jwt from "jsonwebtoken";
639
+
640
+ const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
641
+ const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "24h";
642
+
643
+ async function getAuthApi() {
644
+ ${adapterInit}
645
+ }
646
+
647
+ export const POST: APIRoute = async ({ request }) => {
648
+ try {
649
+ const body = (await request.json()) as {
650
+ email?: string;
651
+ password?: string;
652
+ };
653
+ const { email, password } = body;
654
+
655
+ if (!email || !password) {
656
+ return new Response(
657
+ JSON.stringify({ error: "Email and password required" }),
658
+ { status: 400, headers: { "Content-Type": "application/json" } },
659
+ );
660
+ }
661
+
662
+ const adapter = await getAuthApi();
663
+ await adapter.connect();
664
+
665
+ const user = await adapter.findUserByEmail(email);
666
+ if (!user || !user.passwordHash) {
667
+ await adapter.disconnect();
668
+ return new Response(JSON.stringify({ error: "Invalid credentials" }), {
669
+ status: 401,
670
+ headers: { "Content-Type": "application/json" },
671
+ });
672
+ }
673
+
674
+ const valid = await adapter.verifyPassword(password, user.passwordHash);
675
+ if (!valid) {
676
+ await adapter.disconnect();
677
+ return new Response(JSON.stringify({ error: "Invalid credentials" }), {
678
+ status: 401,
679
+ headers: { "Content-Type": "application/json" },
680
+ });
681
+ }
682
+
683
+ const session = await adapter.createSession(user.id, {
684
+ ipAddress: request.headers.get("x-forwarded-for") || "unknown",
685
+ userAgent: request.headers.get("user-agent") || "",
686
+ });
687
+
688
+ const token = jwt.sign(
689
+ {
690
+ sub: user.id,
691
+ email: user.email,
692
+ role: user.role,
693
+ tenantId: user.tenantId,
694
+ },
695
+ JWT_SECRET,
696
+ { expiresIn: JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] },
697
+ );
698
+
699
+ await adapter.disconnect();
700
+
701
+ const { passwordHash, ...safeUser } = user;
702
+
703
+ return new Response(
704
+ JSON.stringify({
705
+ success: true,
706
+ user: safeUser,
707
+ token,
708
+ refreshToken: session.refreshToken,
709
+ }),
710
+ {
711
+ status: 200,
712
+ headers: { "Content-Type": "application/json" },
713
+ },
714
+ );
715
+ } catch (error) {
716
+ console.error("Login error:", error);
717
+ return new Response(JSON.stringify({ error: "Login failed" }), {
718
+ status: 500,
719
+ headers: { "Content-Type": "application/json" },
720
+ });
721
+ }
722
+ };
723
+ `;
724
+ }
725
+ function generateRegisterEndpoint(database) {
726
+ const adapterImport = database === "sqlite" ? `import { SQLiteAuthAdapter } from "@kyro-cms/core";` : `import { RedisAuthAdapter } from "@kyro-cms/core";`;
727
+ const adapterInit = database === "sqlite" ? ` return new SQLiteAuthAdapter({ path: "./data.db" });` : ` return new RedisAuthAdapter({
728
+ url: process.env.REDIS_URL || "redis://localhost:6379",
729
+ tls: process.env.REDIS_TLS === "true",
730
+ });`;
731
+ const isFirstUserCheck = database === "sqlite" ? ` const isFirstUser = !(await adapter.hasAnyUsers());` : ` const isFirstUser = await checkIsFirstUser(adapter);`;
732
+ const isFirstUserFn = database === "sqlite" ? "" : `
733
+
734
+ async function checkIsFirstUser(adapter: RedisAuthAdapter): Promise<boolean> {
735
+ try {
736
+ const redis = (adapter as any).redis;
737
+ if (!redis) return true;
738
+ const pattern = "kyro:auth:users:email:*";
739
+ const result = await redis.scan("0", "MATCH", pattern, "COUNT", "1");
740
+ const keys = result[1];
741
+ return keys.length === 0;
742
+ } catch {
743
+ return true;
744
+ }
745
+ }`;
746
+ return `import type { APIRoute } from "astro";
747
+ ${adapterImport}
748
+ import jwt from "jsonwebtoken";
749
+
750
+ const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
751
+ const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "24h";
752
+ const ALLOW_REGISTRATION = process.env.KYRO_ALLOW_REGISTRATION !== "false";
753
+
754
+ async function getAuthApi() {
755
+ ${adapterInit}
756
+ }
757
+
758
+ export const POST: APIRoute = async ({ request }) => {
759
+ try {
760
+ const body = (await request.json()) as {
761
+ email?: string;
762
+ password?: string;
763
+ confirmPassword?: string;
764
+ };
765
+ const { email, password, confirmPassword } = body;
766
+
767
+ if (!email || !password) {
768
+ return new Response(
769
+ JSON.stringify({ error: "Email and password required" }),
770
+ { status: 400, headers: { "Content-Type": "application/json" } },
771
+ );
772
+ }
773
+
774
+ if (password !== confirmPassword) {
775
+ return new Response(
776
+ JSON.stringify({ error: "Passwords do not match" }),
777
+ { status: 400, headers: { "Content-Type": "application/json" } },
778
+ );
779
+ }
780
+
781
+ if (password.length < 8) {
782
+ return new Response(
783
+ JSON.stringify({ error: "Password must be at least 8 characters" }),
784
+ { status: 400, headers: { "Content-Type": "application/json" } },
785
+ );
786
+ }
787
+
788
+ const adapter = await getAuthApi();
789
+ try {
790
+ await adapter.connect();
791
+ } catch {
792
+ return new Response(
793
+ JSON.stringify({ error: "Unable to connect to auth storage." }),
794
+ { status: 500, headers: { "Content-Type": "application/json" } },
795
+ );
796
+ }
797
+
798
+ const existingUser = await adapter.findUserByEmail(email);
799
+ if (existingUser) {
800
+ await adapter.disconnect();
801
+ return new Response(
802
+ JSON.stringify({ error: "Email already registered" }),
803
+ { status: 409, headers: { "Content-Type": "application/json" } },
804
+ );
805
+ }
806
+
807
+ ${isFirstUserCheck}
808
+
809
+ if (!isFirstUser && !ALLOW_REGISTRATION) {
810
+ await adapter.disconnect();
811
+ return new Response(
812
+ JSON.stringify({ error: "Registration is disabled" }),
813
+ { status: 403, headers: { "Content-Type": "application/json" } },
814
+ );
815
+ }
816
+
817
+ const passwordHash = await adapter.hashPassword(password);
818
+ const user = await adapter.createUser({
819
+ email,
820
+ passwordHash,
821
+ role: isFirstUser ? "super_admin" : "editor",
822
+ });
823
+
824
+ if (isFirstUser) {
825
+ await adapter.updateUser(user.id, { emailVerified: true });
826
+ }
827
+
828
+ const session = await adapter.createSession(user.id, {
829
+ ipAddress: request.headers.get("x-forwarded-for") || "unknown",
830
+ userAgent: request.headers.get("user-agent") || "",
831
+ });
832
+
833
+ const token = jwt.sign(
834
+ {
835
+ sub: user.id,
836
+ email: user.email,
837
+ role: user.role,
838
+ tenantId: user.tenantId,
839
+ },
840
+ JWT_SECRET,
841
+ { expiresIn: JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] },
842
+ );
843
+
844
+ await adapter.disconnect();
845
+
846
+ const { passwordHash: _, ...safeUser } = user;
847
+
848
+ return new Response(
849
+ JSON.stringify({
850
+ success: true,
851
+ isFirstUser,
852
+ user: safeUser,
853
+ token,
854
+ refreshToken: session.refreshToken,
855
+ }),
856
+ {
857
+ status: 201,
858
+ headers: { "Content-Type": "application/json" },
859
+ },
860
+ );
861
+ } catch (error) {
862
+ console.error("Registration error:", error);
863
+ return new Response(JSON.stringify({ error: "Registration failed" }), {
864
+ status: 500,
865
+ headers: { "Content-Type": "application/json" },
866
+ });
867
+ }
868
+ };${isFirstUserFn}
869
+ `;
870
+ }
871
+ function generateLogoutEndpoint(database) {
872
+ const adapterImport = database === "sqlite" ? `import { SQLiteAuthAdapter } from "@kyro-cms/core";` : `import { RedisAuthAdapter } from "@kyro-cms/core";`;
873
+ const adapterInit = database === "sqlite" ? ` return new SQLiteAuthAdapter({ path: "./data.db" });` : ` return new RedisAuthAdapter({
874
+ url: process.env.REDIS_URL || "redis://localhost:6379",
875
+ tls: process.env.REDIS_TLS === "true",
876
+ });`;
877
+ return `import type { APIRoute } from "astro";
878
+ ${adapterImport}
879
+
880
+ async function getAuthApi() {
881
+ ${adapterInit}
882
+ }
883
+
884
+ export const POST: APIRoute = async ({ request }) => {
885
+ try {
886
+ const authHeader = request.headers.get("authorization");
887
+ const token = authHeader?.startsWith("Bearer ")
888
+ ? authHeader.slice(7)
889
+ : null;
890
+
891
+ if (token) {
892
+ const adapter = await getAuthApi();
893
+ await adapter.connect();
894
+ await adapter.deleteSession(token);
895
+ await adapter.disconnect();
896
+ }
897
+
898
+ return new Response(
899
+ JSON.stringify({ success: true }),
900
+ { status: 200, headers: { "Content-Type": "application/json" } },
901
+ );
902
+ } catch {
903
+ return new Response(
904
+ JSON.stringify({ success: true }),
905
+ { status: 200, headers: { "Content-Type": "application/json" } },
906
+ );
907
+ }
908
+ };
909
+ `;
910
+ }
911
+ function generateMeEndpoint() {
912
+ return `import type { APIRoute } from "astro";
913
+ import jwt from "jsonwebtoken";
914
+
915
+ const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
916
+
917
+ export const GET: APIRoute = async ({ request }) => {
918
+ const authHeader = request.headers.get("authorization");
919
+ const token = authHeader?.startsWith("Bearer ")
920
+ ? authHeader.slice(7)
921
+ : null;
922
+
923
+ if (!token) {
924
+ return new Response(
925
+ JSON.stringify({ error: "Not authenticated" }),
926
+ { status: 401, headers: { "Content-Type": "application/json" } },
927
+ );
928
+ }
929
+
930
+ try {
931
+ const payload = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
932
+ return new Response(
933
+ JSON.stringify({
934
+ id: payload.sub,
935
+ email: payload.email,
936
+ role: payload.role,
937
+ }),
938
+ { status: 200, headers: { "Content-Type": "application/json" } },
939
+ );
940
+ } catch {
941
+ return new Response(
942
+ JSON.stringify({ error: "Invalid token" }),
943
+ { status: 401, headers: { "Content-Type": "application/json" } },
944
+ );
945
+ }
946
+ };
947
+ `;
948
+ }
949
+ function generateUsersEndpoint(database) {
950
+ const adapterImport = database === "sqlite" ? `import { SQLiteAuthAdapter } from "@kyro-cms/core";` : `import { RedisAuthAdapter } from "@kyro-cms/core";`;
951
+ const adapterInit = database === "sqlite" ? ` return new SQLiteAuthAdapter({ path: "./data.db" });` : ` return new RedisAuthAdapter({
952
+ url: process.env.REDIS_URL || "redis://localhost:6379",
953
+ tls: process.env.REDIS_TLS === "true",
954
+ });`;
955
+ const hasUsersCheck = database === "sqlite" ? ` const hasUsers = await adapter.hasAnyUsers();` : ` const redis = (adapter as any).redis;
956
+ if (!redis) {
957
+ await adapter.disconnect();
958
+ return new Response(JSON.stringify({ hasUsers: false }), {
959
+ status: 200,
960
+ headers: { "Content-Type": "application/json" },
961
+ });
962
+ }
963
+
964
+ const pattern = "kyro:auth:users:email:*";
965
+ const result = await redis.scan("0", "MATCH", pattern, "COUNT", "1");
966
+ const keys = result[1];
967
+ const hasUsers = keys.length > 0;`;
968
+ return `import type { APIRoute } from "astro";
969
+ ${adapterImport}
970
+
971
+ async function getAuthApi() {
972
+ ${adapterInit}
973
+ }
974
+
975
+ export const GET: APIRoute = async () => {
976
+ try {
977
+ const adapter = await getAuthApi();
978
+ await adapter.connect();
979
+
980
+ ${hasUsersCheck}
981
+
982
+ await adapter.disconnect();
983
+
984
+ return new Response(JSON.stringify({ hasUsers }), {
985
+ status: 200,
986
+ headers: { "Content-Type": "application/json" },
987
+ });
988
+ } catch {
989
+ return new Response(JSON.stringify({ hasUsers: false }), {
990
+ status: 200,
991
+ headers: { "Content-Type": "application/json" },
992
+ });
993
+ }
994
+ };
995
+ `;
996
+ }
997
+ function generateMiddleware() {
998
+ return `import type { MiddlewareHandler } from "astro";
999
+ import jwt from "jsonwebtoken";
1000
+
1001
+ const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
1002
+
1003
+ const PUBLIC_PATHS = [
1004
+ "/api/auth/login",
1005
+ "/api/auth/logout",
1006
+ "/api/auth/register",
1007
+ "/api/auth/me",
1008
+ "/api/auth/users",
1009
+ "/api/health",
1010
+ "/favicon.svg",
1011
+ ];
1012
+
1013
+ const PUBLIC_PREFIXES = ["/api/auth/", "/admin"];
1014
+
1015
+ export const onRequest: MiddlewareHandler = async ({ request, url }, next) => {
1016
+ const pathname = new URL(url).pathname;
1017
+
1018
+ if (PUBLIC_PATHS.includes(pathname) || PUBLIC_PATHS.includes(pathname.replace(/\\/$/, ""))) {
1019
+ return next();
1020
+ }
1021
+
1022
+ for (const prefix of PUBLIC_PREFIXES) {
1023
+ if (pathname.startsWith(prefix)) {
1024
+ return next();
1025
+ }
1026
+ }
1027
+
1028
+ const authHeader = request.headers.get("authorization");
1029
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
1030
+
1031
+ if (!token) {
1032
+ return new Response(
1033
+ JSON.stringify({ error: "Authentication required" }),
1034
+ { status: 401, headers: { "Content-Type": "application/json" } },
1035
+ );
1036
+ }
1037
+
1038
+ try {
1039
+ jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
1040
+ return next();
1041
+ } catch {
1042
+ return new Response(
1043
+ JSON.stringify({ error: "Invalid or expired token" }),
1044
+ { status: 401, headers: { "Content-Type": "application/json" } },
1045
+ );
1046
+ }
1047
+ };
1048
+ `;
543
1049
  }
544
1050
 
545
1051
  // src/index.ts
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "create-kyro",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Interactive scaffolding for Kyro CMS projects",
5
5
  "type": "module",
6
6
  "bin": {
7
- "create-kyro": "./bin.js"
7
+ "create-kyro": "bin.js"
8
8
  },
9
9
  "main": "./dist/index.js",
10
10
  "types": "./dist/index.d.ts",
@@ -1,14 +0,0 @@
1
- # my-kyro-app
2
-
3
- A Kyro CMS project.
4
-
5
- ## Getting Started
6
-
7
- ```bash
8
- npm install
9
- npm run dev
10
- ```
11
-
12
- ## Documentation
13
-
14
- Visit [https://kyro.cms](https://kyro.cms) for full documentation.
@@ -1,21 +0,0 @@
1
- # my-kyro-app
2
-
3
- ## Overview
4
-
5
- This project uses Kyro CMS - an Astro-native headless CMS.
6
-
7
- ## Configuration
8
-
9
- - **Database**: SQLite (local-first)
10
- - **APIs**: rest, graphql
11
- - **Styling**: tailwind
12
- - **Auth**: Disabled
13
- - **Versioning**: Enabled
14
- - **Admin**: Included
15
- - **Template**: blog
16
-
17
- ## Collections
18
-
19
- - Posts
20
- - Categories
21
- - Media
@@ -1,23 +0,0 @@
1
- import { defineConfig } from 'astro/config';
2
- import react from '@astrojs/react';
3
- import tailwindcss from '@tailwindcss/vite';
4
-
5
- export default defineConfig({
6
- output: 'server',
7
- adapter: node({
8
- mode: 'standalone'
9
- }),
10
-
11
- integrations: [
12
- react(),
13
- ],
14
- vite: {
15
- plugins: [
16
- tailwindcss(),
17
- ],
18
- },
19
- server: {
20
- port: 4321,
21
- host: true,
22
- },
23
- });
@@ -1,19 +0,0 @@
1
- import { defineConfig } from '@kyro-cms/core';
2
- import { localAdapter } from '@kyro-cms/core';
3
- import { blogCollections } from '@kyro-cms/core';
4
-
5
- export default defineConfig({
6
- name: 'my-kyro-app',
7
- prefix: '/api',
8
- adapter: localAdapter({ path: './data.db' }),
9
-
10
- collections: {
11
- ...blogCollections,
12
- },
13
- versioning: true,
14
-
15
- api: {
16
- rest: true,
17
- graphql: true,
18
- },
19
- });
@@ -1,30 +0,0 @@
1
- {
2
- "name": "my-kyro-app",
3
- "version": "0.1.0",
4
- "type": "module",
5
- "private": true,
6
- "scripts": {
7
- "dev": "astro dev",
8
- "build": "astro build",
9
- "preview": "astro preview",
10
- "db:generate": "kyro generate",
11
- "db:push": "kyro push",
12
- "db:studio": "kyro studio"
13
- },
14
- "dependencies": {
15
- "@kyro-cms/core": "latest",
16
- "astro": "^5.4.0",
17
- "@astrojs/react": "^4.2.0",
18
- "react": "^19.0.0",
19
- "react-dom": "^19.0.0",
20
- "tailwindcss": "^4.0.0",
21
- "@tailwindcss/vite": "^4.0.0",
22
- "@kyro-cms/admin": "latest",
23
- "lucide-react": "^0.475.0"
24
- },
25
- "devDependencies": {
26
- "typescript": "^5.7.3",
27
- "@types/react": "^19.0.0",
28
- "@types/react-dom": "^19.0.0"
29
- }
30
- }