create-kyro 0.5.4 → 0.5.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kyro",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Interactive scaffolding for Kyro CMS projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,13 +4,15 @@ export function generateAstroConfig(answers: Answers): string {
4
4
  return `import { defineConfig } from 'astro/config';
5
5
  import { kyro } from '@kyro-cms/core';
6
6
  import { kyroAdmin } from '@kyro-cms/admin';
7
+ import react from '@astrojs/react';
8
+ import tailwind from '@tailwindcss/vite';
7
9
 
8
10
  export default defineConfig({
9
11
  output: 'server',
10
- integrations: [
11
- kyro({ adminPath: '/admin', apiPath: '/api' }),
12
- kyroAdmin({ basePath: '/admin', apiPath: '/api' }),
13
- ],
12
+ integrations: [react(), kyro({ adminPath: '/admin', apiPath: '/api' }), kyroAdmin({ basePath: '/admin', apiPath: '/api' })],
13
+ vite: {
14
+ plugins: [tailwind()],
15
+ },
14
16
  server: {
15
17
  port: 4321,
16
18
  host: true,
@@ -30,22 +30,22 @@ export function generateKyroConfig(answers: Answers): string {
30
30
  switch (answers.template) {
31
31
  case "minimal":
32
32
  templateCollections =
33
- "import { minimalCollections } from '@kyro-cms/core/templates';";
33
+ "import { minimalCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';";
34
34
  templateGlobals =
35
35
  "import { coreSettingsGlobals } from '@kyro-cms/core/templates';";
36
36
  break;
37
37
  case "blog":
38
- templateCollections = "import { blogCollections } from '@kyro-cms/core/templates';";
38
+ templateCollections = "import { blogCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';";
39
39
  templateGlobals =
40
40
  "import { allSettingsGlobals } from '@kyro-cms/core/templates';";
41
41
  break;
42
42
  case "ecommerce":
43
- templateCollections = "import { ecommerceCollections } from '@kyro-cms/core/templates';";
43
+ templateCollections = "import { ecommerceCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';";
44
44
  templateGlobals =
45
45
  "import { allSettingsGlobals, ecommerceSettingsGlobals } from '@kyro-cms/core/templates';";
46
46
  break;
47
47
  case "kitchen-sink":
48
- templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections } from '@kyro-cms/core/templates';`;
48
+ templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections, mediaCollections, authCollections } from '@kyro-cms/core/templates';`;
49
49
  templateGlobals =
50
50
  "import { allSettingsGlobals, ecommerceSettingsGlobals } from '@kyro-cms/core/templates';";
51
51
  break;
@@ -56,17 +56,31 @@ export function generateKyroConfig(answers: Answers): string {
56
56
 
57
57
  let collectionsConfig = "";
58
58
  if (answers.template === "minimal") {
59
- collectionsConfig = ` collections: Object.values(minimalCollections),`;
59
+ collectionsConfig = ` collections: [
60
+ ...Object.values(minimalCollections),
61
+ ...Object.values(mediaCollections),
62
+ ...Object.values(authCollections),
63
+ ],`;
60
64
  } else if (answers.template === "blog") {
61
- collectionsConfig = ` collections: Object.values(blogCollections),`;
65
+ collectionsConfig = ` collections: [
66
+ ...Object.values(blogCollections),
67
+ ...Object.values(mediaCollections),
68
+ ...Object.values(authCollections),
69
+ ],`;
62
70
  } else if (answers.template === "ecommerce") {
63
- collectionsConfig = ` collections: Object.values(ecommerceCollections),`;
71
+ collectionsConfig = ` collections: [
72
+ ...Object.values(ecommerceCollections),
73
+ ...Object.values(mediaCollections),
74
+ ...Object.values(authCollections),
75
+ ],`;
64
76
  } else if (answers.template === "kitchen-sink") {
65
77
  collectionsConfig = ` collections: [
66
78
  ...Object.values(minimalCollections),
67
79
  ...Object.values(blogCollections),
68
80
  ...Object.values(ecommerceCollections),
69
81
  ...Object.values(kitchenSinkCollections),
82
+ ...Object.values(mediaCollections),
83
+ ...Object.values(authCollections),
70
84
  ],`;
71
85
  }
72
86
 
@@ -89,6 +103,8 @@ export default defineConfig({
89
103
  ${adapterLines.join("\n")}
90
104
  ${collectionsConfig}
91
105
  ${globalsConfig}
92
- auth: true,
106
+ auth: {
107
+ secret: process.env.APP_SECRET,
108
+ },
93
109
  });`;
94
110
  }
@@ -1,16 +1,29 @@
1
1
  import type { Answers } from "../prompts.js";
2
2
  import { writeFileSync, mkdirSync } from "fs";
3
3
  import { join } from "path";
4
+ import { randomBytes } from "crypto";
5
+
6
+ function generateAppSecret(): string {
7
+ return randomBytes(32).toString("hex");
8
+ }
9
+
10
+ export interface AdminCredentials {
11
+ adminEmail: string;
12
+ adminPassword: string;
13
+ }
4
14
 
5
15
  export function generateProjectFiles(
6
16
  answers: Answers,
7
17
  projectDir: string,
18
+ adminCredentials?: AdminCredentials,
8
19
  ): void {
9
20
  const srcDir = join(projectDir, "src");
10
21
  const pagesDir = join(srcDir, "pages");
22
+ const stylesDir = join(srcDir, "styles");
11
23
  const publicDir = join(projectDir, "public");
12
24
 
13
25
  mkdirSync(pagesDir, { recursive: true });
26
+ mkdirSync(stylesDir, { recursive: true });
14
27
  mkdirSync(publicDir, { recursive: true });
15
28
 
16
29
  const tsconfig = `{
@@ -64,16 +77,15 @@ Visit [https://kyro.dev](https://kyro.dev) for full documentation.
64
77
  const envExample = `# Kyro CMS Configuration
65
78
  # Copy this file to .env and fill in your values
66
79
 
67
- ${
68
- answers.database === "sqlite"
69
- ? "# SQLite (local) - no additional config needed"
70
- : answers.database === "postgres"
71
- ? "# Database connection (PostgreSQL)\nDATABASE_URL=postgresql://user:password@localhost:5432/kyro_cms"
72
- : "# MongoDB connection\nMONGODB_URI=mongodb://localhost:27017/kyro_cms"
73
- }
80
+ ${answers.database === "sqlite"
81
+ ? "# SQLite (local) - no additional config needed"
82
+ : answers.database === "postgres"
83
+ ? "# Database connection (PostgreSQL)\nDATABASE_URL=postgresql://user:password@localhost:5432/kyro_cms"
84
+ : "# MongoDB connection\nMONGODB_URI=mongodb://localhost:27017/kyro_cms"
85
+ }
74
86
 
75
- # JWT secret for authentication tokens
76
- JWT_SECRET=change-this-to-a-random-64-character-string
87
+ # App secret (set once; on first run it's stored in the database and can be managed via admin UI)
88
+ APP_SECRET=your-secret-here
77
89
 
78
90
  # Admin credentials (used for first-user bootstrap)
79
91
  # KYRO_ADMIN_EMAIL=admin@example.com
@@ -82,7 +94,39 @@ JWT_SECRET=change-this-to-a-random-64-character-string
82
94
 
83
95
  writeFileSync(join(projectDir, ".env.example"), envExample);
84
96
 
97
+ if (adminCredentials) {
98
+ const envFile = `# Kyro CMS Configuration
99
+ # Copy this file to .env and fill in your values
100
+
101
+ ${answers.database === "sqlite"
102
+ ? "# SQLite (local) - no additional config needed"
103
+ : answers.database === "postgres"
104
+ ? "# Database connection (PostgreSQL)\nDATABASE_URL=postgresql://user:password@localhost:5432/kyro_cms"
105
+ : "# MongoDB connection\nMONGODB_URI=mongodb://localhost:27017/kyro_cms"
106
+ }
107
+
108
+ # App secret (auto-generated; stored in database on first run)
109
+ APP_SECRET=${generateAppSecret()}
110
+
111
+ # Admin credentials (used for first-user bootstrap)
112
+ KYRO_ADMIN_EMAIL=${adminCredentials.adminEmail}
113
+ KYRO_ADMIN_PASSWORD=${adminCredentials.adminPassword}
114
+ `;
115
+
116
+ writeFileSync(join(projectDir, ".env"), envFile);
117
+ }
118
+
119
+ const mainCss = `@import "tailwindcss";
120
+
121
+ @theme {
122
+ --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
123
+ }
124
+ `;
125
+
126
+ writeFileSync(join(stylesDir, "main.css"), mainCss);
127
+
85
128
  const indexPage = `---
129
+ import "../styles/main.css";
86
130
  const title = "${answers.projectName}";
87
131
  ---
88
132
  <!DOCTYPE html>
@@ -92,38 +136,52 @@ const title = "${answers.projectName}";
92
136
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
93
137
  <title>{title}</title>
94
138
  </head>
95
- <body>
96
- <main>
97
- <h1>Welcome to {title}</h1>
98
- <p>Your Kyro CMS is ready.</p>
99
- <p><a href="/admin">Go to Admin Dashboard &rarr;</a></p>
100
- </main>
139
+ <body
140
+ class="min-h-screen bg-gradient-to-br from-stone-50 via-white to-stone-100 dark:from-stone-950 dark:via-stone-900 dark:to-stone-950 text-stone-900 dark:text-stone-100 antialiased"
141
+ >
142
+ <div class="relative min-h-screen flex flex-col items-center justify-center px-4">
143
+ <!-- Decorative background blurs -->
144
+ <div class="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
145
+ <div class="absolute -top-40 -right-40 w-96 h-96 rounded-full bg-indigo-100/60 dark:bg-indigo-900/20 blur-3xl"></div>
146
+ <div class="absolute -bottom-40 -left-40 w-96 h-96 rounded-full bg-indigo-100/60 dark:bg-indigo-900/20 blur-3xl"></div>
147
+ </div>
148
+
149
+ <main class="relative text-center max-w-lg">
150
+ <!-- Badge -->
151
+ <div class="mb-8 inline-flex items-center gap-2 px-3 py-1 rounded-full border border-stone-200 dark:border-stone-700 text-xs font-medium text-stone-500 dark:text-stone-400">
152
+ <span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
153
+ Powered by Kyro CMS
154
+ </div>
155
+
156
+ <!-- Title -->
157
+ <h1 class="mb-4 text-5xl sm:text-6xl font-black tracking-tight bg-gradient-to-r from-stone-900 to-stone-500 dark:from-white dark:to-stone-400 bg-clip-text text-transparent">
158
+ {title}
159
+ </h1>
160
+
161
+ <!-- Tagline -->
162
+ <p class="mb-10 text-lg sm:text-xl text-stone-500 dark:text-stone-400 leading-relaxed">
163
+ Your content management system is ready. Start building something amazing.
164
+ </p>
165
+
166
+ <!-- Admin link -->
167
+ <a
168
+ href="/admin"
169
+ class="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-stone-900 dark:bg-white text-white dark:text-stone-900 font-semibold text-sm hover:bg-stone-800 dark:hover:bg-stone-100 transition-all shadow-lg hover:shadow-xl active:scale-[0.97]"
170
+ >
171
+ Go to Admin Dashboard
172
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
173
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M17 8l4 4m0 0l-4 4m4-4H3"/>
174
+ </svg>
175
+ </a>
176
+ </main>
177
+
178
+ <!-- Footer -->
179
+ <footer class="absolute bottom-8 text-xs text-stone-400 dark:text-stone-500">
180
+ &copy; {new Date().getFullYear()} {title}
181
+ </footer>
182
+ </div>
101
183
  </body>
102
184
  </html>
103
-
104
- <style>
105
- main {
106
- max-width: 800px;
107
- margin: 4rem auto;
108
- padding: 2rem;
109
- text-align: center;
110
- font-family: system-ui, sans-serif;
111
- }
112
- h1 {
113
- font-size: 2.5rem;
114
- margin-bottom: 1rem;
115
- }
116
- p {
117
- color: #666;
118
- }
119
- a {
120
- color: #6366f1;
121
- text-decoration: none;
122
- }
123
- a:hover {
124
- text-decoration: underline;
125
- }
126
- </style>
127
185
  `;
128
186
 
129
187
  writeFileSync(join(pagesDir, "index.astro"), indexPage);
@@ -15,6 +15,9 @@ export function generatePackageJson(
15
15
  ): PackageJson {
16
16
  const deps: Record<string, string> = {
17
17
  "astro": "^6.3.1",
18
+ "@astrojs/react": "^5.0.4",
19
+ "@tailwindcss/vite": "^4.0.0",
20
+ "tailwindcss": "^4.0.0",
18
21
  "@kyro-cms/core": "latest",
19
22
  "@kyro-cms/admin": "latest",
20
23
  };
package/src/index.ts CHANGED
@@ -7,6 +7,17 @@ import { generateProjectFiles } from './generators/files.js';
7
7
  import { writeFileSync, mkdirSync, existsSync } from 'fs';
8
8
  import { join } from 'path';
9
9
  import { execSync } from 'child_process';
10
+ import { randomBytes } from 'crypto';
11
+
12
+ function generatePassword(length = 24): string {
13
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()';
14
+ const bytes = randomBytes(length);
15
+ let password = '';
16
+ for (let i = 0; i < length; i++) {
17
+ password += chars[bytes[i] % chars.length];
18
+ }
19
+ return password;
20
+ }
10
21
 
11
22
  const VERSION = '0.4.0';
12
23
 
@@ -21,6 +32,8 @@ async function main() {
21
32
  process.exit(1);
22
33
  }
23
34
 
35
+ const adminPassword = generatePassword();
36
+
24
37
  const steps = [
25
38
  'Creating project directory',
26
39
  'Generating configuration files',
@@ -49,7 +62,7 @@ async function main() {
49
62
  writeFileSync(join(projectDir, 'astro.config.mjs'), astroConfig);
50
63
  logger.success('astro.config.mjs generated');
51
64
 
52
- generateProjectFiles(answers, projectDir);
65
+ generateProjectFiles(answers, projectDir, { adminEmail: answers.adminEmail, adminPassword });
53
66
  logger.success('Project files generated');
54
67
 
55
68
  logger.step(3, steps.length, steps[2]);
@@ -87,7 +100,14 @@ async function main() {
87
100
  console.log(' Visit http://localhost:4321 to see your app.');
88
101
  console.log(' Visit http://localhost:4321/admin for the admin dashboard.');
89
102
  console.log('');
90
- console.log(' The first user to register will be the super admin.');
103
+ console.log(' \x1b[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m');
104
+ console.log(' \x1b[33m Admin Account Created\x1b[0m');
105
+ console.log(' \x1b[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m');
106
+ console.log(` Email: \x1b[36m${answers.adminEmail}\x1b[0m`);
107
+ console.log(` Password: \x1b[36m${adminPassword}\x1b[0m`);
108
+ console.log(' \x1b[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m');
109
+ console.log(' The admin user will be created automatically on first server start.');
110
+ console.log(' You can change these credentials in the .env file.');
91
111
  console.log('');
92
112
  }
93
113
 
package/src/prompts.ts CHANGED
@@ -5,6 +5,7 @@ export interface Answers {
5
5
  projectName: string;
6
6
  database: "sqlite" | "postgres" | "mongodb";
7
7
  template: "minimal" | "blog" | "ecommerce" | "kitchen-sink";
8
+ adminEmail: string;
8
9
  }
9
10
 
10
11
  export async function promptUser(): Promise<Answers> {
@@ -73,6 +74,12 @@ export async function promptUser(): Promise<Answers> {
73
74
  },
74
75
  ],
75
76
  },
77
+ {
78
+ type: "text",
79
+ name: "adminEmail",
80
+ message: "Admin email:",
81
+ initial: (prev, values) => `admin@${values.projectName}.local`,
82
+ },
76
83
  ],
77
84
  {
78
85
  onCancel: () => {
@@ -11,6 +11,7 @@ const baseAnswers: Answers = {
11
11
  projectName: "test-project",
12
12
  database: "sqlite",
13
13
  template: "blog",
14
+ adminEmail: "admin@test-project.local",
14
15
  };
15
16
 
16
17
  const allDatabases = ["sqlite", "postgres", "mongodb"] as const;
@@ -38,9 +39,9 @@ describe("generators", () => {
38
39
  expect(config).toContain("MONGODB_URI");
39
40
  });
40
41
 
41
- it("includes auth (always enabled)", () => {
42
+ it("includes auth with app secret", () => {
42
43
  const config = generateKyroConfig(baseAnswers);
43
- expect(config).toContain("auth: true");
44
+ expect(config).toContain("APP_SECRET");
44
45
  });
45
46
 
46
47
  it("never generates mysql adapter", () => {
@@ -55,15 +56,21 @@ describe("generators", () => {
55
56
  template: "minimal",
56
57
  });
57
58
  expect(minimal).toContain("minimalCollections");
59
+ expect(minimal).toContain("mediaCollections");
60
+ expect(minimal).toContain("authCollections");
58
61
 
59
62
  const blog = generateKyroConfig({ ...baseAnswers, template: "blog" });
60
63
  expect(blog).toContain("blogCollections");
64
+ expect(blog).toContain("mediaCollections");
65
+ expect(blog).toContain("authCollections");
61
66
 
62
67
  const ecommerce = generateKyroConfig({
63
68
  ...baseAnswers,
64
69
  template: "ecommerce",
65
70
  });
66
71
  expect(ecommerce).toContain("ecommerceCollections");
72
+ expect(ecommerce).toContain("mediaCollections");
73
+ expect(ecommerce).toContain("authCollections");
67
74
 
68
75
  const kitchen = generateKyroConfig({
69
76
  ...baseAnswers,
@@ -73,6 +80,8 @@ describe("generators", () => {
73
80
  expect(kitchen).toContain("blogCollections");
74
81
  expect(kitchen).toContain("ecommerceCollections");
75
82
  expect(kitchen).toContain("kitchenSinkCollections");
83
+ expect(kitchen).toContain("mediaCollections");
84
+ expect(kitchen).toContain("authCollections");
76
85
  });
77
86
 
78
87
  it("imports settings globals per template", () => {
@@ -140,13 +149,15 @@ describe("generators", () => {
140
149
  expect(config).toContain("/api");
141
150
  });
142
151
 
143
- it("never uses deprecated integrations", () => {
152
+ it("includes react integration for admin UI", () => {
144
153
  const config = generateAstroConfig(baseAnswers);
145
- expect(config).not.toContain("@astrojs/react");
146
- expect(config).not.toContain("@tailwindcss/vite");
147
- expect(config).not.toContain("@astrojs/node");
148
- expect(config).not.toContain("ssr");
149
- expect(config).not.toContain("optimizeDeps");
154
+ expect(config).toContain("@astrojs/react");
155
+ });
156
+
157
+ it("includes tailwind vite plugin", () => {
158
+ const config = generateAstroConfig(baseAnswers);
159
+ expect(config).toContain("@tailwindcss/vite");
160
+ expect(config).toContain("tailwind()");
150
161
  });
151
162
  });
152
163
 
@@ -208,11 +219,15 @@ describe("generators", () => {
208
219
  expect(pkg.dependencies["react"]).toBeUndefined();
209
220
  expect(pkg.dependencies["react-dom"]).toBeUndefined();
210
221
  expect(pkg.dependencies["lucide-react"]).toBeUndefined();
211
- expect(pkg.dependencies["@astrojs/react"]).toBeUndefined();
212
- expect(pkg.dependencies["tailwindcss"]).toBeUndefined();
213
222
  expect(pkg.dependencies["mysql2"]).toBeUndefined();
214
223
  });
215
224
 
225
+ it("includes tailwindcss and @tailwindcss/vite", () => {
226
+ const pkg = generatePackageJson(baseAnswers);
227
+ expect(pkg.dependencies["tailwindcss"]).toBeDefined();
228
+ expect(pkg.dependencies["@tailwindcss/vite"]).toBeDefined();
229
+ });
230
+
216
231
  it("never includes manual auth bootstrap script", () => {
217
232
  const pkg = generatePackageJson(baseAnswers);
218
233
  expect(pkg.scripts["db:bootstrap"]).toBeUndefined();
@@ -13,6 +13,7 @@ const baseAnswers: Answers = {
13
13
  projectName: "test-project",
14
14
  database: "sqlite",
15
15
  template: "blog",
16
+ adminEmail: "admin@test-project.local",
16
17
  };
17
18
 
18
19
  const allDatabases = ["sqlite", "postgres", "mongodb"] as const;
@@ -116,7 +117,7 @@ describe("file generation", () => {
116
117
  expect(config).not.toContain("mysql");
117
118
  expect(config).not.toContain("api {");
118
119
  expect(config).toContain("defineConfig");
119
- expect(config).toContain("auth: true");
120
+ expect(config).toContain("APP_SECRET");
120
121
  });
121
122
 
122
123
  it("uses correct template import path", () => {
@@ -162,8 +163,8 @@ describe("file generation", () => {
162
163
  content = readFileSync(join(tmpDir, ".env.example"), "utf8");
163
164
  });
164
165
 
165
- it("contains JWT_SECRET", () => {
166
- expect(content).toContain("JWT_SECRET");
166
+ it("contains APP_SECRET", () => {
167
+ expect(content).toContain("APP_SECRET");
167
168
  });
168
169
 
169
170
  it("contains admin credentials comments", () => {
@@ -217,7 +218,7 @@ describe("all database × template combinations produce valid output", () => {
217
218
  for (const db of allDatabases) {
218
219
  for (const template of allTemplates) {
219
220
  it(`${db} + ${template} generates valid config`, () => {
220
- const answers: Answers = { projectName: "combo-test", database: db, template };
221
+ const answers: Answers = { projectName: "combo-test", database: db, template, adminEmail: "admin@combo-test.local" };
221
222
  const config = generateKyroConfig(answers);
222
223
  expect(config).toContain("defineConfig");
223
224
  expect(config).not.toContain("mysql");