@kozojs/cli 0.1.5 → 0.1.6

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.
Files changed (2) hide show
  1. package/lib/index.js +1179 -18
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -35,11 +35,23 @@ var import_execa = require("execa");
35
35
  var import_fs_extra = __toESM(require("fs-extra"));
36
36
  var import_node_path = __toESM(require("path"));
37
37
  async function scaffoldProject(options) {
38
- const { projectName, database, packageSource, template } = options;
38
+ const { projectName, runtime, database, packageSource, template, frontend, extras } = options;
39
39
  const projectDir = import_node_path.default.resolve(process.cwd(), projectName);
40
- const kozoCoreDep = packageSource === "local" ? "workspace:*" : "^0.1.0";
40
+ const kozoCoreDep = packageSource === "local" ? "workspace:*" : "^0.2.0";
41
+ if (frontend !== "none") {
42
+ await scaffoldFullstackProject(projectDir, projectName, kozoCoreDep, runtime, database, frontend, extras, template);
43
+ return;
44
+ }
41
45
  if (template === "complete") {
42
- await scaffoldCompleteTemplate(projectDir, projectName, kozoCoreDep);
46
+ await scaffoldCompleteTemplate(projectDir, projectName, kozoCoreDep, runtime);
47
+ if (extras.includes("docker")) await createDockerfile(projectDir, runtime);
48
+ if (extras.includes("github-actions")) await createGitHubActions(projectDir);
49
+ return;
50
+ }
51
+ if (template === "api-only") {
52
+ await scaffoldApiOnlyTemplate(projectDir, projectName, kozoCoreDep, runtime);
53
+ if (extras.includes("docker")) await createDockerfile(projectDir, runtime);
54
+ if (extras.includes("github-actions")) await createGitHubActions(projectDir);
43
55
  return;
44
56
  }
45
57
  await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "routes"));
@@ -399,7 +411,7 @@ export default async ({ body, services: { db } }: HandlerContext<Body>) => {
399
411
  `;
400
412
  await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "routes", "users", "post.ts"), postUsersRoute);
401
413
  }
402
- async function scaffoldCompleteTemplate(projectDir, projectName, kozoCoreDep) {
414
+ async function scaffoldCompleteTemplate(projectDir, projectName, kozoCoreDep, runtime) {
403
415
  await import_fs_extra.default.ensureDir(projectDir);
404
416
  await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src"));
405
417
  await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "schemas"));
@@ -1073,6 +1085,1127 @@ export function registerPostRoutes(app: Kozo) {
1073
1085
  `;
1074
1086
  await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "routes", "posts", "index.ts"), postRoutes);
1075
1087
  }
1088
+ async function scaffoldApiOnlyTemplate(projectDir, projectName, kozoCoreDep, runtime) {
1089
+ await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src"));
1090
+ const packageJson = {
1091
+ name: projectName,
1092
+ version: "1.0.0",
1093
+ type: "module",
1094
+ scripts: {
1095
+ dev: runtime === "bun" ? "bun --watch src/index.ts" : "tsx watch src/index.ts",
1096
+ build: "tsc",
1097
+ start: runtime === "bun" ? "bun src/index.ts" : "node dist/index.js"
1098
+ },
1099
+ dependencies: {
1100
+ "@kozojs/core": kozoCoreDep,
1101
+ hono: "^4.6.0",
1102
+ zod: "^3.23.0",
1103
+ ...runtime === "node" && { "@hono/node-server": "^1.13.0" }
1104
+ },
1105
+ devDependencies: {
1106
+ "@types/node": "^22.0.0",
1107
+ ...runtime !== "bun" && { tsx: "^4.19.0" },
1108
+ typescript: "^5.6.0"
1109
+ }
1110
+ };
1111
+ await import_fs_extra.default.writeJSON(import_node_path.default.join(projectDir, "package.json"), packageJson, { spaces: 2 });
1112
+ const tsconfig = {
1113
+ compilerOptions: {
1114
+ target: "ES2022",
1115
+ module: "ESNext",
1116
+ moduleResolution: "bundler",
1117
+ strict: true,
1118
+ esModuleInterop: true,
1119
+ skipLibCheck: true,
1120
+ outDir: "dist",
1121
+ rootDir: "src"
1122
+ },
1123
+ include: ["src/**/*"],
1124
+ exclude: ["node_modules", "dist"]
1125
+ };
1126
+ await import_fs_extra.default.writeJSON(import_node_path.default.join(projectDir, "tsconfig.json"), tsconfig, { spaces: 2 });
1127
+ const indexTs = `import { createKozo } from '@kozojs/core';
1128
+ import { z } from 'zod';
1129
+
1130
+ const app = createKozo({ port: 3000 });
1131
+
1132
+ // Health check
1133
+ app.get('/health', {}, () => ({
1134
+ status: 'ok',
1135
+ timestamp: new Date().toISOString(),
1136
+ }));
1137
+
1138
+ // Example endpoint with validation
1139
+ app.get('/hello/:name', {
1140
+ params: z.object({ name: z.string() }),
1141
+ response: z.object({ message: z.string() }),
1142
+ }, (c) => ({
1143
+ message: \`Hello, \${c.params.name}!\`,
1144
+ }));
1145
+
1146
+ console.log('\u{1F525} Kozo running on http://localhost:3000');
1147
+ app.listen();
1148
+ `;
1149
+ await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "index.ts"), indexTs);
1150
+ await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, ".gitignore"), "node_modules/\ndist/\n.env\n");
1151
+ }
1152
+ async function createDockerfile(projectDir, runtime) {
1153
+ const dockerfile = runtime === "bun" ? `FROM oven/bun:1 as builder
1154
+ WORKDIR /app
1155
+ COPY package.json bun.lockb* ./
1156
+ RUN bun install --frozen-lockfile
1157
+
1158
+ COPY . .
1159
+ RUN bun run build
1160
+
1161
+ FROM oven/bun:1-slim
1162
+ WORKDIR /app
1163
+ COPY --from=builder /app/dist ./dist
1164
+ COPY --from=builder /app/package.json ./
1165
+ EXPOSE 3000
1166
+ CMD ["bun", "dist/index.js"]
1167
+ ` : `FROM node:20-alpine as builder
1168
+ WORKDIR /app
1169
+ COPY package*.json ./
1170
+ RUN npm ci
1171
+
1172
+ COPY . .
1173
+ RUN npm run build
1174
+
1175
+ FROM node:20-alpine
1176
+ WORKDIR /app
1177
+ COPY --from=builder /app/dist ./dist
1178
+ COPY --from=builder /app/package*.json ./
1179
+ RUN npm ci --omit=dev
1180
+ EXPOSE 3000
1181
+ CMD ["node", "dist/index.js"]
1182
+ `;
1183
+ await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "Dockerfile"), dockerfile);
1184
+ await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, ".dockerignore"), "node_modules\ndist\n.git\n");
1185
+ }
1186
+ async function createGitHubActions(projectDir) {
1187
+ await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, ".github", "workflows"));
1188
+ const workflow = `name: CI
1189
+
1190
+ on:
1191
+ push:
1192
+ branches: [main]
1193
+ pull_request:
1194
+ branches: [main]
1195
+
1196
+ jobs:
1197
+ build:
1198
+ runs-on: ubuntu-latest
1199
+ steps:
1200
+ - uses: actions/checkout@v4
1201
+ - uses: actions/setup-node@v4
1202
+ with:
1203
+ node-version: '20'
1204
+ cache: 'npm'
1205
+ - run: npm ci
1206
+ - run: npm run build
1207
+ - run: npm test --if-present
1208
+ `;
1209
+ await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, ".github", "workflows", "ci.yml"), workflow);
1210
+ }
1211
+ async function scaffoldFullstackProject(projectDir, projectName, kozoCoreDep, runtime, database, frontend, extras, template) {
1212
+ await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "apps", "api", "src", "routes"));
1213
+ await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "apps", "api", "src", "data"));
1214
+ await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "apps", "web", "src", "lib"));
1215
+ await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, ".vscode"));
1216
+ const rootPackageJson = {
1217
+ name: projectName,
1218
+ private: true,
1219
+ scripts: {
1220
+ dev: "pnpm run --parallel dev",
1221
+ build: "pnpm run --recursive build"
1222
+ }
1223
+ };
1224
+ await import_fs_extra.default.writeJSON(import_node_path.default.join(projectDir, "package.json"), rootPackageJson, { spaces: 2 });
1225
+ await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "pnpm-workspace.yaml"), `packages:
1226
+ - 'apps/*'
1227
+ `);
1228
+ await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, ".gitignore"), "node_modules/\ndist/\n.env\n*.log\n");
1229
+ await scaffoldFullstackApi(projectDir, projectName, kozoCoreDep, runtime);
1230
+ await scaffoldFullstackWeb(projectDir, projectName, frontend);
1231
+ await scaffoldFullstackReadme(projectDir, projectName);
1232
+ if (extras.includes("docker")) await createDockerfile(import_node_path.default.join(projectDir, "apps", "api"), runtime);
1233
+ if (extras.includes("github-actions")) await createGitHubActions(projectDir);
1234
+ }
1235
+ async function scaffoldFullstackApi(projectDir, projectName, kozoCoreDep, runtime) {
1236
+ const apiDir = import_node_path.default.join(projectDir, "apps", "api");
1237
+ const apiPackageJson = {
1238
+ name: `@${projectName}/api`,
1239
+ version: "1.0.0",
1240
+ type: "module",
1241
+ scripts: {
1242
+ dev: runtime === "bun" ? "bun --watch src/index.ts" : "tsx watch src/index.ts",
1243
+ build: "tsc"
1244
+ },
1245
+ dependencies: {
1246
+ "@kozojs/core": kozoCoreDep,
1247
+ hono: "^4.6.0",
1248
+ zod: "^3.23.0",
1249
+ ...runtime === "node" && { "@hono/node-server": "^1.13.0" }
1250
+ },
1251
+ devDependencies: {
1252
+ "@types/node": "^22.0.0",
1253
+ ...runtime !== "bun" && { tsx: "^4.19.0" },
1254
+ typescript: "^5.6.0"
1255
+ }
1256
+ };
1257
+ await import_fs_extra.default.writeJSON(import_node_path.default.join(apiDir, "package.json"), apiPackageJson, { spaces: 2 });
1258
+ const tsconfig = {
1259
+ compilerOptions: {
1260
+ target: "ES2022",
1261
+ module: "ESNext",
1262
+ moduleResolution: "bundler",
1263
+ strict: true,
1264
+ esModuleInterop: true,
1265
+ skipLibCheck: true,
1266
+ outDir: "dist",
1267
+ rootDir: "src"
1268
+ },
1269
+ include: ["src/**/*"],
1270
+ exclude: ["node_modules", "dist"]
1271
+ };
1272
+ await import_fs_extra.default.writeJSON(import_node_path.default.join(apiDir, "tsconfig.json"), tsconfig, { spaces: 2 });
1273
+ await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "index.ts"), `import { createKozo } from '@kozojs/core';
1274
+ import { registerRoutes } from './routes';
1275
+
1276
+ const app = createKozo({ port: 3000 });
1277
+
1278
+ registerRoutes(app);
1279
+
1280
+ export type AppType = typeof app;
1281
+
1282
+ console.log('\u{1F525} ${projectName} API running on http://localhost:3000');
1283
+ console.log('\u{1F4DA} Endpoints: /api/health, /api/users, /api/posts, /api/tasks, /api/stats');
1284
+ app.listen();
1285
+ `);
1286
+ await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "data", "index.ts"), `import { z } from 'zod';
1287
+
1288
+ export const UserSchema = z.object({
1289
+ id: z.string(),
1290
+ name: z.string(),
1291
+ email: z.string().email(),
1292
+ role: z.enum(['admin', 'user']),
1293
+ createdAt: z.string().optional(),
1294
+ });
1295
+
1296
+ export const PostSchema = z.object({
1297
+ id: z.string(),
1298
+ title: z.string(),
1299
+ content: z.string(),
1300
+ authorId: z.string(),
1301
+ published: z.boolean(),
1302
+ createdAt: z.string().optional(),
1303
+ });
1304
+
1305
+ export const TaskSchema = z.object({
1306
+ id: z.string(),
1307
+ title: z.string(),
1308
+ completed: z.boolean(),
1309
+ priority: z.enum(['low', 'medium', 'high']),
1310
+ createdAt: z.string(),
1311
+ });
1312
+
1313
+ export const users: z.infer<typeof UserSchema>[] = [
1314
+ { id: '1', name: 'Alice', email: 'alice@example.com', role: 'admin', createdAt: new Date().toISOString() },
1315
+ { id: '2', name: 'Bob', email: 'bob@example.com', role: 'user', createdAt: new Date().toISOString() },
1316
+ ];
1317
+
1318
+ export const posts: z.infer<typeof PostSchema>[] = [
1319
+ { id: '1', title: 'Hello World', content: 'First post!', authorId: '1', published: true, createdAt: new Date().toISOString() },
1320
+ { id: '2', title: 'Draft', content: 'Work in progress', authorId: '2', published: false, createdAt: new Date().toISOString() },
1321
+ ];
1322
+
1323
+ export const tasks: z.infer<typeof TaskSchema>[] = [
1324
+ { id: '1', title: 'Setup project', completed: true, priority: 'high', createdAt: new Date().toISOString() },
1325
+ { id: '2', title: 'Write tests', completed: false, priority: 'medium', createdAt: new Date().toISOString() },
1326
+ { id: '3', title: 'Deploy', completed: false, priority: 'low', createdAt: new Date().toISOString() },
1327
+ ];
1328
+ `);
1329
+ await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "routes", "index.ts"), `import type { Kozo } from '@kozojs/core';
1330
+ import { registerHealthRoutes } from './health';
1331
+ import { registerUserRoutes } from './users';
1332
+ import { registerPostRoutes } from './posts';
1333
+ import { registerTaskRoutes } from './tasks';
1334
+ import { registerToolRoutes } from './tools';
1335
+
1336
+ export function registerRoutes(app: Kozo) {
1337
+ registerHealthRoutes(app);
1338
+ registerUserRoutes(app);
1339
+ registerPostRoutes(app);
1340
+ registerTaskRoutes(app);
1341
+ registerToolRoutes(app);
1342
+ }
1343
+ `);
1344
+ await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "routes", "health.ts"), `import type { Kozo } from '@kozojs/core';
1345
+ import { users, posts, tasks } from '../data';
1346
+
1347
+ export function registerHealthRoutes(app: Kozo) {
1348
+ app.get('/api/health', {}, () => ({
1349
+ status: 'ok',
1350
+ timestamp: new Date().toISOString(),
1351
+ version: '1.0.0',
1352
+ uptime: process.uptime(),
1353
+ }));
1354
+
1355
+ app.get('/api/stats', {}, () => ({
1356
+ users: users.length,
1357
+ posts: posts.length,
1358
+ tasks: tasks.length,
1359
+ publishedPosts: posts.filter(p => p.published).length,
1360
+ completedTasks: tasks.filter(t => t.completed).length,
1361
+ }));
1362
+ }
1363
+ `);
1364
+ await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "routes", "users.ts"), `import type { Kozo } from '@kozojs/core';
1365
+ import { z } from 'zod';
1366
+ import { users, UserSchema } from '../data';
1367
+
1368
+ export function registerUserRoutes(app: Kozo) {
1369
+ app.get('/api/users', {
1370
+ response: z.array(UserSchema),
1371
+ }, () => users);
1372
+
1373
+ app.get('/api/users/:id', {
1374
+ params: z.object({ id: z.string() }),
1375
+ response: UserSchema,
1376
+ }, (c) => {
1377
+ const user = users.find(u => u.id === c.params.id);
1378
+ if (!user) throw new Error('User not found');
1379
+ return user;
1380
+ });
1381
+
1382
+ app.post('/api/users', {
1383
+ body: z.object({
1384
+ name: z.string().min(1),
1385
+ email: z.string().email(),
1386
+ role: z.enum(['admin', 'user']).optional(),
1387
+ }),
1388
+ response: UserSchema,
1389
+ }, (c) => {
1390
+ const newUser = {
1391
+ id: String(Date.now()),
1392
+ name: c.body.name,
1393
+ email: c.body.email,
1394
+ role: c.body.role || 'user' as const,
1395
+ createdAt: new Date().toISOString(),
1396
+ };
1397
+ users.push(newUser);
1398
+ return newUser;
1399
+ });
1400
+
1401
+ app.put('/api/users/:id', {
1402
+ params: z.object({ id: z.string() }),
1403
+ body: z.object({
1404
+ name: z.string().min(1).optional(),
1405
+ email: z.string().email().optional(),
1406
+ role: z.enum(['admin', 'user']).optional(),
1407
+ }),
1408
+ response: UserSchema,
1409
+ }, (c) => {
1410
+ const idx = users.findIndex(u => u.id === c.params.id);
1411
+ if (idx === -1) throw new Error('User not found');
1412
+ users[idx] = { ...users[idx], ...c.body };
1413
+ return users[idx];
1414
+ });
1415
+
1416
+ app.delete('/api/users/:id', {
1417
+ params: z.object({ id: z.string() }),
1418
+ }, (c) => {
1419
+ const idx = users.findIndex(u => u.id === c.params.id);
1420
+ if (idx === -1) throw new Error('User not found');
1421
+ users.splice(idx, 1);
1422
+ return { success: true, message: 'User deleted' };
1423
+ });
1424
+ }
1425
+ `);
1426
+ await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "routes", "posts.ts"), `import type { Kozo } from '@kozojs/core';
1427
+ import { z } from 'zod';
1428
+ import { posts, PostSchema } from '../data';
1429
+
1430
+ export function registerPostRoutes(app: Kozo) {
1431
+ app.get('/api/posts', {
1432
+ query: z.object({ published: z.coerce.boolean().optional() }),
1433
+ response: z.array(PostSchema),
1434
+ }, (c) => {
1435
+ if (c.query.published !== undefined) {
1436
+ return posts.filter(p => p.published === c.query.published);
1437
+ }
1438
+ return posts;
1439
+ });
1440
+
1441
+ app.get('/api/posts/:id', {
1442
+ params: z.object({ id: z.string() }),
1443
+ response: PostSchema,
1444
+ }, (c) => {
1445
+ const post = posts.find(p => p.id === c.params.id);
1446
+ if (!post) throw new Error('Post not found');
1447
+ return post;
1448
+ });
1449
+
1450
+ app.post('/api/posts', {
1451
+ body: z.object({
1452
+ title: z.string().min(1),
1453
+ content: z.string(),
1454
+ authorId: z.string(),
1455
+ published: z.boolean().optional(),
1456
+ }),
1457
+ response: PostSchema,
1458
+ }, (c) => {
1459
+ const newPost = {
1460
+ id: String(Date.now()),
1461
+ title: c.body.title,
1462
+ content: c.body.content,
1463
+ authorId: c.body.authorId,
1464
+ published: c.body.published ?? false,
1465
+ createdAt: new Date().toISOString(),
1466
+ };
1467
+ posts.push(newPost);
1468
+ return newPost;
1469
+ });
1470
+
1471
+ app.put('/api/posts/:id', {
1472
+ params: z.object({ id: z.string() }),
1473
+ body: z.object({
1474
+ title: z.string().min(1).optional(),
1475
+ content: z.string().optional(),
1476
+ published: z.boolean().optional(),
1477
+ }),
1478
+ response: PostSchema,
1479
+ }, (c) => {
1480
+ const idx = posts.findIndex(p => p.id === c.params.id);
1481
+ if (idx === -1) throw new Error('Post not found');
1482
+ posts[idx] = { ...posts[idx], ...c.body };
1483
+ return posts[idx];
1484
+ });
1485
+
1486
+ app.delete('/api/posts/:id', {
1487
+ params: z.object({ id: z.string() }),
1488
+ }, (c) => {
1489
+ const idx = posts.findIndex(p => p.id === c.params.id);
1490
+ if (idx === -1) throw new Error('Post not found');
1491
+ posts.splice(idx, 1);
1492
+ return { success: true, message: 'Post deleted' };
1493
+ });
1494
+ }
1495
+ `);
1496
+ await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "routes", "tasks.ts"), `import type { Kozo } from '@kozojs/core';
1497
+ import { z } from 'zod';
1498
+ import { tasks, TaskSchema } from '../data';
1499
+
1500
+ export function registerTaskRoutes(app: Kozo) {
1501
+ app.get('/api/tasks', {
1502
+ query: z.object({
1503
+ completed: z.coerce.boolean().optional(),
1504
+ priority: z.enum(['low', 'medium', 'high']).optional(),
1505
+ }),
1506
+ response: z.array(TaskSchema),
1507
+ }, (c) => {
1508
+ let result = [...tasks];
1509
+ if (c.query.completed !== undefined) {
1510
+ result = result.filter(t => t.completed === c.query.completed);
1511
+ }
1512
+ if (c.query.priority) {
1513
+ result = result.filter(t => t.priority === c.query.priority);
1514
+ }
1515
+ return result;
1516
+ });
1517
+
1518
+ app.get('/api/tasks/:id', {
1519
+ params: z.object({ id: z.string() }),
1520
+ response: TaskSchema,
1521
+ }, (c) => {
1522
+ const task = tasks.find(t => t.id === c.params.id);
1523
+ if (!task) throw new Error('Task not found');
1524
+ return task;
1525
+ });
1526
+
1527
+ app.post('/api/tasks', {
1528
+ body: z.object({
1529
+ title: z.string().min(1),
1530
+ priority: z.enum(['low', 'medium', 'high']).optional(),
1531
+ }),
1532
+ response: TaskSchema,
1533
+ }, (c) => {
1534
+ const newTask = {
1535
+ id: String(Date.now()),
1536
+ title: c.body.title,
1537
+ completed: false,
1538
+ priority: c.body.priority || 'medium' as const,
1539
+ createdAt: new Date().toISOString(),
1540
+ };
1541
+ tasks.push(newTask);
1542
+ return newTask;
1543
+ });
1544
+
1545
+ app.put('/api/tasks/:id', {
1546
+ params: z.object({ id: z.string() }),
1547
+ body: z.object({
1548
+ title: z.string().min(1).optional(),
1549
+ completed: z.boolean().optional(),
1550
+ priority: z.enum(['low', 'medium', 'high']).optional(),
1551
+ }),
1552
+ response: TaskSchema,
1553
+ }, (c) => {
1554
+ const idx = tasks.findIndex(t => t.id === c.params.id);
1555
+ if (idx === -1) throw new Error('Task not found');
1556
+ tasks[idx] = { ...tasks[idx], ...c.body };
1557
+ return tasks[idx];
1558
+ });
1559
+
1560
+ app.patch('/api/tasks/:id/toggle', {
1561
+ params: z.object({ id: z.string() }),
1562
+ response: TaskSchema,
1563
+ }, (c) => {
1564
+ const idx = tasks.findIndex(t => t.id === c.params.id);
1565
+ if (idx === -1) throw new Error('Task not found');
1566
+ tasks[idx].completed = !tasks[idx].completed;
1567
+ return tasks[idx];
1568
+ });
1569
+
1570
+ app.delete('/api/tasks/:id', {
1571
+ params: z.object({ id: z.string() }),
1572
+ }, (c) => {
1573
+ const idx = tasks.findIndex(t => t.id === c.params.id);
1574
+ if (idx === -1) throw new Error('Task not found');
1575
+ tasks.splice(idx, 1);
1576
+ return { success: true, message: 'Task deleted' };
1577
+ });
1578
+ }
1579
+ `);
1580
+ await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "routes", "tools.ts"), `import type { Kozo } from '@kozojs/core';
1581
+ import { z } from 'zod';
1582
+
1583
+ export function registerToolRoutes(app: Kozo) {
1584
+ app.get('/api/echo', {
1585
+ query: z.object({ message: z.string() }),
1586
+ }, (c) => ({
1587
+ echo: c.query.message,
1588
+ timestamp: new Date().toISOString(),
1589
+ }));
1590
+
1591
+ app.post('/api/validate', {
1592
+ body: z.object({
1593
+ email: z.string().email(),
1594
+ age: z.number().min(0).max(150),
1595
+ }),
1596
+ }, (c) => ({
1597
+ valid: true,
1598
+ data: c.body,
1599
+ }));
1600
+ }
1601
+ `);
1602
+ }
1603
+ async function scaffoldFullstackWeb(projectDir, projectName, frontend) {
1604
+ const webDir = import_node_path.default.join(projectDir, "apps", "web");
1605
+ const packageJson = {
1606
+ name: `@${projectName}/web`,
1607
+ version: "1.0.0",
1608
+ type: "module",
1609
+ scripts: {
1610
+ dev: "vite",
1611
+ build: "vite build",
1612
+ preview: "vite preview"
1613
+ },
1614
+ dependencies: {
1615
+ react: "^18.2.0",
1616
+ "react-dom": "^18.2.0",
1617
+ "@tanstack/react-query": "^5.0.0",
1618
+ hono: "^4.6.0",
1619
+ "lucide-react": "^0.460.0"
1620
+ },
1621
+ devDependencies: {
1622
+ "@types/react": "^18.2.0",
1623
+ "@types/react-dom": "^18.2.0",
1624
+ "@vitejs/plugin-react": "^4.2.0",
1625
+ typescript: "^5.6.0",
1626
+ vite: "^5.0.0",
1627
+ "@tailwindcss/vite": "^4.0.0",
1628
+ tailwindcss: "^4.0.0"
1629
+ }
1630
+ };
1631
+ await import_fs_extra.default.writeJSON(import_node_path.default.join(webDir, "package.json"), packageJson, { spaces: 2 });
1632
+ await import_fs_extra.default.writeFile(import_node_path.default.join(webDir, "vite.config.ts"), `import { defineConfig } from 'vite';
1633
+ import react from '@vitejs/plugin-react';
1634
+ import tailwindcss from '@tailwindcss/vite';
1635
+
1636
+ export default defineConfig({
1637
+ plugins: [react(), tailwindcss()],
1638
+ server: {
1639
+ proxy: {
1640
+ '/api': 'http://localhost:3000',
1641
+ },
1642
+ },
1643
+ });
1644
+ `);
1645
+ await import_fs_extra.default.writeFile(import_node_path.default.join(webDir, "index.html"), `<!DOCTYPE html>
1646
+ <html lang="en">
1647
+ <head>
1648
+ <meta charset="UTF-8" />
1649
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1650
+ <title>${projectName}</title>
1651
+ </head>
1652
+ <body>
1653
+ <div id="root"></div>
1654
+ <script type="module" src="/src/main.tsx"></script>
1655
+ </body>
1656
+ </html>
1657
+ `);
1658
+ await import_fs_extra.default.writeFile(import_node_path.default.join(webDir, "src", "index.css"), `@import "tailwindcss";
1659
+
1660
+ body {
1661
+ background-color: rgb(15 23 42);
1662
+ color: rgb(241 245 249);
1663
+ }
1664
+ `);
1665
+ await import_fs_extra.default.writeFile(import_node_path.default.join(webDir, "src", "lib", "client.ts"), `import { hc } from 'hono/client';
1666
+ import type { AppType } from '@${projectName}/api';
1667
+
1668
+ // Type-safe RPC client - changes in API break frontend at compile time!
1669
+ export const client = hc<AppType>('/');
1670
+
1671
+ /* Usage:
1672
+ const res = await client.api.users.$get();
1673
+ const users = await res.json(); // Fully typed!
1674
+ */
1675
+ `);
1676
+ await import_fs_extra.default.writeFile(import_node_path.default.join(webDir, "src", "main.tsx"), `import React from 'react';
1677
+ import ReactDOM from 'react-dom/client';
1678
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
1679
+ import App from './App';
1680
+ import './index.css';
1681
+
1682
+ const queryClient = new QueryClient({
1683
+ defaultOptions: {
1684
+ queries: {
1685
+ refetchOnWindowFocus: false,
1686
+ retry: 1,
1687
+ },
1688
+ },
1689
+ });
1690
+
1691
+ ReactDOM.createRoot(document.getElementById('root')!).render(
1692
+ <React.StrictMode>
1693
+ <QueryClientProvider client={queryClient}>
1694
+ <App />
1695
+ </QueryClientProvider>
1696
+ </React.StrictMode>
1697
+ );
1698
+ `);
1699
+ await import_fs_extra.default.writeFile(import_node_path.default.join(webDir, "src", "App.tsx"), generateFullReactApp(projectName));
1700
+ }
1701
+ function generateFullReactApp(projectName) {
1702
+ return `import { useState } from 'react';
1703
+ import { useQuery, useQueryClient } from '@tanstack/react-query';
1704
+ import {
1705
+ Activity, Users, FileText, CheckSquare, Send, Trash2, Edit2,
1706
+ Plus, Server, Zap, Heart
1707
+ } from 'lucide-react';
1708
+
1709
+ const API_BASE = '/api';
1710
+
1711
+ type Tab = 'health' | 'users' | 'posts' | 'tasks' | 'tools';
1712
+
1713
+ interface ApiResponse {
1714
+ status: number;
1715
+ data: unknown;
1716
+ time: number;
1717
+ }
1718
+
1719
+ async function fetchApi(endpoint: string, options?: RequestInit): Promise<ApiResponse> {
1720
+ const start = performance.now();
1721
+ const res = await fetch(\`\${API_BASE}\${endpoint}\`, {
1722
+ headers: { 'Content-Type': 'application/json' },
1723
+ ...options,
1724
+ });
1725
+ const time = Math.round(performance.now() - start);
1726
+ const data = await res.json();
1727
+ return { status: res.status, data, time };
1728
+ }
1729
+
1730
+ function Badge({ children, variant = 'default' }: { children: React.ReactNode; variant?: 'default' | 'success' | 'warning' | 'error' }) {
1731
+ const colors = {
1732
+ default: 'bg-slate-700 text-slate-200',
1733
+ success: 'bg-emerald-900 text-emerald-300',
1734
+ warning: 'bg-amber-900 text-amber-300',
1735
+ error: 'bg-red-900 text-red-300',
1736
+ };
1737
+ return <span className={\`px-2 py-0.5 rounded text-xs font-medium \${colors[variant]}\`}>{children}</span>;
1738
+ }
1739
+
1740
+ function Card({ title, icon: Icon, children }: { title: string; icon: React.ElementType; children: React.ReactNode }) {
1741
+ return (
1742
+ <div className="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
1743
+ <div className="px-4 py-3 border-b border-slate-700 flex items-center gap-2">
1744
+ <Icon className="w-4 h-4 text-blue-400" />
1745
+ <h3 className="font-medium text-slate-200">{title}</h3>
1746
+ </div>
1747
+ <div className="p-4">{children}</div>
1748
+ </div>
1749
+ );
1750
+ }
1751
+
1752
+ function ResponseDisplay({ response, loading }: { response: ApiResponse | null; loading: boolean }) {
1753
+ if (loading) return <div className="text-slate-400 text-sm">Loading...</div>;
1754
+ if (!response) return null;
1755
+
1756
+ const isSuccess = response.status >= 200 && response.status < 300;
1757
+ return (
1758
+ <div className="mt-3 p-3 bg-slate-900 rounded border border-slate-700">
1759
+ <div className="flex items-center gap-2 mb-2">
1760
+ <Badge variant={isSuccess ? 'success' : 'error'}>{response.status}</Badge>
1761
+ <span className="text-xs text-slate-500">{response.time}ms</span>
1762
+ </div>
1763
+ <pre className="text-xs text-slate-300 overflow-auto max-h-48">
1764
+ {JSON.stringify(response.data, null, 2)}
1765
+ </pre>
1766
+ </div>
1767
+ );
1768
+ }
1769
+
1770
+ function HealthPanel() {
1771
+ const [response, setResponse] = useState<ApiResponse | null>(null);
1772
+ const [loading, setLoading] = useState(false);
1773
+
1774
+ const checkHealth = async () => {
1775
+ setLoading(true);
1776
+ const res = await fetchApi('/health');
1777
+ setResponse(res);
1778
+ setLoading(false);
1779
+ };
1780
+
1781
+ const checkStats = async () => {
1782
+ setLoading(true);
1783
+ const res = await fetchApi('/stats');
1784
+ setResponse(res);
1785
+ setLoading(false);
1786
+ };
1787
+
1788
+ return (
1789
+ <Card title="Health & Stats" icon={Heart}>
1790
+ <div className="flex gap-2 mb-3">
1791
+ <button onClick={checkHealth} className="px-3 py-1.5 bg-emerald-600 hover:bg-emerald-700 rounded text-sm font-medium transition">
1792
+ Check Health
1793
+ </button>
1794
+ <button onClick={checkStats} className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 rounded text-sm font-medium transition">
1795
+ Get Stats
1796
+ </button>
1797
+ </div>
1798
+ <ResponseDisplay response={response} loading={loading} />
1799
+ </Card>
1800
+ );
1801
+ }
1802
+
1803
+ function UsersPanel() {
1804
+ const queryClient = useQueryClient();
1805
+ const [response, setResponse] = useState<ApiResponse | null>(null);
1806
+ const [loading, setLoading] = useState(false);
1807
+ const [newUser, setNewUser] = useState({ name: '', email: '', role: 'user' as const });
1808
+
1809
+ const { data: users, isLoading } = useQuery({
1810
+ queryKey: ['users'],
1811
+ queryFn: async () => {
1812
+ const res = await fetchApi('/users');
1813
+ return res.data as Array<{ id: string; name: string; email: string; role: string }>;
1814
+ },
1815
+ });
1816
+
1817
+ const createUser = async () => {
1818
+ if (!newUser.name || !newUser.email) return;
1819
+ setLoading(true);
1820
+ const res = await fetchApi('/users', {
1821
+ method: 'POST',
1822
+ body: JSON.stringify(newUser),
1823
+ });
1824
+ setResponse(res);
1825
+ setLoading(false);
1826
+ setNewUser({ name: '', email: '', role: 'user' });
1827
+ queryClient.invalidateQueries({ queryKey: ['users'] });
1828
+ };
1829
+
1830
+ const deleteUser = async (id: string) => {
1831
+ setLoading(true);
1832
+ const res = await fetchApi(\`/users/\${id}\`, { method: 'DELETE' });
1833
+ setResponse(res);
1834
+ setLoading(false);
1835
+ queryClient.invalidateQueries({ queryKey: ['users'] });
1836
+ };
1837
+
1838
+ return (
1839
+ <Card title="Users" icon={Users}>
1840
+ <div className="space-y-4">
1841
+ <div className="grid grid-cols-3 gap-2">
1842
+ <input
1843
+ placeholder="Name"
1844
+ value={newUser.name}
1845
+ onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
1846
+ className="px-3 py-1.5 bg-slate-900 border border-slate-600 rounded text-sm focus:border-blue-500 outline-none"
1847
+ />
1848
+ <input
1849
+ placeholder="Email"
1850
+ value={newUser.email}
1851
+ onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
1852
+ className="px-3 py-1.5 bg-slate-900 border border-slate-600 rounded text-sm focus:border-blue-500 outline-none"
1853
+ />
1854
+ <button onClick={createUser} disabled={loading} className="px-3 py-1.5 bg-emerald-600 hover:bg-emerald-700 rounded text-sm font-medium transition flex items-center justify-center gap-1">
1855
+ <Plus className="w-4 h-4" /> Add User
1856
+ </button>
1857
+ </div>
1858
+
1859
+ <div className="space-y-2">
1860
+ {isLoading ? (
1861
+ <div className="text-slate-400 text-sm">Loading users...</div>
1862
+ ) : (
1863
+ users?.map((user) => (
1864
+ <div key={user.id} className="flex items-center justify-between p-2 bg-slate-900 rounded border border-slate-700">
1865
+ <div>
1866
+ <span className="font-medium">{user.name}</span>
1867
+ <span className="text-slate-400 text-sm ml-2">{user.email}</span>
1868
+ <Badge variant={user.role === 'admin' ? 'warning' : 'default'}>{user.role}</Badge>
1869
+ </div>
1870
+ <button onClick={() => deleteUser(user.id)} className="p-1 text-red-400 hover:text-red-300 transition">
1871
+ <Trash2 className="w-4 h-4" />
1872
+ </button>
1873
+ </div>
1874
+ ))
1875
+ )}
1876
+ </div>
1877
+
1878
+ <ResponseDisplay response={response} loading={loading} />
1879
+ </div>
1880
+ </Card>
1881
+ );
1882
+ }
1883
+
1884
+ function TasksPanel() {
1885
+ const queryClient = useQueryClient();
1886
+ const [response, setResponse] = useState<ApiResponse | null>(null);
1887
+ const [loading, setLoading] = useState(false);
1888
+ const [newTask, setNewTask] = useState({ title: '', priority: 'medium' as const });
1889
+
1890
+ const { data: tasks, isLoading } = useQuery({
1891
+ queryKey: ['tasks'],
1892
+ queryFn: async () => {
1893
+ const res = await fetchApi('/tasks');
1894
+ return res.data as Array<{ id: string; title: string; completed: boolean; priority: string }>;
1895
+ },
1896
+ });
1897
+
1898
+ const createTask = async () => {
1899
+ if (!newTask.title) return;
1900
+ setLoading(true);
1901
+ const res = await fetchApi('/tasks', {
1902
+ method: 'POST',
1903
+ body: JSON.stringify(newTask),
1904
+ });
1905
+ setResponse(res);
1906
+ setLoading(false);
1907
+ setNewTask({ title: '', priority: 'medium' });
1908
+ queryClient.invalidateQueries({ queryKey: ['tasks'] });
1909
+ };
1910
+
1911
+ const toggleTask = async (id: string) => {
1912
+ setLoading(true);
1913
+ const res = await fetchApi(\`/tasks/\${id}/toggle\`, { method: 'PATCH' });
1914
+ setResponse(res);
1915
+ setLoading(false);
1916
+ queryClient.invalidateQueries({ queryKey: ['tasks'] });
1917
+ };
1918
+
1919
+ const deleteTask = async (id: string) => {
1920
+ setLoading(true);
1921
+ const res = await fetchApi(\`/tasks/\${id}\`, { method: 'DELETE' });
1922
+ setResponse(res);
1923
+ setLoading(false);
1924
+ queryClient.invalidateQueries({ queryKey: ['tasks'] });
1925
+ };
1926
+
1927
+ const priorityColor = (p: string) => {
1928
+ switch (p) {
1929
+ case 'high': return 'error';
1930
+ case 'medium': return 'warning';
1931
+ default: return 'default';
1932
+ }
1933
+ };
1934
+
1935
+ return (
1936
+ <Card title="Tasks" icon={CheckSquare}>
1937
+ <div className="space-y-4">
1938
+ <div className="grid grid-cols-3 gap-2">
1939
+ <input
1940
+ placeholder="Task title"
1941
+ value={newTask.title}
1942
+ onChange={(e) => setNewTask({ ...newTask, title: e.target.value })}
1943
+ className="px-3 py-1.5 bg-slate-900 border border-slate-600 rounded text-sm focus:border-blue-500 outline-none"
1944
+ />
1945
+ <select
1946
+ value={newTask.priority}
1947
+ onChange={(e) => setNewTask({ ...newTask, priority: e.target.value as 'low' | 'medium' | 'high' })}
1948
+ className="px-3 py-1.5 bg-slate-900 border border-slate-600 rounded text-sm focus:border-blue-500 outline-none"
1949
+ >
1950
+ <option value="low">Low</option>
1951
+ <option value="medium">Medium</option>
1952
+ <option value="high">High</option>
1953
+ </select>
1954
+ <button onClick={createTask} disabled={loading} className="px-3 py-1.5 bg-emerald-600 hover:bg-emerald-700 rounded text-sm font-medium transition flex items-center justify-center gap-1">
1955
+ <Plus className="w-4 h-4" /> Add Task
1956
+ </button>
1957
+ </div>
1958
+
1959
+ <div className="space-y-2">
1960
+ {isLoading ? (
1961
+ <div className="text-slate-400 text-sm">Loading tasks...</div>
1962
+ ) : (
1963
+ tasks?.map((task) => (
1964
+ <div key={task.id} className="flex items-center justify-between p-2 bg-slate-900 rounded border border-slate-700">
1965
+ <div className="flex items-center gap-2">
1966
+ <input
1967
+ type="checkbox"
1968
+ checked={task.completed}
1969
+ onChange={() => toggleTask(task.id)}
1970
+ className="rounded"
1971
+ />
1972
+ <span className={task.completed ? 'line-through text-slate-500' : ''}>{task.title}</span>
1973
+ <Badge variant={priorityColor(task.priority) as 'default' | 'success' | 'warning' | 'error'}>{task.priority}</Badge>
1974
+ </div>
1975
+ <button onClick={() => deleteTask(task.id)} className="p-1 text-red-400 hover:text-red-300 transition">
1976
+ <Trash2 className="w-4 h-4" />
1977
+ </button>
1978
+ </div>
1979
+ ))
1980
+ )}
1981
+ </div>
1982
+
1983
+ <ResponseDisplay response={response} loading={loading} />
1984
+ </div>
1985
+ </Card>
1986
+ );
1987
+ }
1988
+
1989
+ function ToolsPanel() {
1990
+ const [echoMessage, setEchoMessage] = useState('');
1991
+ const [validateData, setValidateData] = useState({ email: '', age: '' });
1992
+ const [response, setResponse] = useState<ApiResponse | null>(null);
1993
+ const [loading, setLoading] = useState(false);
1994
+
1995
+ const testEcho = async () => {
1996
+ if (!echoMessage) return;
1997
+ setLoading(true);
1998
+ const res = await fetchApi(\`/echo?message=\${encodeURIComponent(echoMessage)}\`);
1999
+ setResponse(res);
2000
+ setLoading(false);
2001
+ };
2002
+
2003
+ const testValidate = async () => {
2004
+ setLoading(true);
2005
+ const res = await fetchApi('/validate', {
2006
+ method: 'POST',
2007
+ body: JSON.stringify({
2008
+ email: validateData.email,
2009
+ age: parseInt(validateData.age) || 0,
2010
+ }),
2011
+ });
2012
+ setResponse(res);
2013
+ setLoading(false);
2014
+ };
2015
+
2016
+ return (
2017
+ <Card title="Test Tools" icon={Zap}>
2018
+ <div className="space-y-6">
2019
+ <div>
2020
+ <h4 className="text-sm font-medium text-slate-300 mb-2">Echo Endpoint</h4>
2021
+ <div className="flex gap-2">
2022
+ <input
2023
+ placeholder="Message to echo"
2024
+ value={echoMessage}
2025
+ onChange={(e) => setEchoMessage(e.target.value)}
2026
+ className="flex-1 px-3 py-1.5 bg-slate-900 border border-slate-600 rounded text-sm focus:border-blue-500 outline-none"
2027
+ />
2028
+ <button onClick={testEcho} className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 rounded text-sm font-medium transition flex items-center gap-1">
2029
+ <Send className="w-4 h-4" /> Echo
2030
+ </button>
2031
+ </div>
2032
+ </div>
2033
+
2034
+ <div>
2035
+ <h4 className="text-sm font-medium text-slate-300 mb-2">Validation Endpoint</h4>
2036
+ <div className="flex gap-2">
2037
+ <input
2038
+ placeholder="Email"
2039
+ value={validateData.email}
2040
+ onChange={(e) => setValidateData({ ...validateData, email: e.target.value })}
2041
+ className="flex-1 px-3 py-1.5 bg-slate-900 border border-slate-600 rounded text-sm focus:border-blue-500 outline-none"
2042
+ />
2043
+ <input
2044
+ placeholder="Age"
2045
+ type="number"
2046
+ value={validateData.age}
2047
+ onChange={(e) => setValidateData({ ...validateData, age: e.target.value })}
2048
+ className="w-24 px-3 py-1.5 bg-slate-900 border border-slate-600 rounded text-sm focus:border-blue-500 outline-none"
2049
+ />
2050
+ <button onClick={testValidate} className="px-3 py-1.5 bg-purple-600 hover:bg-purple-700 rounded text-sm font-medium transition flex items-center gap-1">
2051
+ <CheckSquare className="w-4 h-4" /> Validate
2052
+ </button>
2053
+ </div>
2054
+ </div>
2055
+
2056
+ <ResponseDisplay response={response} loading={loading} />
2057
+ </div>
2058
+ </Card>
2059
+ );
2060
+ }
2061
+
2062
+ export default function App() {
2063
+ const [activeTab, setActiveTab] = useState<Tab>('health');
2064
+
2065
+ const tabs: { id: Tab; label: string; icon: React.ElementType }[] = [
2066
+ { id: 'health', label: 'Health', icon: Activity },
2067
+ { id: 'users', label: 'Users', icon: Users },
2068
+ { id: 'tasks', label: 'Tasks', icon: CheckSquare },
2069
+ { id: 'tools', label: 'Tools', icon: Zap },
2070
+ ];
2071
+
2072
+ return (
2073
+ <div className="min-h-screen p-6">
2074
+ <div className="max-w-4xl mx-auto">
2075
+ <header className="mb-8">
2076
+ <div className="flex items-center gap-3 mb-2">
2077
+ <Server className="w-8 h-8 text-blue-400" />
2078
+ <h1 className="text-3xl font-bold">Kozo API Tester</h1>
2079
+ </div>
2080
+ <p className="text-slate-400">Test the ${projectName} API endpoints with this interactive UI</p>
2081
+ </header>
2082
+
2083
+ <nav className="flex gap-1 mb-6 bg-slate-800 p-1 rounded-lg">
2084
+ {tabs.map(({ id, label, icon: Icon }) => (
2085
+ <button
2086
+ key={id}
2087
+ onClick={() => setActiveTab(id)}
2088
+ className={\`flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition \${
2089
+ activeTab === id
2090
+ ? 'bg-blue-600 text-white'
2091
+ : 'text-slate-400 hover:text-white hover:bg-slate-700'
2092
+ }\`}
2093
+ >
2094
+ <Icon className="w-4 h-4" />
2095
+ {label}
2096
+ </button>
2097
+ ))}
2098
+ </nav>
2099
+
2100
+ <main>
2101
+ {activeTab === 'health' && <HealthPanel />}
2102
+ {activeTab === 'users' && <UsersPanel />}
2103
+ {activeTab === 'tasks' && <TasksPanel />}
2104
+ {activeTab === 'tools' && <ToolsPanel />}
2105
+ </main>
2106
+
2107
+ <footer className="mt-8 pt-6 border-t border-slate-800 text-center text-slate-500 text-sm">
2108
+ Built with Kozo + React + TailwindCSS
2109
+ </footer>
2110
+ </div>
2111
+ </div>
2112
+ );
2113
+ }
2114
+ `;
2115
+ }
2116
+ async function scaffoldFullstackReadme(projectDir, projectName) {
2117
+ const readme = `# ${projectName}
2118
+
2119
+ Full-stack application built with **Kozo** framework and React.
2120
+
2121
+ ## Project Structure
2122
+
2123
+ \`\`\`
2124
+ apps/
2125
+ \u251C\u2500\u2500 api/ # Backend Kozo
2126
+ \u2502 \u2514\u2500\u2500 src/
2127
+ \u2502 \u251C\u2500\u2500 data/ # Schemas e dati in-memory
2128
+ \u2502 \u2502 \u2514\u2500\u2500 index.ts
2129
+ \u2502 \u251C\u2500\u2500 routes/ # Routes organizzate per risorsa
2130
+ \u2502 \u2502 \u251C\u2500\u2500 health.ts # Health check e stats
2131
+ \u2502 \u2502 \u251C\u2500\u2500 users.ts # CRUD Users
2132
+ \u2502 \u2502 \u251C\u2500\u2500 posts.ts # CRUD Posts
2133
+ \u2502 \u2502 \u251C\u2500\u2500 tasks.ts # CRUD Tasks
2134
+ \u2502 \u2502 \u251C\u2500\u2500 tools.ts # Utility endpoints
2135
+ \u2502 \u2502 \u2514\u2500\u2500 index.ts # Route registration
2136
+ \u2502 \u2514\u2500\u2500 index.ts # Entry point
2137
+ \u2514\u2500\u2500 web/ # Frontend React
2138
+ \u2514\u2500\u2500 src/
2139
+ \u251C\u2500\u2500 App.tsx # UI principale con tabs
2140
+ \u251C\u2500\u2500 main.tsx # Entry point
2141
+ \u2514\u2500\u2500 index.css # TailwindCSS v4
2142
+ \`\`\`
2143
+
2144
+ ## Technologies
2145
+
2146
+ - **Backend**: Kozo (Hono-based), TypeScript, Zod
2147
+ - **Frontend**: React 18, TanStack Query, TailwindCSS v4, Lucide Icons
2148
+ - **Build**: Vite, pnpm workspace
2149
+
2150
+ ## Installation
2151
+
2152
+ \`\`\`bash
2153
+ pnpm install
2154
+ \`\`\`
2155
+
2156
+ ## Development
2157
+
2158
+ \`\`\`bash
2159
+ # Start API and Web in parallel
2160
+ pnpm dev
2161
+
2162
+ # Or separately:
2163
+ pnpm --filter @${projectName}/api dev # API on http://localhost:3000
2164
+ pnpm --filter @${projectName}/web dev # Web on http://localhost:5173
2165
+ \`\`\`
2166
+
2167
+ ## API Endpoints
2168
+
2169
+ ### Health & Stats
2170
+ - \`GET /api/health\` - Health check with uptime
2171
+ - \`GET /api/stats\` - Global statistics
2172
+
2173
+ ### Users (Full CRUD)
2174
+ - \`GET /api/users\` - List all users
2175
+ - \`GET /api/users/:id\` - Get user by ID
2176
+ - \`POST /api/users\` - Create user
2177
+ - \`PUT /api/users/:id\` - Update user
2178
+ - \`DELETE /api/users/:id\` - Delete user
2179
+
2180
+ ### Posts (Full CRUD)
2181
+ - \`GET /api/posts?published=true\` - List posts (optional filter)
2182
+ - \`GET /api/posts/:id\` - Get post by ID
2183
+ - \`POST /api/posts\` - Create post
2184
+ - \`PUT /api/posts/:id\` - Update post
2185
+ - \`DELETE /api/posts/:id\` - Delete post
2186
+
2187
+ ### Tasks (Full CRUD)
2188
+ - \`GET /api/tasks?completed=true&priority=high\` - List tasks (optional filters)
2189
+ - \`GET /api/tasks/:id\` - Get task by ID
2190
+ - \`POST /api/tasks\` - Create task
2191
+ - \`PUT /api/tasks/:id\` - Update task
2192
+ - \`PATCH /api/tasks/:id/toggle\` - Toggle completion
2193
+ - \`DELETE /api/tasks/:id\` - Delete task
2194
+
2195
+ ### Tools
2196
+ - \`GET /api/echo?message=hello\` - Echo test
2197
+ - \`POST /api/validate\` - Zod validation (email + age)
2198
+
2199
+ ## Type Safety
2200
+
2201
+ The frontend uses Hono RPC client for type-safe API calls:
2202
+ \`\`\`typescript
2203
+ const res = await client.api.users.$get();
2204
+ const users = await res.json(); // Fully typed!
2205
+ \`\`\`
2206
+ `;
2207
+ await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "README.md"), readme);
2208
+ }
1076
2209
 
1077
2210
  // src/utils/ascii-art.ts
1078
2211
  var import_picocolors = __toESM(require("picocolors"));
@@ -1112,39 +2245,64 @@ async function newCommand(projectName) {
1112
2245
  }
1113
2246
  });
1114
2247
  },
2248
+ runtime: () => p.select({
2249
+ message: "Target runtime",
2250
+ options: [
2251
+ { value: "node", label: "Node.js / Docker", hint: "Maximum compatibility (default)" },
2252
+ { value: "cloudflare", label: "Cloudflare Workers", hint: "Edge-native, global deployment" },
2253
+ { value: "bun", label: "Bun", hint: "Maximum local speed" }
2254
+ ],
2255
+ initialValue: "node"
2256
+ }),
1115
2257
  template: () => p.select({
1116
2258
  message: "Template",
1117
2259
  options: [
1118
2260
  { value: "complete", label: "Complete Server", hint: "Full production-ready app (Auth, CRUD, Stats)" },
1119
2261
  { value: "starter", label: "Starter", hint: "Minimal setup with database" },
1120
- { value: "saas", label: "SaaS", hint: "Auth + Stripe + Email (coming soon)" },
1121
- { value: "ecommerce", label: "E-commerce", hint: "Products + Orders (coming soon)" }
2262
+ { value: "api-only", label: "API Only", hint: "Minimal, no database" }
1122
2263
  ]
1123
2264
  }),
1124
2265
  database: ({ results }) => {
1125
- if (results.template === "complete") {
2266
+ if (results.template === "complete" || results.template === "api-only") {
1126
2267
  return Promise.resolve("none");
1127
2268
  }
1128
2269
  return p.select({
1129
- message: "Database provider",
2270
+ message: "Database",
1130
2271
  options: [
1131
- { value: "postgresql", label: "PostgreSQL", hint: "Neon, Supabase, Railway" },
1132
- { value: "mysql", label: "MySQL", hint: "PlanetScale" },
1133
- { value: "sqlite", label: "SQLite", hint: "Turso, local dev" }
2272
+ { value: "sqlite", label: "SQLite / Turso", hint: "Perfect for Edge and local dev" },
2273
+ { value: "postgresql", label: "PostgreSQL + Drizzle", hint: "Standard Enterprise" },
2274
+ { value: "mysql", label: "MySQL + Drizzle", hint: "PlanetScale compatible" }
1134
2275
  ]
1135
2276
  });
1136
2277
  },
2278
+ frontend: () => p.select({
2279
+ message: "Frontend",
2280
+ options: [
2281
+ { value: "none", label: "None (API only)", hint: "Backend microservice" },
2282
+ { value: "react", label: "React (Vite + TanStack Query)", hint: "Full-stack type-safe" },
2283
+ { value: "solid", label: "SolidJS (Vite)", hint: "Performance purist choice" },
2284
+ { value: "vue", label: "Vue (Vite)", hint: "Progressive framework" }
2285
+ ],
2286
+ initialValue: "none"
2287
+ }),
2288
+ extras: () => p.multiselect({
2289
+ message: "Extras",
2290
+ options: [
2291
+ { value: "docker", label: "Docker", hint: "Multi-stage Dockerfile" },
2292
+ { value: "github-actions", label: "GitHub Actions", hint: "CI/CD pipeline" },
2293
+ { value: "auth", label: "Authentication", hint: "JWT middleware + login routes" }
2294
+ ],
2295
+ required: false
2296
+ }),
1137
2297
  packageSource: () => p.select({
1138
- message: "Package source for @kozojs/core",
2298
+ message: "Package source",
1139
2299
  options: [
1140
- { value: "npm", label: "npm registry", hint: "Use published version (recommended)" },
1141
- { value: "local", label: "Local workspace", hint: "Link to local monorepo (for development)" }
2300
+ { value: "npm", label: "npm registry", hint: "Published version (recommended)" },
2301
+ { value: "local", label: "Local workspace", hint: "Link to monorepo (dev only)" }
1142
2302
  ],
1143
2303
  initialValue: "npm"
1144
2304
  }),
1145
- install: () => {
1146
- return Promise.resolve(true);
1147
- }
2305
+ install: () => Promise.resolve(true)
1148
2306
  },
1149
2307
  {
1150
2308
  onCancel: () => {
@@ -1158,8 +2316,11 @@ async function newCommand(projectName) {
1158
2316
  try {
1159
2317
  await scaffoldProject({
1160
2318
  projectName: project.name,
1161
- database: project.database,
2319
+ runtime: project.runtime,
1162
2320
  template: project.template,
2321
+ database: project.database,
2322
+ frontend: project.frontend,
2323
+ extras: project.extras,
1163
2324
  packageSource: project.packageSource
1164
2325
  });
1165
2326
  s.stop("Project structure created!");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kozojs/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "CLI to scaffold new Kozo Framework projects - The next-gen TypeScript Backend Framework",
5
5
  "bin": {
6
6
  "create-kozo": "./lib/index.js",