create-kyro 0.1.1 → 0.1.3

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
@@ -27,168 +27,172 @@ function validateProjectName(name) {
27
27
 
28
28
  // src/prompts.ts
29
29
  async function promptUser() {
30
- const response = await prompts([
31
- {
32
- type: "text",
33
- name: "projectName",
34
- message: "Project name:",
35
- initial: "my-kyro-app",
36
- validate: validateProjectName
37
- },
38
- {
39
- type: "select",
40
- name: "database",
41
- message: "Database:",
42
- hint: " ",
43
- choices: [
44
- {
45
- title: "SQLite (local-first, zero config)",
46
- description: "Best for development and small projects. No setup required.",
47
- value: "sqlite"
48
- },
49
- {
50
- title: "PostgreSQL",
51
- description: "Recommended for production. Robust and scalable.",
52
- value: "postgres"
53
- },
54
- {
55
- title: "MySQL",
56
- description: "Popular choice for web applications.",
57
- value: "mysql"
58
- },
59
- {
60
- title: "MongoDB",
61
- description: "Best for flexible, document-based schemas.",
62
- value: "mongodb"
63
- }
64
- ]
65
- },
66
- {
67
- type: "multiselect",
68
- name: "apis",
69
- message: "API protocols (select multiple):",
70
- hint: "Space to select, Enter to confirm",
71
- instructions: false,
72
- choices: [
73
- {
74
- title: "REST",
75
- description: "Simple HTTP API, great for any client",
76
- value: "rest",
77
- selected: true
78
- },
79
- {
80
- title: "GraphQL",
81
- description: "Flexible query language, great for complex data",
82
- value: "graphql",
83
- selected: true
84
- },
85
- {
86
- title: "tRPC",
87
- description: "End-to-end typesafe APIs, great for TypeScript",
88
- value: "trpc"
89
- },
90
- {
91
- title: "WebSocket",
92
- description: "Real-time bidirectional communication",
93
- value: "websocket"
94
- }
95
- ]
96
- },
97
- {
98
- type: "select",
99
- name: "styling",
100
- message: "Styling:",
101
- hint: " ",
102
- choices: [
103
- {
104
- title: "Tailwind CSS",
105
- description: "Utility-first CSS framework, excellent DX",
106
- value: "tailwind"
107
- },
108
- {
109
- title: "CSS Modules",
110
- description: "Scoped CSS, no extra dependencies",
111
- value: "cssmodules"
112
- },
113
- {
114
- title: "Styled Components",
115
- description: "CSS-in-JS with tagged template literals",
116
- value: "styled"
117
- },
118
- {
119
- title: "None",
120
- description: "Bring your own styling solution",
121
- value: "none"
122
- }
123
- ]
124
- },
125
- {
126
- type: "toggle",
127
- name: "auth",
128
- message: "Add authentication (JWT)?",
129
- active: "Yes",
130
- inactive: "No"
131
- },
132
- {
133
- type: "toggle",
134
- name: "versioning",
135
- message: "Add versioning/drafts?",
136
- active: "Yes",
137
- inactive: "No"
138
- },
139
- {
140
- type: "toggle",
141
- name: "admin",
142
- message: "Include admin dashboard?",
143
- initial: true,
144
- active: "Yes",
145
- inactive: "No"
146
- },
30
+ const response = await prompts(
31
+ [
32
+ {
33
+ type: "text",
34
+ name: "projectName",
35
+ message: "Project name:",
36
+ initial: "my-kyro-app",
37
+ validate: validateProjectName
38
+ },
39
+ {
40
+ type: "select",
41
+ name: "database",
42
+ message: "Database:",
43
+ hint: " ",
44
+ choices: [
45
+ {
46
+ title: "SQLite (local-first, zero config)",
47
+ description: "Best for development and small projects. No setup required.",
48
+ value: "sqlite"
49
+ },
50
+ {
51
+ title: "PostgreSQL",
52
+ description: "Recommended for production. Robust and scalable.",
53
+ value: "postgres"
54
+ },
55
+ {
56
+ title: "MySQL",
57
+ description: "Popular choice for web applications.",
58
+ value: "mysql"
59
+ },
60
+ {
61
+ title: "MongoDB",
62
+ description: "Best for flexible, document-based schemas.",
63
+ value: "mongodb"
64
+ }
65
+ ]
66
+ },
67
+ {
68
+ type: "multiselect",
69
+ name: "apis",
70
+ message: "API protocols (select multiple):",
71
+ hint: "Space to select, Enter to confirm",
72
+ instructions: false,
73
+ choices: [
74
+ {
75
+ title: "REST",
76
+ description: "Simple HTTP API, great for any client",
77
+ value: "rest",
78
+ selected: true
79
+ },
80
+ {
81
+ title: "GraphQL",
82
+ description: "Flexible query language, great for complex data",
83
+ value: "graphql",
84
+ selected: true
85
+ },
86
+ {
87
+ title: "tRPC",
88
+ description: "End-to-end typesafe APIs, great for TypeScript",
89
+ value: "trpc"
90
+ },
91
+ {
92
+ title: "WebSocket",
93
+ description: "Real-time bidirectional communication",
94
+ value: "websocket"
95
+ }
96
+ ]
97
+ },
98
+ {
99
+ type: "select",
100
+ name: "styling",
101
+ message: "Styling:",
102
+ hint: " ",
103
+ choices: [
104
+ {
105
+ title: "Tailwind CSS",
106
+ description: "Utility-first CSS framework, excellent DX",
107
+ value: "tailwind"
108
+ },
109
+ {
110
+ title: "CSS Modules",
111
+ description: "Scoped CSS, no extra dependencies",
112
+ value: "cssmodules"
113
+ },
114
+ {
115
+ title: "Styled Components",
116
+ description: "CSS-in-JS with tagged template literals",
117
+ value: "styled"
118
+ },
119
+ {
120
+ title: "None",
121
+ description: "Bring your own styling solution",
122
+ value: "none"
123
+ }
124
+ ]
125
+ },
126
+ {
127
+ type: "toggle",
128
+ name: "auth",
129
+ message: "Add authentication (JWT)?",
130
+ initial: true,
131
+ active: "Yes",
132
+ inactive: "No"
133
+ },
134
+ {
135
+ type: "toggle",
136
+ name: "versioning",
137
+ message: "Add versioning/drafts?",
138
+ initial: true,
139
+ active: "Yes",
140
+ inactive: "No"
141
+ },
142
+ {
143
+ type: "toggle",
144
+ name: "admin",
145
+ message: "Include admin dashboard?",
146
+ initial: true,
147
+ active: "Yes",
148
+ inactive: "No"
149
+ },
150
+ {
151
+ type: "select",
152
+ name: "template",
153
+ message: "Starting template:",
154
+ hint: " ",
155
+ initial: 1,
156
+ choices: [
157
+ {
158
+ title: "Minimal",
159
+ description: "Basic configuration with one example collection + core settings",
160
+ value: "minimal"
161
+ },
162
+ {
163
+ title: "Blog",
164
+ description: "Posts, categories, media library + core settings",
165
+ value: "blog"
166
+ },
167
+ {
168
+ title: "E-commerce",
169
+ description: "Products, orders, customers, coupons + core + store/payment settings",
170
+ value: "ecommerce"
171
+ },
172
+ {
173
+ title: "Kitchen Sink",
174
+ description: "Everything: pages, navigation, blog, e-commerce + all settings",
175
+ value: "kitchen-sink"
176
+ }
177
+ ]
178
+ }
179
+ ],
147
180
  {
148
- type: "select",
149
- name: "template",
150
- message: "Starting template:",
151
- hint: " ",
152
- choices: [
153
- {
154
- title: "Minimal",
155
- description: "Basic configuration with one example collection",
156
- value: "minimal"
157
- },
158
- {
159
- title: "Blog",
160
- description: "Posts, categories, tags, and media library",
161
- value: "blog"
162
- },
163
- {
164
- title: "E-commerce",
165
- description: "Products, orders, customers, inventory, and coupons",
166
- value: "ecommerce"
167
- }
168
- ]
181
+ onCancel: () => {
182
+ console.log("\nCancelled.");
183
+ process.exit(1);
184
+ }
169
185
  }
170
- ], {
171
- onCancel: () => {
172
- console.log("\nCancelled.");
173
- process.exit(1);
174
- }
175
- });
186
+ );
176
187
  return response;
177
188
  }
178
189
 
179
190
  // src/utils/logger.ts
180
- import kolorist from "kolorist";
181
- var { cyan, green, yellow, red, bold, dim } = kolorist;
191
+ import { cyan, green, yellow, red, bold, dim } from "kolorist";
182
192
  var logger = {
183
193
  intro: (name, version) => {
184
194
  console.log(`
185
- ${bold(cyan(`\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557`))}
186
- ${bold(cyan(`\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D`))}
187
- ${bold(cyan(`\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557`))}
188
- ${bold(cyan(`\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551`))}
189
- ${bold(cyan(`\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551`))}
190
- ${bold(cyan(`\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D`))}
191
- ${dim(`v${version}`)}
195
+ ${bold(cyan(`Kyro CMS`))} ${dim(`v${version}`)}
192
196
  ${dim("Astro-native headless CMS")}
193
197
  `);
194
198
  },
@@ -235,10 +239,10 @@ ${cyan("?")} ${bold(msg)}`);
235
239
  function generatePackageJson(answers, projectDir) {
236
240
  const deps = {
237
241
  "@kyro-cms/core": "latest",
238
- "astro": "^5.4.0"
242
+ astro: "^5.4.0"
239
243
  };
240
244
  const devDeps = {
241
- "typescript": "^5.7.3"
245
+ typescript: "^5.7.3"
242
246
  };
243
247
  if (answers.styling === "tailwind") {
244
248
  deps["@astrojs/react"] = "^4.2.0";
@@ -259,13 +263,17 @@ function generatePackageJson(answers, projectDir) {
259
263
  }
260
264
  if (answers.admin) {
261
265
  deps["@kyro-cms/admin"] = "latest";
266
+ deps["@astrojs/node"] = "^9.5.5";
262
267
  deps["lucide-react"] = "^0.475.0";
263
268
  }
264
269
  const scripts = {
265
- "dev": "astro dev",
266
- "build": "astro build",
267
- "preview": "astro preview"
270
+ dev: "astro dev",
271
+ build: "astro build",
272
+ preview: "astro preview"
268
273
  };
274
+ if (answers.auth) {
275
+ scripts["db:bootstrap"] = "kyro auth bootstrap";
276
+ }
269
277
  if (answers.database === "sqlite") {
270
278
  scripts["db:generate"] = "kyro generate";
271
279
  scripts["db:push"] = "kyro push";
@@ -290,21 +298,21 @@ function generateKyroConfig(answers) {
290
298
  const imports = ["import { defineConfig } from '@kyro-cms/core';"];
291
299
  const adapterLines = [];
292
300
  if (answers.database === "sqlite") {
293
- imports.push("import { localAdapter } from '@kyro-cms/core';");
294
- adapterLines.push(` adapter: localAdapter({ path: './data.db' }),`);
301
+ imports.push("import { createLocalAdapter } from '@kyro-cms/core';");
302
+ adapterLines.push(` adapter: createLocalAdapter({ path: './data.db' }),`);
295
303
  } else if (answers.database === "postgres") {
296
- imports.push("import { drizzleAdapter } from '@kyro-cms/core';");
297
- adapterLines.push(` adapter: drizzleAdapter({`);
304
+ imports.push("import { createDrizzleAdapter } from '@kyro-cms/core';");
305
+ adapterLines.push(` adapter: createDrizzleAdapter({`);
298
306
  adapterLines.push(` connectionString: process.env.DATABASE_URL,`);
299
307
  adapterLines.push(` }),`);
300
308
  } else if (answers.database === "mysql") {
301
- imports.push("import { drizzleAdapter } from '@kyro-cms/core';");
302
- adapterLines.push(` adapter: drizzleAdapter({`);
309
+ imports.push("import { createDrizzleAdapter } from '@kyro-cms/core';");
310
+ adapterLines.push(` adapter: createDrizzleAdapter({`);
303
311
  adapterLines.push(` connectionString: process.env.DATABASE_URL,`);
304
312
  adapterLines.push(` }),`);
305
313
  } else if (answers.database === "mongodb") {
306
- imports.push("import { mongoAdapter } from '@kyro-cms/core';");
307
- adapterLines.push(` adapter: mongoAdapter({`);
314
+ imports.push("import { createMongoDBAdapter } from '@kyro-cms/core';");
315
+ adapterLines.push(` adapter: createMongoDBAdapter({`);
308
316
  adapterLines.push(` connectionString: process.env.MONGODB_URI,`);
309
317
  adapterLines.push(` }),`);
310
318
  }
@@ -328,28 +336,46 @@ function generateKyroConfig(answers) {
328
336
  if (answers.versioning) {
329
337
  features.push(" versioning: true,");
330
338
  }
331
- let templateImports = "";
332
- let collectionsLine = "";
339
+ let templateCollections = "";
340
+ let templateGlobals = "";
341
+ switch (answers.template) {
342
+ case "minimal":
343
+ templateCollections = "import { minimalCollections } from '@kyro-cms/core';";
344
+ break;
345
+ case "blog":
346
+ templateCollections = "import { blogCollections } from '@kyro-cms/core';";
347
+ break;
348
+ case "ecommerce":
349
+ templateCollections = "import { ecommerceCollections } from '@kyro-cms/core';";
350
+ break;
351
+ case "kitchen-sink":
352
+ templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections } from '@kyro-cms/core';`;
353
+ break;
354
+ }
355
+ if (templateCollections) {
356
+ imports.push(templateCollections);
357
+ }
358
+ let collectionsConfig = "";
333
359
  if (answers.template === "minimal") {
334
- templateImports = `import { minimalCollections } from '@kyro-cms/core';`;
335
- collectionsLine = " ...minimalCollections,";
360
+ collectionsConfig = ` collections: Object.values(minimalCollections),`;
336
361
  } else if (answers.template === "blog") {
337
- templateImports = `import { blogCollections } from '@kyro-cms/core';`;
338
- collectionsLine = " ...blogCollections,";
362
+ collectionsConfig = ` collections: Object.values(blogCollections),`;
339
363
  } else if (answers.template === "ecommerce") {
340
- templateImports = `import { ecommerceCollections } from '@kyro-cms/core';`;
341
- collectionsLine = " ...ecommerceCollections,";
364
+ collectionsConfig = ` collections: Object.values(ecommerceCollections),`;
365
+ } else if (answers.template === "kitchen-sink") {
366
+ collectionsConfig = ` collections: [
367
+ ...Object.values(minimalCollections),
368
+ ...Object.values(blogCollections),
369
+ ...Object.values(ecommerceCollections),
370
+ ...Object.values(kitchenSinkCollections),
371
+ ],`;
342
372
  }
343
373
  const config = `${imports.join("\n")}
344
- ${templateImports}
345
374
 
346
375
  export default defineConfig({
347
376
  name: '${answers.projectName}',
348
377
  prefix: '/api',${adapterLines.length > 0 ? "\n" + adapterLines.join("\n") : ""}
349
-
350
- collections: {
351
- ${collectionsLine}
352
- },${features.length > 0 ? "\n" + features.join("\n") : ""}
378
+ ${collectionsConfig ? collectionsConfig : ""}${features.length > 0 ? "\n" + features.join("\n") : ""}
353
379
 
354
380
  api: {
355
381
  ${apiConfig.join("\n")}
@@ -371,6 +397,7 @@ function generateAstroConfig(answers) {
371
397
  mode: 'standalone'
372
398
  }),` : "";
373
399
  const config = `import { defineConfig } from 'astro/config';
400
+ import node from '@astrojs/node';
374
401
  ${answers.styling === "tailwind" ? "import react from '@astrojs/react';\nimport tailwindcss from '@tailwindcss/vite';" : ""}
375
402
 
376
403
  export default defineConfig({
@@ -428,18 +455,67 @@ data/
428
455
 
429
456
  A Kyro CMS project.
430
457
 
431
- ## Getting Started
458
+ ## Quick Start
432
459
 
433
460
  \`\`\`bash
434
461
  npm install
435
462
  npm run dev
436
463
  \`\`\`
437
464
 
465
+ ## Admin Dashboard
466
+
467
+ Visit [http://localhost:4321/admin](http://localhost:4321/admin) to access the admin.
468
+
469
+ ${answers.auth ? `## Creating Your Admin User
470
+
471
+ Before logging into the admin, you need to create an admin user. Run:
472
+
473
+ \`\`\`bash
474
+ npm run db:bootstrap
475
+ \`\`\`
476
+
477
+ Or set environment variables to auto-bootstrap on startup:
478
+
479
+ \`\`\`bash
480
+ # .env
481
+ KYRO_ADMIN_EMAIL=admin@example.com
482
+ KYRO_ADMIN_PASSWORD=SecurePass123!
483
+ \`\`\`
484
+
485
+ Then restart the dev server.
486
+ ` : ""}
487
+
438
488
  ## Documentation
439
489
 
440
490
  Visit [https://kyro.cms](https://kyro.cms) for full documentation.
441
491
  `;
442
492
  writeFileSync(join(projectDir, "README.md"), readme);
493
+ const envExample = `# Kyro CMS Configuration
494
+
495
+ ${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"}
496
+
497
+ ${answers.auth ? `# Authentication (required for auth)
498
+ JWT_SECRET=change-this-to-a-random-32-character-string
499
+ JWT_EXPIRES_IN=24h
500
+
501
+ # Bootstrap admin user (creates on first run)
502
+ KYRO_ADMIN_EMAIL=admin@example.com
503
+ KYRO_ADMIN_PASSWORD=SecurePass123!
504
+ KYRO_ADMIN_ROLE=super_admin
505
+
506
+ # Optional: Redis for sessions (recommended for production)
507
+ # REDIS_URL=redis://localhost:6379
508
+ # REDIS_TLS=false
509
+
510
+ # Optional: SMTP for emails
511
+ # SMTP_HOST=smtp.example.com
512
+ # SMTP_PORT=587
513
+ # SMTP_SECURE=false
514
+ # SMTP_USER=your-email@example.com
515
+ # SMTP_PASS=your-password
516
+ # SMTP_FROM=noreply@example.com` : ""}
517
+ `;
518
+ writeFileSync(join(projectDir, ".env.example"), envExample);
443
519
  const spec = `# ${answers.projectName}
444
520
 
445
521
  ## Overview
@@ -506,11 +582,11 @@ const title = "${answers.projectName}";
506
582
  `;
507
583
  writeFileSync(join(pagesDir, "index.astro"), indexPage);
508
584
  if (answers.admin) {
509
- const adminDir = join(srcDir, "admin");
585
+ const adminDir = join(pagesDir, "admin");
510
586
  mkdirSync(adminDir, { recursive: true });
511
587
  const adminIndex = `---
512
588
  import { Admin } from '@kyro-cms/admin';
513
- import config from '../kyro.config';
589
+ import config from '../../../kyro.config';
514
590
  ---
515
591
  <!DOCTYPE html>
516
592
  <html lang="en">
@@ -526,13 +602,455 @@ import config from '../kyro.config';
526
602
  `;
527
603
  writeFileSync(join(adminDir, "index.astro"), adminIndex);
528
604
  }
605
+ if (answers.auth) {
606
+ const authApiDir = join(pagesDir, "api", "auth");
607
+ mkdirSync(authApiDir, { recursive: true });
608
+ writeFileSync(
609
+ join(authApiDir, "login.ts"),
610
+ generateLoginEndpoint(answers.database)
611
+ );
612
+ writeFileSync(
613
+ join(authApiDir, "register.ts"),
614
+ generateRegisterEndpoint(answers.database)
615
+ );
616
+ writeFileSync(
617
+ join(authApiDir, "logout.ts"),
618
+ generateLogoutEndpoint(answers.database)
619
+ );
620
+ writeFileSync(join(authApiDir, "me.ts"), generateMeEndpoint());
621
+ writeFileSync(
622
+ join(authApiDir, "users.ts"),
623
+ generateUsersEndpoint(answers.database)
624
+ );
625
+ writeFileSync(join(srcDir, "middleware.ts"), generateMiddleware());
626
+ }
627
+ }
628
+ function generateLoginEndpoint(database) {
629
+ const adapterImport = database === "sqlite" ? `import { SQLiteAuthAdapter } from "@kyro-cms/core";` : `import { RedisAuthAdapter } from "@kyro-cms/core";`;
630
+ const adapterInit = database === "sqlite" ? ` return new SQLiteAuthAdapter({ path: "./data.db" });` : ` return new RedisAuthAdapter({
631
+ url: process.env.REDIS_URL || "redis://localhost:6379",
632
+ tls: process.env.REDIS_TLS === "true",
633
+ });`;
634
+ return `import type { APIRoute } from "astro";
635
+ ${adapterImport}
636
+ import jwt from "jsonwebtoken";
637
+
638
+ const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
639
+ const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "24h";
640
+
641
+ async function getAuthApi() {
642
+ ${adapterInit}
643
+ }
644
+
645
+ export const POST: APIRoute = async ({ request }) => {
646
+ try {
647
+ const body = (await request.json()) as {
648
+ email?: string;
649
+ password?: string;
650
+ };
651
+ const { email, password } = body;
652
+
653
+ if (!email || !password) {
654
+ return new Response(
655
+ JSON.stringify({ error: "Email and password required" }),
656
+ { status: 400, headers: { "Content-Type": "application/json" } },
657
+ );
658
+ }
659
+
660
+ const adapter = await getAuthApi();
661
+ await adapter.connect();
662
+
663
+ const user = await adapter.findUserByEmail(email);
664
+ if (!user || !user.passwordHash) {
665
+ await adapter.disconnect();
666
+ return new Response(JSON.stringify({ error: "Invalid credentials" }), {
667
+ status: 401,
668
+ headers: { "Content-Type": "application/json" },
669
+ });
670
+ }
671
+
672
+ const valid = await adapter.verifyPassword(password, user.passwordHash);
673
+ if (!valid) {
674
+ await adapter.disconnect();
675
+ return new Response(JSON.stringify({ error: "Invalid credentials" }), {
676
+ status: 401,
677
+ headers: { "Content-Type": "application/json" },
678
+ });
679
+ }
680
+
681
+ const session = await adapter.createSession(user.id, {
682
+ ipAddress: request.headers.get("x-forwarded-for") || "unknown",
683
+ userAgent: request.headers.get("user-agent") || "",
684
+ });
685
+
686
+ const token = jwt.sign(
687
+ {
688
+ sub: user.id,
689
+ email: user.email,
690
+ role: user.role,
691
+ tenantId: user.tenantId,
692
+ },
693
+ JWT_SECRET,
694
+ { expiresIn: JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] },
695
+ );
696
+
697
+ await adapter.disconnect();
698
+
699
+ const { passwordHash, ...safeUser } = user;
700
+
701
+ return new Response(
702
+ JSON.stringify({
703
+ success: true,
704
+ user: safeUser,
705
+ token,
706
+ refreshToken: session.refreshToken,
707
+ }),
708
+ {
709
+ status: 200,
710
+ headers: { "Content-Type": "application/json" },
711
+ },
712
+ );
713
+ } catch (error) {
714
+ console.error("Login error:", error);
715
+ return new Response(JSON.stringify({ error: "Login failed" }), {
716
+ status: 500,
717
+ headers: { "Content-Type": "application/json" },
718
+ });
719
+ }
720
+ };
721
+ `;
722
+ }
723
+ function generateRegisterEndpoint(database) {
724
+ const adapterImport = database === "sqlite" ? `import { SQLiteAuthAdapter } from "@kyro-cms/core";` : `import { RedisAuthAdapter } from "@kyro-cms/core";`;
725
+ const adapterInit = database === "sqlite" ? ` return new SQLiteAuthAdapter({ path: "./data.db" });` : ` return new RedisAuthAdapter({
726
+ url: process.env.REDIS_URL || "redis://localhost:6379",
727
+ tls: process.env.REDIS_TLS === "true",
728
+ });`;
729
+ const isFirstUserCheck = database === "sqlite" ? ` const isFirstUser = !(await adapter.hasAnyUsers());` : ` const isFirstUser = await checkIsFirstUser(adapter);`;
730
+ const isFirstUserFn = database === "sqlite" ? "" : `
731
+
732
+ async function checkIsFirstUser(adapter: RedisAuthAdapter): Promise<boolean> {
733
+ try {
734
+ const redis = (adapter as any).redis;
735
+ if (!redis) return true;
736
+ const pattern = "kyro:auth:users:email:*";
737
+ const result = await redis.scan("0", "MATCH", pattern, "COUNT", "1");
738
+ const keys = result[1];
739
+ return keys.length === 0;
740
+ } catch {
741
+ return true;
742
+ }
743
+ }`;
744
+ return `import type { APIRoute } from "astro";
745
+ ${adapterImport}
746
+ import jwt from "jsonwebtoken";
747
+
748
+ const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
749
+ const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "24h";
750
+ const ALLOW_REGISTRATION = process.env.KYRO_ALLOW_REGISTRATION !== "false";
751
+
752
+ async function getAuthApi() {
753
+ ${adapterInit}
754
+ }
755
+
756
+ export const POST: APIRoute = async ({ request }) => {
757
+ try {
758
+ const body = (await request.json()) as {
759
+ email?: string;
760
+ password?: string;
761
+ confirmPassword?: string;
762
+ };
763
+ const { email, password, confirmPassword } = body;
764
+
765
+ if (!email || !password) {
766
+ return new Response(
767
+ JSON.stringify({ error: "Email and password required" }),
768
+ { status: 400, headers: { "Content-Type": "application/json" } },
769
+ );
770
+ }
771
+
772
+ if (password !== confirmPassword) {
773
+ return new Response(
774
+ JSON.stringify({ error: "Passwords do not match" }),
775
+ { status: 400, headers: { "Content-Type": "application/json" } },
776
+ );
777
+ }
778
+
779
+ if (password.length < 8) {
780
+ return new Response(
781
+ JSON.stringify({ error: "Password must be at least 8 characters" }),
782
+ { status: 400, headers: { "Content-Type": "application/json" } },
783
+ );
784
+ }
785
+
786
+ const adapter = await getAuthApi();
787
+ try {
788
+ await adapter.connect();
789
+ } catch {
790
+ return new Response(
791
+ JSON.stringify({ error: "Unable to connect to auth storage." }),
792
+ { status: 500, headers: { "Content-Type": "application/json" } },
793
+ );
794
+ }
795
+
796
+ const existingUser = await adapter.findUserByEmail(email);
797
+ if (existingUser) {
798
+ await adapter.disconnect();
799
+ return new Response(
800
+ JSON.stringify({ error: "Email already registered" }),
801
+ { status: 409, headers: { "Content-Type": "application/json" } },
802
+ );
803
+ }
804
+
805
+ ${isFirstUserCheck}
806
+
807
+ if (!isFirstUser && !ALLOW_REGISTRATION) {
808
+ await adapter.disconnect();
809
+ return new Response(
810
+ JSON.stringify({ error: "Registration is disabled" }),
811
+ { status: 403, headers: { "Content-Type": "application/json" } },
812
+ );
813
+ }
814
+
815
+ const passwordHash = await adapter.hashPassword(password);
816
+ const user = await adapter.createUser({
817
+ email,
818
+ passwordHash,
819
+ role: isFirstUser ? "super_admin" : "editor",
820
+ });
821
+
822
+ if (isFirstUser) {
823
+ await adapter.updateUser(user.id, { emailVerified: true });
824
+ }
825
+
826
+ const session = await adapter.createSession(user.id, {
827
+ ipAddress: request.headers.get("x-forwarded-for") || "unknown",
828
+ userAgent: request.headers.get("user-agent") || "",
829
+ });
830
+
831
+ const token = jwt.sign(
832
+ {
833
+ sub: user.id,
834
+ email: user.email,
835
+ role: user.role,
836
+ tenantId: user.tenantId,
837
+ },
838
+ JWT_SECRET,
839
+ { expiresIn: JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] },
840
+ );
841
+
842
+ await adapter.disconnect();
843
+
844
+ const { passwordHash: _, ...safeUser } = user;
845
+
846
+ return new Response(
847
+ JSON.stringify({
848
+ success: true,
849
+ isFirstUser,
850
+ user: safeUser,
851
+ token,
852
+ refreshToken: session.refreshToken,
853
+ }),
854
+ {
855
+ status: 201,
856
+ headers: { "Content-Type": "application/json" },
857
+ },
858
+ );
859
+ } catch (error) {
860
+ console.error("Registration error:", error);
861
+ return new Response(JSON.stringify({ error: "Registration failed" }), {
862
+ status: 500,
863
+ headers: { "Content-Type": "application/json" },
864
+ });
865
+ }
866
+ };${isFirstUserFn}
867
+ `;
868
+ }
869
+ function generateLogoutEndpoint(database) {
870
+ const adapterImport = database === "sqlite" ? `import { SQLiteAuthAdapter } from "@kyro-cms/core";` : `import { RedisAuthAdapter } from "@kyro-cms/core";`;
871
+ const adapterInit = database === "sqlite" ? ` return new SQLiteAuthAdapter({ path: "./data.db" });` : ` return new RedisAuthAdapter({
872
+ url: process.env.REDIS_URL || "redis://localhost:6379",
873
+ tls: process.env.REDIS_TLS === "true",
874
+ });`;
875
+ return `import type { APIRoute } from "astro";
876
+ ${adapterImport}
877
+
878
+ async function getAuthApi() {
879
+ ${adapterInit}
880
+ }
881
+
882
+ export const POST: APIRoute = async ({ request }) => {
883
+ try {
884
+ const authHeader = request.headers.get("authorization");
885
+ const token = authHeader?.startsWith("Bearer ")
886
+ ? authHeader.slice(7)
887
+ : null;
888
+
889
+ if (token) {
890
+ const adapter = await getAuthApi();
891
+ await adapter.connect();
892
+ await adapter.deleteSession(token);
893
+ await adapter.disconnect();
894
+ }
895
+
896
+ return new Response(
897
+ JSON.stringify({ success: true }),
898
+ { status: 200, headers: { "Content-Type": "application/json" } },
899
+ );
900
+ } catch {
901
+ return new Response(
902
+ JSON.stringify({ success: true }),
903
+ { status: 200, headers: { "Content-Type": "application/json" } },
904
+ );
905
+ }
906
+ };
907
+ `;
908
+ }
909
+ function generateMeEndpoint() {
910
+ return `import type { APIRoute } from "astro";
911
+ import jwt from "jsonwebtoken";
912
+
913
+ const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
914
+
915
+ export const GET: APIRoute = async ({ request }) => {
916
+ const authHeader = request.headers.get("authorization");
917
+ const token = authHeader?.startsWith("Bearer ")
918
+ ? authHeader.slice(7)
919
+ : null;
920
+
921
+ if (!token) {
922
+ return new Response(
923
+ JSON.stringify({ error: "Not authenticated" }),
924
+ { status: 401, headers: { "Content-Type": "application/json" } },
925
+ );
926
+ }
927
+
928
+ try {
929
+ const payload = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
930
+ return new Response(
931
+ JSON.stringify({
932
+ id: payload.sub,
933
+ email: payload.email,
934
+ role: payload.role,
935
+ }),
936
+ { status: 200, headers: { "Content-Type": "application/json" } },
937
+ );
938
+ } catch {
939
+ return new Response(
940
+ JSON.stringify({ error: "Invalid token" }),
941
+ { status: 401, headers: { "Content-Type": "application/json" } },
942
+ );
943
+ }
944
+ };
945
+ `;
946
+ }
947
+ function generateUsersEndpoint(database) {
948
+ const adapterImport = database === "sqlite" ? `import { SQLiteAuthAdapter } from "@kyro-cms/core";` : `import { RedisAuthAdapter } from "@kyro-cms/core";`;
949
+ const adapterInit = database === "sqlite" ? ` return new SQLiteAuthAdapter({ path: "./data.db" });` : ` return new RedisAuthAdapter({
950
+ url: process.env.REDIS_URL || "redis://localhost:6379",
951
+ tls: process.env.REDIS_TLS === "true",
952
+ });`;
953
+ const hasUsersCheck = database === "sqlite" ? ` const hasUsers = await adapter.hasAnyUsers();` : ` const redis = (adapter as any).redis;
954
+ if (!redis) {
955
+ await adapter.disconnect();
956
+ return new Response(JSON.stringify({ hasUsers: false }), {
957
+ status: 200,
958
+ headers: { "Content-Type": "application/json" },
959
+ });
960
+ }
961
+
962
+ const pattern = "kyro:auth:users:email:*";
963
+ const result = await redis.scan("0", "MATCH", pattern, "COUNT", "1");
964
+ const keys = result[1];
965
+ const hasUsers = keys.length > 0;`;
966
+ return `import type { APIRoute } from "astro";
967
+ ${adapterImport}
968
+
969
+ async function getAuthApi() {
970
+ ${adapterInit}
971
+ }
972
+
973
+ export const GET: APIRoute = async () => {
974
+ try {
975
+ const adapter = await getAuthApi();
976
+ await adapter.connect();
977
+
978
+ ${hasUsersCheck}
979
+
980
+ await adapter.disconnect();
981
+
982
+ return new Response(JSON.stringify({ hasUsers }), {
983
+ status: 200,
984
+ headers: { "Content-Type": "application/json" },
985
+ });
986
+ } catch {
987
+ return new Response(JSON.stringify({ hasUsers: false }), {
988
+ status: 200,
989
+ headers: { "Content-Type": "application/json" },
990
+ });
991
+ }
992
+ };
993
+ `;
994
+ }
995
+ function generateMiddleware() {
996
+ return `import type { MiddlewareHandler } from "astro";
997
+ import jwt from "jsonwebtoken";
998
+
999
+ const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
1000
+
1001
+ const PUBLIC_PATHS = [
1002
+ "/api/auth/login",
1003
+ "/api/auth/logout",
1004
+ "/api/auth/register",
1005
+ "/api/auth/me",
1006
+ "/api/auth/users",
1007
+ "/api/health",
1008
+ "/favicon.svg",
1009
+ ];
1010
+
1011
+ const PUBLIC_PREFIXES = ["/api/auth/", "/admin"];
1012
+
1013
+ export const onRequest: MiddlewareHandler = async ({ request, url }, next) => {
1014
+ const pathname = new URL(url).pathname;
1015
+
1016
+ if (PUBLIC_PATHS.includes(pathname) || PUBLIC_PATHS.includes(pathname.replace(/\\/$/, ""))) {
1017
+ return next();
1018
+ }
1019
+
1020
+ for (const prefix of PUBLIC_PREFIXES) {
1021
+ if (pathname.startsWith(prefix)) {
1022
+ return next();
1023
+ }
1024
+ }
1025
+
1026
+ const authHeader = request.headers.get("authorization");
1027
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
1028
+
1029
+ if (!token) {
1030
+ return new Response(
1031
+ JSON.stringify({ error: "Authentication required" }),
1032
+ { status: 401, headers: { "Content-Type": "application/json" } },
1033
+ );
1034
+ }
1035
+
1036
+ try {
1037
+ jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
1038
+ return next();
1039
+ } catch {
1040
+ return new Response(
1041
+ JSON.stringify({ error: "Invalid or expired token" }),
1042
+ { status: 401, headers: { "Content-Type": "application/json" } },
1043
+ );
1044
+ }
1045
+ };
1046
+ `;
529
1047
  }
530
1048
 
531
1049
  // src/index.ts
532
1050
  import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync } from "fs";
533
1051
  import { join as join2 } from "path";
534
1052
  import { execSync } from "child_process";
535
- var VERSION = "0.1.0";
1053
+ var VERSION = "0.1.1";
536
1054
  async function main() {
537
1055
  logger.intro("create-kyro", VERSION);
538
1056
  const answers = await promptUser();