ilana-orm 1.0.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/cli/ilana.js ADDED
@@ -0,0 +1,928 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const Database = require('../database/connection');
6
+ const MigrationRunner = require('../orm/MigrationRunner');
7
+
8
+ // Default database configuration
9
+ const defaultConfig = {
10
+ default: 'mysql',
11
+ connections: {
12
+ mysql: {
13
+ client: 'mysql2',
14
+ connection: {
15
+ host: 'localhost',
16
+ port: 3306,
17
+ user: 'root',
18
+ password: '',
19
+ database: 'ilana_orm'
20
+ }
21
+ }
22
+ },
23
+ migrations: {
24
+ directory: './migrations',
25
+ tableName: 'migrations'
26
+ },
27
+ seeds: {
28
+ directory: './seeds'
29
+ }
30
+ };
31
+
32
+ // Load configuration and auto-initialize
33
+ function loadConfig() {
34
+ const configPath = path.join(process.cwd(), 'ilana.config.js');
35
+ if (fs.existsSync(configPath)) {
36
+ delete require.cache[configPath];
37
+ const config = require(configPath);
38
+ // Config file already initializes database
39
+ return config;
40
+ }
41
+ // No config file found - don't initialize with hardcoded default
42
+ console.error('No ilana.config.js found. Run "npx ilana setup" first.');
43
+ process.exit(1);
44
+ }
45
+
46
+ // Initialize database
47
+ function initializeDatabase() {
48
+ loadConfig(); // Config handles initialization
49
+ }
50
+
51
+ // Helper functions
52
+ function isTypeScriptProject() {
53
+ return fs.existsSync(path.join(process.cwd(), 'tsconfig.json'));
54
+ }
55
+
56
+ function getFileExtension() {
57
+ return isTypeScriptProject() ? '.ts' : '.js';
58
+ }
59
+
60
+ function toPascalCase(str) {
61
+ return str.replace(/(^|_)(.)/g, (_, __, char) => char.toUpperCase());
62
+ }
63
+
64
+ function toSnakeCase(str) {
65
+ return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
66
+ }
67
+
68
+ function pluralize(str) {
69
+ if (str.endsWith('y')) return str.slice(0, -1) + 'ies';
70
+ if (str.endsWith('s')) return str + 'es';
71
+ return str + 's';
72
+ }
73
+
74
+ function generateModel(name, options = {}) {
75
+ const className = toPascalCase(name);
76
+ const tableName = pluralize(toSnakeCase(name));
77
+ const fileName = `${className}${getFileExtension()}`;
78
+ const filePath = path.join(process.cwd(), 'models', fileName);
79
+
80
+ if (fs.existsSync(filePath)) {
81
+ console.error(`Model ${className} already exists at models/${fileName}`);
82
+ process.exit(1);
83
+ }
84
+
85
+ if (!fs.existsSync(path.dirname(filePath))) {
86
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
87
+ }
88
+
89
+ const template = options.pivot ? getPivotModelTemplate(className, tableName) : getModelTemplate(className, tableName);
90
+ fs.writeFileSync(filePath, template);
91
+ console.log(`Created model: models/${fileName}`);
92
+
93
+ if (options.migration || options.all) {
94
+ const migrationName = `create_${tableName}_table`;
95
+ const runner = new MigrationRunner();
96
+ runner.generateMigration(migrationName);
97
+ }
98
+
99
+ if (options.factory || options.all) {
100
+ generateFactory(className);
101
+ }
102
+
103
+ if (options.seed || options.all) {
104
+ generateSeeder(className);
105
+ }
106
+ }
107
+
108
+ function getModelTemplate(className, tableName) {
109
+ if (isTypeScriptProject()) {
110
+ return `import Model from 'ilana-orm/orm/Model';
111
+ // import { MoneyCast, EncryptedCast } from 'ilana-orm/orm/CustomCasts';
112
+
113
+ export default class ${className} extends Model {
114
+ protected static table = '${tableName}';
115
+ protected static timestamps = true;
116
+ protected static softDeletes = false;
117
+
118
+ // For UUID primary keys, uncomment:
119
+ // protected static keyType = 'string' as const;
120
+ // protected static incrementing = false;
121
+
122
+ protected fillable: string[] = [];
123
+ protected hidden: string[] = [];
124
+ protected appends: string[] = [];
125
+ protected casts = {
126
+ // Basic casts
127
+ // is_active: 'boolean' as const,
128
+ // metadata: 'json' as const,
129
+ // tags: 'array' as const,
130
+
131
+ // Custom casts
132
+ // price: new MoneyCast(),
133
+ // secret: new EncryptedCast('your-key'),
134
+ };
135
+
136
+ // Define relationships here
137
+ // example() {
138
+ // return this.hasMany(RelatedModel, 'foreign_key');
139
+ // }
140
+
141
+ // Define scopes here
142
+ // static scopeActive(query: any) {
143
+ // query.where('is_active', true);
144
+ // }
145
+
146
+ // Register for polymorphic relationships
147
+ // static {
148
+ // this.register();
149
+ // }
150
+ }
151
+ `;
152
+ }
153
+
154
+ return `const Model = require('ilana-orm/orm/Model');
155
+ // const { MoneyCast, EncryptedCast } = require('ilana-orm/orm/CustomCasts');
156
+
157
+ class ${className} extends Model {
158
+ static table = '${tableName}';
159
+ static timestamps = true;
160
+ static softDeletes = false;
161
+
162
+ // For UUID primary keys, uncomment:
163
+ // static keyType = 'string';
164
+ // static incrementing = false;
165
+
166
+ fillable = [];
167
+ hidden = [];
168
+ appends = [];
169
+ casts = {
170
+ // Basic casts
171
+ // is_active: 'boolean',
172
+ // metadata: 'json',
173
+ // tags: 'array',
174
+
175
+ // Custom casts
176
+ // price: new MoneyCast(),
177
+ // secret: new EncryptedCast('your-key'),
178
+ };
179
+
180
+ // Define relationships here
181
+ // example() {
182
+ // return this.hasMany(RelatedModel, 'foreign_key');
183
+ // }
184
+
185
+ // Define scopes here
186
+ // static scopeActive(query) {
187
+ // query.where('is_active', true);
188
+ // }
189
+
190
+ // Register for polymorphic relationships
191
+ // static {
192
+ // this.register();
193
+ // }
194
+ }
195
+
196
+ module.exports = ${className};
197
+ `;
198
+ }
199
+
200
+ function getPivotModelTemplate(className, tableName) {
201
+ if (isTypeScriptProject()) {
202
+ return `import Model from 'ilana-orm/orm/Model';
203
+
204
+ export default class ${className} extends Model {
205
+ protected static table = '${tableName}';
206
+ protected static timestamps = true;
207
+
208
+ protected fillable: string[] = [];
209
+
210
+ // Define pivot relationships here
211
+ }
212
+ `;
213
+ }
214
+
215
+ return `const Model = require('ilana-orm/orm/Model');
216
+
217
+ class ${className} extends Model {
218
+ static table = '${tableName}';
219
+ static timestamps = true;
220
+
221
+ fillable = [];
222
+
223
+ // Define pivot relationships here
224
+ }
225
+
226
+ module.exports = ${className};
227
+ `;
228
+ }
229
+
230
+ function generateFactory(className) {
231
+ const fileName = `${className}Factory${getFileExtension()}`;
232
+ const filePath = path.join(process.cwd(), 'database/factories', fileName);
233
+
234
+ if (!fs.existsSync(path.dirname(filePath))) {
235
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
236
+ }
237
+
238
+ const template = isTypeScriptProject() ?
239
+ `import { defineFactory } from 'ilana-orm/orm/Factory';
240
+ import ${className} from '../../models/${className}';
241
+
242
+ export default defineFactory(${className}, (faker) => ({
243
+ // Define your factory attributes here
244
+ // name: faker.person.fullName(),
245
+ // email: faker.internet.email(),
246
+ }))
247
+ .state('example', (faker) => ({
248
+ // Define state modifications here
249
+ }));
250
+ ` :
251
+ `const { defineFactory } = require('ilana-orm/orm/Factory');
252
+ const ${className} = require('../../models/${className}');
253
+
254
+ module.exports = defineFactory(${className}, (faker) => ({
255
+ // Define your factory attributes here
256
+ // name: faker.person.fullName(),
257
+ // email: faker.internet.email(),
258
+ }))
259
+ .state('example', (faker) => ({
260
+ // Define state modifications here
261
+ }));
262
+ `;
263
+
264
+ fs.writeFileSync(filePath, template);
265
+ console.log(`Created factory: factories/${fileName}`);
266
+ }
267
+
268
+ function generateSeeder(className) {
269
+ const fileName = `${className}Seeder${getFileExtension()}`;
270
+ const filePath = path.join(process.cwd(), 'database/seeds', fileName);
271
+
272
+ if (!fs.existsSync(path.dirname(filePath))) {
273
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
274
+ }
275
+
276
+ const template = isTypeScriptProject() ?
277
+ `import Seeder from 'ilana-orm/orm/Seeder';
278
+ import ${className} from '../../models/${className}';
279
+ import '../factories/${className}Factory';
280
+
281
+ export default class ${className}Seeder extends Seeder {
282
+ async run(): Promise<void> {
283
+ console.log('Seeding ${className.toLowerCase()}s...');
284
+
285
+ // Create sample data
286
+ await ${className}.factory().times(10).create();
287
+
288
+ console.log('${className}s seeded successfully');
289
+ }
290
+ }
291
+ ` :
292
+ `const Seeder = require('ilana-orm/orm/Seeder');
293
+ const ${className} = require('../../models/${className}');
294
+ require('../factories/${className}Factory');
295
+
296
+ class ${className}Seeder extends Seeder {
297
+ async run() {
298
+ console.log('Seeding ${className.toLowerCase()}s...');
299
+
300
+ // Create sample data
301
+ await ${className}.factory().times(10).create();
302
+
303
+ console.log('${className}s seeded successfully');
304
+ }
305
+ }
306
+
307
+ module.exports = ${className}Seeder;
308
+ `;
309
+
310
+ fs.writeFileSync(filePath, template);
311
+ console.log(`Created seeder: seeds/${fileName}`);
312
+ }
313
+
314
+ // CLI Commands
315
+ const commands = {
316
+ async setup() {
317
+ console.log('Setting up Ilana ORM...');
318
+
319
+ // Create directories
320
+ const dirs = ['models', 'database/migrations', 'database/factories', 'database/seeds'];
321
+ for (const dir of dirs) {
322
+ if (!fs.existsSync(dir)) {
323
+ fs.mkdirSync(dir, { recursive: true });
324
+ console.log(`Created directory: ${dir}/`);
325
+ }
326
+ }
327
+
328
+ // Create config file if it doesn't exist
329
+ const configPath = 'ilana.config.js';
330
+ if (!fs.existsSync(configPath)) {
331
+ const configTemplate = `require('dotenv').config();
332
+ const Database = require('ilana-orm/database/connection');
333
+
334
+ const config = {
335
+ default: process.env.DB_CONNECTION || 'mysql',
336
+ timezone: process.env.DB_TIMEZONE || 'UTC',
337
+
338
+ connections: {
339
+ sqlite: {
340
+ client: 'sqlite3',
341
+ connection: {
342
+ filename: process.env.DB_FILENAME || './database.sqlite'
343
+ },
344
+ useNullAsDefault: true
345
+ },
346
+
347
+ mysql: {
348
+ client: 'mysql2',
349
+ connection: {
350
+ host: process.env.DB_HOST || 'localhost',
351
+ port: process.env.DB_PORT || 3306,
352
+ user: process.env.DB_USERNAME || 'root',
353
+ password: process.env.DB_PASSWORD || '',
354
+ database: process.env.DB_DATABASE || 'your_database',
355
+ timezone: process.env.DB_TIMEZONE || 'UTC'
356
+ }
357
+ },
358
+
359
+ postgres: {
360
+ client: 'pg',
361
+ connection: {
362
+ host: process.env.DB_HOST || 'localhost',
363
+ port: process.env.DB_PORT || 5432,
364
+ user: process.env.DB_USERNAME || 'postgres',
365
+ password: process.env.DB_PASSWORD || '',
366
+ database: process.env.DB_DATABASE || 'your_database'
367
+ }
368
+ }
369
+ },
370
+
371
+ migrations: {
372
+ directory: './database/migrations',
373
+ tableName: 'migrations'
374
+ },
375
+
376
+ seeds: {
377
+ directory: './database/seeds'
378
+ }
379
+ };
380
+
381
+ // Auto-initialize database connections
382
+ Database.configure(config);
383
+
384
+ module.exports = config;
385
+ `;
386
+ fs.writeFileSync(configPath, configTemplate);
387
+ console.log(`Created config file: ${configPath}`);
388
+ }
389
+
390
+ // Create .env file if it doesn't exist
391
+ const envPath = '.env';
392
+ if (!fs.existsSync(envPath)) {
393
+ const envTemplate = `# Database Configuration
394
+ DB_CONNECTION=mysql
395
+ DB_HOST=localhost
396
+ DB_PORT=3306
397
+ DB_DATABASE=your_database
398
+ DB_USERNAME=root
399
+ DB_PASSWORD=
400
+ DB_TIMEZONE=UTC
401
+
402
+ # SQLite Configuration
403
+ # DB_CONNECTION=sqlite
404
+ # DB_FILENAME=./database.sqlite
405
+
406
+ # MySQL Configuration (alternative)
407
+ # DB_HOST=localhost
408
+ # DB_PORT=3306
409
+ # DB_DATABASE=your_database
410
+ # DB_USERNAME=root
411
+ # DB_PASSWORD=
412
+ # DB_TIMEZONE=America/New_York
413
+
414
+ # PostgreSQL Configuration
415
+ # DB_CONNECTION=postgres
416
+ # DB_HOST=localhost
417
+ # DB_PORT=5432
418
+ # DB_DATABASE=your_database
419
+ # DB_USERNAME=postgres
420
+ # DB_PASSWORD=
421
+ # DB_TIMEZONE=Europe/London
422
+ `;
423
+ fs.writeFileSync(envPath, envTemplate);
424
+ console.log(`Created environment file: ${envPath}`);
425
+ }
426
+
427
+ console.log('\nIlana ORM setup complete!');
428
+ console.log('\nNext steps:');
429
+ console.log('1. Update .env file with your database credentials');
430
+ console.log('2. Run: ilana make:model User -m');
431
+ console.log('3. Run: ilana migrate');
432
+ },
433
+ async 'make:migration'(name, ...flags) {
434
+ if (!name) {
435
+ console.error('Migration name is required');
436
+ console.log('Usage: ilana make:migration <name> [--table=table] [--create=table]');
437
+ process.exit(1);
438
+ }
439
+
440
+ let tableName = '';
441
+ let isCreate = false;
442
+
443
+ for (const flag of flags) {
444
+ if (flag.startsWith('--table=')) {
445
+ tableName = flag.split('=')[1];
446
+ } else if (flag.startsWith('--create=')) {
447
+ tableName = flag.split('=')[1];
448
+ isCreate = true;
449
+ }
450
+ }
451
+
452
+ initializeDatabase();
453
+ const runner = new MigrationRunner();
454
+ runner.generateMigration(name, tableName, isCreate);
455
+ },
456
+
457
+ async 'make:factory'(name) {
458
+ if (!name) {
459
+ console.error('Factory name is required');
460
+ console.log('Usage: ilana make:factory <FactoryName>');
461
+ process.exit(1);
462
+ }
463
+
464
+ const className = name.replace('Factory', '');
465
+ generateFactory(className);
466
+ },
467
+
468
+ async 'make:seeder'(name) {
469
+ if (!name) {
470
+ console.error('Seeder name is required');
471
+ console.log('Usage: ilana make:seeder <SeederName>');
472
+ process.exit(1);
473
+ }
474
+
475
+ const className = name.replace('Seeder', '');
476
+ generateSeeder(className);
477
+ },
478
+
479
+ async 'make:model'(name, ...flags) {
480
+ if (!name) {
481
+ console.error('Model name is required');
482
+ console.log('Usage: ilana make:model <ModelName> [options]');
483
+ process.exit(1);
484
+ }
485
+
486
+ // Validate model name format
487
+ if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
488
+ console.error('Model name must start with uppercase letter and contain only letters and numbers');
489
+ console.error('Invalid: App_Post, App/Post, app_post');
490
+ console.error('Valid: AppPost, User, BlogPost');
491
+ process.exit(1);
492
+ }
493
+
494
+ const options = {};
495
+
496
+ for (const flag of flags) {
497
+ switch (flag) {
498
+ case '-m':
499
+ case '--migration':
500
+ options.migration = true;
501
+ break;
502
+ case '-f':
503
+ case '--factory':
504
+ options.factory = true;
505
+ break;
506
+ case '-s':
507
+ case '--seed':
508
+ options.seed = true;
509
+ break;
510
+ case '-p':
511
+ case '--pivot':
512
+ options.pivot = true;
513
+ break;
514
+ case '-a':
515
+ case '--all':
516
+ options.all = true;
517
+ break;
518
+ case '-mfs':
519
+ options.migration = true;
520
+ options.factory = true;
521
+ options.seed = true;
522
+ break;
523
+ }
524
+ }
525
+
526
+ if (options.migration || options.all) {
527
+ initializeDatabase();
528
+ }
529
+
530
+ generateModel(name, options);
531
+ },
532
+
533
+ async migrate(...args) {
534
+ initializeDatabase();
535
+ const runner = new MigrationRunner();
536
+
537
+ let connection;
538
+ let onlyFile;
539
+ let toFile;
540
+
541
+ for (let i = 0; i < args.length; i++) {
542
+ const arg = args[i];
543
+ if (arg === '--connection' && args[i + 1]) {
544
+ connection = args[i + 1];
545
+ i++;
546
+ } else if (arg.startsWith('--connection=')) {
547
+ connection = arg.split('=')[1];
548
+ } else if (arg === '--only' && args[i + 1]) {
549
+ onlyFile = args[i + 1];
550
+ i++;
551
+ } else if (arg.startsWith('--only=')) {
552
+ onlyFile = arg.split('=')[1];
553
+ } else if (arg === '--to' && args[i + 1]) {
554
+ toFile = args[i + 1];
555
+ i++;
556
+ } else if (arg.startsWith('--to=')) {
557
+ toFile = arg.split('=')[1];
558
+ } else if (!arg.startsWith('--')) {
559
+ connection = arg;
560
+ }
561
+ }
562
+
563
+ await runner.migrate(connection, onlyFile, toFile);
564
+ process.exit(0);
565
+ },
566
+
567
+ async 'migrate:fresh'(...args) {
568
+ initializeDatabase();
569
+ const runner = new MigrationRunner();
570
+
571
+ let connection;
572
+ let withSeed = false;
573
+
574
+ for (const arg of args) {
575
+ if (arg === '--seed') {
576
+ withSeed = true;
577
+ } else if (arg.startsWith('--connection=')) {
578
+ connection = arg.split('=')[1];
579
+ } else if (!arg.startsWith('--')) {
580
+ connection = arg;
581
+ }
582
+ }
583
+
584
+ await runner.fresh(connection);
585
+
586
+ if (withSeed) {
587
+ await commands.seed();
588
+ }
589
+
590
+ process.exit(0);
591
+ },
592
+
593
+ async 'migrate:list'(connection) {
594
+ initializeDatabase();
595
+ const runner = new MigrationRunner();
596
+ await runner.list(connection);
597
+ process.exit(0);
598
+ },
599
+
600
+ async 'migrate:unlock'(connection) {
601
+ initializeDatabase();
602
+ const runner = new MigrationRunner();
603
+ await runner.unlock(connection);
604
+ process.exit(0);
605
+ },
606
+
607
+ async 'migrate:rollback'(...args) {
608
+ initializeDatabase();
609
+ const runner = new MigrationRunner();
610
+
611
+ let steps = 1;
612
+ let connection;
613
+ let toFile;
614
+
615
+ for (let i = 0; i < args.length; i++) {
616
+ const arg = args[i];
617
+ if (arg === '--step' && args[i + 1]) {
618
+ steps = parseInt(args[i + 1]);
619
+ i++;
620
+ } else if (arg.startsWith('--step=')) {
621
+ steps = parseInt(arg.split('=')[1]);
622
+ } else if (arg === '--connection' && args[i + 1]) {
623
+ connection = args[i + 1];
624
+ i++;
625
+ } else if (arg.startsWith('--connection=')) {
626
+ connection = arg.split('=')[1];
627
+ } else if (arg === '--to' && args[i + 1]) {
628
+ toFile = args[i + 1];
629
+ i++;
630
+ } else if (arg.startsWith('--to=')) {
631
+ toFile = arg.split('=')[1];
632
+ } else if (!isNaN(parseInt(arg))) {
633
+ steps = parseInt(arg);
634
+ } else if (!arg.startsWith('--')) {
635
+ connection = arg;
636
+ }
637
+ }
638
+
639
+ await runner.rollback(steps, connection, toFile);
640
+ process.exit(0);
641
+ },
642
+
643
+ async 'migrate:reset'(connection) {
644
+ initializeDatabase();
645
+ const runner = new MigrationRunner();
646
+ await runner.reset(connection);
647
+ process.exit(0);
648
+ },
649
+
650
+ async 'migrate:refresh'(connection) {
651
+ initializeDatabase();
652
+ const runner = new MigrationRunner();
653
+ await runner.refresh(connection);
654
+ process.exit(0);
655
+ },
656
+
657
+ async 'migrate:status'(connection) {
658
+ initializeDatabase();
659
+ const runner = new MigrationRunner();
660
+ await runner.status(connection);
661
+ process.exit(0);
662
+ },
663
+
664
+ async seed(...args) {
665
+ initializeDatabase();
666
+
667
+ let seederName;
668
+ let connection;
669
+
670
+ for (let i = 0; i < args.length; i++) {
671
+ const arg = args[i];
672
+ if (arg === '--class' && args[i + 1]) {
673
+ seederName = args[i + 1];
674
+ i++;
675
+ } else if (arg.startsWith('--class=')) {
676
+ seederName = arg.split('=')[1];
677
+ } else if (arg === '--connection' && args[i + 1]) {
678
+ connection = args[i + 1];
679
+ i++;
680
+ } else if (arg.startsWith('--connection=')) {
681
+ connection = arg.split('=')[1];
682
+ } else if (!arg.startsWith('--')) {
683
+ if (!seederName) seederName = arg;
684
+ else connection = arg;
685
+ }
686
+ }
687
+
688
+ const seedsPath = path.join(process.cwd(), 'database/seeds');
689
+ if (!fs.existsSync(seedsPath)) {
690
+ console.log('No seeds directory found');
691
+ process.exit(0);
692
+ }
693
+
694
+ const seedFiles = fs.readdirSync(seedsPath)
695
+ .filter(file => file.endsWith('.ts') || file.endsWith('.js'))
696
+ .sort();
697
+
698
+ if (seedFiles.length === 0) {
699
+ console.log('No seed files found');
700
+ process.exit(0);
701
+ }
702
+
703
+ const filesToRun = seederName
704
+ ? seedFiles.filter(file => file.includes(seederName))
705
+ : seedFiles;
706
+
707
+ console.log(`Running ${filesToRun.length} seeders...`);
708
+
709
+ for (const file of filesToRun) {
710
+ console.log(`Seeding: ${file}`);
711
+ const filepath = path.join(seedsPath, file);
712
+ delete require.cache[filepath];
713
+ const seederModule = require(filepath);
714
+ const SeederClass = seederModule.default || seederModule;
715
+ const seeder = new SeederClass();
716
+
717
+ if (connection) {
718
+ seeder.connection = connection;
719
+ }
720
+
721
+ if (typeof seeder.run === 'function') {
722
+ await seeder.run();
723
+ }
724
+
725
+ console.log(`Seeded: ${file}`);
726
+ }
727
+
728
+ console.log('Seeding completed');
729
+ process.exit(0);
730
+ },
731
+
732
+ async 'db:seed'(seederName) {
733
+ return commands.seed(seederName);
734
+ },
735
+
736
+ async 'db:wipe'(connection) {
737
+ initializeDatabase();
738
+ const runner = new MigrationRunner();
739
+ await runner.wipe(connection);
740
+ process.exit(0);
741
+ },
742
+
743
+ async 'make:observer'(name, ...flags) {
744
+ if (!name) {
745
+ console.error('Observer name is required');
746
+ console.log('Usage: ilana make:observer <ObserverName> [--model=ModelName]');
747
+ process.exit(1);
748
+ }
749
+
750
+ let modelName = '';
751
+
752
+ for (const flag of flags) {
753
+ if (flag.startsWith('--model=')) {
754
+ modelName = flag.split('=')[1];
755
+ }
756
+ }
757
+
758
+ const className = toPascalCase(name.replace('Observer', ''));
759
+ const fileName = `${className}Observer${getFileExtension()}`;
760
+ const filePath = path.join(process.cwd(), 'observers', fileName);
761
+
762
+ if (!fs.existsSync(path.dirname(filePath))) {
763
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
764
+ }
765
+
766
+ const template = isTypeScriptProject() ?
767
+ `${modelName ? `import ${modelName} from '../models/${modelName}';\n\n` : ''}export default class ${className}Observer {
768
+ async creating(model: ${modelName || 'any'}): Promise<void> {}
769
+ async created(model: ${modelName || 'any'}): Promise<void> {}
770
+ async updating(model: ${modelName || 'any'}): Promise<void> {}
771
+ async updated(model: ${modelName || 'any'}): Promise<void> {}
772
+ async saving(model: ${modelName || 'any'}): Promise<void> {}
773
+ async saved(model: ${modelName || 'any'}): Promise<void> {}
774
+ async deleting(model: ${modelName || 'any'}): Promise<void> {}
775
+ async deleted(model: ${modelName || 'any'}): Promise<void> {}
776
+ }
777
+ ` :
778
+ `${modelName ? `const ${modelName} = require('../models/${modelName}');\n\n` : ''}class ${className}Observer {
779
+ async creating(model) {}
780
+ async created(model) {}
781
+ async updating(model) {}
782
+ async updated(model) {}
783
+ async saving(model) {}
784
+ async saved(model) {}
785
+ async deleting(model) {}
786
+ async deleted(model) {}
787
+ }
788
+
789
+ module.exports = ${className}Observer;
790
+ `;
791
+
792
+ fs.writeFileSync(filePath, template);
793
+ console.log(`Created observer: observers/${fileName}`);
794
+
795
+ if (modelName) {
796
+ console.log(`Observer configured for model: ${modelName}`);
797
+ }
798
+ },
799
+
800
+ async 'make:cast'(name) {
801
+ if (!name) {
802
+ console.error('Cast name is required');
803
+ process.exit(1);
804
+ }
805
+
806
+ const className = toPascalCase(name.replace('Cast', ''));
807
+ const fileName = `${className}Cast${getFileExtension()}`;
808
+ const filePath = path.join(process.cwd(), 'casts', fileName);
809
+
810
+ if (!fs.existsSync(path.dirname(filePath))) {
811
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
812
+ }
813
+
814
+ const template = isTypeScriptProject() ?
815
+ `import { CustomCast } from 'ilana-orm/orm/Model';
816
+
817
+ export default class ${className}Cast implements CustomCast {
818
+ get(value: any): any {
819
+ return value;
820
+ }
821
+
822
+ set(value: any): any {
823
+ return value;
824
+ }
825
+ }
826
+ ` :
827
+ `class ${className}Cast {
828
+ get(value) {
829
+ return value;
830
+ }
831
+
832
+ set(value) {
833
+ return value;
834
+ }
835
+ }
836
+
837
+ module.exports = ${className}Cast;
838
+ `;
839
+
840
+ fs.writeFileSync(filePath, template);
841
+ console.log(`Created cast: casts/${fileName}`);
842
+ },
843
+
844
+ help() {
845
+ console.log(`
846
+ Ilana ORM CLI
847
+
848
+ Available commands:
849
+ setup Initialize Ilana ORM project structure
850
+
851
+ make:model <name> [options] Create a new model
852
+ -m, --migration Generate migration
853
+ -f, --factory Generate factory
854
+ -s, --seed Generate seeder
855
+ -p, --pivot Generate pivot model
856
+ -a, --all Generate all (model + migration + factory + seeder)
857
+ -mfs Generate model + migration + factory + seeder
858
+
859
+ make:migration <name> Create a new migration file
860
+ migrate [connection] Run all pending migrations
861
+ migrate:rollback [steps] [connection] Rollback the last batch of migrations
862
+ migrate:reset [connection] Rollback all migrations
863
+ migrate:refresh [connection] Reset and re-run all migrations
864
+ migrate:status [connection] Show migration status
865
+ seed [name] Run database seeders
866
+ db:seed [name] Alias for seed command
867
+ help Show this help message
868
+
869
+ Examples:
870
+ ilana setup
871
+ ilana make:model User -m
872
+ ilana make:model Post --migration --factory
873
+ ilana make:model UserRole -mfs
874
+ ilana make:model Permission --all
875
+ ilana make:model UserPost --pivot
876
+ ilana make:migration create_users_table
877
+ ilana migrate
878
+ ilana migrate mysql
879
+ ilana migrate:rollback 2 postgres
880
+ ilana seed UserSeeder
881
+ `);
882
+ }
883
+ };
884
+
885
+ // Parse command line arguments
886
+ async function main() {
887
+ const args = process.argv.slice(2);
888
+ const command = args[0];
889
+ const params = args.slice(1);
890
+
891
+ if (!command || command === 'help') {
892
+ commands.help();
893
+ return;
894
+ }
895
+
896
+ const commandHandler = commands[command];
897
+
898
+ if (!commandHandler) {
899
+ console.error(`Unknown command: ${command}`);
900
+ commands.help();
901
+ process.exit(1);
902
+ }
903
+
904
+ try {
905
+ await commandHandler(...params);
906
+ } catch (error) {
907
+ console.error('Error:', error.message);
908
+ process.exit(1);
909
+ }
910
+ }
911
+
912
+ // Handle uncaught errors
913
+ process.on('unhandledRejection', (error) => {
914
+ console.error('Unhandled rejection:', error);
915
+ process.exit(1);
916
+ });
917
+
918
+ process.on('uncaughtException', (error) => {
919
+ console.error('Uncaught exception:', error);
920
+ process.exit(1);
921
+ });
922
+
923
+ // Run CLI
924
+ if (require.main === module) {
925
+ main();
926
+ }
927
+
928
+ module.exports = commands;