@kozojs/cli 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +1210 -0
- package/package.json +50 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/index.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
|
|
29
|
+
// src/commands/new.ts
|
|
30
|
+
var p = __toESM(require("@clack/prompts"));
|
|
31
|
+
var import_picocolors2 = __toESM(require("picocolors"));
|
|
32
|
+
var import_execa = require("execa");
|
|
33
|
+
|
|
34
|
+
// src/utils/scaffold.ts
|
|
35
|
+
var import_fs_extra = __toESM(require("fs-extra"));
|
|
36
|
+
var import_node_path = __toESM(require("path"));
|
|
37
|
+
async function scaffoldProject(options) {
|
|
38
|
+
const { projectName, database, packageSource, template } = options;
|
|
39
|
+
const projectDir = import_node_path.default.resolve(process.cwd(), projectName);
|
|
40
|
+
const kozoCoreDep = packageSource === "local" ? "workspace:*" : "^0.1.0";
|
|
41
|
+
if (template === "complete") {
|
|
42
|
+
await scaffoldCompleteTemplate(projectDir, projectName, kozoCoreDep);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "routes"));
|
|
46
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "db"));
|
|
47
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "services"));
|
|
48
|
+
const packageJson = {
|
|
49
|
+
name: projectName,
|
|
50
|
+
version: "0.1.0",
|
|
51
|
+
type: "module",
|
|
52
|
+
scripts: {
|
|
53
|
+
dev: "tsx watch src/index.ts",
|
|
54
|
+
build: "tsc",
|
|
55
|
+
start: "node dist/index.js",
|
|
56
|
+
"db:generate": "drizzle-kit generate",
|
|
57
|
+
"db:push": "drizzle-kit push",
|
|
58
|
+
"db:studio": "drizzle-kit studio"
|
|
59
|
+
},
|
|
60
|
+
dependencies: {
|
|
61
|
+
"@kozojs/core": kozoCoreDep,
|
|
62
|
+
hono: "^4.6.0",
|
|
63
|
+
zod: "^3.23.0",
|
|
64
|
+
"drizzle-orm": "^0.36.0",
|
|
65
|
+
...database === "postgresql" && { postgres: "^3.4.0" },
|
|
66
|
+
...database === "mysql" && { mysql2: "^3.11.0" },
|
|
67
|
+
...database === "sqlite" && { "better-sqlite3": "^11.0.0" }
|
|
68
|
+
},
|
|
69
|
+
devDependencies: {
|
|
70
|
+
"@types/node": "^22.0.0",
|
|
71
|
+
tsx: "^4.19.0",
|
|
72
|
+
typescript: "^5.6.0",
|
|
73
|
+
"drizzle-kit": "^0.28.0",
|
|
74
|
+
...database === "sqlite" && { "@types/better-sqlite3": "^7.6.0" }
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
await import_fs_extra.default.writeJSON(import_node_path.default.join(projectDir, "package.json"), packageJson, { spaces: 2 });
|
|
78
|
+
const tsconfig = {
|
|
79
|
+
compilerOptions: {
|
|
80
|
+
target: "ES2022",
|
|
81
|
+
module: "ESNext",
|
|
82
|
+
moduleResolution: "bundler",
|
|
83
|
+
strict: true,
|
|
84
|
+
esModuleInterop: true,
|
|
85
|
+
skipLibCheck: true,
|
|
86
|
+
outDir: "dist",
|
|
87
|
+
rootDir: "src",
|
|
88
|
+
declaration: true
|
|
89
|
+
},
|
|
90
|
+
include: ["src/**/*"],
|
|
91
|
+
exclude: ["node_modules", "dist"]
|
|
92
|
+
};
|
|
93
|
+
await import_fs_extra.default.writeJSON(import_node_path.default.join(projectDir, "tsconfig.json"), tsconfig, { spaces: 2 });
|
|
94
|
+
const drizzleConfig = `import { defineConfig } from 'drizzle-kit';
|
|
95
|
+
|
|
96
|
+
export default defineConfig({
|
|
97
|
+
schema: './src/db/schema.ts',
|
|
98
|
+
out: './drizzle',
|
|
99
|
+
dialect: '${database === "postgresql" ? "postgresql" : database === "mysql" ? "mysql" : "sqlite"}',
|
|
100
|
+
dbCredentials: {
|
|
101
|
+
${database === "sqlite" ? "url: './data.db'" : "url: process.env.DATABASE_URL!"}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
`;
|
|
105
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "drizzle.config.ts"), drizzleConfig);
|
|
106
|
+
const envExample = `# Database
|
|
107
|
+
${database === "sqlite" ? "# SQLite uses local file, no URL needed" : "DATABASE_URL="}
|
|
108
|
+
|
|
109
|
+
# Server
|
|
110
|
+
PORT=3000
|
|
111
|
+
`;
|
|
112
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, ".env.example"), envExample);
|
|
113
|
+
const gitignore = `node_modules/
|
|
114
|
+
dist/
|
|
115
|
+
.env
|
|
116
|
+
*.db
|
|
117
|
+
.turbo/
|
|
118
|
+
`;
|
|
119
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, ".gitignore"), gitignore);
|
|
120
|
+
const indexTs = `import { createKozo } from '@kozojs/core';
|
|
121
|
+
import { services } from './services/index.js';
|
|
122
|
+
|
|
123
|
+
const app = createKozo({
|
|
124
|
+
services,
|
|
125
|
+
port: Number(process.env.PORT) || 3000,
|
|
126
|
+
openapi: {
|
|
127
|
+
info: {
|
|
128
|
+
title: '${projectName} API',
|
|
129
|
+
version: '1.0.0',
|
|
130
|
+
description: 'API documentation for ${projectName}'
|
|
131
|
+
},
|
|
132
|
+
servers: [
|
|
133
|
+
{
|
|
134
|
+
url: 'http://localhost:3000',
|
|
135
|
+
description: 'Development server'
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
app.listen();
|
|
142
|
+
`;
|
|
143
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "index.ts"), indexTs);
|
|
144
|
+
const servicesTs = `import { db } from '../db/index.js';
|
|
145
|
+
|
|
146
|
+
export const services = {
|
|
147
|
+
db
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Type augmentation for autocomplete
|
|
151
|
+
declare module '@kozojs/core' {
|
|
152
|
+
interface Services {
|
|
153
|
+
db: typeof db;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
`;
|
|
157
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "services", "index.ts"), servicesTs);
|
|
158
|
+
const schemaTs = getDatabaseSchema(database);
|
|
159
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "db", "schema.ts"), schemaTs);
|
|
160
|
+
const dbIndexTs = getDatabaseIndex(database);
|
|
161
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "db", "index.ts"), dbIndexTs);
|
|
162
|
+
if (database === "sqlite") {
|
|
163
|
+
const seedTs = getSQLiteSeed();
|
|
164
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "db", "seed.ts"), seedTs);
|
|
165
|
+
}
|
|
166
|
+
await createExampleRoutes(projectDir);
|
|
167
|
+
const readme = `# ${projectName}
|
|
168
|
+
|
|
169
|
+
Built with \u{1F525} **Kozo Framework**
|
|
170
|
+
|
|
171
|
+
## Getting Started
|
|
172
|
+
|
|
173
|
+
\`\`\`bash
|
|
174
|
+
# Install dependencies
|
|
175
|
+
pnpm install
|
|
176
|
+
|
|
177
|
+
# Start development server
|
|
178
|
+
pnpm dev
|
|
179
|
+
\`\`\`
|
|
180
|
+
|
|
181
|
+
The server will start at http://localhost:3000
|
|
182
|
+
|
|
183
|
+
## Try the API
|
|
184
|
+
|
|
185
|
+
\`\`\`bash
|
|
186
|
+
# Get all users
|
|
187
|
+
curl http://localhost:3000/users
|
|
188
|
+
|
|
189
|
+
# Create a new user
|
|
190
|
+
curl -X POST http://localhost:3000/users \\
|
|
191
|
+
-H "Content-Type: application/json" \\
|
|
192
|
+
-d '{"name":"Alice","email":"alice@example.com"}'
|
|
193
|
+
|
|
194
|
+
# Health check
|
|
195
|
+
curl http://localhost:3000
|
|
196
|
+
|
|
197
|
+
# Open Swagger UI
|
|
198
|
+
open http://localhost:3000/swagger
|
|
199
|
+
\`\`\`
|
|
200
|
+
|
|
201
|
+
## API Documentation
|
|
202
|
+
|
|
203
|
+
Once the server is running, visit:
|
|
204
|
+
- **Swagger UI**: http://localhost:3000/swagger
|
|
205
|
+
- **OpenAPI JSON**: http://localhost:3000/doc
|
|
206
|
+
|
|
207
|
+
## Project Structure
|
|
208
|
+
|
|
209
|
+
\`\`\`
|
|
210
|
+
src/
|
|
211
|
+
\u251C\u2500\u2500 db/
|
|
212
|
+
\u2502 \u251C\u2500\u2500 schema.ts # Drizzle schema
|
|
213
|
+
\u2502 \u251C\u2500\u2500 seed.ts # Database initialization${database === "sqlite" ? " (SQLite)" : ""}
|
|
214
|
+
\u2502 \u2514\u2500\u2500 index.ts # Database client
|
|
215
|
+
\u251C\u2500\u2500 routes/
|
|
216
|
+
\u2502 \u251C\u2500\u2500 index.ts # GET /
|
|
217
|
+
\u2502 \u2514\u2500\u2500 users/
|
|
218
|
+
\u2502 \u251C\u2500\u2500 get.ts # GET /users
|
|
219
|
+
\u2502 \u2514\u2500\u2500 post.ts # POST /users
|
|
220
|
+
\u251C\u2500\u2500 services/
|
|
221
|
+
\u2502 \u2514\u2500\u2500 index.ts # Service definitions
|
|
222
|
+
\u2514\u2500\u2500 index.ts # Entry point
|
|
223
|
+
\`\`\`
|
|
224
|
+
|
|
225
|
+
## Database Commands
|
|
226
|
+
|
|
227
|
+
\`\`\`bash
|
|
228
|
+
pnpm db:generate # Generate migrations
|
|
229
|
+
pnpm db:push # Push schema to database
|
|
230
|
+
pnpm db:studio # Open Drizzle Studio
|
|
231
|
+
\`\`\`
|
|
232
|
+
|
|
233
|
+
${database === "sqlite" ? "## SQLite Notes\n\nThe database is automatically initialized with example data on first run.\nDatabase file: `./data.db`\n" : ""}
|
|
234
|
+
## Documentation
|
|
235
|
+
|
|
236
|
+
- [Kozo Docs](https://kozo.dev/docs)
|
|
237
|
+
- [Drizzle ORM](https://orm.drizzle.team)
|
|
238
|
+
- [Hono](https://hono.dev)
|
|
239
|
+
`;
|
|
240
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "README.md"), readme);
|
|
241
|
+
}
|
|
242
|
+
function getDatabaseSchema(database) {
|
|
243
|
+
if (database === "postgresql") {
|
|
244
|
+
return `import { pgTable, uuid, varchar, timestamp } from 'drizzle-orm/pg-core';
|
|
245
|
+
|
|
246
|
+
export const users = pgTable('users', {
|
|
247
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
248
|
+
name: varchar('name', { length: 255 }).notNull(),
|
|
249
|
+
email: varchar('email', { length: 255 }).unique().notNull(),
|
|
250
|
+
createdAt: timestamp('created_at').defaultNow().notNull()
|
|
251
|
+
});
|
|
252
|
+
`;
|
|
253
|
+
}
|
|
254
|
+
if (database === "mysql") {
|
|
255
|
+
return `import { mysqlTable, varchar, timestamp } from 'drizzle-orm/mysql-core';
|
|
256
|
+
|
|
257
|
+
export const users = mysqlTable('users', {
|
|
258
|
+
id: varchar('id', { length: 36 }).primaryKey(),
|
|
259
|
+
name: varchar('name', { length: 255 }).notNull(),
|
|
260
|
+
email: varchar('email', { length: 255 }).unique().notNull(),
|
|
261
|
+
createdAt: timestamp('created_at').defaultNow().notNull()
|
|
262
|
+
});
|
|
263
|
+
`;
|
|
264
|
+
}
|
|
265
|
+
return `import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
|
|
266
|
+
|
|
267
|
+
export const users = sqliteTable('users', {
|
|
268
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
269
|
+
name: text('name').notNull(),
|
|
270
|
+
email: text('email').unique().notNull(),
|
|
271
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date())
|
|
272
|
+
});
|
|
273
|
+
`;
|
|
274
|
+
}
|
|
275
|
+
function getDatabaseIndex(database) {
|
|
276
|
+
if (database === "postgresql") {
|
|
277
|
+
return `import { drizzle } from 'drizzle-orm/postgres-js';
|
|
278
|
+
import postgres from 'postgres';
|
|
279
|
+
import * as schema from './schema.js';
|
|
280
|
+
|
|
281
|
+
const client = postgres(process.env.DATABASE_URL!);
|
|
282
|
+
export const db = drizzle(client, { schema });
|
|
283
|
+
`;
|
|
284
|
+
}
|
|
285
|
+
if (database === "mysql") {
|
|
286
|
+
return `import { drizzle } from 'drizzle-orm/mysql2';
|
|
287
|
+
import mysql from 'mysql2/promise';
|
|
288
|
+
import * as schema from './schema.js';
|
|
289
|
+
|
|
290
|
+
const connection = await mysql.createConnection(process.env.DATABASE_URL!);
|
|
291
|
+
export const db = drizzle(connection, { schema, mode: 'default' });
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
return `import { drizzle } from 'drizzle-orm/better-sqlite3';
|
|
295
|
+
import Database from 'better-sqlite3';
|
|
296
|
+
import * as schema from './schema.js';
|
|
297
|
+
import { initDatabase } from './seed.js';
|
|
298
|
+
|
|
299
|
+
const sqlite = new Database('./data.db');
|
|
300
|
+
export const db = drizzle(sqlite, { schema });
|
|
301
|
+
|
|
302
|
+
// Initialize database tables on first run
|
|
303
|
+
initDatabase(db);
|
|
304
|
+
`;
|
|
305
|
+
}
|
|
306
|
+
function getSQLiteSeed() {
|
|
307
|
+
return `import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
|
|
308
|
+
import { users } from './schema.js';
|
|
309
|
+
|
|
310
|
+
let initialized = false;
|
|
311
|
+
|
|
312
|
+
export function initDatabase(db: BetterSQLite3Database<any>) {
|
|
313
|
+
if (initialized) return;
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
// Create tables if they don't exist
|
|
317
|
+
db.run(\`
|
|
318
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
319
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
320
|
+
name TEXT NOT NULL,
|
|
321
|
+
email TEXT UNIQUE NOT NULL,
|
|
322
|
+
created_at INTEGER
|
|
323
|
+
)
|
|
324
|
+
\`);
|
|
325
|
+
|
|
326
|
+
// Check if we need to seed
|
|
327
|
+
const count = db.get(\`SELECT COUNT(*) as count FROM users\`) as { count: number };
|
|
328
|
+
|
|
329
|
+
if (count.count === 0) {
|
|
330
|
+
console.log('\u{1F331} Seeding database with example data...');
|
|
331
|
+
|
|
332
|
+
// Insert example users
|
|
333
|
+
db.insert(users).values([
|
|
334
|
+
{ name: 'John Doe', email: 'john@example.com', createdAt: new Date() },
|
|
335
|
+
{ name: 'Jane Smith', email: 'jane@example.com', createdAt: new Date() }
|
|
336
|
+
]).run();
|
|
337
|
+
|
|
338
|
+
console.log('\u2705 Database seeded!');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
initialized = true;
|
|
342
|
+
} catch (err) {
|
|
343
|
+
console.error('Failed to initialize database:', err);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
async function createExampleRoutes(projectDir) {
|
|
349
|
+
const indexRoute = `export default async () => {
|
|
350
|
+
return {
|
|
351
|
+
status: 'ok',
|
|
352
|
+
framework: 'Kozo \u{1F525}',
|
|
353
|
+
timestamp: new Date().toISOString()
|
|
354
|
+
};
|
|
355
|
+
};
|
|
356
|
+
`;
|
|
357
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "routes", "index.ts"), indexRoute);
|
|
358
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "routes", "users"));
|
|
359
|
+
const getUsersRoute = `import type { HandlerContext } from '@kozojs/core';
|
|
360
|
+
import { users } from '../../db/schema.js';
|
|
361
|
+
|
|
362
|
+
export const meta = {
|
|
363
|
+
summary: 'Get all users',
|
|
364
|
+
description: 'Returns a list of all users in the database'
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
export default async ({ services: { db } }: HandlerContext) => {
|
|
368
|
+
const allUsers = db.select().from(users).all();
|
|
369
|
+
return { users: allUsers };
|
|
370
|
+
};
|
|
371
|
+
`;
|
|
372
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "routes", "users", "get.ts"), getUsersRoute);
|
|
373
|
+
const postUsersRoute = `import { z } from 'zod';
|
|
374
|
+
import type { HandlerContext } from '@kozojs/core';
|
|
375
|
+
import { users } from '../../db/schema.js';
|
|
376
|
+
|
|
377
|
+
export const schema = {
|
|
378
|
+
body: z.object({
|
|
379
|
+
name: z.string().min(2),
|
|
380
|
+
email: z.string().email()
|
|
381
|
+
})
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
export const meta = {
|
|
385
|
+
summary: 'Create a new user',
|
|
386
|
+
description: 'Creates a new user with name and email'
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
type Body = z.infer<typeof schema.body>;
|
|
390
|
+
|
|
391
|
+
export default async ({ body, services: { db } }: HandlerContext<Body>) => {
|
|
392
|
+
const user = db.insert(users).values({
|
|
393
|
+
...body,
|
|
394
|
+
createdAt: new Date()
|
|
395
|
+
}).returning().get();
|
|
396
|
+
|
|
397
|
+
return { success: true, user };
|
|
398
|
+
};
|
|
399
|
+
`;
|
|
400
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "routes", "users", "post.ts"), postUsersRoute);
|
|
401
|
+
}
|
|
402
|
+
async function scaffoldCompleteTemplate(projectDir, projectName, kozoCoreDep) {
|
|
403
|
+
await import_fs_extra.default.ensureDir(projectDir);
|
|
404
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src"));
|
|
405
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "schemas"));
|
|
406
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "routes"));
|
|
407
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "routes", "auth"));
|
|
408
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "routes", "users"));
|
|
409
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "routes", "posts"));
|
|
410
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "utils"));
|
|
411
|
+
await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "data"));
|
|
412
|
+
const packageJson = {
|
|
413
|
+
name: projectName,
|
|
414
|
+
version: "1.0.0",
|
|
415
|
+
type: "module",
|
|
416
|
+
scripts: {
|
|
417
|
+
dev: "tsx watch src/index.ts",
|
|
418
|
+
build: "tsc",
|
|
419
|
+
start: "node dist/index.js",
|
|
420
|
+
"type-check": "tsc --noEmit"
|
|
421
|
+
},
|
|
422
|
+
dependencies: {
|
|
423
|
+
"@kozojs/core": kozoCoreDep,
|
|
424
|
+
"@hono/node-server": "^1.13.0",
|
|
425
|
+
hono: "^4.6.0",
|
|
426
|
+
zod: "^3.23.0"
|
|
427
|
+
},
|
|
428
|
+
devDependencies: {
|
|
429
|
+
"@types/node": "^22.0.0",
|
|
430
|
+
tsx: "^4.19.0",
|
|
431
|
+
typescript: "^5.6.0"
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
await import_fs_extra.default.writeJSON(import_node_path.default.join(projectDir, "package.json"), packageJson, { spaces: 2 });
|
|
435
|
+
const tsconfig = {
|
|
436
|
+
compilerOptions: {
|
|
437
|
+
target: "ES2022",
|
|
438
|
+
module: "ESNext",
|
|
439
|
+
moduleResolution: "bundler",
|
|
440
|
+
strict: true,
|
|
441
|
+
esModuleInterop: true,
|
|
442
|
+
skipLibCheck: true,
|
|
443
|
+
outDir: "dist",
|
|
444
|
+
rootDir: "src",
|
|
445
|
+
declaration: true,
|
|
446
|
+
experimentalDecorators: true,
|
|
447
|
+
emitDecoratorMetadata: true
|
|
448
|
+
},
|
|
449
|
+
include: ["src/**/*"],
|
|
450
|
+
exclude: ["node_modules", "dist"]
|
|
451
|
+
};
|
|
452
|
+
await import_fs_extra.default.writeJSON(import_node_path.default.join(projectDir, "tsconfig.json"), tsconfig, { spaces: 2 });
|
|
453
|
+
const gitignore = `node_modules/
|
|
454
|
+
dist/
|
|
455
|
+
.env
|
|
456
|
+
.turbo/
|
|
457
|
+
*.log
|
|
458
|
+
`;
|
|
459
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, ".gitignore"), gitignore);
|
|
460
|
+
const envExample = `# Server
|
|
461
|
+
PORT=3000
|
|
462
|
+
NODE_ENV=development
|
|
463
|
+
`;
|
|
464
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, ".env.example"), envExample);
|
|
465
|
+
const indexTs = `import { createKozo } from '@kozojs/core';
|
|
466
|
+
import { registerAuthRoutes } from './routes/auth/index.js';
|
|
467
|
+
import { registerUserRoutes } from './routes/users/index.js';
|
|
468
|
+
import { registerPostRoutes } from './routes/posts/index.js';
|
|
469
|
+
import { registerHealthRoute } from './routes/health.js';
|
|
470
|
+
import { registerStatsRoute } from './routes/stats.js';
|
|
471
|
+
|
|
472
|
+
const app = createKozo({
|
|
473
|
+
port: Number(process.env.PORT) || 3000,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// Register all routes
|
|
477
|
+
registerHealthRoute(app);
|
|
478
|
+
registerAuthRoutes(app);
|
|
479
|
+
registerUserRoutes(app);
|
|
480
|
+
registerPostRoutes(app);
|
|
481
|
+
registerStatsRoute(app);
|
|
482
|
+
|
|
483
|
+
console.log('\u{1F525} Kozo server running on http://localhost:3000');
|
|
484
|
+
console.log('\u{1F4CA} Features: Auth, Users CRUD, Posts, Stats');
|
|
485
|
+
console.log('\u26A1 Optimized with pre-compiled handlers and Zod schemas');
|
|
486
|
+
console.log('');
|
|
487
|
+
console.log('\u{1F4DA} Try these endpoints:');
|
|
488
|
+
console.log(' GET /health');
|
|
489
|
+
console.log(' GET /users');
|
|
490
|
+
console.log(' POST /auth/login');
|
|
491
|
+
console.log(' GET /posts?published=true&page=1&limit=10');
|
|
492
|
+
console.log(' GET /stats');
|
|
493
|
+
|
|
494
|
+
app.listen();
|
|
495
|
+
`;
|
|
496
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "index.ts"), indexTs);
|
|
497
|
+
await createCompleteSchemas(projectDir);
|
|
498
|
+
await createCompleteUtils(projectDir);
|
|
499
|
+
await createCompleteDataStore(projectDir);
|
|
500
|
+
await createCompleteRoutes(projectDir);
|
|
501
|
+
const readme = `# ${projectName}
|
|
502
|
+
|
|
503
|
+
Built with \u{1F525} **Kozo Framework** - Production-ready server template
|
|
504
|
+
|
|
505
|
+
## Features
|
|
506
|
+
|
|
507
|
+
\u2728 **Complete API Implementation**
|
|
508
|
+
- \u2705 Authentication (login, me)
|
|
509
|
+
- \u2705 User CRUD (Create, Read, Update, Delete)
|
|
510
|
+
- \u2705 Posts with filtering and pagination
|
|
511
|
+
- \u2705 Statistics endpoint
|
|
512
|
+
- \u2705 Health check
|
|
513
|
+
|
|
514
|
+
\u26A1 **Performance Optimized**
|
|
515
|
+
- Pre-compiled Zod schemas (no runtime overhead)
|
|
516
|
+
- Fast-path routes for health checks
|
|
517
|
+
- Optimized handler closures
|
|
518
|
+
- Zero runtime decisions
|
|
519
|
+
|
|
520
|
+
\u{1F3AF} **Type-Safe**
|
|
521
|
+
- Full TypeScript inference
|
|
522
|
+
- Zod validation for all inputs
|
|
523
|
+
- Auto-generated types from schemas
|
|
524
|
+
|
|
525
|
+
## Quick Start
|
|
526
|
+
|
|
527
|
+
\`\`\`bash
|
|
528
|
+
# Install dependencies
|
|
529
|
+
npm install
|
|
530
|
+
|
|
531
|
+
# Start development server
|
|
532
|
+
npm run dev
|
|
533
|
+
\`\`\`
|
|
534
|
+
|
|
535
|
+
The server will start at **http://localhost:3000**
|
|
536
|
+
|
|
537
|
+
## API Endpoints
|
|
538
|
+
|
|
539
|
+
### Authentication
|
|
540
|
+
| Method | Endpoint | Description |
|
|
541
|
+
|--------|----------|-------------|
|
|
542
|
+
| POST | /auth/login | Login with email/password |
|
|
543
|
+
| GET | /auth/me | Get current user |
|
|
544
|
+
|
|
545
|
+
### Users
|
|
546
|
+
| Method | Endpoint | Description |
|
|
547
|
+
|--------|----------|-------------|
|
|
548
|
+
| GET | /users | List all users (paginated) |
|
|
549
|
+
| GET | /users/:id | Get user by ID |
|
|
550
|
+
| POST | /users | Create new user |
|
|
551
|
+
| PUT | /users/:id | Update user |
|
|
552
|
+
| DELETE | /users/:id | Delete user |
|
|
553
|
+
|
|
554
|
+
### Posts
|
|
555
|
+
| Method | Endpoint | Description |
|
|
556
|
+
|--------|----------|-------------|
|
|
557
|
+
| GET | /posts | List posts (with filters) |
|
|
558
|
+
| GET | /posts/:id | Get post with author |
|
|
559
|
+
| POST | /posts | Create new post |
|
|
560
|
+
|
|
561
|
+
### System
|
|
562
|
+
| Method | Endpoint | Description |
|
|
563
|
+
|--------|----------|-------------|
|
|
564
|
+
| GET | /health | Health check |
|
|
565
|
+
| GET | /stats | System statistics |
|
|
566
|
+
|
|
567
|
+
## Example Requests
|
|
568
|
+
|
|
569
|
+
### Create User
|
|
570
|
+
\`\`\`bash
|
|
571
|
+
curl -X POST http://localhost:3000/users \\
|
|
572
|
+
-H "Content-Type: application/json" \\
|
|
573
|
+
-d '{
|
|
574
|
+
"name": "Alice Smith",
|
|
575
|
+
"email": "alice@example.com",
|
|
576
|
+
"role": "user"
|
|
577
|
+
}'
|
|
578
|
+
\`\`\`
|
|
579
|
+
|
|
580
|
+
### Login
|
|
581
|
+
\`\`\`bash
|
|
582
|
+
curl -X POST http://localhost:3000/auth/login \\
|
|
583
|
+
-H "Content-Type: application/json" \\
|
|
584
|
+
-d '{
|
|
585
|
+
"email": "admin@kozo.dev",
|
|
586
|
+
"password": "secret123"
|
|
587
|
+
}'
|
|
588
|
+
\`\`\`
|
|
589
|
+
|
|
590
|
+
### List Users (Paginated)
|
|
591
|
+
\`\`\`bash
|
|
592
|
+
curl "http://localhost:3000/users?page=1&limit=10"
|
|
593
|
+
\`\`\`
|
|
594
|
+
|
|
595
|
+
### Filter Posts
|
|
596
|
+
\`\`\`bash
|
|
597
|
+
curl "http://localhost:3000/posts?published=true&tag=framework"
|
|
598
|
+
\`\`\`
|
|
599
|
+
|
|
600
|
+
### Get Statistics
|
|
601
|
+
\`\`\`bash
|
|
602
|
+
curl http://localhost:3000/stats
|
|
603
|
+
\`\`\`
|
|
604
|
+
|
|
605
|
+
## Project Structure
|
|
606
|
+
|
|
607
|
+
\`\`\`
|
|
608
|
+
${projectName}/
|
|
609
|
+
\u251C\u2500\u2500 src/
|
|
610
|
+
\u2502 \u251C\u2500\u2500 data/
|
|
611
|
+
\u2502 \u2502 \u2514\u2500\u2500 store.ts # In-memory data store
|
|
612
|
+
\u2502 \u251C\u2500\u2500 routes/
|
|
613
|
+
\u2502 \u2502 \u251C\u2500\u2500 auth/
|
|
614
|
+
\u2502 \u2502 \u2502 \u2514\u2500\u2500 index.ts # Auth routes (login, me)
|
|
615
|
+
\u2502 \u2502 \u251C\u2500\u2500 users/
|
|
616
|
+
\u2502 \u2502 \u2502 \u2514\u2500\u2500 index.ts # User CRUD routes
|
|
617
|
+
\u2502 \u2502 \u251C\u2500\u2500 posts/
|
|
618
|
+
\u2502 \u2502 \u2502 \u2514\u2500\u2500 index.ts # Post routes
|
|
619
|
+
\u2502 \u2502 \u251C\u2500\u2500 health.ts # Health check
|
|
620
|
+
\u2502 \u2502 \u2514\u2500\u2500 stats.ts # Statistics
|
|
621
|
+
\u2502 \u251C\u2500\u2500 schemas/
|
|
622
|
+
\u2502 \u2502 \u251C\u2500\u2500 user.ts # User schemas
|
|
623
|
+
\u2502 \u2502 \u251C\u2500\u2500 post.ts # Post schemas
|
|
624
|
+
\u2502 \u2502 \u2514\u2500\u2500 common.ts # Common schemas
|
|
625
|
+
\u2502 \u251C\u2500\u2500 utils/
|
|
626
|
+
\u2502 \u2502 \u2514\u2500\u2500 helpers.ts # Helper functions
|
|
627
|
+
\u2502 \u2514\u2500\u2500 index.ts # Entry point
|
|
628
|
+
\u251C\u2500\u2500 package.json
|
|
629
|
+
\u251C\u2500\u2500 tsconfig.json
|
|
630
|
+
\u2514\u2500\u2500 README.md
|
|
631
|
+
\`\`\`
|
|
632
|
+
|
|
633
|
+
## Development
|
|
634
|
+
|
|
635
|
+
\`\`\`bash
|
|
636
|
+
npm run dev # Start with hot reload
|
|
637
|
+
npm run build # Build for production
|
|
638
|
+
npm start # Run production build
|
|
639
|
+
npm run type-check # TypeScript validation
|
|
640
|
+
\`\`\`
|
|
641
|
+
|
|
642
|
+
## Performance Notes
|
|
643
|
+
|
|
644
|
+
This template uses Kozo's optimized patterns:
|
|
645
|
+
- **Pre-compiled schemas**: Zod schemas compiled to Ajv validators at boot
|
|
646
|
+
- **Handler closures**: Routes capture dependencies at startup
|
|
647
|
+
- **Fast paths**: Simple routes skip unnecessary middleware
|
|
648
|
+
- **Minimal allocations**: Context objects only include what's needed
|
|
649
|
+
|
|
650
|
+
These optimizations make Kozo competitive with Fastify while providing:
|
|
651
|
+
- Better type safety with Zod
|
|
652
|
+
- Simpler API than NestJS
|
|
653
|
+
- More features than plain Hono
|
|
654
|
+
|
|
655
|
+
## Next Steps
|
|
656
|
+
|
|
657
|
+
1. **Add database**: Integrate Drizzle ORM for persistent storage
|
|
658
|
+
2. **Add authentication**: Implement JWT with proper password hashing
|
|
659
|
+
3. **Add validation**: Enhance error handling and input validation
|
|
660
|
+
4. **Add tests**: Write unit and integration tests
|
|
661
|
+
5. **Deploy**: Deploy to Vercel, Railway, or any Node.js host
|
|
662
|
+
|
|
663
|
+
## Documentation
|
|
664
|
+
|
|
665
|
+
- [Kozo Documentation](https://kozo.dev/docs)
|
|
666
|
+
- [Zod Schema Validation](https://zod.dev)
|
|
667
|
+
- [Hono Framework](https://hono.dev)
|
|
668
|
+
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
Built with \u2764\uFE0F using Kozo Framework
|
|
672
|
+
`;
|
|
673
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "README.md"), readme);
|
|
674
|
+
}
|
|
675
|
+
async function createCompleteSchemas(projectDir) {
|
|
676
|
+
const userSchemas = `import { z } from 'zod';
|
|
677
|
+
|
|
678
|
+
export const UserSchema = z.object({
|
|
679
|
+
id: z.string().uuid(),
|
|
680
|
+
email: z.string().email(),
|
|
681
|
+
name: z.string().min(2).max(50),
|
|
682
|
+
role: z.enum(['user', 'admin']).default('user'),
|
|
683
|
+
createdAt: z.date(),
|
|
684
|
+
updatedAt: z.date(),
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
export const CreateUserSchema = z.object({
|
|
688
|
+
email: z.string().email(),
|
|
689
|
+
name: z.string().min(2).max(50),
|
|
690
|
+
role: z.enum(['user', 'admin']).optional(),
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
export const UpdateUserSchema = z.object({
|
|
694
|
+
name: z.string().min(2).max(50).optional(),
|
|
695
|
+
role: z.enum(['user', 'admin']).optional(),
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
export type User = z.infer<typeof UserSchema>;
|
|
699
|
+
export type CreateUser = z.infer<typeof CreateUserSchema>;
|
|
700
|
+
export type UpdateUser = z.infer<typeof UpdateUserSchema>;
|
|
701
|
+
`;
|
|
702
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "schemas", "user.ts"), userSchemas);
|
|
703
|
+
const postSchemas = `import { z } from 'zod';
|
|
704
|
+
import { UserSchema } from './user.js';
|
|
705
|
+
|
|
706
|
+
export const PostSchema = z.object({
|
|
707
|
+
id: z.string().uuid(),
|
|
708
|
+
title: z.string().min(1).max(200),
|
|
709
|
+
content: z.string().min(1),
|
|
710
|
+
authorId: z.string().uuid(),
|
|
711
|
+
published: z.boolean().default(false),
|
|
712
|
+
tags: z.array(z.string()).default([]),
|
|
713
|
+
createdAt: z.date(),
|
|
714
|
+
updatedAt: z.date(),
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
export const PostWithAuthorSchema = PostSchema.extend({
|
|
718
|
+
author: UserSchema,
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
export const CreatePostSchema = z.object({
|
|
722
|
+
title: z.string().min(1).max(200),
|
|
723
|
+
content: z.string().min(1),
|
|
724
|
+
published: z.boolean().optional(),
|
|
725
|
+
tags: z.array(z.string()).optional(),
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
export type Post = z.infer<typeof PostSchema>;
|
|
729
|
+
export type PostWithAuthor = z.infer<typeof PostWithAuthorSchema>;
|
|
730
|
+
export type CreatePost = z.infer<typeof CreatePostSchema>;
|
|
731
|
+
`;
|
|
732
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "schemas", "post.ts"), postSchemas);
|
|
733
|
+
const commonSchemas = `import { z } from 'zod';
|
|
734
|
+
|
|
735
|
+
export const PaginationSchema = z.object({
|
|
736
|
+
page: z.coerce.number().min(1).default(1),
|
|
737
|
+
limit: z.coerce.number().min(1).max(100).default(10),
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
export const PostFiltersSchema = z.object({
|
|
741
|
+
published: z.coerce.boolean().optional(),
|
|
742
|
+
authorId: z.string().uuid().optional(),
|
|
743
|
+
tag: z.string().optional(),
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
export type Pagination = z.infer<typeof PaginationSchema>;
|
|
747
|
+
export type PostFilters = z.infer<typeof PostFiltersSchema>;
|
|
748
|
+
`;
|
|
749
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "schemas", "common.ts"), commonSchemas);
|
|
750
|
+
}
|
|
751
|
+
async function createCompleteUtils(projectDir) {
|
|
752
|
+
const helpers = `export function generateUUID(): string {
|
|
753
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
754
|
+
const r = Math.random() * 16 | 0;
|
|
755
|
+
const v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
756
|
+
return v.toString(16);
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export function paginate<T>(items: T[], page: number, limit: number) {
|
|
761
|
+
const start = (page - 1) * limit;
|
|
762
|
+
const end = start + limit;
|
|
763
|
+
return {
|
|
764
|
+
data: items.slice(start, end),
|
|
765
|
+
total: items.length,
|
|
766
|
+
page,
|
|
767
|
+
limit,
|
|
768
|
+
totalPages: Math.ceil(items.length / limit),
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
`;
|
|
772
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "utils", "helpers.ts"), helpers);
|
|
773
|
+
}
|
|
774
|
+
async function createCompleteDataStore(projectDir) {
|
|
775
|
+
const store = `import type { User } from '../schemas/user.js';
|
|
776
|
+
import type { Post } from '../schemas/post.js';
|
|
777
|
+
|
|
778
|
+
export const users: User[] = [
|
|
779
|
+
{
|
|
780
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
781
|
+
email: 'admin@kozo.dev',
|
|
782
|
+
name: 'Admin User',
|
|
783
|
+
role: 'admin',
|
|
784
|
+
createdAt: new Date('2024-01-01'),
|
|
785
|
+
updatedAt: new Date('2024-01-01'),
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
id: '550e8400-e29b-41d4-a716-446655440001',
|
|
789
|
+
email: 'john@example.com',
|
|
790
|
+
name: 'John Doe',
|
|
791
|
+
role: 'user',
|
|
792
|
+
createdAt: new Date('2024-01-15'),
|
|
793
|
+
updatedAt: new Date('2024-01-15'),
|
|
794
|
+
},
|
|
795
|
+
];
|
|
796
|
+
|
|
797
|
+
export const posts: Post[] = [
|
|
798
|
+
{
|
|
799
|
+
id: '550e8400-e29b-41d4-a716-446655440010',
|
|
800
|
+
title: 'Welcome to Kozo Framework',
|
|
801
|
+
content: 'This is the first post in our amazing framework...',
|
|
802
|
+
authorId: '550e8400-e29b-41d4-a716-446655440000',
|
|
803
|
+
published: true,
|
|
804
|
+
tags: ['framework', 'typescript', 'backend'],
|
|
805
|
+
createdAt: new Date('2024-01-01'),
|
|
806
|
+
updatedAt: new Date('2024-01-01'),
|
|
807
|
+
},
|
|
808
|
+
];
|
|
809
|
+
`;
|
|
810
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "data", "store.ts"), store);
|
|
811
|
+
}
|
|
812
|
+
async function createCompleteRoutes(projectDir) {
|
|
813
|
+
const healthRoute = `import type { Kozo } from '@kozojs/core';
|
|
814
|
+
|
|
815
|
+
export function registerHealthRoute(app: Kozo) {
|
|
816
|
+
app.get('/health', {}, (c) => {
|
|
817
|
+
return {
|
|
818
|
+
status: 'ok',
|
|
819
|
+
timestamp: new Date().toISOString(),
|
|
820
|
+
version: '1.0.0',
|
|
821
|
+
uptime: process.uptime(),
|
|
822
|
+
};
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
`;
|
|
826
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "routes", "health.ts"), healthRoute);
|
|
827
|
+
const statsRoute = `import { z } from 'zod';
|
|
828
|
+
import type { Kozo } from '@kozojs/core';
|
|
829
|
+
import { users } from '../data/store.js';
|
|
830
|
+
import { posts } from '../data/store.js';
|
|
831
|
+
|
|
832
|
+
export function registerStatsRoute(app: Kozo) {
|
|
833
|
+
app.get('/stats', {
|
|
834
|
+
response: z.object({
|
|
835
|
+
users: z.object({
|
|
836
|
+
total: z.number(),
|
|
837
|
+
admins: z.number(),
|
|
838
|
+
regular: z.number(),
|
|
839
|
+
}),
|
|
840
|
+
posts: z.object({
|
|
841
|
+
total: z.number(),
|
|
842
|
+
published: z.number(),
|
|
843
|
+
drafts: z.number(),
|
|
844
|
+
totalTags: z.number(),
|
|
845
|
+
}),
|
|
846
|
+
performance: z.object({
|
|
847
|
+
uptime: z.number(),
|
|
848
|
+
memoryUsage: z.object({
|
|
849
|
+
rss: z.number(),
|
|
850
|
+
heapTotal: z.number(),
|
|
851
|
+
heapUsed: z.number(),
|
|
852
|
+
}),
|
|
853
|
+
}),
|
|
854
|
+
}),
|
|
855
|
+
}, (c) => {
|
|
856
|
+
const memUsage = process.memoryUsage();
|
|
857
|
+
return {
|
|
858
|
+
users: {
|
|
859
|
+
total: users.length,
|
|
860
|
+
admins: users.filter(u => u.role === 'admin').length,
|
|
861
|
+
regular: users.filter(u => u.role === 'user').length,
|
|
862
|
+
},
|
|
863
|
+
posts: {
|
|
864
|
+
total: posts.length,
|
|
865
|
+
published: posts.filter(p => p.published).length,
|
|
866
|
+
drafts: posts.filter(p => !p.published).length,
|
|
867
|
+
totalTags: [...new Set(posts.flatMap(p => p.tags))].length,
|
|
868
|
+
},
|
|
869
|
+
performance: {
|
|
870
|
+
uptime: process.uptime(),
|
|
871
|
+
memoryUsage: {
|
|
872
|
+
rss: memUsage.rss,
|
|
873
|
+
heapTotal: memUsage.heapTotal,
|
|
874
|
+
heapUsed: memUsage.heapUsed,
|
|
875
|
+
},
|
|
876
|
+
},
|
|
877
|
+
};
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
`;
|
|
881
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "routes", "stats.ts"), statsRoute);
|
|
882
|
+
const authRoutes = `import { z } from 'zod';
|
|
883
|
+
import type { Kozo } from '@kozojs/core';
|
|
884
|
+
import { UserSchema } from '../../schemas/user.js';
|
|
885
|
+
import { users } from '../../data/store.js';
|
|
886
|
+
|
|
887
|
+
export function registerAuthRoutes(app: Kozo) {
|
|
888
|
+
// POST /auth/login
|
|
889
|
+
app.post('/auth/login', {
|
|
890
|
+
body: z.object({
|
|
891
|
+
email: z.string().email(),
|
|
892
|
+
password: z.string().min(6),
|
|
893
|
+
}),
|
|
894
|
+
response: z.object({
|
|
895
|
+
success: z.boolean(),
|
|
896
|
+
token: z.string(),
|
|
897
|
+
user: UserSchema,
|
|
898
|
+
}),
|
|
899
|
+
}, (c) => {
|
|
900
|
+
const user = users.find(u => u.email === c.body.email);
|
|
901
|
+
if (!user) {
|
|
902
|
+
return c.json({ error: 'Invalid credentials' }, 401);
|
|
903
|
+
}
|
|
904
|
+
const token = \`mock_jwt_\${user.id}_\${Date.now()}\`;
|
|
905
|
+
return { success: true, token, user };
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
// GET /auth/me
|
|
909
|
+
app.get('/auth/me', {
|
|
910
|
+
response: UserSchema,
|
|
911
|
+
}, (c) => users[0]);
|
|
912
|
+
}
|
|
913
|
+
`;
|
|
914
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "routes", "auth", "index.ts"), authRoutes);
|
|
915
|
+
const userRoutes = `import { z } from 'zod';
|
|
916
|
+
import type { Kozo } from '@kozojs/core';
|
|
917
|
+
import { UserSchema, CreateUserSchema, UpdateUserSchema } from '../../schemas/user.js';
|
|
918
|
+
import { PaginationSchema } from '../../schemas/common.js';
|
|
919
|
+
import { users } from '../../data/store.js';
|
|
920
|
+
import { generateUUID, paginate } from '../../utils/helpers.js';
|
|
921
|
+
|
|
922
|
+
export function registerUserRoutes(app: Kozo) {
|
|
923
|
+
// GET /users
|
|
924
|
+
app.get('/users', {
|
|
925
|
+
query: PaginationSchema,
|
|
926
|
+
response: z.object({
|
|
927
|
+
data: z.array(UserSchema),
|
|
928
|
+
total: z.number(),
|
|
929
|
+
page: z.number(),
|
|
930
|
+
limit: z.number(),
|
|
931
|
+
totalPages: z.number(),
|
|
932
|
+
}),
|
|
933
|
+
}, (c) => {
|
|
934
|
+
const { page, limit } = c.query;
|
|
935
|
+
return paginate(users, page, limit);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
// GET /users/:id
|
|
939
|
+
app.get('/users/:id', {
|
|
940
|
+
params: z.object({ id: z.string().uuid() }),
|
|
941
|
+
response: UserSchema,
|
|
942
|
+
}, (c) => {
|
|
943
|
+
const user = users.find(u => u.id === c.params.id);
|
|
944
|
+
if (!user) return c.json({ error: 'User not found' }, 404);
|
|
945
|
+
return user;
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
// POST /users
|
|
949
|
+
app.post('/users', {
|
|
950
|
+
body: CreateUserSchema,
|
|
951
|
+
response: UserSchema,
|
|
952
|
+
}, (c) => {
|
|
953
|
+
const existing = users.find(u => u.email === c.body.email);
|
|
954
|
+
if (existing) return c.json({ error: 'Email already exists' }, 409);
|
|
955
|
+
|
|
956
|
+
const newUser = {
|
|
957
|
+
id: generateUUID(),
|
|
958
|
+
email: c.body.email,
|
|
959
|
+
name: c.body.name,
|
|
960
|
+
role: c.body.role || 'user' as const,
|
|
961
|
+
createdAt: new Date(),
|
|
962
|
+
updatedAt: new Date(),
|
|
963
|
+
};
|
|
964
|
+
users.push(newUser);
|
|
965
|
+
return newUser;
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
// PUT /users/:id
|
|
969
|
+
app.put('/users/:id', {
|
|
970
|
+
params: z.object({ id: z.string().uuid() }),
|
|
971
|
+
body: UpdateUserSchema,
|
|
972
|
+
response: UserSchema,
|
|
973
|
+
}, (c) => {
|
|
974
|
+
const user = users.find(u => u.id === c.params.id);
|
|
975
|
+
if (!user) return c.json({ error: 'User not found' }, 404);
|
|
976
|
+
|
|
977
|
+
if (c.body.name) user.name = c.body.name;
|
|
978
|
+
if (c.body.role) user.role = c.body.role;
|
|
979
|
+
user.updatedAt = new Date();
|
|
980
|
+
return user;
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
// DELETE /users/:id
|
|
984
|
+
app.delete('/users/:id', {
|
|
985
|
+
params: z.object({ id: z.string().uuid() }),
|
|
986
|
+
response: z.object({
|
|
987
|
+
success: z.boolean(),
|
|
988
|
+
deletedId: z.string(),
|
|
989
|
+
}),
|
|
990
|
+
}, (c) => {
|
|
991
|
+
const index = users.findIndex(u => u.id === c.params.id);
|
|
992
|
+
if (index === -1) return c.json({ error: 'User not found' }, 404);
|
|
993
|
+
|
|
994
|
+
users.splice(index, 1);
|
|
995
|
+
return { success: true, deletedId: c.params.id };
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
`;
|
|
999
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "routes", "users", "index.ts"), userRoutes);
|
|
1000
|
+
const postRoutes = `import { z } from 'zod';
|
|
1001
|
+
import type { Kozo } from '@kozojs/core';
|
|
1002
|
+
import { PostSchema, PostWithAuthorSchema, CreatePostSchema } from '../../schemas/post.js';
|
|
1003
|
+
import { PaginationSchema, PostFiltersSchema } from '../../schemas/common.js';
|
|
1004
|
+
import { posts, users } from '../../data/store.js';
|
|
1005
|
+
import { generateUUID, paginate } from '../../utils/helpers.js';
|
|
1006
|
+
|
|
1007
|
+
export function registerPostRoutes(app: Kozo) {
|
|
1008
|
+
// GET /posts
|
|
1009
|
+
app.get('/posts', {
|
|
1010
|
+
query: PaginationSchema.merge(PostFiltersSchema),
|
|
1011
|
+
response: z.object({
|
|
1012
|
+
data: z.array(PostWithAuthorSchema),
|
|
1013
|
+
total: z.number(),
|
|
1014
|
+
page: z.number(),
|
|
1015
|
+
limit: z.number(),
|
|
1016
|
+
totalPages: z.number(),
|
|
1017
|
+
}),
|
|
1018
|
+
}, (c) => {
|
|
1019
|
+
const { page, limit, published, authorId, tag } = c.query;
|
|
1020
|
+
|
|
1021
|
+
let filteredPosts = posts;
|
|
1022
|
+
if (published !== undefined) {
|
|
1023
|
+
filteredPosts = filteredPosts.filter(p => p.published === published);
|
|
1024
|
+
}
|
|
1025
|
+
if (authorId) {
|
|
1026
|
+
filteredPosts = filteredPosts.filter(p => p.authorId === authorId);
|
|
1027
|
+
}
|
|
1028
|
+
if (tag) {
|
|
1029
|
+
filteredPosts = filteredPosts.filter(p => p.tags.includes(tag));
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const postsWithAuthors = filteredPosts.map(post => ({
|
|
1033
|
+
...post,
|
|
1034
|
+
author: users.find(u => u.id === post.authorId)!,
|
|
1035
|
+
}));
|
|
1036
|
+
|
|
1037
|
+
return paginate(postsWithAuthors, page, limit);
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
// GET /posts/:id
|
|
1041
|
+
app.get('/posts/:id', {
|
|
1042
|
+
params: z.object({ id: z.string().uuid() }),
|
|
1043
|
+
response: PostWithAuthorSchema,
|
|
1044
|
+
}, (c) => {
|
|
1045
|
+
const post = posts.find(p => p.id === c.params.id);
|
|
1046
|
+
if (!post) return c.json({ error: 'Post not found' }, 404);
|
|
1047
|
+
|
|
1048
|
+
const author = users.find(u => u.id === post.authorId);
|
|
1049
|
+
if (!author) return c.json({ error: 'Post author not found' }, 500);
|
|
1050
|
+
|
|
1051
|
+
return { ...post, author };
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
// POST /posts
|
|
1055
|
+
app.post('/posts', {
|
|
1056
|
+
body: CreatePostSchema,
|
|
1057
|
+
response: PostSchema,
|
|
1058
|
+
}, (c) => {
|
|
1059
|
+
const authorId = users[0].id;
|
|
1060
|
+
const newPost = {
|
|
1061
|
+
id: generateUUID(),
|
|
1062
|
+
title: c.body.title,
|
|
1063
|
+
content: c.body.content,
|
|
1064
|
+
authorId,
|
|
1065
|
+
published: c.body.published || false,
|
|
1066
|
+
tags: c.body.tags || [],
|
|
1067
|
+
createdAt: new Date(),
|
|
1068
|
+
updatedAt: new Date(),
|
|
1069
|
+
};
|
|
1070
|
+
posts.push(newPost);
|
|
1071
|
+
return newPost;
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
`;
|
|
1075
|
+
await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "routes", "posts", "index.ts"), postRoutes);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// src/utils/ascii-art.ts
|
|
1079
|
+
var import_picocolors = __toESM(require("picocolors"));
|
|
1080
|
+
var KOZO_LOGO = `
|
|
1081
|
+
${import_picocolors.default.red(" _ __")}${import_picocolors.default.yellow("___ ")}${import_picocolors.default.red("______")}${import_picocolors.default.yellow("___ ")}
|
|
1082
|
+
${import_picocolors.default.red("| |/ /")}${import_picocolors.default.yellow(" _ \\\\")}${import_picocolors.default.red("|_ /")}${import_picocolors.default.yellow(" _ \\\\")}
|
|
1083
|
+
${import_picocolors.default.red("| ' /")}${import_picocolors.default.yellow(" (_) |")}${import_picocolors.default.red("/ /")}${import_picocolors.default.yellow(" (_) |")}
|
|
1084
|
+
${import_picocolors.default.red("|_|\\_\\\\")}${import_picocolors.default.yellow("___/")}${import_picocolors.default.red("___|\\\\")}${import_picocolors.default.yellow("___/")}
|
|
1085
|
+
`;
|
|
1086
|
+
var KOZO_BANNER = `
|
|
1087
|
+
${import_picocolors.default.bold(import_picocolors.default.red("\u{1F525} KOZO"))} ${import_picocolors.default.dim("- The Structure for the Edge")}
|
|
1088
|
+
`;
|
|
1089
|
+
function printLogo() {
|
|
1090
|
+
console.log(KOZO_LOGO);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/commands/new.ts
|
|
1094
|
+
async function newCommand(projectName) {
|
|
1095
|
+
printLogo();
|
|
1096
|
+
p.intro(import_picocolors2.default.bold(import_picocolors2.default.red("\u{1F525} Create a new Kozo project")));
|
|
1097
|
+
const project = await p.group(
|
|
1098
|
+
{
|
|
1099
|
+
name: () => {
|
|
1100
|
+
if (projectName) {
|
|
1101
|
+
if (!/^[a-z0-9-]+$/.test(projectName)) {
|
|
1102
|
+
p.log.error("Project name must use lowercase letters, numbers, and hyphens only");
|
|
1103
|
+
process.exit(1);
|
|
1104
|
+
}
|
|
1105
|
+
return Promise.resolve(projectName);
|
|
1106
|
+
}
|
|
1107
|
+
return p.text({
|
|
1108
|
+
message: "Project name",
|
|
1109
|
+
placeholder: "my-kozo-app",
|
|
1110
|
+
validate: (value) => {
|
|
1111
|
+
if (!value) return "Project name is required";
|
|
1112
|
+
if (!/^[a-z0-9-]+$/.test(value)) return "Use lowercase letters, numbers, and hyphens only";
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
},
|
|
1116
|
+
template: () => p.select({
|
|
1117
|
+
message: "Template",
|
|
1118
|
+
options: [
|
|
1119
|
+
{ value: "complete", label: "Complete Server", hint: "Full production-ready app (Auth, CRUD, Stats)" },
|
|
1120
|
+
{ value: "starter", label: "Starter", hint: "Minimal setup with database" },
|
|
1121
|
+
{ value: "saas", label: "SaaS", hint: "Auth + Stripe + Email (coming soon)" },
|
|
1122
|
+
{ value: "ecommerce", label: "E-commerce", hint: "Products + Orders (coming soon)" }
|
|
1123
|
+
]
|
|
1124
|
+
}),
|
|
1125
|
+
database: ({ results }) => {
|
|
1126
|
+
if (results.template === "complete") {
|
|
1127
|
+
return Promise.resolve("none");
|
|
1128
|
+
}
|
|
1129
|
+
return p.select({
|
|
1130
|
+
message: "Database provider",
|
|
1131
|
+
options: [
|
|
1132
|
+
{ value: "postgresql", label: "PostgreSQL", hint: "Neon, Supabase, Railway" },
|
|
1133
|
+
{ value: "mysql", label: "MySQL", hint: "PlanetScale" },
|
|
1134
|
+
{ value: "sqlite", label: "SQLite", hint: "Turso, local dev" }
|
|
1135
|
+
]
|
|
1136
|
+
});
|
|
1137
|
+
},
|
|
1138
|
+
packageSource: () => p.select({
|
|
1139
|
+
message: "Package source for @kozojs/core",
|
|
1140
|
+
options: [
|
|
1141
|
+
{ value: "npm", label: "npm registry", hint: "Use published version (recommended)" },
|
|
1142
|
+
{ value: "local", label: "Local workspace", hint: "Link to local monorepo (for development)" }
|
|
1143
|
+
],
|
|
1144
|
+
initialValue: "npm"
|
|
1145
|
+
}),
|
|
1146
|
+
install: () => {
|
|
1147
|
+
return Promise.resolve(true);
|
|
1148
|
+
}
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
onCancel: () => {
|
|
1152
|
+
p.cancel("Operation cancelled");
|
|
1153
|
+
process.exit(0);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
);
|
|
1157
|
+
const s = p.spinner();
|
|
1158
|
+
s.start("Creating project structure...");
|
|
1159
|
+
try {
|
|
1160
|
+
await scaffoldProject({
|
|
1161
|
+
projectName: project.name,
|
|
1162
|
+
database: project.database,
|
|
1163
|
+
template: project.template,
|
|
1164
|
+
packageSource: project.packageSource
|
|
1165
|
+
});
|
|
1166
|
+
s.stop("Project structure created!");
|
|
1167
|
+
} catch (err) {
|
|
1168
|
+
s.stop("Failed to create project");
|
|
1169
|
+
p.log.error(String(err));
|
|
1170
|
+
process.exit(1);
|
|
1171
|
+
}
|
|
1172
|
+
if (project.install) {
|
|
1173
|
+
s.start("Installing dependencies...");
|
|
1174
|
+
try {
|
|
1175
|
+
await (0, import_execa.execa)("pnpm", ["install"], {
|
|
1176
|
+
cwd: project.name,
|
|
1177
|
+
stdio: "pipe"
|
|
1178
|
+
});
|
|
1179
|
+
s.stop("Dependencies installed!");
|
|
1180
|
+
} catch {
|
|
1181
|
+
try {
|
|
1182
|
+
await (0, import_execa.execa)("npm", ["install"], {
|
|
1183
|
+
cwd: project.name,
|
|
1184
|
+
stdio: "pipe"
|
|
1185
|
+
});
|
|
1186
|
+
s.stop("Dependencies installed!");
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
s.stop("Failed to install dependencies");
|
|
1189
|
+
p.log.warn("Run `pnpm install` or `npm install` manually");
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
p.outro(import_picocolors2.default.green("\u2728 Project ready!"));
|
|
1194
|
+
console.log(`
|
|
1195
|
+
${import_picocolors2.default.bold("Next steps:")}
|
|
1196
|
+
|
|
1197
|
+
${import_picocolors2.default.cyan(`cd ${project.name}`)}
|
|
1198
|
+
${!project.install ? import_picocolors2.default.cyan("pnpm install") + "\n " : ""}${import_picocolors2.default.cyan("pnpm dev")}
|
|
1199
|
+
|
|
1200
|
+
${import_picocolors2.default.dim("Documentation:")} ${import_picocolors2.default.underline("https://kozo.dev/docs")}
|
|
1201
|
+
`);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// src/index.ts
|
|
1205
|
+
var program = new import_commander.Command();
|
|
1206
|
+
program.name("kozo").description("CLI to scaffold new Kozo Framework projects").version("0.2.6");
|
|
1207
|
+
program.argument("[project-name]", "Name of the project").action(async (projectName) => {
|
|
1208
|
+
await newCommand(projectName);
|
|
1209
|
+
});
|
|
1210
|
+
program.parse();
|