@zero-server/cli 0.9.0 → 0.9.2
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/LICENSE +21 -21
- package/README.md +1 -1
- package/index.js +3 -3
- package/lib/cli.js +845 -0
- package/package.json +12 -3
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Tony Wiedman
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tony Wiedman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ This package narrows `@zero-server/sdk` to **2** exports. See the [scope page](h
|
|
|
30
30
|
|
|
31
31
|
- [Scope page](https://github.com/tonywied17/zero-server/blob/main/docs/scopes/cli.md)
|
|
32
32
|
- [Full API reference](https://github.com/tonywied17/zero-server/blob/main/API.md)
|
|
33
|
-
- [Live docs](https://z-server.
|
|
33
|
+
- [Live docs](https://z-server.dev)
|
|
34
34
|
|
|
35
35
|
## License
|
|
36
36
|
|
package/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// AUTO-GENERATED by .tools/generate-package-stubs.js — edit .tools/scope-manifest.js and re-run `npm run packages:generate`.
|
|
2
2
|
'use strict';
|
|
3
|
-
const
|
|
3
|
+
const lib = require("./lib/cli");
|
|
4
4
|
|
|
5
5
|
module.exports = {
|
|
6
|
-
CLI:
|
|
7
|
-
runCLI:
|
|
6
|
+
CLI: lib.CLI,
|
|
7
|
+
runCLI: lib.runCLI,
|
|
8
8
|
};
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zero-server/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Programmatic access to the `zh` / `zs` CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"zero-server",
|
|
7
|
-
"zero-
|
|
7
|
+
"zero-server",
|
|
8
8
|
"cli"
|
|
9
9
|
],
|
|
10
10
|
"author": "Anthony Wiedman",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"./package.json": "./package.json"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
|
+
"lib",
|
|
23
24
|
"index.js",
|
|
24
25
|
"index.d.ts",
|
|
25
26
|
"README.md",
|
|
@@ -43,6 +44,14 @@
|
|
|
43
44
|
},
|
|
44
45
|
"sideEffects": false,
|
|
45
46
|
"dependencies": {
|
|
46
|
-
"@zero-server/
|
|
47
|
+
"@zero-server/orm": "0.9.2"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@zero-server/sdk": ">=0.9.2"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"@zero-server/sdk": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
47
56
|
}
|
|
48
57
|
}
|