@zero-server/cli 0.9.1 → 0.9.3

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/lib/cli.js ADDED
@@ -0,0 +1,845 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @module cli
5
+ * @description CLI tool for zero-server ORM operations.
6
+ * Provides commands for migrations, seeding, and scaffolding.
7
+ *
8
+ * Requires a `zero.config.js` (or `.zero-server.js` / legacy `.zero-http.js`) in your project root
9
+ * that exports your database adapter and connection settings.
10
+ *
11
+ * @example
12
+ * // zero.config.js — required by all CLI commands except make:* and help
13
+ * module.exports = {
14
+ * adapter: 'sqlite',
15
+ * connection: { filename: './app.db' },
16
+ * migrationsDir: './migrations',
17
+ * seedersDir: './seeders',
18
+ * };
19
+ *
20
+ * // Run via npx (no global install needed):
21
+ * // npx zh migrate
22
+ * // npx zh migrate:rollback
23
+ * // npx zh migrate:status
24
+ * // npx zh seed
25
+ * // npx zh make:model User
26
+ * // npx zh make:migration create_posts
27
+ * // npx zh make:seeder Users
28
+ *
29
+ * // Or programmatically:
30
+ * const { runCLI } = require('@zero-server/sdk');
31
+ * await runCLI(['migrate']);
32
+ * await runCLI(['make:model', 'User', '--dir=src/models']);
33
+ */
34
+
35
+ 'use strict';
36
+
37
+ const fs = require('fs');
38
+ const path = require('path');
39
+
40
+ // -- Helpers -------------------------------------------------
41
+
42
+ /**
43
+ * @private
44
+ * Print coloured text (ANSI escape codes).
45
+ */
46
+ function color(text, code) { return `\x1b[${code}m${text}\x1b[0m`; }
47
+ const green = (t) => color(t, '32');
48
+ const red = (t) => color(t, '31');
49
+ const yellow = (t) => color(t, '33');
50
+ const cyan = (t) => color(t, '36');
51
+ const bold = (t) => color(t, '1');
52
+ const dim = (t) => color(t, '2');
53
+
54
+ /**
55
+ * @private
56
+ * Timestamp for file names.
57
+ */
58
+ function timestamp()
59
+ {
60
+ const d = new Date();
61
+ return d.toISOString().replace(/[-:T]/g, '').slice(0, 14);
62
+ }
63
+
64
+ /**
65
+ * @private
66
+ * Convert a name to PascalCase.
67
+ */
68
+ function pascalCase(str)
69
+ {
70
+ return str.replace(/(^|[_-])([a-z])/g, (_, __, c) => c.toUpperCase())
71
+ .replace(/[_-]/g, '');
72
+ }
73
+
74
+ /**
75
+ * @private
76
+ * Convert a name to snake_case.
77
+ */
78
+ function snakeCase(str)
79
+ {
80
+ return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
81
+ }
82
+
83
+ /**
84
+ * @private
85
+ * Resolve a config file path relative to CWD.
86
+ */
87
+ function resolveConfig(configPath)
88
+ {
89
+ const cwd = process.cwd();
90
+ const candidates = [
91
+ configPath,
92
+ path.join(cwd, 'zero.config.js'),
93
+ path.join(cwd, 'zero.config.mjs'),
94
+ path.join(cwd, '.zero-server.js'),
95
+ path.join(cwd, '.zero-http.js'),
96
+ ].filter(Boolean);
97
+
98
+ for (const p of candidates)
99
+ {
100
+ const resolved = path.resolve(p);
101
+ if (fs.existsSync(resolved)) return resolved;
102
+ }
103
+ return null;
104
+ }
105
+
106
+ // -- CLI Class -----------------------------------------------
107
+
108
+ /**
109
+ * CLI runner for zero-server ORM commands.
110
+ * Parses arguments and dispatches to command handlers.
111
+ */
112
+ class CLI
113
+ {
114
+ /**
115
+ * @constructor
116
+ * @param {string[]} argv - Process arguments (process.argv.slice(2)).
117
+ */
118
+ constructor(argv = [])
119
+ {
120
+ /** @type {string} */
121
+ this.command = argv[0] || 'help';
122
+
123
+ /** @type {string[]} */
124
+ this.args = argv.slice(1);
125
+
126
+ /** @type {Map<string, string>} */
127
+ this.flags = new Map();
128
+
129
+ // Parse flags
130
+ for (let i = 0; i < this.args.length; i++)
131
+ {
132
+ const arg = this.args[i];
133
+ if (arg.startsWith('--'))
134
+ {
135
+ const [key, val] = arg.slice(2).split('=');
136
+ this.flags.set(key, val || 'true');
137
+ }
138
+ else if (arg.startsWith('-'))
139
+ {
140
+ this.flags.set(arg.slice(1), this.args[i + 1] || 'true');
141
+ i++;
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Run the CLI command.
148
+ *
149
+ * @returns {Promise<void>}
150
+ */
151
+ async run()
152
+ {
153
+ const commands = {
154
+ 'migrate': () => this._migrate(),
155
+ 'migrate:rollback': () => this._rollback(),
156
+ 'migrate:status': () => this._status(),
157
+ 'migrate:reset': () => this._reset(),
158
+ 'migrate:fresh': () => this._fresh(),
159
+ 'migrate:remove': () => this._removeMigration(),
160
+ 'seed': () => this._seed(),
161
+ 'make:model': () => this._makeModel(),
162
+ 'make:migration': () => this._makeMigration(),
163
+ 'make:seeder': () => this._makeSeeder(),
164
+ 'help': () => this._help(),
165
+ '--help': () => this._help(),
166
+ '-h': () => this._help(),
167
+ 'version': () => this._version(),
168
+ '--version': () => this._version(),
169
+ '-v': () => this._version(),
170
+ };
171
+
172
+ const handler = commands[this.command];
173
+ if (!handler)
174
+ {
175
+ console.error(red(`Unknown command: "${this.command}"`));
176
+ this._help();
177
+ process.exitCode = 1;
178
+ return;
179
+ }
180
+
181
+ try
182
+ {
183
+ await handler();
184
+ }
185
+ catch (err)
186
+ {
187
+ console.error(red(`Error: ${err.message}`));
188
+ if (this.flags.has('verbose')) console.error(err.stack);
189
+ process.exitCode = 1;
190
+ }
191
+ }
192
+
193
+ // -- Config Loading ----------------------------------
194
+
195
+ /**
196
+ * @private
197
+ * Load the database configuration from the project.
198
+ */
199
+ async _loadConfig()
200
+ {
201
+ const configPath = this.flags.get('config') || null;
202
+ const resolved = resolveConfig(configPath);
203
+
204
+ if (!resolved)
205
+ {
206
+ throw new Error(
207
+ 'No configuration file found.\n' +
208
+ 'Create a zero.config.js with database and migration settings.\n' +
209
+ 'See "zh help" for examples.'
210
+ );
211
+ }
212
+
213
+ const config = require(resolved);
214
+ return typeof config === 'function' ? await config() : config;
215
+ }
216
+
217
+ /**
218
+ * @private
219
+ * Connect to the database using config.
220
+ */
221
+ async _connectDb(config)
222
+ {
223
+ const { Database } = require('@zero-server/orm');
224
+ return Database.connect(config.adapter || config.type || 'memory', config.connection || config.options || {});
225
+ }
226
+
227
+ /**
228
+ * @private
229
+ * Create the Migrator from config.
230
+ */
231
+ async _createMigrator(config)
232
+ {
233
+ const db = await this._connectDb(config);
234
+ const { Migrator } = require('@zero-server/orm');
235
+ const migrator = new Migrator(db, { table: config.migrationsTable || '_migrations' });
236
+
237
+ // Load migrations from directory
238
+ const migrationsDir = path.resolve(config.migrationsDir || config.migrations || 'migrations');
239
+ if (fs.existsSync(migrationsDir))
240
+ {
241
+ const files = fs.readdirSync(migrationsDir)
242
+ .filter(f => f.endsWith('.js'))
243
+ .sort();
244
+
245
+ for (const file of files)
246
+ {
247
+ const migration = require(path.join(migrationsDir, file));
248
+ if (migration.name && migration.up)
249
+ {
250
+ migrator.add(migration);
251
+ }
252
+ }
253
+ }
254
+
255
+ return { db, migrator };
256
+ }
257
+
258
+ // -- Migration Commands ------------------------------
259
+
260
+ /**
261
+ * @private
262
+ */
263
+ async _migrate()
264
+ {
265
+ const config = await this._loadConfig();
266
+ const { db, migrator } = await this._createMigrator(config);
267
+
268
+ console.log(cyan('Running migrations...'));
269
+ const result = await migrator.migrate();
270
+
271
+ if (result.migrated.length === 0)
272
+ {
273
+ console.log(dim('Nothing to migrate.'));
274
+ }
275
+ else
276
+ {
277
+ for (const name of result.migrated)
278
+ {
279
+ console.log(green(` ✓ ${name}`));
280
+ }
281
+ console.log(bold(`\n${result.migrated.length} migration(s) completed (batch ${result.batch}).`));
282
+ }
283
+
284
+ await db.close();
285
+ }
286
+
287
+ /**
288
+ * @private
289
+ */
290
+ async _rollback()
291
+ {
292
+ const config = await this._loadConfig();
293
+ const { db, migrator } = await this._createMigrator(config);
294
+
295
+ console.log(cyan('Rolling back...'));
296
+ const result = await migrator.rollback();
297
+
298
+ if (result.rolledBack.length === 0)
299
+ {
300
+ console.log(dim('Nothing to rollback.'));
301
+ }
302
+ else
303
+ {
304
+ for (const name of result.rolledBack)
305
+ {
306
+ console.log(yellow(` ↺ ${name}`));
307
+ }
308
+ console.log(bold(`\n${result.rolledBack.length} migration(s) rolled back.`));
309
+ }
310
+
311
+ await db.close();
312
+ }
313
+
314
+ /**
315
+ * @private
316
+ */
317
+ async _status()
318
+ {
319
+ const config = await this._loadConfig();
320
+ const { db, migrator } = await this._createMigrator(config);
321
+
322
+ const status = await migrator.status();
323
+
324
+ console.log(bold('\nMigration Status'));
325
+ console.log('-'.repeat(50));
326
+
327
+ if (status.executed.length > 0)
328
+ {
329
+ console.log(green('\nExecuted:'));
330
+ for (const name of status.executed)
331
+ {
332
+ console.log(green(` ✓ ${name}`));
333
+ }
334
+ }
335
+
336
+ if (status.pending.length > 0)
337
+ {
338
+ console.log(yellow('\nPending:'));
339
+ for (const name of status.pending)
340
+ {
341
+ console.log(yellow(` ○ ${name}`));
342
+ }
343
+ }
344
+
345
+ if (status.executed.length === 0 && status.pending.length === 0)
346
+ {
347
+ console.log(dim(' No migrations registered.'));
348
+ }
349
+
350
+ console.log(`\nLast batch: ${status.lastBatch || 'none'}`);
351
+
352
+ await db.close();
353
+ }
354
+
355
+ /**
356
+ * @private
357
+ */
358
+ async _reset()
359
+ {
360
+ const config = await this._loadConfig();
361
+ const { db, migrator } = await this._createMigrator(config);
362
+
363
+ console.log(cyan('Resetting database (rollback all + re-migrate)...'));
364
+ await migrator.reset();
365
+ console.log(green('Database reset complete.'));
366
+
367
+ await db.close();
368
+ }
369
+
370
+ /**
371
+ * @private
372
+ */
373
+ async _fresh()
374
+ {
375
+ const config = await this._loadConfig();
376
+ const { db, migrator } = await this._createMigrator(config);
377
+
378
+ console.log(yellow('⚠ Fresh migration: dropping all tables and re-migrating...'));
379
+ await migrator.fresh();
380
+ console.log(green('Fresh migration complete.'));
381
+
382
+ await db.close();
383
+ }
384
+
385
+ /**
386
+ * @private
387
+ * Remove the last unapplied migration file and revert the schema snapshot
388
+ * (like EF Core's `remove-migration`).
389
+ */
390
+ async _removeMigration()
391
+ {
392
+ const config = await this._loadConfig();
393
+ const { db, migrator } = await this._createMigrator(config);
394
+
395
+ const migrationsDir = path.resolve(config.migrationsDir || config.migrations || 'migrations');
396
+
397
+ if (!fs.existsSync(migrationsDir))
398
+ {
399
+ console.log(dim('No migrations directory found.'));
400
+ await db.close();
401
+ return;
402
+ }
403
+
404
+ // Find migration files sorted descending
405
+ const files = fs.readdirSync(migrationsDir)
406
+ .filter(f => f.endsWith('.js'))
407
+ .sort()
408
+ .reverse();
409
+
410
+ if (files.length === 0)
411
+ {
412
+ console.log(dim('No migration files to remove.'));
413
+ await db.close();
414
+ return;
415
+ }
416
+
417
+ const lastFile = files[0];
418
+ const lastMigration = require(path.join(migrationsDir, lastFile));
419
+
420
+ // Check if it has already been applied
421
+ const status = await migrator.status();
422
+ if (status.executed.includes(lastMigration.name))
423
+ {
424
+ console.error(red(`Cannot remove "${lastMigration.name}" — it has already been applied.`));
425
+ console.error(dim('Run "zh migrate:rollback" first, then try again.'));
426
+ process.exitCode = 1;
427
+ await db.close();
428
+ return;
429
+ }
430
+
431
+ // Delete the file
432
+ fs.unlinkSync(path.join(migrationsDir, lastFile));
433
+ console.log(yellow(`Removed: ${lastFile}`));
434
+
435
+ // Rebuild snapshot from remaining models (if models exist)
436
+ const {
437
+ buildSnapshot,
438
+ saveSnapshot,
439
+ discoverModels,
440
+ } = require('@zero-server/orm');
441
+ const { Model } = require('@zero-server/orm');
442
+
443
+ const modelsDir = path.resolve(config.modelsDir || config.models || 'models');
444
+ const models = discoverModels(modelsDir, Model);
445
+
446
+ if (models.length > 0)
447
+ {
448
+ // Re-read remaining migration files to reconstruct the snapshot
449
+ // that existed before the removed migration was generated.
450
+ // The cleanest approach: rebuild from models but revert to what
451
+ // the second-to-last migration captured.
452
+ // Since we can't replay old snapshots, we rebuild from current models.
453
+ // The next make:migration will diff against this and detect
454
+ // the changes that the removed migration was supposed to capture.
455
+ const rebuilt = buildSnapshot(models);
456
+ saveSnapshot(migrationsDir, rebuilt);
457
+ console.log(dim('Schema snapshot updated.'));
458
+ }
459
+
460
+ console.log(green('Migration removed successfully.'));
461
+ await db.close();
462
+ }
463
+
464
+ // -- Seed Commands -----------------------------------
465
+
466
+ /**
467
+ * @private
468
+ */
469
+ async _seed()
470
+ {
471
+ const config = await this._loadConfig();
472
+ const db = await this._connectDb(config);
473
+ const { SeederRunner } = require('@zero-server/orm');
474
+
475
+ const runner = new SeederRunner(db);
476
+ const seedDir = path.resolve(config.seedersDir || config.seeders || 'seeders');
477
+
478
+ if (!fs.existsSync(seedDir))
479
+ {
480
+ console.log(dim('No seeders directory found.'));
481
+ await db.close();
482
+ return;
483
+ }
484
+
485
+ const files = fs.readdirSync(seedDir)
486
+ .filter(f => f.endsWith('.js'))
487
+ .sort();
488
+
489
+ const seeders = files.map(f => require(path.join(seedDir, f)));
490
+
491
+ console.log(cyan('Running seeders...'));
492
+ const result = await runner.run(...seeders);
493
+
494
+ for (const name of result)
495
+ {
496
+ console.log(green(` ✓ ${name}`));
497
+ }
498
+ console.log(bold(`\n${result.length} seeder(s) completed.`));
499
+
500
+ await db.close();
501
+ }
502
+
503
+ // -- Scaffolding Commands ----------------------------
504
+
505
+ /**
506
+ * @private
507
+ */
508
+ _makeModel()
509
+ {
510
+ const name = this.args.find(a => !a.startsWith('-'));
511
+ if (!name)
512
+ {
513
+ console.error(red('Usage: zh make:model <Name>'));
514
+ process.exitCode = 1;
515
+ return;
516
+ }
517
+
518
+ const className = pascalCase(name);
519
+ const tableName = snakeCase(name) + 's';
520
+ const dir = this.flags.get('dir') || 'models';
521
+ const filePath = path.resolve(dir, `${className}.js`);
522
+
523
+ if (fs.existsSync(filePath))
524
+ {
525
+ console.error(red(`File already exists: ${filePath}`));
526
+ process.exitCode = 1;
527
+ return;
528
+ }
529
+
530
+ const content =
531
+ `'use strict';
532
+
533
+ const { Model, TYPES } = require('@zero-server/sdk');
534
+
535
+ class ${className} extends Model
536
+ {
537
+ static table = '${tableName}';
538
+
539
+ static schema = {
540
+ id: { type: TYPES.INTEGER, primaryKey: true, autoIncrement: true },
541
+ // Add your columns here
542
+ };
543
+
544
+ static timestamps = true;
545
+ }
546
+
547
+ module.exports = ${className};
548
+ `;
549
+
550
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
551
+ fs.writeFileSync(filePath, content, 'utf8');
552
+ console.log(green(`Model created: ${filePath}`));
553
+ }
554
+
555
+ /**
556
+ * @private
557
+ * Generate a migration file.
558
+ *
559
+ * If `--empty` is passed, a blank template is generated (legacy behaviour).
560
+ * Otherwise the CLI discovers Model classes from `modelsDir`, compares
561
+ * them against the stored schema snapshot and auto-generates migration
562
+ * code that mirrors the detected changes (EF Core–style).
563
+ */
564
+ _makeMigration()
565
+ {
566
+ const name = this.args.find(a => !a.startsWith('-'));
567
+ if (!name)
568
+ {
569
+ console.error(red('Usage: zh make:migration <name>'));
570
+ process.exitCode = 1;
571
+ return;
572
+ }
573
+
574
+ const ts = timestamp();
575
+ const slug = snakeCase(name);
576
+ const migrationName = `${ts}_${slug}`;
577
+ const dir = this.flags.get('dir') || 'migrations';
578
+ const migrationsDir = path.resolve(dir);
579
+
580
+ // --empty : legacy blank-template mode
581
+ if (this.flags.has('empty'))
582
+ {
583
+ const filePath = path.resolve(migrationsDir, `${migrationName}.js`);
584
+ const content =
585
+ `'use strict';
586
+
587
+ module.exports = {
588
+ name: '${migrationName}',
589
+
590
+ async up(db) {
591
+ // Write your migration here
592
+ },
593
+
594
+ async down(db) {
595
+ // Write your rollback here
596
+ },
597
+ };
598
+ `;
599
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
600
+ fs.writeFileSync(filePath, content, 'utf8');
601
+ console.log(green(`Migration created: ${filePath}`));
602
+ return;
603
+ }
604
+
605
+ // -- Auto-diff mode -----------------------------------
606
+ const {
607
+ buildSnapshot,
608
+ loadSnapshot,
609
+ saveSnapshot,
610
+ diffSnapshots,
611
+ hasNoChanges,
612
+ generateMigrationCode,
613
+ discoverModels,
614
+ } = require('@zero-server/orm');
615
+
616
+ const { Model } = require('@zero-server/orm');
617
+
618
+ // Resolve models directory
619
+ let modelsDir;
620
+ try
621
+ {
622
+ const config = this._loadConfigSync();
623
+ modelsDir = path.resolve(config.modelsDir || config.models || 'models');
624
+ }
625
+ catch (_)
626
+ {
627
+ modelsDir = path.resolve(this.flags.get('models') || 'models');
628
+ }
629
+
630
+ // Discover model classes
631
+ const models = discoverModels(modelsDir, Model);
632
+
633
+ if (models.length === 0)
634
+ {
635
+ console.log(yellow(`No models found in ${modelsDir}`));
636
+ console.log(dim('Use --empty to create a blank migration, or check your modelsDir config.'));
637
+ process.exitCode = 1;
638
+ return;
639
+ }
640
+
641
+ // Build current & previous snapshots, diff
642
+ const current = buildSnapshot(models);
643
+ const previous = loadSnapshot(migrationsDir);
644
+ const changes = diffSnapshots(previous, current);
645
+
646
+ if (hasNoChanges(changes))
647
+ {
648
+ console.log(dim('No schema changes detected — nothing to migrate.'));
649
+ return;
650
+ }
651
+
652
+ // Summarise detected changes
653
+ console.log(cyan('Detected schema changes:'));
654
+ for (const t of changes.tables.created) console.log(green(` + Table ${t}`));
655
+ for (const t of changes.tables.dropped) console.log(red(` - Table ${t}`));
656
+ for (const c of changes.columns.added) console.log(green(` + ${c.table}.${c.column}`));
657
+ for (const c of changes.columns.dropped) console.log(red(` - ${c.table}.${c.column}`));
658
+ for (const c of changes.columns.altered) console.log(yellow(` ~ ${c.table}.${c.column}`));
659
+
660
+ // Generate & write migration file
661
+ const code = generateMigrationCode(migrationName, changes, current);
662
+ const filePath = path.resolve(migrationsDir, `${migrationName}.js`);
663
+
664
+ fs.mkdirSync(migrationsDir, { recursive: true });
665
+ fs.writeFileSync(filePath, code, 'utf8');
666
+
667
+ // Update snapshot
668
+ saveSnapshot(migrationsDir, current);
669
+
670
+ console.log(green(`\nMigration created: ${filePath}`));
671
+ }
672
+
673
+ /**
674
+ * @private
675
+ * Synchronous config loader (for make commands that don't need async db).
676
+ */
677
+ _loadConfigSync()
678
+ {
679
+ const configPath = this.flags.get('config') || null;
680
+ const resolved = resolveConfig(configPath);
681
+ if (!resolved) throw new Error('No config');
682
+ const config = require(resolved);
683
+ if (typeof config === 'function') throw new Error('Async config not supported here');
684
+ return config;
685
+ }
686
+
687
+ /**
688
+ * @private
689
+ */
690
+ _makeSeeder()
691
+ {
692
+ const name = this.args.find(a => !a.startsWith('-'));
693
+ if (!name)
694
+ {
695
+ console.error(red('Usage: zh make:seeder <name>'));
696
+ process.exitCode = 1;
697
+ return;
698
+ }
699
+
700
+ const className = pascalCase(name) + 'Seeder';
701
+ const dir = this.flags.get('dir') || 'seeders';
702
+ const filePath = path.resolve(dir, `${className}.js`);
703
+
704
+ if (fs.existsSync(filePath))
705
+ {
706
+ console.error(red(`File already exists: ${filePath}`));
707
+ process.exitCode = 1;
708
+ return;
709
+ }
710
+
711
+ const content =
712
+ `'use strict';
713
+
714
+ const { Seeder } = require('@zero-server/sdk');
715
+
716
+ class ${className} extends Seeder
717
+ {
718
+ async run(db) {
719
+ // Write your seeder here
720
+ // Example:
721
+ // const User = db.model('users');
722
+ // await User.createMany([
723
+ // { name: 'Alice', email: 'alice@example.com' },
724
+ // { name: 'Bob', email: 'bob@example.com' },
725
+ // ]);
726
+ }
727
+ }
728
+
729
+ module.exports = ${className};
730
+ `;
731
+
732
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
733
+ fs.writeFileSync(filePath, content, 'utf8');
734
+ console.log(green(`Seeder created: ${filePath}`));
735
+ }
736
+
737
+ // -- Help & Version ----------------------------------
738
+
739
+ /**
740
+ * @private
741
+ */
742
+ _help()
743
+ {
744
+ console.log(`
745
+ ${bold('zh CLI')} — zero-server ORM tooling
746
+
747
+ ${bold('Usage:')} npx zh <command> [options]
748
+
749
+ ${bold('Commands:')}
750
+
751
+ ${cyan('migrate')} Run pending migrations
752
+ ${cyan('migrate:rollback')} Rollback the last migration batch
753
+ ${cyan('migrate:status')} Show migration status
754
+ ${cyan('migrate:reset')} Rollback all + re-migrate
755
+ ${cyan('migrate:fresh')} Drop all tables + re-migrate
756
+ ${cyan('migrate:remove')} Remove the last unapplied migration
757
+
758
+ ${cyan('seed')} Run all seeders
759
+
760
+ ${cyan('make:model')} <name> Scaffold a new Model file
761
+ ${cyan('make:migration')} <n> Auto-generate migration from model changes
762
+ ${cyan('make:seeder')} <name> Scaffold a new seeder file
763
+
764
+ ${cyan('help')} Show this help message
765
+ ${cyan('version')} Show version
766
+
767
+ ${bold('Options:')}
768
+
769
+ --config=<path> Path to config file (default: zero.config.js)
770
+ --dir=<path> Output directory for make commands
771
+ --models=<path> Models directory (default: modelsDir from config or 'models')
772
+ --empty Generate a blank migration template (skip auto-diff)
773
+ --verbose Show full error stack traces
774
+
775
+ ${bold('Config file:')} ${dim('zero.config.js (or .zero-server.js / legacy .zero-http.js)')}
776
+
777
+ All commands except ${cyan('make:*')} and ${cyan('help')} require a config file
778
+ in your project root. Create ${bold('zero.config.js')} with:
779
+
780
+ ${dim('// zero.config.js')}
781
+ module.exports = {
782
+ adapter: 'sqlite', ${dim('// memory | json | sqlite | mysql | postgres | mongo | redis')}
783
+ connection: { filename: './app.db' }, ${dim('// adapter-specific options')}
784
+ migrationsDir: './migrations', ${dim('// where migration files live')}
785
+ seedersDir: './seeders', ${dim('// where seeder files live')}
786
+ modelsDir: './models', ${dim('// where Model classes live (auto-diff)')}
787
+ };
788
+
789
+ ${bold('Auto-generated migrations:')}
790
+
791
+ ${dim('$')} npx zh make:migration create_users ${dim('# detects new User model → generates CREATE TABLE')}
792
+ ${dim('$')} npx zh make:migration add_email ${dim('# detects new email column → generates ADD COLUMN')}
793
+ ${dim('$')} npx zh make:migration --empty init ${dim('# blank migration (manual mode)')}
794
+ ${dim('$')} npx zh migrate ${dim('# apply pending migrations')}
795
+ ${dim('$')} npx zh migrate:remove ${dim('# undo last make:migration')}
796
+
797
+ ${bold('Examples:')}
798
+
799
+ ${dim('$')} npx zh make:model User ${dim('# creates models/User.js')}
800
+ ${dim('$')} npx zh make:migration create_users ${dim('# auto-generates from models')}
801
+ ${dim('$')} npx zh migrate ${dim('# runs all pending migrations')}
802
+ ${dim('$')} npx zh migrate --config=db.config.js
803
+ ${dim('$')} npx zh seed ${dim('# runs all seeders')}
804
+ `);
805
+ }
806
+
807
+ /**
808
+ * @private
809
+ */
810
+ _version()
811
+ {
812
+ const pkg = require('../package.json');
813
+ console.log(`zh v${pkg.version} (zero-server)`);
814
+ }
815
+ }
816
+
817
+ // -- Entry point ---------------------------------------------
818
+
819
+ /**
820
+ * Create and run the CLI.
821
+ *
822
+ * @param {string[]} [argv] - Arguments (defaults to process.argv.slice(2)).
823
+ * @returns {Promise<void>}
824
+ *
825
+ * @example
826
+ * const { runCLI } = require('@zero-server/sdk');
827
+ * await runCLI(['migrate', '--config=./myconfig.js']);
828
+ */
829
+ async function runCLI(argv)
830
+ {
831
+ const cli = new CLI(argv || process.argv.slice(2));
832
+ await cli.run();
833
+ }
834
+
835
+ module.exports = { CLI, runCLI };
836
+
837
+ // Run directly if executed as script
838
+ if (require.main === module)
839
+ {
840
+ runCLI().catch(err =>
841
+ {
842
+ console.error(err);
843
+ process.exitCode = 1;
844
+ });
845
+ }