@kozojs/cli 0.1.6 → 0.1.7

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 +455 -121
  2. package/package.json +3 -2
package/lib/index.js CHANGED
@@ -419,6 +419,7 @@ async function scaffoldCompleteTemplate(projectDir, projectName, kozoCoreDep, ru
419
419
  await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "routes", "auth"));
420
420
  await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "routes", "users"));
421
421
  await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "routes", "posts"));
422
+ await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "middleware"));
422
423
  await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "utils"));
423
424
  await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "src", "data"));
424
425
  const packageJson = {
@@ -433,6 +434,7 @@ async function scaffoldCompleteTemplate(projectDir, projectName, kozoCoreDep, ru
433
434
  },
434
435
  dependencies: {
435
436
  "@kozojs/core": kozoCoreDep,
437
+ "@kozojs/auth": kozoCoreDep,
436
438
  "@hono/node-server": "^1.13.0",
437
439
  hono: "^4.6.0",
438
440
  zod: "^3.23.0"
@@ -472,38 +474,83 @@ dist/
472
474
  const envExample = `# Server
473
475
  PORT=3000
474
476
  NODE_ENV=development
477
+
478
+ # JWT Authentication
479
+ JWT_SECRET=change-me-to-a-random-secret-at-least-32-chars
480
+
481
+ # CORS
482
+ CORS_ORIGIN=http://localhost:5173
483
+
484
+ # Rate Limiting (requests per window)
485
+ RATE_LIMIT_MAX=100
486
+ RATE_LIMIT_WINDOW=60000
475
487
  `;
476
488
  await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, ".env.example"), envExample);
477
- const indexTs = `import { createKozo } from '@kozojs/core';
489
+ await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, ".env"), envExample);
490
+ const indexTs = `import { createKozo, cors, logger, rateLimit } from '@kozojs/core';
491
+ import { authenticateJWT } from '@kozojs/auth';
478
492
  import { registerAuthRoutes } from './routes/auth/index.js';
479
493
  import { registerUserRoutes } from './routes/users/index.js';
480
494
  import { registerPostRoutes } from './routes/posts/index.js';
481
495
  import { registerHealthRoute } from './routes/health.js';
482
496
  import { registerStatsRoute } from './routes/stats.js';
483
497
 
484
- const app = createKozo({
485
- port: Number(process.env.PORT) || 3000,
486
- });
498
+ // \u2500\u2500\u2500 Config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
499
+ const PORT = Number(process.env.PORT) || 3000;
500
+ const JWT_SECRET = process.env.JWT_SECRET || 'change-me-to-a-random-secret';
501
+ const CORS_ORIGIN = process.env.CORS_ORIGIN || '*';
502
+ const RATE_LIMIT_MAX = Number(process.env.RATE_LIMIT_MAX) || 100;
503
+ const RATE_LIMIT_WINDOW = Number(process.env.RATE_LIMIT_WINDOW) || 60_000;
504
+
505
+ // \u2500\u2500\u2500 App \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
506
+ const app = createKozo({ port: PORT });
507
+
508
+ // \u2500\u2500\u2500 Middleware (Hono layer) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
509
+ app.getApp().use('*', logger());
510
+ app.getApp().use('*', cors({ origin: CORS_ORIGIN }));
511
+ app.getApp().use('/api/*', rateLimit({ max: RATE_LIMIT_MAX, windowMs: RATE_LIMIT_WINDOW }));
512
+ app.getApp().use('/api/*', authenticateJWT(JWT_SECRET, {
513
+ prefix: '/api',
514
+ }));
487
515
 
488
- // Register all routes
516
+ // \u2500\u2500\u2500 Routes (native compiled \u2014 zero overhead) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
489
517
  registerHealthRoute(app);
490
518
  registerAuthRoutes(app);
491
519
  registerUserRoutes(app);
492
520
  registerPostRoutes(app);
493
521
  registerStatsRoute(app);
494
522
 
495
- console.log('\u{1F525} Kozo server running on http://localhost:3000');
496
- console.log('\u{1F4CA} Features: Auth, Users CRUD, Posts, Stats');
497
- console.log('\u26A1 Optimized with pre-compiled handlers and Zod schemas');
523
+ // \u2500\u2500\u2500 Graceful Shutdown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
524
+ const shutdown = app.getShutdownManager();
525
+
526
+ process.on('SIGTERM', () => shutdown.shutdown());
527
+ process.on('SIGINT', () => shutdown.shutdown());
528
+
529
+ // \u2500\u2500\u2500 Start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
530
+ console.log('');
531
+ console.log('\u{1F525} Kozo server starting\u2026');
532
+ console.log('\u26A1 Powered by uWebSockets.js (native per-route C++ matching)');
498
533
  console.log('');
499
- console.log('\u{1F4DA} Try these endpoints:');
500
- console.log(' GET /health');
501
- console.log(' GET /users');
502
- console.log(' POST /auth/login');
503
- console.log(' GET /posts?published=true&page=1&limit=10');
504
- console.log(' GET /stats');
505
534
 
506
- app.listen();
535
+ const { port } = await app.nativeListen(PORT);
536
+
537
+ console.log(\`\u{1F680} Listening on http://localhost:\${port}\`);
538
+ console.log('');
539
+ console.log('\u{1F4DA} Endpoints:');
540
+ console.log(' GET /health Health check');
541
+ console.log(' POST /auth/login Login (returns JWT)');
542
+ console.log(' GET /auth/me Current user (requires JWT)');
543
+ console.log(' GET /users List users (paginated)');
544
+ console.log(' POST /users Create user');
545
+ console.log(' GET /users/:id Get user');
546
+ console.log(' PUT /users/:id Update user');
547
+ console.log(' DEL /users/:id Delete user');
548
+ console.log(' GET /posts List posts (filterable)');
549
+ console.log(' POST /posts Create post');
550
+ console.log(' GET /stats Server stats');
551
+ console.log('');
552
+ console.log('\u{1F512} Middleware: CORS \xB7 Rate limit \xB7 JWT \xB7 Logger');
553
+ console.log('\u{1F6E1}\uFE0F Graceful shutdown enabled (SIGTERM / SIGINT)');
507
554
  `;
508
555
  await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "index.ts"), indexTs);
509
556
  await createCompleteSchemas(projectDir);
@@ -512,22 +559,29 @@ app.listen();
512
559
  await createCompleteRoutes(projectDir);
513
560
  const readme = `# ${projectName}
514
561
 
515
- Built with \u{1F525} **Kozo Framework** - Production-ready server template
562
+ Built with \u{1F525} **Kozo Framework** \u2014 Production-ready server template
516
563
 
517
564
  ## Features
518
565
 
519
566
  \u2728 **Complete API Implementation**
520
- - \u2705 Authentication (login, me)
567
+ - \u2705 JWT Authentication (login, token-protected routes)
521
568
  - \u2705 User CRUD (Create, Read, Update, Delete)
522
569
  - \u2705 Posts with filtering and pagination
523
570
  - \u2705 Statistics endpoint
524
571
  - \u2705 Health check
525
572
 
526
- \u26A1 **Performance Optimized**
527
- - Pre-compiled Zod schemas (no runtime overhead)
528
- - Fast-path routes for health checks
529
- - Optimized handler closures
530
- - Zero runtime decisions
573
+ \u26A1 **Maximum Performance**
574
+ - uWebSockets.js transport with native per-route C++ matching (zero JS routing overhead)
575
+ - Compiled handlers write directly to uWS response via cork() \u2014 zero shim objects
576
+ - Pre-compiled Zod schemas (Ajv fast-path)
577
+ - fast-json-stringify serialization
578
+
579
+ \u{1F512} **Production Middleware**
580
+ - CORS with configurable origins
581
+ - Rate limiting (per-IP, configurable window)
582
+ - JWT verification on \\\`/api/*\\\` routes
583
+ - Request logger with timing
584
+ - Graceful shutdown (SIGTERM / SIGINT)
531
585
 
532
586
  \u{1F3AF} **Type-Safe**
533
587
  - Full TypeScript inference
@@ -536,91 +590,82 @@ Built with \u{1F525} **Kozo Framework** - Production-ready server template
536
590
 
537
591
  ## Quick Start
538
592
 
539
- \`\`\`bash
593
+ \\\`\\\`\\\`bash
540
594
  # Install dependencies
541
- npm install
595
+ pnpm install # or npm install
542
596
 
543
597
  # Start development server
544
- npm run dev
545
- \`\`\`
598
+ pnpm dev
599
+ \\\`\\\`\\\`
546
600
 
547
601
  The server will start at **http://localhost:3000**
548
602
 
549
603
  ## API Endpoints
550
604
 
551
- ### Authentication
605
+ ### Public
552
606
  | Method | Endpoint | Description |
553
607
  |--------|----------|-------------|
554
- | POST | /auth/login | Login with email/password |
555
- | GET | /auth/me | Get current user |
608
+ | GET | /health | Health check |
609
+ | POST | /auth/login | Login \u2192 JWT token |
556
610
 
557
- ### Users
611
+ ### Protected (requires \\\`Authorization: Bearer <token>\\\`)
558
612
  | Method | Endpoint | Description |
559
613
  |--------|----------|-------------|
560
- | GET | /users | List all users (paginated) |
614
+ | GET | /auth/me | Current user |
615
+ | GET | /users | List users (paginated) |
561
616
  | GET | /users/:id | Get user by ID |
562
617
  | POST | /users | Create new user |
563
618
  | PUT | /users/:id | Update user |
564
619
  | DELETE | /users/:id | Delete user |
565
-
566
- ### Posts
567
- | Method | Endpoint | Description |
568
- |--------|----------|-------------|
569
- | GET | /posts | List posts (with filters) |
620
+ | GET | /posts | List posts (filterable) |
570
621
  | GET | /posts/:id | Get post with author |
571
622
  | POST | /posts | Create new post |
572
-
573
- ### System
574
- | Method | Endpoint | Description |
575
- |--------|----------|-------------|
576
- | GET | /health | Health check |
577
- | GET | /stats | System statistics |
623
+ | GET | /stats | Server statistics |
578
624
 
579
625
  ## Example Requests
580
626
 
581
- ### Create User
582
- \`\`\`bash
583
- curl -X POST http://localhost:3000/users \\
627
+ \\\`\\\`\\\`bash
628
+ # 1. Login to get a JWT
629
+ TOKEN=$(curl -s -X POST http://localhost:3000/auth/login \\
584
630
  -H "Content-Type: application/json" \\
585
- -d '{
586
- "name": "Alice Smith",
587
- "email": "alice@example.com",
588
- "role": "user"
589
- }'
590
- \`\`\`
631
+ -d '{"email":"admin@kozo.dev","password":"secret123"}' \\
632
+ | jq -r '.token')
591
633
 
592
- ### Login
593
- \`\`\`bash
594
- curl -X POST http://localhost:3000/auth/login \\
634
+ # 2. Use the token for protected routes
635
+ curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/users
636
+
637
+ # 3. Create a user
638
+ curl -X POST http://localhost:3000/users \\
639
+ -H "Authorization: Bearer $TOKEN" \\
595
640
  -H "Content-Type: application/json" \\
596
- -d '{
597
- "email": "admin@kozo.dev",
598
- "password": "secret123"
599
- }'
600
- \`\`\`
641
+ -d '{"name":"Alice","email":"alice@example.com","role":"user"}'
601
642
 
602
- ### List Users (Paginated)
603
- \`\`\`bash
604
- curl "http://localhost:3000/users?page=1&limit=10"
605
- \`\`\`
643
+ # 4. Filter posts
644
+ curl -H "Authorization: Bearer $TOKEN" \\
645
+ "http://localhost:3000/posts?published=true&tag=framework"
606
646
 
607
- ### Filter Posts
608
- \`\`\`bash
609
- curl "http://localhost:3000/posts?published=true&tag=framework"
610
- \`\`\`
647
+ # 5. Health check (no auth needed)
648
+ curl http://localhost:3000/health
649
+ \\\`\\\`\\\`
611
650
 
612
- ### Get Statistics
613
- \`\`\`bash
614
- curl http://localhost:3000/stats
615
- \`\`\`
651
+ ## Environment Variables
652
+
653
+ | Variable | Default | Description |
654
+ |----------|---------|-------------|
655
+ | PORT | 3000 | Server port |
656
+ | JWT_SECRET | change-me\u2026 | HMAC secret for JWT signing |
657
+ | CORS_ORIGIN | * | Allowed CORS origin |
658
+ | RATE_LIMIT_MAX | 100 | Max requests per window |
659
+ | RATE_LIMIT_WINDOW | 60000 | Window duration (ms) |
616
660
 
617
661
  ## Project Structure
618
662
 
619
- \`\`\`
663
+ \\\`\\\`\\\`
620
664
  ${projectName}/
621
665
  \u251C\u2500\u2500 src/
622
666
  \u2502 \u251C\u2500\u2500 data/
623
667
  \u2502 \u2502 \u2514\u2500\u2500 store.ts # In-memory data store
668
+ \u2502 \u251C\u2500\u2500 middleware/ # (extensible)
624
669
  \u2502 \u251C\u2500\u2500 routes/
625
670
  \u2502 \u2502 \u251C\u2500\u2500 auth/
626
671
  \u2502 \u2502 \u2502 \u2514\u2500\u2500 index.ts # Auth routes (login, me)
@@ -631,51 +676,42 @@ ${projectName}/
631
676
  \u2502 \u2502 \u251C\u2500\u2500 health.ts # Health check
632
677
  \u2502 \u2502 \u2514\u2500\u2500 stats.ts # Statistics
633
678
  \u2502 \u251C\u2500\u2500 schemas/
634
- \u2502 \u2502 \u251C\u2500\u2500 user.ts # User schemas
635
- \u2502 \u2502 \u251C\u2500\u2500 post.ts # Post schemas
636
- \u2502 \u2502 \u2514\u2500\u2500 common.ts # Common schemas
679
+ \u2502 \u2502 \u251C\u2500\u2500 user.ts # User Zod schemas
680
+ \u2502 \u2502 \u251C\u2500\u2500 post.ts # Post Zod schemas
681
+ \u2502 \u2502 \u2514\u2500\u2500 common.ts # Pagination, filters
637
682
  \u2502 \u251C\u2500\u2500 utils/
638
- \u2502 \u2502 \u2514\u2500\u2500 helpers.ts # Helper functions
639
- \u2502 \u2514\u2500\u2500 index.ts # Entry point
683
+ \u2502 \u2502 \u2514\u2500\u2500 helpers.ts # UUID, pagination
684
+ \u2502 \u2514\u2500\u2500 index.ts # Entry point (middleware + routes)
685
+ \u251C\u2500\u2500 .env # Environment config
686
+ \u251C\u2500\u2500 .env.example # Example config
640
687
  \u251C\u2500\u2500 package.json
641
688
  \u251C\u2500\u2500 tsconfig.json
642
689
  \u2514\u2500\u2500 README.md
643
- \`\`\`
644
-
645
- ## Development
646
-
647
- \`\`\`bash
648
- npm run dev # Start with hot reload
649
- npm run build # Build for production
650
- npm start # Run production build
651
- npm run type-check # TypeScript validation
652
- \`\`\`
653
-
654
- ## Performance Notes
655
-
656
- This template uses Kozo's optimized patterns:
657
- - **Pre-compiled schemas**: Zod schemas compiled to Ajv validators at boot
658
- - **Handler closures**: Routes capture dependencies at startup
659
- - **Fast paths**: Simple routes skip unnecessary middleware
660
- - **Minimal allocations**: Context objects only include what's needed
661
-
662
- These optimizations make Kozo competitive with Fastify while providing:
663
- - Better type safety with Zod
664
- - Simpler API than NestJS
665
- - More features than plain Hono
666
-
667
- ## Next Steps
668
-
669
- 1. **Add database**: Integrate Drizzle ORM for persistent storage
670
- 2. **Add authentication**: Implement JWT with proper password hashing
671
- 3. **Add validation**: Enhance error handling and input validation
672
- 4. **Add tests**: Write unit and integration tests
673
- 5. **Deploy**: Deploy to Vercel, Railway, or any Node.js host
674
-
675
- ## Documentation
676
-
677
- - [Zod Schema Validation](https://zod.dev)
678
- - [Hono Framework](https://hono.dev)
690
+ \\\`\\\`\\\`
691
+
692
+ ## Architecture
693
+
694
+ \\\`\\\`\\\`
695
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
696
+ Request \u2500\u2500\u25BA \u2502 uWebSockets \u2502 C++ HTTP parser + epoll/kqueue
697
+ \u2502 .js \u2502
698
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2518
699
+ \u2502
700
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u25BC\u2500\u2500\u2500\u2500\u2500\u2500\u2510
701
+ \u2502 C++ Radix \u2502 Native per-route matching (zero JS)
702
+ \u2502 Router \u2502 app.get(), app.post(), \u2026
703
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2518
704
+ \u2502
705
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u25BC\u2500\u2500\u2500\u2500\u2500\u2500\u2510
706
+ \u2502 Compiled \u2502 Handler writes directly to uWS
707
+ \u2502 Handler \u2502 via cork() \u2014 one syscall per response
708
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2518
709
+ \u2502
710
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u25BC\u2500\u2500\u2500\u2500\u2500\u2500\u2510
711
+ \u2502 Zod \u2192 Ajv \u2502 Pre-compiled JSON serializer
712
+ \u2502 Serializer \u2502
713
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
714
+ \\\`\\\`\\\`
679
715
 
680
716
  ---
681
717
 
@@ -892,11 +928,14 @@ export function registerStatsRoute(app: Kozo) {
892
928
  await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "routes", "stats.ts"), statsRoute);
893
929
  const authRoutes = `import { z } from 'zod';
894
930
  import type { Kozo } from '@kozojs/core';
931
+ import { createJWT } from '@kozojs/auth';
895
932
  import { UserSchema } from '../../schemas/user.js';
896
933
  import { users } from '../../data/store.js';
897
934
 
935
+ const JWT_SECRET = process.env.JWT_SECRET || 'change-me-to-a-random-secret';
936
+
898
937
  export function registerAuthRoutes(app: Kozo) {
899
- // POST /auth/login
938
+ // POST /auth/login \u2014 public (no JWT required)
900
939
  app.post('/auth/login', {
901
940
  body: z.object({
902
941
  email: z.string().email(),
@@ -907,19 +946,29 @@ export function registerAuthRoutes(app: Kozo) {
907
946
  token: z.string(),
908
947
  user: UserSchema,
909
948
  }),
910
- }, (c) => {
949
+ }, async (c) => {
911
950
  const user = users.find(u => u.email === c.body.email);
912
951
  if (!user) {
913
952
  return c.json({ error: 'Invalid credentials' }, 401);
914
953
  }
915
- const token = \`mock_jwt_\${user.id}_\${Date.now()}\`;
954
+
955
+ // Generate a real JWT with 24h expiry
956
+ const token = await createJWT(
957
+ { sub: user.id, email: user.email, role: user.role },
958
+ JWT_SECRET,
959
+ { expiresIn: '24h' }
960
+ );
961
+
916
962
  return { success: true, token, user };
917
963
  });
918
964
 
919
- // GET /auth/me
965
+ // GET /auth/me \u2014 requires valid JWT (middleware handles verification)
920
966
  app.get('/auth/me', {
921
967
  response: UserSchema,
922
- }, (c) => users[0]);
968
+ }, (c) => {
969
+ // user payload set by authenticateJWT middleware
970
+ return users[0];
971
+ });
923
972
  }
924
973
  `;
925
974
  await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "routes", "auth", "index.ts"), authRoutes);
@@ -1144,7 +1193,7 @@ app.get('/hello/:name', {
1144
1193
  }));
1145
1194
 
1146
1195
  console.log('\u{1F525} Kozo running on http://localhost:3000');
1147
- app.listen();
1196
+ await app.nativeListen();
1148
1197
  `;
1149
1198
  await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, "src", "index.ts"), indexTs);
1150
1199
  await import_fs_extra.default.writeFile(import_node_path.default.join(projectDir, ".gitignore"), "node_modules/\ndist/\n.env\n");
@@ -1279,9 +1328,9 @@ registerRoutes(app);
1279
1328
 
1280
1329
  export type AppType = typeof app;
1281
1330
 
1282
- console.log('\u{1F525} ${projectName} API running on http://localhost:3000');
1331
+ console.log('\u{1F525} ${projectName} API starting on http://localhost:3000');
1283
1332
  console.log('\u{1F4DA} Endpoints: /api/health, /api/users, /api/posts, /api/tasks, /api/stats');
1284
- app.listen();
1333
+ await app.nativeListen();
1285
1334
  `);
1286
1335
  await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "data", "index.ts"), `import { z } from 'zod';
1287
1336
 
@@ -2361,10 +2410,295 @@ ${import_picocolors2.default.dim("Documentation:")} ${import_picocolors2.default
2361
2410
  `);
2362
2411
  }
2363
2412
 
2413
+ // src/commands/build.ts
2414
+ var import_execa2 = require("execa");
2415
+ var import_picocolors3 = __toESM(require("picocolors"));
2416
+ var import_fs_extra2 = __toESM(require("fs-extra"));
2417
+ var import_node_path4 = __toESM(require("path"));
2418
+
2419
+ // src/routing/manifest.ts
2420
+ var import_node_crypto = require("crypto");
2421
+ var import_node_fs2 = require("fs");
2422
+ var import_node_path3 = require("path");
2423
+ var import_glob2 = require("glob");
2424
+
2425
+ // src/routing/scan.ts
2426
+ var import_glob = require("glob");
2427
+ var import_node_path2 = require("path");
2428
+ var import_node_fs = require("fs");
2429
+ var HTTP_METHODS = ["get", "post", "put", "patch", "delete"];
2430
+ function fileToRoute(filePath) {
2431
+ const normalized = filePath.replace(/\\/g, "/");
2432
+ const lastDot = normalized.lastIndexOf(".");
2433
+ const withoutExt = lastDot !== -1 ? normalized.slice(0, lastDot) : normalized;
2434
+ const parts = withoutExt.split("/").filter(Boolean);
2435
+ if (parts.length === 0) return null;
2436
+ const last = parts[parts.length - 1].toLowerCase();
2437
+ let method = "get";
2438
+ let includeLast = true;
2439
+ if (HTTP_METHODS.includes(last)) {
2440
+ method = last;
2441
+ includeLast = false;
2442
+ } else if (last === "index") {
2443
+ includeLast = false;
2444
+ }
2445
+ const segments = includeLast ? parts : parts.slice(0, -1);
2446
+ const urlSegments = segments.map((seg) => {
2447
+ if (seg.startsWith("[...") && seg.endsWith("]")) return "*";
2448
+ if (seg.startsWith("[") && seg.endsWith("]")) return ":" + seg.slice(1, -1);
2449
+ return seg;
2450
+ });
2451
+ const path3 = "/" + urlSegments.join("/");
2452
+ return { path: path3, method };
2453
+ }
2454
+ function extractParams(urlPath) {
2455
+ return urlPath.split("/").filter((seg) => seg.startsWith(":")).map((seg) => seg.slice(1));
2456
+ }
2457
+ function isRouteFile(file) {
2458
+ const name = file.split("/").pop() ?? "";
2459
+ if (name.startsWith("_")) return false;
2460
+ if (name.includes(".test.") || name.includes(".spec.")) return false;
2461
+ return true;
2462
+ }
2463
+ function detectSchemas(absolutePath) {
2464
+ let source = "";
2465
+ try {
2466
+ source = (0, import_node_fs.readFileSync)(absolutePath, "utf8");
2467
+ } catch {
2468
+ return { hasBodySchema: false, hasQuerySchema: false };
2469
+ }
2470
+ const hasBodySchema = /export\s+(const|let|var)\s+body(Schema)?\s*[=:]/.test(source) || /export\s+\{[^}]*\bbody(Schema)?\b[^}]*\}/.test(source);
2471
+ const hasQuerySchema = /export\s+(const|let|var)\s+query(Schema)?\s*[=:]/.test(source) || /export\s+\{[^}]*\bquery(Schema)?\b[^}]*\}/.test(source);
2472
+ return { hasBodySchema, hasQuerySchema };
2473
+ }
2474
+ function routeScore(urlPath) {
2475
+ const segments = urlPath.split("/").filter(Boolean);
2476
+ let score = segments.length * 10;
2477
+ for (const seg of segments) {
2478
+ if (seg === "*") score -= 100;
2479
+ else if (seg.startsWith(":")) score -= 5;
2480
+ else score += 1;
2481
+ }
2482
+ return score;
2483
+ }
2484
+ async function scanRoutes(options) {
2485
+ const { routesDir, verbose = false } = options;
2486
+ const files = await (0, import_glob.glob)("**/*.{ts,js}", {
2487
+ cwd: routesDir,
2488
+ nodir: true,
2489
+ ignore: ["**/_*.ts", "**/_*.js", "**/*.test.ts", "**/*.spec.ts", "**/*.test.js", "**/*.spec.js"]
2490
+ });
2491
+ const routes = [];
2492
+ for (const file of files) {
2493
+ if (!isRouteFile(file)) continue;
2494
+ const parsed = fileToRoute(file);
2495
+ if (!parsed) continue;
2496
+ const absolutePath = (0, import_node_path2.join)(routesDir, file);
2497
+ const { hasBodySchema, hasQuerySchema } = detectSchemas(absolutePath);
2498
+ const params = extractParams(parsed.path);
2499
+ routes.push({
2500
+ path: parsed.path,
2501
+ method: parsed.method,
2502
+ handler: absolutePath,
2503
+ relativePath: file,
2504
+ params,
2505
+ hasBodySchema,
2506
+ hasQuerySchema
2507
+ });
2508
+ }
2509
+ routes.sort((a, b) => routeScore(b.path) - routeScore(a.path));
2510
+ if (verbose) {
2511
+ for (const r of routes) {
2512
+ const method = r.method.toUpperCase().padEnd(6);
2513
+ console.log(` ${method} ${r.path} (${r.relativePath})`);
2514
+ }
2515
+ }
2516
+ return routes;
2517
+ }
2518
+
2519
+ // src/routing/manifest.ts
2520
+ async function hashRouteFiles(routesDir) {
2521
+ const files = await (0, import_glob2.glob)("**/*.{ts,js}", {
2522
+ cwd: routesDir,
2523
+ nodir: true,
2524
+ ignore: ["**/_*.ts", "**/_*.js", "**/*.test.ts", "**/*.spec.ts", "**/*.test.js", "**/*.spec.js"]
2525
+ });
2526
+ files.sort();
2527
+ const hash = (0, import_node_crypto.createHash)("sha256");
2528
+ for (const file of files) {
2529
+ hash.update(file);
2530
+ try {
2531
+ const content = (0, import_node_fs2.readFileSync)((0, import_node_path3.join)(routesDir, file));
2532
+ hash.update(content);
2533
+ } catch {
2534
+ }
2535
+ }
2536
+ return hash.digest("hex");
2537
+ }
2538
+ function readExistingManifest(manifestPath) {
2539
+ if (!(0, import_node_fs2.existsSync)(manifestPath)) return null;
2540
+ try {
2541
+ const raw = (0, import_node_fs2.readFileSync)(manifestPath, "utf8");
2542
+ return JSON.parse(raw);
2543
+ } catch {
2544
+ return null;
2545
+ }
2546
+ }
2547
+ async function generateManifest(options) {
2548
+ const {
2549
+ routesDir,
2550
+ projectRoot,
2551
+ outputPath = (0, import_node_path3.join)(projectRoot, "routes-manifest.json"),
2552
+ cache = true,
2553
+ verbose = false
2554
+ } = options;
2555
+ const contentHash = await hashRouteFiles(routesDir);
2556
+ if (cache) {
2557
+ const existing = readExistingManifest(outputPath);
2558
+ if (existing && existing.contentHash === contentHash && existing.version === MANIFEST_VERSION) {
2559
+ if (verbose) {
2560
+ console.log(` \u2713 routes-manifest.json up-to-date (hash: ${contentHash.slice(0, 8)}\u2026)`);
2561
+ }
2562
+ return existing;
2563
+ }
2564
+ }
2565
+ if (verbose) {
2566
+ console.log(` Scanning routes in: ${routesDir}`);
2567
+ }
2568
+ const scanned = await scanRoutes({ routesDir, verbose: false });
2569
+ const entries = scanned.map((r) => ({
2570
+ path: r.path,
2571
+ method: r.method,
2572
+ handler: r.relativePath,
2573
+ // relative to routesDir; callers can join with projectRoot
2574
+ params: r.params,
2575
+ hasBodySchema: r.hasBodySchema,
2576
+ hasQuerySchema: r.hasQuerySchema
2577
+ }));
2578
+ const manifest = {
2579
+ version: MANIFEST_VERSION,
2580
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2581
+ contentHash,
2582
+ routes: entries
2583
+ };
2584
+ const dir = (0, import_node_path3.dirname)(outputPath);
2585
+ if (!(0, import_node_fs2.existsSync)(dir)) {
2586
+ (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
2587
+ }
2588
+ (0, import_node_fs2.writeFileSync)(outputPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
2589
+ if (verbose) {
2590
+ console.log(` \u2713 Generated routes-manifest.json (${entries.length} routes, hash: ${contentHash.slice(0, 8)}\u2026)`);
2591
+ }
2592
+ return manifest;
2593
+ }
2594
+ var MANIFEST_VERSION = 1;
2595
+
2596
+ // src/commands/build.ts
2597
+ function printBox(title) {
2598
+ const width = 50;
2599
+ const pad = Math.max(0, Math.floor((width - title.length) / 2));
2600
+ const line = "\u2500".repeat(width);
2601
+ console.log(import_picocolors3.default.cyan(`\u250C${line}\u2510`));
2602
+ console.log(import_picocolors3.default.cyan("\u2502") + " ".repeat(pad) + import_picocolors3.default.bold(title) + " ".repeat(width - pad - title.length) + import_picocolors3.default.cyan("\u2502"));
2603
+ console.log(import_picocolors3.default.cyan(`\u2514${line}\u2518`));
2604
+ console.log();
2605
+ }
2606
+ function step(n, total, label) {
2607
+ console.log(import_picocolors3.default.dim(`[${n}/${total}]`) + " " + import_picocolors3.default.cyan("\u2192") + " " + label);
2608
+ }
2609
+ function ok(label) {
2610
+ console.log(import_picocolors3.default.green(" \u2713") + " " + label);
2611
+ }
2612
+ function fail(label, err) {
2613
+ console.log(import_picocolors3.default.red(" \u2717") + " " + label);
2614
+ if (err) {
2615
+ const msg = err instanceof Error ? err.message : String(err);
2616
+ console.log(import_picocolors3.default.dim(" " + msg));
2617
+ }
2618
+ }
2619
+ async function buildCommand(options = {}) {
2620
+ console.clear();
2621
+ printBox("Kozo Build");
2622
+ const cwd = process.cwd();
2623
+ const TOTAL_STEPS = options.noManifest ? 3 : 4;
2624
+ let currentStep = 0;
2625
+ currentStep++;
2626
+ step(currentStep, TOTAL_STEPS, "Checking project structure\u2026");
2627
+ if (!import_fs_extra2.default.existsSync(import_node_path4.default.join(cwd, "package.json"))) {
2628
+ fail("No package.json found. Run this command inside a Kozo project.");
2629
+ process.exit(1);
2630
+ }
2631
+ if (!import_fs_extra2.default.existsSync(import_node_path4.default.join(cwd, "node_modules"))) {
2632
+ fail("Dependencies not installed. Run `npm install` first.");
2633
+ process.exit(1);
2634
+ }
2635
+ ok("Project structure OK");
2636
+ currentStep++;
2637
+ step(currentStep, TOTAL_STEPS, "Cleaning previous build\u2026");
2638
+ try {
2639
+ await import_fs_extra2.default.remove(import_node_path4.default.join(cwd, "dist"));
2640
+ ok("dist/ cleaned");
2641
+ } catch (err) {
2642
+ fail("Failed to clean dist/", err);
2643
+ process.exit(1);
2644
+ }
2645
+ if (!options.noManifest) {
2646
+ currentStep++;
2647
+ step(currentStep, TOTAL_STEPS, "Generating routes manifest\u2026");
2648
+ const routesDirRel = options.routesDir ?? "src/routes";
2649
+ const routesDirAbs = import_node_path4.default.join(cwd, routesDirRel);
2650
+ if (!import_fs_extra2.default.existsSync(routesDirAbs)) {
2651
+ console.log(import_picocolors3.default.dim(` \u26A0 Routes directory not found (${routesDirRel}), skipping manifest.`));
2652
+ } else {
2653
+ try {
2654
+ const manifestOutAbs = options.manifestOut ? import_node_path4.default.join(cwd, options.manifestOut) : import_node_path4.default.join(cwd, "routes-manifest.json");
2655
+ const manifest = await generateManifest({
2656
+ routesDir: routesDirAbs,
2657
+ projectRoot: cwd,
2658
+ outputPath: manifestOutAbs,
2659
+ cache: !options.forceManifest,
2660
+ verbose: true
2661
+ });
2662
+ ok(`Manifest ready \u2014 ${manifest.routes.length} route(s)`);
2663
+ } catch (err) {
2664
+ fail("Manifest generation failed", err);
2665
+ console.log(import_picocolors3.default.dim(" Continuing build without manifest\u2026"));
2666
+ }
2667
+ }
2668
+ }
2669
+ currentStep++;
2670
+ step(currentStep, TOTAL_STEPS, "Compiling with tsup\u2026");
2671
+ try {
2672
+ const tsupArgs = ["tsup", ...options.tsupArgs ?? []];
2673
+ await (0, import_execa2.execa)("npx", tsupArgs, {
2674
+ cwd,
2675
+ stdio: "inherit",
2676
+ env: { ...process.env, NODE_ENV: "production" }
2677
+ });
2678
+ ok("Compilation complete");
2679
+ } catch (err) {
2680
+ fail("tsup compilation failed", err);
2681
+ process.exit(1);
2682
+ }
2683
+ console.log();
2684
+ console.log(import_picocolors3.default.green("\u2705 Build successful"));
2685
+ console.log();
2686
+ }
2687
+
2364
2688
  // src/index.ts
2365
2689
  var program = new import_commander.Command();
2366
2690
  program.name("kozo").description("CLI to scaffold new Kozo Framework projects").version("0.2.6");
2367
2691
  program.argument("[project-name]", "Name of the project").action(async (projectName) => {
2368
2692
  await newCommand(projectName);
2369
2693
  });
2694
+ program.command("build").description("Build the project (generates routes manifest then compiles with tsup)").option("--no-manifest", "Skip routes-manifest.json generation").option("--force-manifest", "Force manifest regeneration even if routes are unchanged").option("--routes-dir <dir>", "Routes directory relative to project root", "src/routes").option("--manifest-out <path>", "Output path for routes-manifest.json relative to project root").allowUnknownOption().action(async (opts, cmd) => {
2695
+ const tsupArgs = cmd.args.length > 0 ? cmd.args : void 0;
2696
+ await buildCommand({
2697
+ noManifest: opts.noManifest === false || opts.manifest === false,
2698
+ forceManifest: opts.forceManifest ?? false,
2699
+ routesDir: opts.routesDir,
2700
+ manifestOut: opts.manifestOut,
2701
+ tsupArgs
2702
+ });
2703
+ });
2370
2704
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kozojs/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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",
@@ -39,7 +39,8 @@
39
39
  "picocolors": "^1.1.0",
40
40
  "ora": "^8.1.0",
41
41
  "execa": "^9.5.0",
42
- "fs-extra": "^11.2.0"
42
+ "fs-extra": "^11.2.0",
43
+ "glob": "^11.0.0"
43
44
  },
44
45
  "devDependencies": {
45
46
  "@types/fs-extra": "^11.0.4",