speexjs 0.2.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -124,7 +124,7 @@ import { existsSync, mkdirSync, writeFileSync } from "fs";
124
124
  import { dirname, resolve } from "path";
125
125
  var TEMPLATES = {
126
126
  blank: {
127
- dirs: ["src"],
127
+ dirs: ["src", "src/config"],
128
128
  files: {
129
129
  "package.json": (name) => JSON.stringify(
130
130
  {
@@ -135,13 +135,15 @@ var TEMPLATES = {
135
135
  scripts: {
136
136
  dev: "speexjs serve",
137
137
  build: "speexjs build",
138
- start: "node dist/index.js"
138
+ start: "node dist/index.js",
139
+ lint: "tsc --noEmit"
139
140
  },
140
141
  dependencies: {
141
142
  speexjs: "latest"
142
143
  },
143
144
  devDependencies: {
144
145
  "@types/node": "^26.0.1",
146
+ tsx: "^4.19.0",
145
147
  typescript: "^5.7.0"
146
148
  }
147
149
  },
@@ -161,7 +163,8 @@ var TEMPLATES = {
161
163
  isolatedModules: true,
162
164
  resolveJsonModule: true,
163
165
  outDir: "./dist",
164
- rootDir: "./src"
166
+ rootDir: "./src",
167
+ skipLibCheck: true
165
168
  },
166
169
  include: ["src/**/*.ts"],
167
170
  exclude: ["node_modules", "dist"]
@@ -170,28 +173,61 @@ var TEMPLATES = {
170
173
  2
171
174
  ),
172
175
  "src/index.ts": `import { speexjs } from 'speexjs/server'
176
+ import { schema } from 'speexjs/schema'
177
+ import { Config } from './config/index.js'
173
178
 
179
+ // \u2500\u2500\u2500 Application \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
174
180
  const app = speexjs()
175
181
 
176
- const PORT = Number(process.env.PORT) || 3000
177
-
182
+ // \u2500\u2500\u2500 Routes \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
178
183
  app.get('/', async ({ response }) => {
179
- return response.html('<h1>SpeexJS \u{1F680}</h1>')
184
+ return response.html(\`
185
+ <!DOCTYPE html>
186
+ <html lang="en">
187
+ <head>
188
+ <meta charset="UTF-8" />
189
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
190
+ <title>SpeexJS App</title>
191
+ </head>
192
+ <body>
193
+ <h1>SpeexJS \u{1F680}</h1>
194
+ <p>Server is running on port \${Config.port}</p>
195
+ </body>
196
+ </html>
197
+ \`)
180
198
  })
181
199
 
182
- app.listen(PORT, () => {
183
- console.log(\`SpeexJS running on http://localhost:\${PORT}\`)
200
+ // \u2500\u2500\u2500 Health Check \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
201
+ app.get('/api/health', async ({ response }) => {
202
+ return response.json({
203
+ status: 'ok',
204
+ timestamp: new Date().toISOString(),
205
+ uptime: process.uptime(),
206
+ })
184
207
  })
185
- `,
186
- "src/app.ts": `import { speexjs } from 'speexjs/server'
187
208
 
188
- export function createApp() {
189
- const app = speexjs()
190
- return app
191
- }
209
+ // \u2500\u2500\u2500 Start Server \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
210
+ app.listen(Config.port, () => {
211
+ console.log(\`\u2713 SpeexJS running at http://localhost:\${Config.port}\`)
212
+ })
192
213
  `,
193
- ".env.example": `PORT=3000
214
+ "src/config/index.ts": `export const Config = {
215
+ port: Number(process.env.PORT) || 3000,
216
+ host: process.env.HOST || 'localhost',
217
+ env: process.env.NODE_ENV || 'development',
218
+ appKey: process.env.APP_KEY || '',
219
+ isDev: process.env.NODE_ENV !== 'production',
220
+ isProd: process.env.NODE_ENV === 'production',
221
+ } as const
222
+ `,
223
+ ".env.example": `# Server
224
+ PORT=3000
194
225
  NODE_ENV=development
226
+ HOST=localhost
227
+
228
+ # Auth (change in production!)
229
+ APP_KEY=your-base64-32-byte-key-here
230
+ SESSION_SECRET=change-this-in-production
195
231
  `,
196
232
  ".gitignore": `node_modules/
197
233
  dist/
@@ -228,6 +264,7 @@ dist/
228
264
  },
229
265
  devDependencies: {
230
266
  "@types/node": "^26.0.1",
267
+ tsx: "^4.19.0",
231
268
  typescript: "^5.7.0"
232
269
  }
233
270
  },
@@ -258,14 +295,17 @@ dist/
258
295
  2
259
296
  ),
260
297
  "src/server/index.ts": `import { speexjs } from 'speexjs/server'
298
+ import { schema } from 'speexjs/schema'
261
299
  import { UserController } from './controllers/user.controller.js'
262
300
 
263
301
  const PORT = Number(process.env.PORT) || 3000
264
302
 
265
303
  const app = speexjs()
266
304
 
305
+ // \u2500\u2500\u2500 Controllers \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
267
306
  app.controller(UserController)
268
307
 
308
+ // \u2500\u2500\u2500 Routes \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
269
309
  app.get('/', async ({ response }) => {
270
310
  return response.html(\`
271
311
  <!DOCTYPE html>
@@ -283,8 +323,9 @@ app.get('/', async ({ response }) => {
283
323
  \`)
284
324
  })
285
325
 
326
+ // \u2500\u2500\u2500 Start Server \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
286
327
  app.listen(PORT, () => {
287
- console.log(\`SpeexJS running on http://localhost:\${PORT}\`)
328
+ console.log(\`\u2713 SpeexJS running on http://localhost:\${PORT}\`)
288
329
  })
289
330
  `,
290
331
  "src/server/controllers/user.controller.ts": `import { Controller, get, post } from 'speexjs/server'
@@ -352,8 +393,17 @@ body {
352
393
  message?: string
353
394
  }
354
395
  `,
355
- ".env.example": `PORT=3000
396
+ ".env.example": `# Server
397
+ PORT=3000
356
398
  NODE_ENV=development
399
+ HOST=localhost
400
+
401
+ # Auth (change in production!)
402
+ APP_KEY=your-base64-32-byte-key-here
403
+ SESSION_SECRET=change-this-in-production
404
+
405
+ # Database (example)
406
+ DATABASE_URL=postgresql://localhost:5432/myapp
357
407
  `,
358
408
  ".gitignore": `node_modules/
359
409
  dist/
@@ -363,7 +413,7 @@ dist/
363
413
  }
364
414
  },
365
415
  "api-only": {
366
- dirs: ["src", "src/controllers", "src/middleware"],
416
+ dirs: ["src", "src/config", "src/controllers", "src/middleware"],
367
417
  files: {
368
418
  "package.json": (name) => JSON.stringify(
369
419
  {
@@ -381,6 +431,7 @@ dist/
381
431
  },
382
432
  devDependencies: {
383
433
  "@types/node": "^26.0.1",
434
+ tsx: "^4.19.0",
384
435
  typescript: "^5.7.0"
385
436
  }
386
437
  },
@@ -409,28 +460,59 @@ dist/
409
460
  2
410
461
  ),
411
462
  "src/index.ts": `import { speexjs } from 'speexjs/server'
463
+ import { schema } from 'speexjs/schema'
464
+ import { Config } from './config/index.js'
465
+ import { HealthController } from './controllers/health.controller.js'
412
466
 
413
- const PORT = Number(process.env.PORT) || 3000
414
-
467
+ // \u2500\u2500\u2500 Application \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
415
468
  const app = speexjs()
416
469
 
470
+ // \u2500\u2500\u2500 Controllers \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
471
+ app.controller(HealthController)
472
+
473
+ // \u2500\u2500\u2500 Routes \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
417
474
  app.get('/api/health', async ({ response }) => {
418
- return response.json({ status: 'ok', timestamp: new Date().toISOString() })
475
+ return response.json({
476
+ status: 'ok',
477
+ timestamp: new Date().toISOString(),
478
+ uptime: process.uptime(),
479
+ env: Config.env,
480
+ })
419
481
  })
420
482
 
421
- app.listen(PORT, () => {
422
- console.log(\`SpeexJS API running on http://localhost:\${PORT}\`)
483
+ // \u2500\u2500\u2500 Start Server \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
484
+ app.listen(Config.port, () => {
485
+ console.log(\`\u2713 SpeexJS API running at http://localhost:\${Config.port}\`)
423
486
  })
487
+ `,
488
+ "src/config/index.ts": `export const Config = {
489
+ port: Number(process.env.PORT) || 3000,
490
+ host: process.env.HOST || 'localhost',
491
+ env: process.env.NODE_ENV || 'development',
492
+ appKey: process.env.APP_KEY || '',
493
+ isDev: process.env.NODE_ENV !== 'production',
494
+ isProd: process.env.NODE_ENV === 'production',
495
+ } as const
424
496
  `,
425
497
  "src/controllers/health.controller.ts": `import { Controller, get } from 'speexjs/server'
498
+ import { schema } from 'speexjs/schema'
499
+
500
+ // \u2500\u2500\u2500 Health Response Schema \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
501
+ const HealthResponse = schema.object({
502
+ status: schema.string(),
503
+ uptime: schema.number(),
504
+ timestamp: schema.string(),
505
+ })
426
506
 
427
507
  export class HealthController extends Controller {
428
508
  @get('/health')
429
509
  async check({ response }) {
430
- return response.json({
510
+ const payload = HealthResponse.parse({
431
511
  status: 'ok',
432
512
  uptime: process.uptime(),
513
+ timestamp: new Date().toISOString(),
433
514
  })
515
+ return response.json(payload)
434
516
  }
435
517
  }
436
518
  `,
@@ -452,8 +534,17 @@ export function auth() {
452
534
  }
453
535
  }
454
536
  `,
455
- ".env.example": `PORT=3000
537
+ ".env.example": `# Server
538
+ PORT=3000
456
539
  NODE_ENV=development
540
+ HOST=localhost
541
+
542
+ # Auth (change in production!)
543
+ APP_KEY=your-base64-32-byte-key-here
544
+ SESSION_SECRET=change-this-in-production
545
+
546
+ # Database (example)
547
+ DATABASE_URL=postgresql://localhost:5432/myapp
457
548
  `,
458
549
  ".gitignore": `node_modules/
459
550
  dist/
@@ -476,7 +567,7 @@ function toPascalCase(str) {
476
567
  async function initProject(name, options) {
477
568
  const targetDir = resolve(process.cwd(), name);
478
569
  if (existsSync(targetDir)) {
479
- console.error(colors.red(`Directory '${name}' sudah ada!`));
570
+ console.error(colors.red(`Directory '${name}' already exists!`));
480
571
  process.exit(1);
481
572
  }
482
573
  const templateName = getTemplate(String(options.template || "blank"));
@@ -484,7 +575,7 @@ async function initProject(name, options) {
484
575
  if (!template) {
485
576
  console.error(
486
577
  colors.red(
487
- `Template '${options.template}' tidak dikenal. Gunakan: blank, fullstack, api-only`
578
+ `Unknown template '${options.template}'. Use: blank, fullstack, api-only`
488
579
  )
489
580
  );
490
581
  process.exit(1);
@@ -552,7 +643,7 @@ function makeController(name) {
552
643
  const fullPath = resolve2(targetDir, fileName);
553
644
  const varName = toCamelCase(name);
554
645
  if (existsSync2(fullPath)) {
555
- console.error(colors.red(`File ${fileName} sudah ada!`));
646
+ console.error(colors.red(`File ${fileName} already exists!`));
556
647
  process.exit(1);
557
648
  }
558
649
  mkdirSync2(targetDir, { recursive: true });
@@ -592,7 +683,7 @@ export const ${varName}Controller = ${className}
592
683
  `;
593
684
  writeFileSync2(fullPath, content, "utf-8");
594
685
  console.log(
595
- `${colors.green("\u2705")} Controller ${colors.bold(className)} dibuat di ${colors.cyan(fileName)}`
686
+ `${colors.green("\u2705")} Controller ${colors.bold(className)} created at ${colors.cyan(fileName)}`
596
687
  );
597
688
  }
598
689
 
@@ -615,7 +706,7 @@ function makeMiddleware(name) {
615
706
  const targetDir = resolve3(process.cwd(), "src/server/middleware");
616
707
  const fullPath = resolve3(targetDir, fileName);
617
708
  if (existsSync3(fullPath)) {
618
- console.error(colors.red(`File ${fileName} sudah ada!`));
709
+ console.error(colors.red(`File ${fileName} already exists!`));
619
710
  process.exit(1);
620
711
  }
621
712
  mkdirSync3(targetDir, { recursive: true });
@@ -634,7 +725,7 @@ export function ${functionName}(options?: Record<string, unknown>) {
634
725
  `;
635
726
  writeFileSync3(fullPath, content, "utf-8");
636
727
  console.log(
637
- `${colors.green("\u2705")} Middleware ${colors.bold(functionName)} dibuat di ${colors.cyan(fileName)}`
728
+ `${colors.green("\u2705")} Middleware ${colors.bold(functionName)} created at ${colors.cyan(fileName)}`
638
729
  );
639
730
  }
640
731
 
@@ -654,36 +745,111 @@ function makeSchema(name) {
654
745
  const targetDir = resolve4(process.cwd(), "src/schemas");
655
746
  const fullPath = resolve4(targetDir, fileName);
656
747
  if (existsSync4(fullPath)) {
657
- console.error(colors.red(`File ${fileName} sudah ada!`));
748
+ console.error(colors.red(`File ${fileName} already exists!`));
658
749
  process.exit(1);
659
750
  }
660
751
  mkdirSync4(targetDir, { recursive: true });
661
- const content = `import { s, type Infer } from 'speexjs/schema'
752
+ const content = `import { schema, type Infer } from 'speexjs/schema'
662
753
 
663
- export const ${schemaName} = s.object({
664
- id: s.string().uuid(),
665
- name: s.string().min(1).max(255),
666
- createdAt: s.string().datetime(),
667
- updatedAt: s.string().datetime().optional(),
754
+ export const ${schemaName} = schema.object({
755
+ id: schema.string().uuid(),
756
+ name: schema.string().min(1).max(255),
757
+ createdAt: schema.string().datetime(),
758
+ updatedAt: schema.string().datetime().optional(),
668
759
  })
669
760
 
670
761
  export type ${typeName} = Infer<typeof ${schemaName}>
671
762
 
672
- export const create${typeName}Schema = s.object({
673
- name: s.string().min(1).max(255),
763
+ export const create${typeName}Schema = schema.object({
764
+ name: schema.string().min(1).max(255),
674
765
  })
675
766
 
676
767
  export type Create${typeName} = Infer<typeof create${typeName}Schema>
677
768
  `;
678
769
  writeFileSync4(fullPath, content, "utf-8");
679
770
  console.log(
680
- `${colors.green("\u2705")} Schema ${colors.bold(schemaName)} dibuat di ${colors.cyan(fileName)}`
771
+ `${colors.green("\u2705")} Schema ${colors.bold(schemaName)} created at ${colors.cyan(fileName)}`
681
772
  );
682
773
  }
683
774
 
684
- // src/cli/commands/list-routes.ts
685
- import { existsSync as existsSync5, readdirSync, readFileSync } from "fs";
775
+ // src/cli/commands/make-migration.ts
776
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
686
777
  import { resolve as resolve5 } from "path";
778
+ function toSnakeCase(str) {
779
+ return str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
780
+ }
781
+ function toCamelCase3(str) {
782
+ return str.replace(/[-_\s]+(.)?/g, (_, c) => (c ?? "").toUpperCase()).replace(/^(.)/, (c) => c.toLowerCase());
783
+ }
784
+ function makeMigration(name) {
785
+ const fileName = `${Date.now()}_${toSnakeCase(name)}.ts`;
786
+ const targetDir = resolve5(process.cwd(), "src/database/migrations");
787
+ const fullPath = resolve5(targetDir, fileName);
788
+ if (existsSync5(fullPath)) {
789
+ console.error(colors.red(`File ${fileName} already exists!`));
790
+ process.exit(1);
791
+ }
792
+ mkdirSync5(targetDir, { recursive: true });
793
+ const className = toCamelCase3(name).charAt(0).toUpperCase() + toCamelCase3(name).slice(1);
794
+ const content = `import { SchemaBuilder } from 'speexjs/server/database'
795
+
796
+ export async function up(schema: SchemaBuilder): Promise<void> {
797
+ schema.createTable('${toSnakeCase(name)}', (table) => {
798
+ table.increments('id')
799
+ table.timestamps()
800
+ })
801
+ }
802
+
803
+ export async function down(schema: SchemaBuilder): Promise<void> {
804
+ schema.dropTable('${toSnakeCase(name)}')
805
+ }
806
+ `;
807
+ writeFileSync5(fullPath, content, "utf-8");
808
+ console.log(`${colors.green("\u2705")} Migration ${colors.bold(className)} created at ${colors.cyan(fileName)}`);
809
+ }
810
+
811
+ // src/cli/commands/make-model.ts
812
+ import { existsSync as existsSync6, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
813
+ import { resolve as resolve6 } from "path";
814
+ function toPascalCase5(str) {
815
+ return str.replace(/[-_\s]+(.)?/g, (_, c) => (c ?? "").toUpperCase()).replace(/^(.)/, (c) => c.toUpperCase());
816
+ }
817
+ function toSnakeCase2(str) {
818
+ return str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
819
+ }
820
+ function toPlural(str) {
821
+ if (str.endsWith("s")) return str;
822
+ if (str.endsWith("y")) return str.slice(0, -1) + "ies";
823
+ return str + "s";
824
+ }
825
+ function makeModel(name) {
826
+ const className = toPascalCase5(name);
827
+ const tableName = toPlural(toSnakeCase2(name));
828
+ const fileName = `${toSnakeCase2(name)}.model.ts`;
829
+ const targetDir = resolve6(process.cwd(), "src/models");
830
+ const fullPath = resolve6(targetDir, fileName);
831
+ if (existsSync6(fullPath)) {
832
+ console.error(colors.red(`File ${fileName} already exists!`));
833
+ process.exit(1);
834
+ }
835
+ mkdirSync6(targetDir, { recursive: true });
836
+ const content = `import { Model } from 'speexjs/server/database'
837
+
838
+ export class ${className} extends Model {
839
+ static table = '${tableName}'
840
+
841
+ // Define relationships here
842
+ // belongsTo(RelatedModel, 'foreign_key', 'owner_key')
843
+ // hasMany(RelatedModel, 'foreign_key', 'local_key')
844
+ }
845
+ `;
846
+ writeFileSync6(fullPath, content, "utf-8");
847
+ console.log(`${colors.green("\u2705")} Model ${colors.bold(className)} created at ${colors.cyan(fileName)}`);
848
+ }
849
+
850
+ // src/cli/commands/list-routes.ts
851
+ import { existsSync as existsSync7, readdirSync, readFileSync } from "fs";
852
+ import { resolve as resolve7 } from "path";
687
853
  function extractDecorators(content) {
688
854
  const pattern = /@(get|post|put|patch|del|delete)\s*\(\s*'([^']+)'\s*\)/g;
689
855
  const results = [];
@@ -697,10 +863,10 @@ function extractDecorators(content) {
697
863
  return results;
698
864
  }
699
865
  function listRoutes() {
700
- const routesDir = resolve5(process.cwd(), "src/server/controllers");
701
- if (!existsSync5(routesDir)) {
866
+ const routesDir = resolve7(process.cwd(), "src/server/controllers");
867
+ if (!existsSync7(routesDir)) {
702
868
  console.log(
703
- ` ${colors.yellow("!")} Tidak ada route terdaftar. Buat controller dulu:`
869
+ ` ${colors.yellow("!")} No routes registered. Create a controller first:`
704
870
  );
705
871
  console.log(` ${colors.cyan("speexjs make:controller <name>")}`);
706
872
  return;
@@ -708,17 +874,17 @@ function listRoutes() {
708
874
  const files = readdirSync(routesDir).filter((f) => f.endsWith(".ts"));
709
875
  if (files.length === 0) {
710
876
  console.log(
711
- ` ${colors.yellow("!")} Tidak ada route terdaftar. Buat controller dulu:`
877
+ ` ${colors.yellow("!")} No routes registered. Create a controller first:`
712
878
  );
713
879
  console.log(` ${colors.cyan("speexjs make:controller <name>")}`);
714
880
  return;
715
881
  }
716
882
  let total = 0;
717
883
  console.log();
718
- console.log(` ${colors.bold("\u{1F4CB} Daftar Route:")}`);
884
+ console.log(` ${colors.bold("\u{1F4CB} Route List:")}`);
719
885
  console.log();
720
886
  for (const file of files) {
721
- const content = readFileSync(resolve5(routesDir, file), "utf-8");
887
+ const content = readFileSync(resolve7(routesDir, file), "utf-8");
722
888
  const routes = extractDecorators(content);
723
889
  if (routes.length > 0) {
724
890
  console.log(` ${colors.cyan("\u2500\u2500")} ${colors.bold(file.replace(".controller.ts", ""))} ${colors.cyan("\u2500\u2500")}`);
@@ -730,13 +896,14 @@ function listRoutes() {
730
896
  console.log();
731
897
  }
732
898
  }
733
- console.log(` ${colors.dim(`${total} route${total !== 1 ? "s" : ""} ditemukan`)}`);
899
+ console.log(` ${colors.dim(`${total} route${total !== 1 ? "s" : ""} found`)}`);
734
900
  console.log();
735
901
  }
736
902
 
737
903
  // src/cli/commands/serve.ts
738
- import { existsSync as existsSync6 } from "fs";
739
- import { resolve as resolve6 } from "path";
904
+ import { existsSync as existsSync8 } from "fs";
905
+ import { resolve as resolve8 } from "path";
906
+ import { pathToFileURL } from "url";
740
907
 
741
908
  // src/native/logger.ts
742
909
  var LOG_LEVELS = {
@@ -745,27 +912,18 @@ var LOG_LEVELS = {
745
912
  warn: 2,
746
913
  error: 3
747
914
  };
748
- var TIMEZONE_OFFSETS = {
749
- WIB: 7,
750
- WITA: 8,
751
- WIT: 9,
752
- UTC: 0
753
- };
754
915
  function pad(n) {
755
916
  return n.toString().padStart(2, "0");
756
917
  }
757
918
  function formatTimestamp(tz) {
758
- const offset = TIMEZONE_OFFSETS[tz ?? ""] ?? 7;
759
919
  const now = /* @__PURE__ */ new Date();
760
- const utc = now.getTime() + now.getTimezoneOffset() * 6e4;
761
- const local = new Date(utc + offset * 36e5);
762
- const y = local.getFullYear();
763
- const M = pad(local.getMonth() + 1);
764
- const d = pad(local.getDate());
765
- const h = pad(local.getHours());
766
- const m = pad(local.getMinutes());
767
- const s = pad(local.getSeconds());
768
- return `${y}-${M}-${d} ${h}:${m}:${s} ${tz ?? "WIB"}`;
920
+ const y = now.getUTCFullYear();
921
+ const M = pad(now.getUTCMonth() + 1);
922
+ const d = pad(now.getUTCDate());
923
+ const h = pad(now.getUTCHours());
924
+ const m = pad(now.getUTCMinutes());
925
+ const s = pad(now.getUTCSeconds());
926
+ return `${y}-${M}-${d} ${h}:${m}:${s} UTC`;
769
927
  }
770
928
  var LEVEL_PREFIX = {
771
929
  debug: "DEBUG",
@@ -790,7 +948,7 @@ var Logger = class _Logger {
790
948
  this._name = options?.name ?? "";
791
949
  this._useColors = options?.colors ?? isColorSupported();
792
950
  this._useTimestamps = options?.timestamps ?? true;
793
- this._timezone = options?.timezone ?? "WIB";
951
+ this._timezone = options?.timezone ?? "UTC";
794
952
  }
795
953
  _format(level, msg, meta) {
796
954
  const parts = [];
@@ -848,6 +1006,22 @@ var Logger = class _Logger {
848
1006
  var logger = new Logger();
849
1007
 
850
1008
  // src/cli/commands/serve.ts
1009
+ function toFileUrl(path) {
1010
+ return pathToFileURL(path).href;
1011
+ }
1012
+ async function ensureTsLoader() {
1013
+ if (process.execArgv.some((a) => a.includes("strip-types") || a.includes("tsx") || a.includes("ts-node"))) {
1014
+ return;
1015
+ }
1016
+ for (const mod of ["tsx", "ts-node/esm"]) {
1017
+ try {
1018
+ await import(mod);
1019
+ return;
1020
+ } catch {
1021
+ continue;
1022
+ }
1023
+ }
1024
+ }
851
1025
  async function serve(options) {
852
1026
  const opts = {
853
1027
  port: options.port || options.p || 3e3,
@@ -856,31 +1030,38 @@ async function serve(options) {
856
1030
  };
857
1031
  const port = parseInt(String(opts.port), 10);
858
1032
  const host = String(opts.host);
859
- const serverEntry = resolve6(process.cwd(), "src/app.ts");
860
- const serverEntryAlt = resolve6(process.cwd(), "src/server/index.ts");
861
- const serverEntryIndex = resolve6(process.cwd(), "src/index.ts");
1033
+ const serverEntry = resolve8(process.cwd(), "src/app.ts");
1034
+ const serverEntryAlt = resolve8(process.cwd(), "src/server/index.ts");
1035
+ const serverEntryIndex = resolve8(process.cwd(), "src/index.ts");
862
1036
  let entryPath = null;
863
- if (existsSync6(serverEntry)) entryPath = serverEntry;
864
- else if (existsSync6(serverEntryAlt)) entryPath = serverEntryAlt;
865
- else if (existsSync6(serverEntryIndex)) entryPath = serverEntryIndex;
1037
+ if (existsSync8(serverEntry)) entryPath = serverEntry;
1038
+ else if (existsSync8(serverEntryAlt)) entryPath = serverEntryAlt;
1039
+ else if (existsSync8(serverEntryIndex)) entryPath = serverEntryIndex;
866
1040
  if (!entryPath) {
867
1041
  console.error(
868
1042
  colors.red(
869
- "Tidak ditemukan file entry point. Buat src/app.ts atau src/index.ts"
1043
+ "Entry point not found. Create src/app.ts or src/index.ts"
870
1044
  )
871
1045
  );
872
1046
  process.exit(1);
873
1047
  }
1048
+ if (opts.dev) {
1049
+ try {
1050
+ await ensureTsLoader();
1051
+ } catch {
1052
+ }
1053
+ }
1054
+ const entryUrl = toFileUrl(entryPath);
874
1055
  if (opts.dev) {
875
1056
  logger.info(
876
1057
  `Development server starting at ${colors.cyan(`http://${host}:${port}`)}`
877
1058
  );
878
1059
  try {
879
- const { app } = await import(entryPath);
1060
+ const { app } = await import(entryUrl);
880
1061
  if (!app || typeof app.listen !== "function") {
881
1062
  console.error(
882
1063
  colors.red(
883
- "Entry point harus export { app } dengan method .listen()"
1064
+ "Entry point must export { app } with .listen() method"
884
1065
  )
885
1066
  );
886
1067
  process.exit(1);
@@ -892,16 +1073,16 @@ async function serve(options) {
892
1073
  console.log();
893
1074
  });
894
1075
  } catch (err) {
895
- console.error(colors.red(`Gagal menjalankan server: ${err.message}`));
1076
+ console.error(colors.red(`Failed to start server: ${err.message}`));
896
1077
  process.exit(1);
897
1078
  }
898
1079
  } else {
899
1080
  try {
900
- const { app } = await import(entryPath);
1081
+ const { app } = await import(entryUrl);
901
1082
  if (!app || typeof app.listen !== "function") {
902
1083
  console.error(
903
1084
  colors.red(
904
- "Entry point harus export { app } dengan method .listen()"
1085
+ "Entry point must export { app } with .listen() method"
905
1086
  )
906
1087
  );
907
1088
  process.exit(1);
@@ -913,7 +1094,7 @@ async function serve(options) {
913
1094
  console.log();
914
1095
  });
915
1096
  } catch (err) {
916
- console.error(colors.red(`Gagal menjalankan server: ${err.message}`));
1097
+ console.error(colors.red(`Failed to start server: ${err.message}`));
917
1098
  process.exit(1);
918
1099
  }
919
1100
  }
@@ -925,16 +1106,20 @@ function showHelp() {
925
1106
  console.log("Fullstack JavaScript/TypeScript Framework");
926
1107
  console.log();
927
1108
  console.log(`${colors.bold("Usage:")}`);
928
- console.log(" SpeexJS init [name] [options] Buat project baru");
1109
+ console.log(" SpeexJS init [name] [options] Create new project");
929
1110
  console.log(" SpeexJS make:controller <name> Generate controller");
930
1111
  console.log(" SpeexJS make:middleware <name> Generate middleware");
1112
+ console.log(" SpeexJS make:migration <name> Generate migration");
1113
+ console.log(" SpeexJS make:model <name> Generate model");
931
1114
  console.log(" SpeexJS make:schema <name> Generate schema");
932
- console.log(" SpeexJS list-routes Lihat semua route");
933
- console.log(" SpeexJS serve [options] Jalankan server");
934
- console.log(" SpeexJS --help Bantuan ini");
1115
+ console.log(" SpeexJS migrate Run migrations");
1116
+ console.log(" SpeexJS db:seed Seed the database");
1117
+ console.log(" SpeexJS list-routes View all routes");
1118
+ console.log(" SpeexJS serve [options] Run server");
1119
+ console.log(" SpeexJS --help Show help");
935
1120
  console.log();
936
1121
  console.log(`${colors.bold("Aliases:")}`);
937
- console.log(" SpeexJS -v, --version Lihat versi");
1122
+ console.log(" SpeexJS -v, --version View version");
938
1123
  console.log();
939
1124
  console.log(`${colors.bold("Options:")}`);
940
1125
  console.log(" --template <type> blank, fullstack, api-only");
@@ -952,7 +1137,7 @@ async function main() {
952
1137
  }
953
1138
  case "make:controller": {
954
1139
  if (!parsed.args[0]) {
955
- console.error(colors.red("Nama controller diperlukan"));
1140
+ console.error(colors.red("Controller name required"));
956
1141
  console.log(` ${colors.cyan("SpeexJS make:controller <name>")}`);
957
1142
  process.exit(1);
958
1143
  }
@@ -961,7 +1146,7 @@ async function main() {
961
1146
  }
962
1147
  case "make:middleware": {
963
1148
  if (!parsed.args[0]) {
964
- console.error(colors.red("Nama middleware diperlukan"));
1149
+ console.error(colors.red("Middleware name required"));
965
1150
  console.log(` ${colors.cyan("SpeexJS make:middleware <name>")}`);
966
1151
  process.exit(1);
967
1152
  }
@@ -970,13 +1155,37 @@ async function main() {
970
1155
  }
971
1156
  case "make:schema": {
972
1157
  if (!parsed.args[0]) {
973
- console.error(colors.red("Nama schema diperlukan"));
1158
+ console.error(colors.red("Schema name required"));
974
1159
  console.log(` ${colors.cyan("SpeexJS make:schema <name>")}`);
975
1160
  process.exit(1);
976
1161
  }
977
1162
  await makeSchema(parsed.args[0]);
978
1163
  break;
979
1164
  }
1165
+ case "make:migration": {
1166
+ if (!parsed.args[0]) {
1167
+ console.error(colors.red("Migration name required"));
1168
+ console.log(` ${colors.cyan("SpeexJS make:migration <name>")}`);
1169
+ process.exit(1);
1170
+ }
1171
+ await makeMigration(parsed.args[0]);
1172
+ break;
1173
+ }
1174
+ case "make:model": {
1175
+ if (!parsed.args[0]) {
1176
+ console.error(colors.red("Model name required"));
1177
+ console.log(` ${colors.cyan("SpeexJS make:model <name>")}`);
1178
+ process.exit(1);
1179
+ }
1180
+ await makeModel(parsed.args[0]);
1181
+ break;
1182
+ }
1183
+ case "migrate":
1184
+ case "db:seed": {
1185
+ const label = command === "migrate" ? "Migration" : "Database seeding";
1186
+ console.log(`${colors.yellow("\u23F3")} ${label} coming soon...`);
1187
+ break;
1188
+ }
980
1189
  case "list-routes":
981
1190
  case "routes":
982
1191
  case "lr": {
@@ -1002,7 +1211,7 @@ async function main() {
1002
1211
  }
1003
1212
  default: {
1004
1213
  if (command) {
1005
- console.error(`${colors.red(`Command '${command}' tidak dikenal`)}`);
1214
+ console.error(`${colors.red(`Unknown command '${command}'`)}`);
1006
1215
  console.log();
1007
1216
  }
1008
1217
  showHelp();