create-kyro 0.1.8 → 0.2.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 +7 -47
- package/package.json +2 -2
- package/src/generators/astro.ts +45 -0
- package/src/generators/config.ts +95 -0
- package/src/generators/files.ts +629 -0
- package/src/generators/packagejson.ts +80 -0
- package/src/index.ts +97 -0
- package/src/prompts.ts +147 -0
- package/src/utils/logger.ts +52 -0
- package/src/validators.ts +36 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +11 -0
package/dist/index.js
CHANGED
|
@@ -64,39 +64,6 @@ async function promptUser() {
|
|
|
64
64
|
}
|
|
65
65
|
]
|
|
66
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
|
-
selected: true
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
title: "WebSocket",
|
|
94
|
-
description: "Real-time bidirectional communication",
|
|
95
|
-
value: "websocket",
|
|
96
|
-
selected: true
|
|
97
|
-
}
|
|
98
|
-
]
|
|
99
|
-
},
|
|
100
67
|
{
|
|
101
68
|
type: "select",
|
|
102
69
|
name: "styling",
|
|
@@ -318,19 +285,12 @@ function generateKyroConfig(answers) {
|
|
|
318
285
|
adapterLines.push(` connectionString: process.env.MONGODB_URI,`);
|
|
319
286
|
adapterLines.push(` }),`);
|
|
320
287
|
}
|
|
321
|
-
const apiConfig = [
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
if (answers.apis.includes("trpc")) {
|
|
329
|
-
apiConfig.push(" trpc: true,");
|
|
330
|
-
}
|
|
331
|
-
if (answers.apis.includes("websocket")) {
|
|
332
|
-
apiConfig.push(" websocket: true,");
|
|
333
|
-
}
|
|
288
|
+
const apiConfig = [
|
|
289
|
+
" rest: true,",
|
|
290
|
+
" graphql: true,",
|
|
291
|
+
" trpc: true,",
|
|
292
|
+
" websocket: true,"
|
|
293
|
+
];
|
|
334
294
|
const features = [];
|
|
335
295
|
if (answers.auth) {
|
|
336
296
|
features.push(" auth: true,");
|
|
@@ -524,7 +484,7 @@ This project uses Kyro CMS - an Astro-native headless CMS.
|
|
|
524
484
|
## Configuration
|
|
525
485
|
|
|
526
486
|
- **Database**: ${answers.database === "sqlite" ? "SQLite (local-first)" : answers.database}
|
|
527
|
-
- **APIs**:
|
|
487
|
+
- **APIs**: REST, GraphQL, tRPC, WebSocket
|
|
528
488
|
- **Styling**: ${answers.styling}
|
|
529
489
|
- **Auth**: ${answers.auth ? "Enabled" : "Disabled"}
|
|
530
490
|
- **Versioning**: ${answers.versioning ? "Enabled" : "Disabled"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Answers } from "../prompts.js";
|
|
2
|
+
|
|
3
|
+
export function generateAstroConfig(answers: Answers): string {
|
|
4
|
+
const integrations: string[] = [];
|
|
5
|
+
const vitePlugins: string[] = [];
|
|
6
|
+
|
|
7
|
+
if (answers.styling === "tailwind") {
|
|
8
|
+
integrations.push(" react(),");
|
|
9
|
+
vitePlugins.push(" tailwindcss(),");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const adapter = answers.admin
|
|
13
|
+
? `
|
|
14
|
+
adapter: node({
|
|
15
|
+
mode: 'standalone'
|
|
16
|
+
}),`
|
|
17
|
+
: "";
|
|
18
|
+
|
|
19
|
+
const config = `import { defineConfig } from 'astro/config';
|
|
20
|
+
import node from '@astrojs/node';
|
|
21
|
+
${answers.styling === "tailwind" ? "import react from '@astrojs/react';\nimport tailwindcss from '@tailwindcss/vite';" : ""}
|
|
22
|
+
|
|
23
|
+
export default defineConfig({
|
|
24
|
+
output: 'server',${adapter}
|
|
25
|
+
|
|
26
|
+
integrations: [
|
|
27
|
+
${integrations.join("\n")}
|
|
28
|
+
],${
|
|
29
|
+
vitePlugins.length > 0
|
|
30
|
+
? `
|
|
31
|
+
vite: {
|
|
32
|
+
plugins: [
|
|
33
|
+
${vitePlugins.join("\n")}
|
|
34
|
+
],
|
|
35
|
+
},`
|
|
36
|
+
: ""
|
|
37
|
+
}
|
|
38
|
+
server: {
|
|
39
|
+
port: 4321,
|
|
40
|
+
host: true,
|
|
41
|
+
},
|
|
42
|
+
});`;
|
|
43
|
+
|
|
44
|
+
return config;
|
|
45
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Answers } from "../prompts.js";
|
|
2
|
+
|
|
3
|
+
export function generateKyroConfig(answers: Answers): string {
|
|
4
|
+
const imports: string[] = ["import { defineConfig } from '@kyro-cms/core';"];
|
|
5
|
+
const adapterLines: string[] = [];
|
|
6
|
+
|
|
7
|
+
if (answers.database === "sqlite") {
|
|
8
|
+
imports.push("import { createLocalAdapter } from '@kyro-cms/core';");
|
|
9
|
+
adapterLines.push(` adapter: createLocalAdapter({ path: './data.db' }),`);
|
|
10
|
+
} else if (answers.database === "postgres") {
|
|
11
|
+
imports.push("import { createDrizzleAdapter } from '@kyro-cms/core';");
|
|
12
|
+
adapterLines.push(` adapter: createDrizzleAdapter({`);
|
|
13
|
+
adapterLines.push(` connectionString: process.env.DATABASE_URL,`);
|
|
14
|
+
adapterLines.push(` }),`);
|
|
15
|
+
} else if (answers.database === "mysql") {
|
|
16
|
+
imports.push("import { createDrizzleAdapter } from '@kyro-cms/core';");
|
|
17
|
+
adapterLines.push(` adapter: createDrizzleAdapter({`);
|
|
18
|
+
adapterLines.push(` connectionString: process.env.DATABASE_URL,`);
|
|
19
|
+
adapterLines.push(` }),`);
|
|
20
|
+
} else if (answers.database === "mongodb") {
|
|
21
|
+
imports.push("import { createMongoDBAdapter } from '@kyro-cms/core';");
|
|
22
|
+
adapterLines.push(` adapter: createMongoDBAdapter({`);
|
|
23
|
+
adapterLines.push(` connectionString: process.env.MONGODB_URI,`);
|
|
24
|
+
adapterLines.push(` }),`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const apiConfig = [
|
|
28
|
+
" rest: true,",
|
|
29
|
+
" graphql: true,",
|
|
30
|
+
" trpc: true,",
|
|
31
|
+
" websocket: true,",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const features: string[] = [];
|
|
35
|
+
if (answers.auth) {
|
|
36
|
+
features.push(" auth: true,");
|
|
37
|
+
}
|
|
38
|
+
if (answers.versioning) {
|
|
39
|
+
features.push(" versioning: true,");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let templateCollections = "";
|
|
43
|
+
let templateGlobals = "";
|
|
44
|
+
|
|
45
|
+
switch (answers.template) {
|
|
46
|
+
case "minimal":
|
|
47
|
+
templateCollections =
|
|
48
|
+
"import { minimalCollections } from '@kyro-cms/core';";
|
|
49
|
+
break;
|
|
50
|
+
case "blog":
|
|
51
|
+
templateCollections = "import { blogCollections } from '@kyro-cms/core';";
|
|
52
|
+
break;
|
|
53
|
+
case "ecommerce":
|
|
54
|
+
templateCollections =
|
|
55
|
+
"import { ecommerceCollections } from '@kyro-cms/core';";
|
|
56
|
+
break;
|
|
57
|
+
case "kitchen-sink":
|
|
58
|
+
templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections } from '@kyro-cms/core';`;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (templateCollections) {
|
|
63
|
+
imports.push(templateCollections);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let collectionsConfig = "";
|
|
67
|
+
if (answers.template === "minimal") {
|
|
68
|
+
collectionsConfig = ` collections: Object.values(minimalCollections),`;
|
|
69
|
+
} else if (answers.template === "blog") {
|
|
70
|
+
collectionsConfig = ` collections: Object.values(blogCollections),`;
|
|
71
|
+
} else if (answers.template === "ecommerce") {
|
|
72
|
+
collectionsConfig = ` collections: Object.values(ecommerceCollections),`;
|
|
73
|
+
} else if (answers.template === "kitchen-sink") {
|
|
74
|
+
collectionsConfig = ` collections: [
|
|
75
|
+
...Object.values(minimalCollections),
|
|
76
|
+
...Object.values(blogCollections),
|
|
77
|
+
...Object.values(ecommerceCollections),
|
|
78
|
+
...Object.values(kitchenSinkCollections),
|
|
79
|
+
],`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const config = `${imports.join("\n")}
|
|
83
|
+
|
|
84
|
+
export default defineConfig({
|
|
85
|
+
name: '${answers.projectName}',
|
|
86
|
+
prefix: '/api',${adapterLines.length > 0 ? "\n" + adapterLines.join("\n") : ""}
|
|
87
|
+
${collectionsConfig ? collectionsConfig : ""}${features.length > 0 ? "\n" + features.join("\n") : ""}
|
|
88
|
+
|
|
89
|
+
api: {
|
|
90
|
+
${apiConfig.join("\n")}
|
|
91
|
+
},
|
|
92
|
+
});`;
|
|
93
|
+
|
|
94
|
+
return config;
|
|
95
|
+
}
|
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import type { Answers } from "../prompts.js";
|
|
2
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
export function generateProjectFiles(
|
|
6
|
+
answers: Answers,
|
|
7
|
+
projectDir: string,
|
|
8
|
+
): void {
|
|
9
|
+
const srcDir = join(projectDir, "src");
|
|
10
|
+
const pagesDir = join(srcDir, "pages");
|
|
11
|
+
const publicDir = join(projectDir, "public");
|
|
12
|
+
|
|
13
|
+
mkdirSync(pagesDir, { recursive: true });
|
|
14
|
+
mkdirSync(publicDir, { recursive: true });
|
|
15
|
+
|
|
16
|
+
if (answers.database === "sqlite") {
|
|
17
|
+
mkdirSync(join(projectDir, "data"), { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const tsconfig = `{
|
|
21
|
+
"extends": "astro/tsconfigs/strict",
|
|
22
|
+
"compilerOptions": {
|
|
23
|
+
"baseUrl": ".",
|
|
24
|
+
"paths": {
|
|
25
|
+
"@/*": ["./src/*"]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}`;
|
|
29
|
+
|
|
30
|
+
writeFileSync(join(projectDir, "tsconfig.json"), tsconfig);
|
|
31
|
+
|
|
32
|
+
const gitignore = `node_modules/
|
|
33
|
+
dist/
|
|
34
|
+
.astro/
|
|
35
|
+
data/
|
|
36
|
+
*.db
|
|
37
|
+
.env
|
|
38
|
+
.env.local
|
|
39
|
+
.DS_Store
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
writeFileSync(join(projectDir, ".gitignore"), gitignore);
|
|
43
|
+
|
|
44
|
+
const readme = `# ${answers.projectName}
|
|
45
|
+
|
|
46
|
+
A Kyro CMS project.
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
\`\`\`bash
|
|
51
|
+
npm install
|
|
52
|
+
npm run dev
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
55
|
+
## Admin Dashboard
|
|
56
|
+
|
|
57
|
+
Visit [http://localhost:4321/admin](http://localhost:4321/admin) to access the admin.
|
|
58
|
+
|
|
59
|
+
${
|
|
60
|
+
answers.auth
|
|
61
|
+
? `## Creating Your Admin User
|
|
62
|
+
|
|
63
|
+
Before logging into the admin, you need to create an admin user. Run:
|
|
64
|
+
|
|
65
|
+
\`\`\`bash
|
|
66
|
+
npm run db:bootstrap
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
Or set environment variables to auto-bootstrap on startup:
|
|
70
|
+
|
|
71
|
+
\`\`\`bash
|
|
72
|
+
# .env
|
|
73
|
+
KYRO_ADMIN_EMAIL=admin@example.com
|
|
74
|
+
KYRO_ADMIN_PASSWORD=SecurePass123!
|
|
75
|
+
\`\`\`
|
|
76
|
+
|
|
77
|
+
Then restart the dev server.
|
|
78
|
+
`
|
|
79
|
+
: ""
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
## Documentation
|
|
83
|
+
|
|
84
|
+
Visit [https://kyro.cms](https://kyro.cms) for full documentation.
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
writeFileSync(join(projectDir, "README.md"), readme);
|
|
88
|
+
|
|
89
|
+
const envExample = `# Kyro CMS Configuration
|
|
90
|
+
|
|
91
|
+
${
|
|
92
|
+
answers.database === "sqlite"
|
|
93
|
+
? "# SQLite (local) - no additional config needed"
|
|
94
|
+
: answers.database === "postgres" || answers.database === "mysql"
|
|
95
|
+
? "# Database connection (PostgreSQL/MySQL)\nDATABASE_URL=postgresql://user:password@localhost:5432/kyro_cms\nDATABASE_SSL=false"
|
|
96
|
+
: "# MongoDB connection\nMONGODB_URI=mongodb://localhost:27017/kyro_cms"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
${
|
|
100
|
+
answers.auth
|
|
101
|
+
? `# Authentication (uses SQLite at ./data/auth.db - no Redis needed)
|
|
102
|
+
JWT_SECRET=change-this-to-a-random-32-character-string
|
|
103
|
+
JWT_EXPIRES_IN=24h
|
|
104
|
+
|
|
105
|
+
# Registration control (set to false to disable public registration after first user)
|
|
106
|
+
KYRO_ALLOW_REGISTRATION=true
|
|
107
|
+
|
|
108
|
+
# Optional: Custom auth database path (default: ./data/auth.db)
|
|
109
|
+
# KYRO_AUTH_DB_PATH=./data/auth.db
|
|
110
|
+
|
|
111
|
+
# Optional: SMTP for emails
|
|
112
|
+
# SMTP_HOST=smtp.example.com
|
|
113
|
+
# SMTP_PORT=587
|
|
114
|
+
# SMTP_SECURE=false
|
|
115
|
+
# SMTP_USER=your-email@example.com
|
|
116
|
+
# SMTP_PASS=your-password
|
|
117
|
+
# SMTP_FROM=noreply@example.com`
|
|
118
|
+
: ""
|
|
119
|
+
}
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
writeFileSync(join(projectDir, ".env.example"), envExample);
|
|
123
|
+
|
|
124
|
+
const spec = `# ${answers.projectName}
|
|
125
|
+
|
|
126
|
+
## Overview
|
|
127
|
+
|
|
128
|
+
This project uses Kyro CMS - an Astro-native headless CMS.
|
|
129
|
+
|
|
130
|
+
## Configuration
|
|
131
|
+
|
|
132
|
+
- **Database**: ${answers.database === "sqlite" ? "SQLite (local-first)" : answers.database}
|
|
133
|
+
- **APIs**: REST, GraphQL, tRPC, WebSocket
|
|
134
|
+
- **Styling**: ${answers.styling}
|
|
135
|
+
- **Auth**: ${answers.auth ? "Enabled" : "Disabled"}
|
|
136
|
+
- **Versioning**: ${answers.versioning ? "Enabled" : "Disabled"}
|
|
137
|
+
- **Admin**: ${answers.admin ? "Included" : "Not included"}
|
|
138
|
+
- **Template**: ${answers.template}
|
|
139
|
+
|
|
140
|
+
## Collections
|
|
141
|
+
|
|
142
|
+
${
|
|
143
|
+
answers.template === "minimal"
|
|
144
|
+
? "- Posts"
|
|
145
|
+
: answers.template === "blog"
|
|
146
|
+
? "- Posts\n- Categories\n- Media"
|
|
147
|
+
: "- Products\n- Categories\n- Customers\n- Orders\n- Coupons"
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
writeFileSync(join(projectDir, "SPEC.md"), spec);
|
|
152
|
+
|
|
153
|
+
const indexPage = `---
|
|
154
|
+
const title = "${answers.projectName}";
|
|
155
|
+
---
|
|
156
|
+
<!DOCTYPE html>
|
|
157
|
+
<html lang="en">
|
|
158
|
+
<head>
|
|
159
|
+
<meta charset="UTF-8" />
|
|
160
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
161
|
+
<title>${answers.projectName}</title>
|
|
162
|
+
</head>
|
|
163
|
+
<body>
|
|
164
|
+
<main>
|
|
165
|
+
<h1>Welcome to ${answers.projectName}</h1>
|
|
166
|
+
<p>Your Kyro CMS is ready.</p>
|
|
167
|
+
${
|
|
168
|
+
answers.admin
|
|
169
|
+
? '<p><a href="/admin">Go to Admin Dashboard →</a></p>'
|
|
170
|
+
: ""
|
|
171
|
+
}
|
|
172
|
+
</main>
|
|
173
|
+
</body>
|
|
174
|
+
</html>
|
|
175
|
+
|
|
176
|
+
<style>
|
|
177
|
+
main {
|
|
178
|
+
max-width: 800px;
|
|
179
|
+
margin: 4rem auto;
|
|
180
|
+
padding: 2rem;
|
|
181
|
+
text-align: center;
|
|
182
|
+
font-family: system-ui, sans-serif;
|
|
183
|
+
}
|
|
184
|
+
h1 {
|
|
185
|
+
font-size: 2.5rem;
|
|
186
|
+
margin-bottom: 1rem;
|
|
187
|
+
}
|
|
188
|
+
p {
|
|
189
|
+
color: #666;
|
|
190
|
+
}
|
|
191
|
+
a {
|
|
192
|
+
color: #6366f1;
|
|
193
|
+
text-decoration: none;
|
|
194
|
+
}
|
|
195
|
+
a:hover {
|
|
196
|
+
text-decoration: underline;
|
|
197
|
+
}
|
|
198
|
+
</style>
|
|
199
|
+
`;
|
|
200
|
+
|
|
201
|
+
writeFileSync(join(pagesDir, "index.astro"), indexPage);
|
|
202
|
+
|
|
203
|
+
if (answers.admin) {
|
|
204
|
+
const adminDir = join(pagesDir, "admin");
|
|
205
|
+
mkdirSync(adminDir, { recursive: true });
|
|
206
|
+
|
|
207
|
+
const adminIndex = `---
|
|
208
|
+
import { Admin } from '@kyro-cms/admin';
|
|
209
|
+
import config from '../../../kyro.config';
|
|
210
|
+
---
|
|
211
|
+
<!DOCTYPE html>
|
|
212
|
+
<html lang="en">
|
|
213
|
+
<head>
|
|
214
|
+
<meta charset="UTF-8" />
|
|
215
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
216
|
+
<title>Admin - ${answers.projectName}</title>
|
|
217
|
+
</head>
|
|
218
|
+
<body>
|
|
219
|
+
<Admin client:load config={config} />
|
|
220
|
+
</body>
|
|
221
|
+
</html>
|
|
222
|
+
`;
|
|
223
|
+
|
|
224
|
+
writeFileSync(join(adminDir, "index.astro"), adminIndex);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (answers.auth) {
|
|
228
|
+
const authApiDir = join(pagesDir, "api", "auth");
|
|
229
|
+
mkdirSync(authApiDir, { recursive: true });
|
|
230
|
+
|
|
231
|
+
writeFileSync(
|
|
232
|
+
join(authApiDir, "login.ts"),
|
|
233
|
+
generateLoginEndpoint(answers.database),
|
|
234
|
+
);
|
|
235
|
+
writeFileSync(
|
|
236
|
+
join(authApiDir, "register.ts"),
|
|
237
|
+
generateRegisterEndpoint(answers.database),
|
|
238
|
+
);
|
|
239
|
+
writeFileSync(
|
|
240
|
+
join(authApiDir, "logout.ts"),
|
|
241
|
+
generateLogoutEndpoint(answers.database),
|
|
242
|
+
);
|
|
243
|
+
writeFileSync(join(authApiDir, "me.ts"), generateMeEndpoint());
|
|
244
|
+
writeFileSync(
|
|
245
|
+
join(authApiDir, "users.ts"),
|
|
246
|
+
generateUsersEndpoint(answers.database),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
writeFileSync(join(srcDir, "middleware.ts"), generateMiddleware());
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function generateLoginEndpoint(database: string): string {
|
|
254
|
+
return `import type { APIRoute } from "astro";
|
|
255
|
+
import { SQLiteAuthAdapter } from "@kyro-cms/core";
|
|
256
|
+
import jwt from "jsonwebtoken";
|
|
257
|
+
|
|
258
|
+
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
259
|
+
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "24h";
|
|
260
|
+
|
|
261
|
+
async function getAuthApi() {
|
|
262
|
+
return new SQLiteAuthAdapter({ path: "./data/auth.db" });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
266
|
+
try {
|
|
267
|
+
const body = (await request.json()) as {
|
|
268
|
+
email?: string;
|
|
269
|
+
password?: string;
|
|
270
|
+
};
|
|
271
|
+
const { email, password } = body;
|
|
272
|
+
|
|
273
|
+
if (!email || !password) {
|
|
274
|
+
return new Response(
|
|
275
|
+
JSON.stringify({ error: "Email and password required" }),
|
|
276
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const adapter = await getAuthApi();
|
|
281
|
+
await adapter.connect();
|
|
282
|
+
|
|
283
|
+
const user = await adapter.findUserByEmail(email);
|
|
284
|
+
if (!user || !user.passwordHash) {
|
|
285
|
+
await adapter.disconnect();
|
|
286
|
+
return new Response(JSON.stringify({ error: "Invalid credentials" }), {
|
|
287
|
+
status: 401,
|
|
288
|
+
headers: { "Content-Type": "application/json" },
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const valid = await adapter.verifyPassword(password, user.passwordHash);
|
|
293
|
+
if (!valid) {
|
|
294
|
+
await adapter.disconnect();
|
|
295
|
+
return new Response(JSON.stringify({ error: "Invalid credentials" }), {
|
|
296
|
+
status: 401,
|
|
297
|
+
headers: { "Content-Type": "application/json" },
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const session = await adapter.createSession(user.id, {
|
|
302
|
+
ipAddress: request.headers.get("x-forwarded-for") || "unknown",
|
|
303
|
+
userAgent: request.headers.get("user-agent") || "",
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const token = jwt.sign(
|
|
307
|
+
{
|
|
308
|
+
sub: user.id,
|
|
309
|
+
email: user.email,
|
|
310
|
+
role: user.role,
|
|
311
|
+
tenantId: user.tenantId,
|
|
312
|
+
},
|
|
313
|
+
JWT_SECRET,
|
|
314
|
+
{ expiresIn: JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] },
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
await adapter.disconnect();
|
|
318
|
+
|
|
319
|
+
const { passwordHash, ...safeUser } = user;
|
|
320
|
+
|
|
321
|
+
return new Response(
|
|
322
|
+
JSON.stringify({
|
|
323
|
+
success: true,
|
|
324
|
+
user: safeUser,
|
|
325
|
+
token,
|
|
326
|
+
refreshToken: session.refreshToken,
|
|
327
|
+
}),
|
|
328
|
+
{
|
|
329
|
+
status: 200,
|
|
330
|
+
headers: { "Content-Type": "application/json" },
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
console.error("Login error:", error);
|
|
335
|
+
return new Response(JSON.stringify({ error: "Login failed" }), {
|
|
336
|
+
status: 500,
|
|
337
|
+
headers: { "Content-Type": "application/json" },
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function generateRegisterEndpoint(database: string): string {
|
|
345
|
+
return `import type { APIRoute } from "astro";
|
|
346
|
+
import { SQLiteAuthAdapter } from "@kyro-cms/core";
|
|
347
|
+
import jwt from "jsonwebtoken";
|
|
348
|
+
|
|
349
|
+
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
350
|
+
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "24h";
|
|
351
|
+
const ALLOW_REGISTRATION = process.env.KYRO_ALLOW_REGISTRATION !== "false";
|
|
352
|
+
|
|
353
|
+
async function getAuthApi() {
|
|
354
|
+
return new SQLiteAuthAdapter({ path: "./data/auth.db" });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
358
|
+
try {
|
|
359
|
+
const body = (await request.json()) as {
|
|
360
|
+
email?: string;
|
|
361
|
+
password?: string;
|
|
362
|
+
confirmPassword?: string;
|
|
363
|
+
};
|
|
364
|
+
const { email, password, confirmPassword } = body;
|
|
365
|
+
|
|
366
|
+
if (!email || !password) {
|
|
367
|
+
return new Response(
|
|
368
|
+
JSON.stringify({ error: "Email and password required" }),
|
|
369
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (password !== confirmPassword) {
|
|
374
|
+
return new Response(
|
|
375
|
+
JSON.stringify({ error: "Passwords do not match" }),
|
|
376
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (password.length < 8) {
|
|
381
|
+
return new Response(
|
|
382
|
+
JSON.stringify({ error: "Password must be at least 8 characters" }),
|
|
383
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const adapter = await getAuthApi();
|
|
388
|
+
try {
|
|
389
|
+
await adapter.connect();
|
|
390
|
+
} catch {
|
|
391
|
+
return new Response(
|
|
392
|
+
JSON.stringify({ error: "Unable to connect to auth storage." }),
|
|
393
|
+
{ status: 500, headers: { "Content-Type": "application/json" } },
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const existingUser = await adapter.findUserByEmail(email);
|
|
398
|
+
if (existingUser) {
|
|
399
|
+
await adapter.disconnect();
|
|
400
|
+
return new Response(
|
|
401
|
+
JSON.stringify({ error: "Email already registered" }),
|
|
402
|
+
{ status: 409, headers: { "Content-Type": "application/json" } },
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const isFirstUser = !(await adapter.hasAnyUsers());
|
|
407
|
+
|
|
408
|
+
if (!isFirstUser && !ALLOW_REGISTRATION) {
|
|
409
|
+
await adapter.disconnect();
|
|
410
|
+
return new Response(
|
|
411
|
+
JSON.stringify({ error: "Registration is disabled" }),
|
|
412
|
+
{ status: 403, headers: { "Content-Type": "application/json" } },
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const passwordHash = await adapter.hashPassword(password);
|
|
417
|
+
const user = await adapter.createUser({
|
|
418
|
+
email,
|
|
419
|
+
passwordHash,
|
|
420
|
+
role: isFirstUser ? "super_admin" : "editor",
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
if (isFirstUser) {
|
|
424
|
+
await adapter.updateUser(user.id, { emailVerified: true });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const session = await adapter.createSession(user.id, {
|
|
428
|
+
ipAddress: request.headers.get("x-forwarded-for") || "unknown",
|
|
429
|
+
userAgent: request.headers.get("user-agent") || "",
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const token = jwt.sign(
|
|
433
|
+
{
|
|
434
|
+
sub: user.id,
|
|
435
|
+
email: user.email,
|
|
436
|
+
role: user.role,
|
|
437
|
+
tenantId: user.tenantId,
|
|
438
|
+
},
|
|
439
|
+
JWT_SECRET,
|
|
440
|
+
{ expiresIn: JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] },
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
await adapter.disconnect();
|
|
444
|
+
|
|
445
|
+
const { passwordHash: _, ...safeUser } = user;
|
|
446
|
+
|
|
447
|
+
return new Response(
|
|
448
|
+
JSON.stringify({
|
|
449
|
+
success: true,
|
|
450
|
+
isFirstUser,
|
|
451
|
+
user: safeUser,
|
|
452
|
+
token,
|
|
453
|
+
refreshToken: session.refreshToken,
|
|
454
|
+
}),
|
|
455
|
+
{
|
|
456
|
+
status: 201,
|
|
457
|
+
headers: { "Content-Type": "application/json" },
|
|
458
|
+
},
|
|
459
|
+
);
|
|
460
|
+
} catch (error) {
|
|
461
|
+
console.error("Registration error:", error);
|
|
462
|
+
return new Response(JSON.stringify({ error: "Registration failed" }), {
|
|
463
|
+
status: 500,
|
|
464
|
+
headers: { "Content-Type": "application/json" },
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function generateLogoutEndpoint(database: string): string {
|
|
472
|
+
return `import type { APIRoute } from "astro";
|
|
473
|
+
import { SQLiteAuthAdapter } from "@kyro-cms/core";
|
|
474
|
+
|
|
475
|
+
async function getAuthApi() {
|
|
476
|
+
return new SQLiteAuthAdapter({ path: "./data/auth.db" });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
480
|
+
try {
|
|
481
|
+
const authHeader = request.headers.get("authorization");
|
|
482
|
+
const token = authHeader?.startsWith("Bearer ")
|
|
483
|
+
? authHeader.slice(7)
|
|
484
|
+
: null;
|
|
485
|
+
|
|
486
|
+
if (token) {
|
|
487
|
+
const adapter = await getAuthApi();
|
|
488
|
+
await adapter.connect();
|
|
489
|
+
await adapter.deleteSession(token);
|
|
490
|
+
await adapter.disconnect();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return new Response(
|
|
494
|
+
JSON.stringify({ success: true }),
|
|
495
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
496
|
+
);
|
|
497
|
+
} catch {
|
|
498
|
+
return new Response(
|
|
499
|
+
JSON.stringify({ success: true }),
|
|
500
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function generateMeEndpoint(): string {
|
|
508
|
+
return `import type { APIRoute } from "astro";
|
|
509
|
+
import jwt from "jsonwebtoken";
|
|
510
|
+
|
|
511
|
+
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
512
|
+
|
|
513
|
+
export const GET: APIRoute = async ({ request }) => {
|
|
514
|
+
const authHeader = request.headers.get("authorization");
|
|
515
|
+
const token = authHeader?.startsWith("Bearer ")
|
|
516
|
+
? authHeader.slice(7)
|
|
517
|
+
: null;
|
|
518
|
+
|
|
519
|
+
if (!token) {
|
|
520
|
+
return new Response(
|
|
521
|
+
JSON.stringify({ error: "Not authenticated" }),
|
|
522
|
+
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
const payload = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
|
|
528
|
+
return new Response(
|
|
529
|
+
JSON.stringify({
|
|
530
|
+
id: payload.sub,
|
|
531
|
+
email: payload.email,
|
|
532
|
+
role: payload.role,
|
|
533
|
+
}),
|
|
534
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
535
|
+
);
|
|
536
|
+
} catch {
|
|
537
|
+
return new Response(
|
|
538
|
+
JSON.stringify({ error: "Invalid token" }),
|
|
539
|
+
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function generateUsersEndpoint(database: string): string {
|
|
547
|
+
return `import type { APIRoute } from "astro";
|
|
548
|
+
import { SQLiteAuthAdapter } from "@kyro-cms/core";
|
|
549
|
+
|
|
550
|
+
async function getAuthApi() {
|
|
551
|
+
return new SQLiteAuthAdapter({ path: "./data/auth.db" });
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export const GET: APIRoute = async () => {
|
|
555
|
+
try {
|
|
556
|
+
const adapter = await getAuthApi();
|
|
557
|
+
await adapter.connect();
|
|
558
|
+
|
|
559
|
+
const hasUsers = await adapter.hasAnyUsers();
|
|
560
|
+
|
|
561
|
+
await adapter.disconnect();
|
|
562
|
+
|
|
563
|
+
return new Response(JSON.stringify({ hasUsers }), {
|
|
564
|
+
status: 200,
|
|
565
|
+
headers: { "Content-Type": "application/json" },
|
|
566
|
+
});
|
|
567
|
+
} catch {
|
|
568
|
+
return new Response(JSON.stringify({ hasUsers: false }), {
|
|
569
|
+
status: 200,
|
|
570
|
+
headers: { "Content-Type": "application/json" },
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
`;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function generateMiddleware(): string {
|
|
578
|
+
return `import type { MiddlewareHandler } from "astro";
|
|
579
|
+
import jwt from "jsonwebtoken";
|
|
580
|
+
|
|
581
|
+
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
582
|
+
|
|
583
|
+
const PUBLIC_PATHS = [
|
|
584
|
+
"/api/auth/login",
|
|
585
|
+
"/api/auth/logout",
|
|
586
|
+
"/api/auth/register",
|
|
587
|
+
"/api/auth/me",
|
|
588
|
+
"/api/auth/users",
|
|
589
|
+
"/api/health",
|
|
590
|
+
"/favicon.svg",
|
|
591
|
+
];
|
|
592
|
+
|
|
593
|
+
const PUBLIC_PREFIXES = ["/api/auth/", "/admin"];
|
|
594
|
+
|
|
595
|
+
export const onRequest: MiddlewareHandler = async ({ request, url }, next) => {
|
|
596
|
+
const pathname = new URL(url).pathname;
|
|
597
|
+
|
|
598
|
+
if (PUBLIC_PATHS.includes(pathname) || PUBLIC_PATHS.includes(pathname.replace(/\\/$/, ""))) {
|
|
599
|
+
return next();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
for (const prefix of PUBLIC_PREFIXES) {
|
|
603
|
+
if (pathname.startsWith(prefix)) {
|
|
604
|
+
return next();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const authHeader = request.headers.get("authorization");
|
|
609
|
+
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
610
|
+
|
|
611
|
+
if (!token) {
|
|
612
|
+
return new Response(
|
|
613
|
+
JSON.stringify({ error: "Authentication required" }),
|
|
614
|
+
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
|
|
620
|
+
return next();
|
|
621
|
+
} catch {
|
|
622
|
+
return new Response(
|
|
623
|
+
JSON.stringify({ error: "Invalid or expired token" }),
|
|
624
|
+
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
`;
|
|
629
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Answers } from "../prompts.js";
|
|
2
|
+
|
|
3
|
+
export interface PackageJson {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
type: string;
|
|
7
|
+
private: boolean;
|
|
8
|
+
scripts: Record<string, string>;
|
|
9
|
+
dependencies: Record<string, string>;
|
|
10
|
+
devDependencies: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function generatePackageJson(
|
|
14
|
+
answers: Answers,
|
|
15
|
+
projectDir: string,
|
|
16
|
+
): PackageJson {
|
|
17
|
+
const deps: Record<string, string> = {
|
|
18
|
+
"@kyro-cms/core": "latest",
|
|
19
|
+
astro: "^5.4.0",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const devDeps: Record<string, string> = {
|
|
23
|
+
typescript: "^5.7.3",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (answers.styling === "tailwind") {
|
|
27
|
+
deps["@astrojs/react"] = "^4.2.0";
|
|
28
|
+
deps["react"] = "^19.0.0";
|
|
29
|
+
deps["react-dom"] = "^19.0.0";
|
|
30
|
+
deps["tailwindcss"] = "^4.0.0";
|
|
31
|
+
deps["@tailwindcss/vite"] = "^4.0.0";
|
|
32
|
+
devDeps["@types/react"] = "^19.0.0";
|
|
33
|
+
devDeps["@types/react-dom"] = "^19.0.0";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (answers.database === "postgres") {
|
|
37
|
+
deps["pg"] = "^8.13.1";
|
|
38
|
+
deps["@types/pg"] = "^8.11.0";
|
|
39
|
+
} else if (answers.database === "mysql") {
|
|
40
|
+
deps["mysql2"] = "^3.12.0";
|
|
41
|
+
} else if (answers.database === "mongodb") {
|
|
42
|
+
deps["mongodb"] = "^6.12.0";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (answers.admin) {
|
|
46
|
+
deps["@kyro-cms/admin"] = "latest";
|
|
47
|
+
deps["@astrojs/node"] = "^9.5.5";
|
|
48
|
+
deps["lucide-react"] = "^0.475.0";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const scripts: Record<string, string> = {
|
|
52
|
+
dev: "astro dev",
|
|
53
|
+
build: "astro build",
|
|
54
|
+
preview: "astro preview",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (answers.auth) {
|
|
58
|
+
scripts["db:bootstrap"] = "kyro auth bootstrap";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (answers.database === "sqlite") {
|
|
62
|
+
scripts["db:generate"] = "kyro generate";
|
|
63
|
+
scripts["db:push"] = "kyro push";
|
|
64
|
+
scripts["db:studio"] = "kyro studio";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
name: answers.projectName,
|
|
69
|
+
version: "0.1.0",
|
|
70
|
+
type: "module",
|
|
71
|
+
private: true,
|
|
72
|
+
scripts,
|
|
73
|
+
dependencies: deps,
|
|
74
|
+
devDependencies: devDeps,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function formatPackageJson(pkg: PackageJson): string {
|
|
79
|
+
return JSON.stringify(pkg, null, 2);
|
|
80
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { promptUser, type Answers } from './prompts.js';
|
|
2
|
+
import { logger } from './utils/logger.js';
|
|
3
|
+
import { generatePackageJson, formatPackageJson } from './generators/packagejson.js';
|
|
4
|
+
import { generateKyroConfig } from './generators/config.js';
|
|
5
|
+
import { generateAstroConfig } from './generators/astro.js';
|
|
6
|
+
import { generateProjectFiles } from './generators/files.js';
|
|
7
|
+
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
|
|
11
|
+
const VERSION = '0.1.1';
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
logger.intro('create-kyro', VERSION);
|
|
15
|
+
|
|
16
|
+
const answers = await promptUser();
|
|
17
|
+
const projectDir = join(process.cwd(), answers.projectName);
|
|
18
|
+
|
|
19
|
+
if (existsSync(projectDir)) {
|
|
20
|
+
logger.error(`Directory "${answers.projectName}" already exists.`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const steps = [
|
|
25
|
+
'Creating project directory',
|
|
26
|
+
'Generating configuration files',
|
|
27
|
+
'Installing dependencies',
|
|
28
|
+
'Initializing git repository',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
logger.step(1, steps.length, steps[0]);
|
|
32
|
+
mkdirSync(projectDir, { recursive: true });
|
|
33
|
+
logger.success('Project directory created');
|
|
34
|
+
|
|
35
|
+
logger.step(2, steps.length, steps[1]);
|
|
36
|
+
|
|
37
|
+
const pkg = generatePackageJson(answers, projectDir);
|
|
38
|
+
writeFileSync(
|
|
39
|
+
join(projectDir, 'package.json'),
|
|
40
|
+
formatPackageJson(pkg)
|
|
41
|
+
);
|
|
42
|
+
logger.success('package.json generated');
|
|
43
|
+
|
|
44
|
+
const kyroConfig = generateKyroConfig(answers);
|
|
45
|
+
writeFileSync(join(projectDir, 'kyro.config.ts'), kyroConfig);
|
|
46
|
+
logger.success('kyro.config.ts generated');
|
|
47
|
+
|
|
48
|
+
const astroConfig = generateAstroConfig(answers);
|
|
49
|
+
writeFileSync(join(projectDir, 'astro.config.mjs'), astroConfig);
|
|
50
|
+
logger.success('astro.config.mjs generated');
|
|
51
|
+
|
|
52
|
+
generateProjectFiles(answers, projectDir);
|
|
53
|
+
logger.success('Project files generated');
|
|
54
|
+
|
|
55
|
+
logger.step(3, steps.length, steps[2]);
|
|
56
|
+
console.log(' Installing dependencies...');
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
execSync('npm install', {
|
|
60
|
+
cwd: projectDir,
|
|
61
|
+
stdio: 'inherit',
|
|
62
|
+
env: { ...process.env, npm_config_loglevel: 'warn' }
|
|
63
|
+
});
|
|
64
|
+
logger.success('Dependencies installed');
|
|
65
|
+
} catch (error) {
|
|
66
|
+
logger.error('Failed to install dependencies');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
logger.step(4, steps.length, steps[3]);
|
|
71
|
+
try {
|
|
72
|
+
execSync('git init && git add . && git commit -m "Initial commit - created with create-kyro"', {
|
|
73
|
+
cwd: projectDir,
|
|
74
|
+
stdio: 'pipe'
|
|
75
|
+
});
|
|
76
|
+
logger.success('Git repository initialized');
|
|
77
|
+
} catch {
|
|
78
|
+
logger.warning('Could not initialize git repository');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
logger.done();
|
|
82
|
+
|
|
83
|
+
console.log(' To get started:');
|
|
84
|
+
console.log(` ${logger ? '\x1b[36m' : ''}cd ${answers.projectName}${logger ? '\x1b[0m' : ''}`);
|
|
85
|
+
console.log(` ${logger ? '\x1b[36m' : ''}npm run dev${logger ? '\x1b[0m' : ''}`);
|
|
86
|
+
console.log('');
|
|
87
|
+
console.log(' Visit http://localhost:4321 to see your app.');
|
|
88
|
+
if (answers.admin) {
|
|
89
|
+
console.log(' Visit http://localhost:4321/admin for the admin dashboard.');
|
|
90
|
+
}
|
|
91
|
+
console.log('');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
main().catch((error) => {
|
|
95
|
+
logger.error(`Unexpected error: ${error.message}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
});
|
package/src/prompts.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
import { validateProjectName } from "./validators.js";
|
|
3
|
+
|
|
4
|
+
export interface Answers {
|
|
5
|
+
projectName: string;
|
|
6
|
+
database: "sqlite" | "postgres" | "mysql" | "mongodb";
|
|
7
|
+
styling: "tailwind" | "cssmodules" | "styled" | "none";
|
|
8
|
+
auth: boolean;
|
|
9
|
+
versioning: boolean;
|
|
10
|
+
admin: boolean;
|
|
11
|
+
template: "minimal" | "blog" | "ecommerce" | "kitchen-sink";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function promptUser(): Promise<Answers> {
|
|
15
|
+
const response = await prompts(
|
|
16
|
+
[
|
|
17
|
+
{
|
|
18
|
+
type: "text",
|
|
19
|
+
name: "projectName",
|
|
20
|
+
message: "Project name:",
|
|
21
|
+
initial: "my-kyro-app",
|
|
22
|
+
validate: validateProjectName,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: "select",
|
|
26
|
+
name: "database",
|
|
27
|
+
message: "Database:",
|
|
28
|
+
hint: " ",
|
|
29
|
+
choices: [
|
|
30
|
+
{
|
|
31
|
+
title: "SQLite (local-first, zero config)",
|
|
32
|
+
description:
|
|
33
|
+
"Best for development and small projects. No setup required.",
|
|
34
|
+
value: "sqlite",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
title: "PostgreSQL",
|
|
38
|
+
description: "Recommended for production. Robust and scalable.",
|
|
39
|
+
value: "postgres",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
title: "MySQL",
|
|
43
|
+
description: "Popular choice for web applications.",
|
|
44
|
+
value: "mysql",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
title: "MongoDB",
|
|
48
|
+
description: "Best for flexible, document-based schemas.",
|
|
49
|
+
value: "mongodb",
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: "select",
|
|
55
|
+
name: "styling",
|
|
56
|
+
message: "Styling:",
|
|
57
|
+
hint: " ",
|
|
58
|
+
choices: [
|
|
59
|
+
{
|
|
60
|
+
title: "Tailwind CSS",
|
|
61
|
+
description: "Utility-first CSS framework, excellent DX",
|
|
62
|
+
value: "tailwind",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
title: "CSS Modules",
|
|
66
|
+
description: "Scoped CSS, no extra dependencies",
|
|
67
|
+
value: "cssmodules",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
title: "Styled Components",
|
|
71
|
+
description: "CSS-in-JS with tagged template literals",
|
|
72
|
+
value: "styled",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
title: "None",
|
|
76
|
+
description: "Bring your own styling solution",
|
|
77
|
+
value: "none",
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: "toggle",
|
|
83
|
+
name: "auth",
|
|
84
|
+
message: "Add authentication (JWT)?",
|
|
85
|
+
initial: true,
|
|
86
|
+
active: "Yes",
|
|
87
|
+
inactive: "No",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: "toggle",
|
|
91
|
+
name: "versioning",
|
|
92
|
+
message: "Add versioning/drafts?",
|
|
93
|
+
initial: true,
|
|
94
|
+
active: "Yes",
|
|
95
|
+
inactive: "No",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
type: "toggle",
|
|
99
|
+
name: "admin",
|
|
100
|
+
message: "Include admin dashboard?",
|
|
101
|
+
initial: true,
|
|
102
|
+
active: "Yes",
|
|
103
|
+
inactive: "No",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
type: "select",
|
|
107
|
+
name: "template",
|
|
108
|
+
message: "Starting template:",
|
|
109
|
+
hint: " ",
|
|
110
|
+
initial: 1,
|
|
111
|
+
choices: [
|
|
112
|
+
{
|
|
113
|
+
title: "Minimal",
|
|
114
|
+
description:
|
|
115
|
+
"Basic configuration with one example collection + core settings",
|
|
116
|
+
value: "minimal",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
title: "Blog",
|
|
120
|
+
description: "Posts, categories, media library + core settings",
|
|
121
|
+
value: "blog",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
title: "E-commerce",
|
|
125
|
+
description:
|
|
126
|
+
"Products, orders, customers, coupons + core + store/payment settings",
|
|
127
|
+
value: "ecommerce",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
title: "Kitchen Sink",
|
|
131
|
+
description:
|
|
132
|
+
"Everything: pages, navigation, blog, e-commerce + all settings",
|
|
133
|
+
value: "kitchen-sink",
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
{
|
|
139
|
+
onCancel: () => {
|
|
140
|
+
console.log("\nCancelled.");
|
|
141
|
+
process.exit(1);
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return response as Answers;
|
|
147
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { cyan, green, yellow, red, bold, dim } from 'kolorist';
|
|
2
|
+
|
|
3
|
+
export const logger = {
|
|
4
|
+
intro: (name: string, version: string) => {
|
|
5
|
+
console.log(`
|
|
6
|
+
${bold(cyan(`Kyro CMS`))} ${dim(`v${version}`)}
|
|
7
|
+
${dim('Astro-native headless CMS')}
|
|
8
|
+
`);
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
success: (msg: string) => {
|
|
12
|
+
console.log(`${green('✓')} ${msg}`);
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
error: (msg: string) => {
|
|
16
|
+
console.log(`${red('✗')} ${msg}`);
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
warning: (msg: string) => {
|
|
20
|
+
console.log(`${yellow('⚠')} ${msg}`);
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
info: (msg: string) => {
|
|
24
|
+
console.log(`${cyan('ℹ')} ${msg}`);
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
step: (num: number, total: number, msg: string) => {
|
|
28
|
+
console.log(`${dim(`[${num}/${total}]`)} ${msg}`);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
heading: (msg: string) => {
|
|
32
|
+
console.log(`\n${bold(cyan(msg))}`);
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
subheading: (msg: string) => {
|
|
36
|
+
console.log(`\n${bold(msg)}`);
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
list: (items: string[]) => {
|
|
40
|
+
items.forEach(item => {
|
|
41
|
+
console.log(` ${green('•')} ${item}`);
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
done: () => {
|
|
46
|
+
console.log(`\n${green(bold('Done!'))} Project created successfully.\n`);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
prompt: (msg: string) => {
|
|
50
|
+
console.log(`\n${cyan('?')} ${bold(msg)}`);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function validateProjectName(name: string): string | true {
|
|
2
|
+
if (!name) {
|
|
3
|
+
return 'Project name is required';
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
7
|
+
return 'Use lowercase letters, numbers, and hyphens only';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (name.length < 2) {
|
|
11
|
+
return 'Project name must be at least 2 characters';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (name.length > 50) {
|
|
15
|
+
return 'Project name must be less than 50 characters';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (/^[-0-9]/.test(name)) {
|
|
19
|
+
return 'Project name cannot start with a number or hyphen';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const reserved = ['node_modules', 'dist', 'build', 'public', 'src', 'test', 'tests'];
|
|
23
|
+
if (reserved.includes(name)) {
|
|
24
|
+
return `"${name}" is a reserved name`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function validateEmail(email: string): string | true {
|
|
31
|
+
if (!email) return true;
|
|
32
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
33
|
+
return 'Invalid email address';
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"lib": ["ES2022"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "dist"]
|
|
15
|
+
}
|