@xubylele/schema-forge 1.2.0 → 1.5.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.
Files changed (3) hide show
  1. package/README.md +128 -13
  2. package/dist/cli.js +255 -911
  3. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -24,12 +24,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli.ts
27
- var import_commander4 = require("commander");
27
+ var import_commander6 = require("commander");
28
28
 
29
29
  // package.json
30
30
  var package_default = {
31
31
  name: "@xubylele/schema-forge",
32
- version: "1.2.0",
32
+ version: "1.5.0",
33
33
  description: "Universal migration generator from schema DSL",
34
34
  main: "dist/cli.js",
35
35
  type: "commonjs",
@@ -69,6 +69,7 @@ var package_default = {
69
69
  node: ">=18.0.0"
70
70
  },
71
71
  dependencies: {
72
+ "@xubylele/schema-forge-core": "^1.0.4",
72
73
  boxen: "^8.0.1",
73
74
  chalk: "^5.6.2",
74
75
  commander: "^14.0.3"
@@ -85,120 +86,7 @@ var package_default = {
85
86
 
86
87
  // src/commands/diff.ts
87
88
  var import_commander = require("commander");
88
- var import_path4 = __toESM(require("path"));
89
-
90
- // src/core/diff.ts
91
- function getTableNamesFromState(state) {
92
- return new Set(Object.keys(state.tables));
93
- }
94
- function getTableNamesFromSchema(schema) {
95
- return new Set(Object.keys(schema.tables));
96
- }
97
- function getColumnNamesFromState(stateColumns) {
98
- return new Set(Object.keys(stateColumns));
99
- }
100
- function getColumnNamesFromSchema(dbColumns) {
101
- return new Set(dbColumns.map((column) => column.name));
102
- }
103
- function getSortedNames(names) {
104
- return Array.from(names).sort((a, b) => a.localeCompare(b));
105
- }
106
- function normalizeColumnType(type) {
107
- return type.toLowerCase().trim().replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*,\s*/g, ",").replace(/\s*\)\s*/g, ")");
108
- }
109
- function diffSchemas(oldState, newSchema) {
110
- const operations = [];
111
- const oldTableNames = getTableNamesFromState(oldState);
112
- const newTableNames = getTableNamesFromSchema(newSchema);
113
- const sortedNewTableNames = getSortedNames(newTableNames);
114
- const sortedOldTableNames = getSortedNames(oldTableNames);
115
- for (const tableName of sortedNewTableNames) {
116
- if (!oldTableNames.has(tableName)) {
117
- operations.push({
118
- kind: "create_table",
119
- table: newSchema.tables[tableName]
120
- });
121
- }
122
- }
123
- const commonTableNames = sortedNewTableNames.filter(
124
- (tableName) => oldTableNames.has(tableName)
125
- );
126
- for (const tableName of commonTableNames) {
127
- const newTable = newSchema.tables[tableName];
128
- const oldTable = oldState.tables[tableName];
129
- if (!newTable || !oldTable) {
130
- continue;
131
- }
132
- for (const column of newTable.columns) {
133
- const previousColumn = oldTable.columns[column.name];
134
- if (!previousColumn) {
135
- continue;
136
- }
137
- const previousType = normalizeColumnType(previousColumn.type);
138
- const currentType = normalizeColumnType(column.type);
139
- if (previousType !== currentType) {
140
- operations.push({
141
- kind: "column_type_changed",
142
- tableName,
143
- columnName: column.name,
144
- fromType: previousColumn.type,
145
- toType: column.type
146
- });
147
- }
148
- }
149
- }
150
- for (const tableName of commonTableNames) {
151
- const newTable = newSchema.tables[tableName];
152
- const oldTable = oldState.tables[tableName];
153
- if (!newTable || !oldTable) {
154
- continue;
155
- }
156
- const oldColumnNames = getColumnNamesFromState(oldTable.columns);
157
- for (const column of newTable.columns) {
158
- if (!oldColumnNames.has(column.name)) {
159
- operations.push({
160
- kind: "add_column",
161
- tableName,
162
- column
163
- });
164
- }
165
- }
166
- }
167
- for (const tableName of commonTableNames) {
168
- const newTable = newSchema.tables[tableName];
169
- const oldTable = oldState.tables[tableName];
170
- if (!newTable || !oldTable) {
171
- continue;
172
- }
173
- const newColumnNames = getColumnNamesFromSchema(newTable.columns);
174
- for (const columnName of Object.keys(oldTable.columns)) {
175
- if (!newColumnNames.has(columnName)) {
176
- operations.push({
177
- kind: "drop_column",
178
- tableName,
179
- columnName
180
- });
181
- }
182
- }
183
- }
184
- for (const tableName of sortedOldTableNames) {
185
- if (!newTableNames.has(tableName)) {
186
- operations.push({
187
- kind: "drop_table",
188
- tableName
189
- });
190
- }
191
- }
192
- return { operations };
193
- }
194
-
195
- // src/core/errors.ts
196
- var SchemaValidationError = class extends Error {
197
- constructor(message) {
198
- super(message);
199
- this.name = "SchemaValidationError";
200
- }
201
- };
89
+ var import_path3 = __toESM(require("path"));
202
90
 
203
91
  // src/core/fs.ts
204
92
  var import_fs = require("fs");
@@ -254,309 +142,6 @@ async function writeJsonFile(filePath, data) {
254
142
  throw new Error(`Failed to write JSON file ${filePath}: ${error2}`);
255
143
  }
256
144
  }
257
- async function findFiles(dirPath, pattern) {
258
- const results = [];
259
- try {
260
- const items = await import_fs.promises.readdir(dirPath, { withFileTypes: true });
261
- for (const item of items) {
262
- const fullPath = import_path.default.join(dirPath, item.name);
263
- if (item.isDirectory()) {
264
- const subResults = await findFiles(fullPath, pattern);
265
- results.push(...subResults);
266
- } else if (item.isFile() && pattern.test(item.name)) {
267
- results.push(fullPath);
268
- }
269
- }
270
- } catch (error2) {
271
- throw new Error(`Failed to find files in ${dirPath}: ${error2}`);
272
- }
273
- return results;
274
- }
275
-
276
- // src/utils/output.ts
277
- var import_boxen = __toESM(require("boxen"));
278
- var import_chalk = require("chalk");
279
- var isInteractive = Boolean(process.stdout?.isTTY);
280
- var colorsEnabled = isInteractive && process.env.FORCE_COLOR !== "0" && !("NO_COLOR" in process.env);
281
- var color = new import_chalk.Chalk({ level: colorsEnabled ? 3 : 0 });
282
- var theme = {
283
- primary: color.cyanBright,
284
- success: color.hex("#00FF88"),
285
- warning: color.hex("#FFD166"),
286
- error: color.hex("#EF476F"),
287
- accent: color.magentaBright
288
- };
289
- function success(message) {
290
- const text = theme.success(`[OK] ${message}`);
291
- if (!isInteractive) {
292
- console.log(text);
293
- return;
294
- }
295
- try {
296
- console.log(
297
- (0, import_boxen.default)(text, {
298
- padding: 1,
299
- borderColor: "cyan",
300
- borderStyle: "round"
301
- })
302
- );
303
- } catch {
304
- console.log(text);
305
- }
306
- }
307
- function info(message) {
308
- console.log(theme.primary(message));
309
- }
310
- function warning(message) {
311
- console.warn(theme.warning(`[WARN] ${message}`));
312
- }
313
- function error(message) {
314
- console.error(theme.error(`[ERROR] ${message}`));
315
- }
316
-
317
- // src/core/parser.ts
318
- var SchemaParser = class {
319
- /**
320
- * Parse a schema from a JSON file
321
- */
322
- async parseSchemaFile(filePath) {
323
- try {
324
- const schema = await readJsonFile(filePath, {});
325
- return this.normalizeSchema(schema);
326
- } catch (error2) {
327
- throw new Error(`Failed to parse schema file ${filePath}: ${error2}`);
328
- }
329
- }
330
- /**
331
- * Parse multiple schema files from a directory
332
- */
333
- async parseSchemaDirectory(dirPath) {
334
- const schemaFiles = await findFiles(dirPath, /\.schema\.json$/);
335
- const schemas = [];
336
- for (const file of schemaFiles) {
337
- try {
338
- const schema = await this.parseSchemaFile(file);
339
- schemas.push(schema);
340
- } catch (error2) {
341
- const reason = error2 instanceof Error ? error2.message : String(error2);
342
- warning(`Could not parse ${file}: ${reason}`);
343
- }
344
- }
345
- return schemas;
346
- }
347
- /**
348
- * Merge multiple schemas into one
349
- */
350
- mergeSchemas(schemas) {
351
- if (schemas.length === 0) {
352
- throw new Error("Cannot merge empty schema array");
353
- }
354
- const baseSchema = schemas[0];
355
- const mergedTables = [];
356
- for (const schema of schemas) {
357
- for (const table of schema.tables) {
358
- const existingIndex = mergedTables.findIndex((t) => t.name === table.name);
359
- if (existingIndex >= 0) {
360
- warning(`Duplicate table '${table.name}' found, using first occurrence`);
361
- } else {
362
- mergedTables.push(table);
363
- }
364
- }
365
- }
366
- return {
367
- version: baseSchema.version,
368
- database: baseSchema.database,
369
- tables: mergedTables
370
- };
371
- }
372
- /**
373
- * Normalize schema to ensure consistent structure
374
- */
375
- normalizeSchema(schema) {
376
- return {
377
- version: schema.version || "1.0.0",
378
- database: schema.database || "postgres",
379
- tables: schema.tables.map((table) => ({
380
- ...table,
381
- fields: table.fields.map((field) => ({
382
- ...field,
383
- required: field.required ?? false,
384
- unique: field.unique ?? false
385
- })),
386
- indexes: table.indexes || [],
387
- constraints: table.constraints || []
388
- }))
389
- };
390
- }
391
- /**
392
- * Convert schema to JSON string
393
- */
394
- schemaToJson(schema, pretty = true) {
395
- return pretty ? JSON.stringify(schema, null, 2) : JSON.stringify(schema);
396
- }
397
- /**
398
- * Parse schema from JSON string
399
- */
400
- parseSchemaString(jsonString) {
401
- try {
402
- const schema = JSON.parse(jsonString);
403
- return this.normalizeSchema(schema);
404
- } catch (error2) {
405
- throw new Error(`Failed to parse schema JSON: ${error2}`);
406
- }
407
- }
408
- };
409
- var defaultParser = new SchemaParser();
410
- function parseSchema(source) {
411
- const lines = source.split("\n");
412
- const tables = {};
413
- let currentLine = 0;
414
- const validBaseColumnTypes = /* @__PURE__ */ new Set([
415
- "uuid",
416
- "varchar",
417
- "text",
418
- "int",
419
- "bigint",
420
- "boolean",
421
- "timestamptz",
422
- "date"
423
- ]);
424
- function normalizeColumnType2(type) {
425
- return type.toLowerCase().trim().replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*,\s*/g, ",").replace(/\s*\)\s*/g, ")");
426
- }
427
- function isValidColumnType2(type) {
428
- const normalizedType = normalizeColumnType2(type);
429
- if (validBaseColumnTypes.has(normalizedType)) {
430
- return true;
431
- }
432
- return /^varchar\(\d+\)$/.test(normalizedType) || /^numeric\(\d+,\d+\)$/.test(normalizedType);
433
- }
434
- function cleanLine(line) {
435
- const commentIndex = line.search(/(?:\/\/|#)/);
436
- if (commentIndex !== -1) {
437
- line = line.substring(0, commentIndex);
438
- }
439
- return line.trim();
440
- }
441
- function parseForeignKey(fkRef, lineNum) {
442
- const parts = fkRef.split(".");
443
- if (parts.length !== 2 || !parts[0] || !parts[1]) {
444
- throw new Error(`Line ${lineNum}: Invalid foreign key format '${fkRef}'. Expected format: table.column`);
445
- }
446
- return {
447
- table: parts[0],
448
- column: parts[1]
449
- };
450
- }
451
- function parseColumn(line, lineNum) {
452
- const tokens = line.split(/\s+/).filter((t) => t.length > 0);
453
- if (tokens.length < 2) {
454
- throw new Error(`Line ${lineNum}: Invalid column definition. Expected: <name> <type> [modifiers...]`);
455
- }
456
- const colName = tokens[0];
457
- const colType = normalizeColumnType2(tokens[1]);
458
- if (!isValidColumnType2(colType)) {
459
- throw new Error(
460
- `Line ${lineNum}: Invalid column type '${tokens[1]}'. Valid types: ${Array.from(validBaseColumnTypes).join(", ")}, varchar(n), numeric(p,s)`
461
- );
462
- }
463
- const column = {
464
- name: colName,
465
- type: colType
466
- };
467
- let i = 2;
468
- while (i < tokens.length) {
469
- const modifier = tokens[i];
470
- switch (modifier) {
471
- case "pk":
472
- column.primaryKey = true;
473
- i++;
474
- break;
475
- case "unique":
476
- column.unique = true;
477
- i++;
478
- break;
479
- case "nullable":
480
- column.nullable = true;
481
- i++;
482
- break;
483
- case "default":
484
- i++;
485
- if (i >= tokens.length) {
486
- throw new Error(`Line ${lineNum}: 'default' modifier requires a value`);
487
- }
488
- column.default = tokens[i];
489
- i++;
490
- break;
491
- case "fk":
492
- i++;
493
- if (i >= tokens.length) {
494
- throw new Error(`Line ${lineNum}: 'fk' modifier requires a table.column reference`);
495
- }
496
- column.foreignKey = parseForeignKey(tokens[i], lineNum);
497
- i++;
498
- break;
499
- default:
500
- throw new Error(`Line ${lineNum}: Unknown modifier '${modifier}'`);
501
- }
502
- }
503
- return column;
504
- }
505
- function parseTableBlock(startLine) {
506
- const firstLine = cleanLine(lines[startLine]);
507
- const match = firstLine.match(/^table\s+(\w+)\s*\{?\s*$/);
508
- if (!match) {
509
- throw new Error(`Line ${startLine + 1}: Invalid table definition. Expected: table <name> {`);
510
- }
511
- const tableName = match[1];
512
- if (tables[tableName]) {
513
- throw new Error(`Line ${startLine + 1}: Duplicate table definition '${tableName}'`);
514
- }
515
- const columns = [];
516
- let lineIdx = startLine + 1;
517
- let foundClosingBrace = false;
518
- while (lineIdx < lines.length) {
519
- const cleaned = cleanLine(lines[lineIdx]);
520
- if (!cleaned) {
521
- lineIdx++;
522
- continue;
523
- }
524
- if (cleaned === "}") {
525
- foundClosingBrace = true;
526
- break;
527
- }
528
- try {
529
- const column = parseColumn(cleaned, lineIdx + 1);
530
- columns.push(column);
531
- } catch (error2) {
532
- throw error2;
533
- }
534
- lineIdx++;
535
- }
536
- if (!foundClosingBrace) {
537
- throw new Error(`Line ${startLine + 1}: Table '${tableName}' block not closed (missing '}')`);
538
- }
539
- tables[tableName] = {
540
- name: tableName,
541
- columns
542
- };
543
- return lineIdx;
544
- }
545
- while (currentLine < lines.length) {
546
- const cleaned = cleanLine(lines[currentLine]);
547
- if (!cleaned) {
548
- currentLine++;
549
- continue;
550
- }
551
- if (cleaned.startsWith("table ")) {
552
- currentLine = parseTableBlock(currentLine);
553
- } else {
554
- throw new Error(`Line ${currentLine + 1}: Unexpected content '${cleaned}'. Expected table definition.`);
555
- }
556
- currentLine++;
557
- }
558
- return { tables };
559
- }
560
145
 
561
146
  // src/core/paths.ts
562
147
  var import_path2 = __toESM(require("path"));
@@ -581,491 +166,129 @@ function getStatePath(root, config) {
581
166
  return import_path2.default.join(schemaForgeDir, fileName);
582
167
  }
583
168
 
584
- // src/core/state-manager.ts
585
- var import_path3 = __toESM(require("path"));
586
- var StateManager = class {
587
- constructor(root = process.cwd()) {
588
- this.config = null;
589
- this.root = root;
590
- }
591
- /**
592
- * Initialize a new SchemaForge project
593
- */
594
- async initializeProject(directory = ".", force = false) {
595
- const configPath = import_path3.default.join(directory, "schemaforge.config.json");
596
- if (await fileExists(configPath) && !force) {
597
- throw new Error("SchemaForge project already initialized. Use --force to overwrite.");
598
- }
599
- const defaultConfig = {
600
- version: "1.0.0",
601
- database: "postgres",
602
- schemaDir: "schemas",
603
- outputDir: "output",
604
- migrationDir: "migrations"
605
- };
606
- await writeJsonFile(configPath, defaultConfig);
607
- await ensureDir(import_path3.default.join(directory, defaultConfig.schemaDir));
608
- await ensureDir(import_path3.default.join(directory, defaultConfig.outputDir));
609
- await ensureDir(import_path3.default.join(directory, defaultConfig.migrationDir));
610
- const exampleSchema = {
611
- version: "1.0.0",
612
- database: "postgres",
613
- tables: [
614
- {
615
- name: "users",
616
- fields: [
617
- { name: "id", type: "uuid", required: true, unique: true },
618
- { name: "email", type: "string", required: true, unique: true, length: 255 },
619
- { name: "name", type: "string", required: true, length: 255 },
620
- { name: "created_at", type: "datetime", required: true }
621
- ],
622
- indexes: [
623
- { name: "idx_users_email", fields: ["email"], unique: true }
624
- ]
625
- }
626
- ]
627
- };
628
- const exampleSchemaPath = import_path3.default.join(
629
- directory,
630
- defaultConfig.schemaDir,
631
- "example.schema.json"
632
- );
633
- await writeJsonFile(exampleSchemaPath, exampleSchema);
634
- this.config = defaultConfig;
635
- }
636
- /**
637
- * Load configuration from file
638
- */
639
- async loadConfig(directory = ".") {
640
- const configPath = import_path3.default.join(directory, "schemaforge.config.json");
641
- if (!await fileExists(configPath)) {
642
- throw new Error('SchemaForge project not initialized. Run "schemaforge init" first.');
643
- }
644
- this.config = await readJsonFile(configPath, {});
645
- return this.config;
646
- }
647
- /**
648
- * Save configuration to file
649
- */
650
- async saveConfig(config, directory = ".") {
651
- const configPath = import_path3.default.join(directory, "schemaforge.config.json");
652
- await writeJsonFile(configPath, config);
653
- this.config = config;
654
- }
655
- /**
656
- * Get current configuration
657
- */
658
- getConfig() {
659
- return this.config;
660
- }
661
- /**
662
- * Update configuration
663
- */
664
- updateConfig(updates) {
665
- if (!this.config) {
666
- throw new Error("No configuration loaded");
667
- }
668
- this.config = { ...this.config, ...updates };
169
+ // src/core/provider.ts
170
+ var DEFAULT_PROVIDER = "postgres";
171
+ function resolveProvider(provider) {
172
+ if (!provider) {
173
+ return { provider: DEFAULT_PROVIDER, usedDefault: true };
669
174
  }
670
- /**
671
- * Check if project is initialized
672
- */
673
- async isInitialized(directory = ".") {
674
- const configPath = import_path3.default.join(directory, "schemaforge.config.json");
675
- return await fileExists(configPath);
676
- }
677
- /**
678
- * Get schema directory path
679
- */
680
- getSchemaDir() {
681
- if (!this.config) {
682
- throw new Error("No configuration loaded");
683
- }
684
- return import_path3.default.join(this.root, this.config.schemaDir);
685
- }
686
- /**
687
- * Get output directory path
688
- */
689
- getOutputDir() {
690
- if (!this.config) {
691
- throw new Error("No configuration loaded");
692
- }
693
- return import_path3.default.join(this.root, this.config.outputDir);
694
- }
695
- /**
696
- * Get migration directory path
697
- */
698
- getMigrationDir() {
699
- if (!this.config) {
700
- throw new Error("No configuration loaded");
701
- }
702
- return import_path3.default.join(this.root, this.config.migrationDir);
175
+ return { provider, usedDefault: false };
176
+ }
177
+
178
+ // src/domain.ts
179
+ var corePromise;
180
+ async function loadCore() {
181
+ if (!corePromise) {
182
+ corePromise = import("@xubylele/schema-forge-core");
703
183
  }
704
- };
184
+ return corePromise;
185
+ }
186
+ async function parseSchema(source) {
187
+ const core = await loadCore();
188
+ return core.parseSchema(source);
189
+ }
190
+ async function validateSchema(schema) {
191
+ const core = await loadCore();
192
+ core.validateSchema(schema);
193
+ }
194
+ async function diffSchemas(previousState, currentSchema) {
195
+ const core = await loadCore();
196
+ return core.diffSchemas(previousState, currentSchema);
197
+ }
198
+ async function generateSql(diff, provider, config) {
199
+ const core = await loadCore();
200
+ return core.generateSql(diff, provider, config);
201
+ }
705
202
  async function schemaToState(schema) {
706
- const tables = {};
707
- for (const [tableName, table] of Object.entries(schema.tables)) {
708
- const columns = {};
709
- for (const column of table.columns) {
710
- columns[column.name] = {
711
- type: column.type,
712
- ...column.primaryKey !== void 0 && { primaryKey: column.primaryKey },
713
- ...column.unique !== void 0 && { unique: column.unique },
714
- ...column.nullable !== void 0 && { nullable: column.nullable },
715
- ...column.default !== void 0 && { default: column.default },
716
- ...column.foreignKey !== void 0 && { foreignKey: column.foreignKey }
717
- };
718
- }
719
- tables[tableName] = { columns };
720
- }
721
- return {
722
- version: 1,
723
- tables
724
- };
203
+ const core = await loadCore();
204
+ return core.schemaToState(schema);
725
205
  }
726
206
  async function loadState(statePath) {
727
- return await readJsonFile(statePath, { version: 1, tables: {} });
207
+ const core = await loadCore();
208
+ return core.loadState(statePath);
728
209
  }
729
210
  async function saveState(statePath, state) {
730
- const dirPath = import_path3.default.dirname(statePath);
731
- await ensureDir(dirPath);
732
- await writeJsonFile(statePath, state);
211
+ const core = await loadCore();
212
+ return core.saveState(statePath, state);
733
213
  }
734
- var defaultStateManager = new StateManager();
735
-
736
- // src/core/validator.ts
737
- var SchemaValidator = class {
738
- /**
739
- * Validate a complete schema
740
- */
741
- validateSchema(schema) {
742
- const errors = [];
743
- if (!schema.version) {
744
- errors.push({
745
- path: "schema.version",
746
- message: "Schema version is required",
747
- severity: "error"
748
- });
749
- }
750
- if (!schema.database) {
751
- errors.push({
752
- path: "schema.database",
753
- message: "Database type is required",
754
- severity: "error"
755
- });
756
- }
757
- if (!schema.tables || schema.tables.length === 0) {
758
- errors.push({
759
- path: "schema.tables",
760
- message: "Schema must contain at least one table",
761
- severity: "error"
762
- });
763
- }
764
- if (schema.tables) {
765
- const tableNames = /* @__PURE__ */ new Set();
766
- for (let i = 0; i < schema.tables.length; i++) {
767
- const table = schema.tables[i];
768
- const tableErrors = this.validateTable(table, i);
769
- errors.push(...tableErrors);
770
- if (tableNames.has(table.name)) {
771
- errors.push({
772
- path: `schema.tables[${i}].name`,
773
- message: `Duplicate table name: ${table.name}`,
774
- severity: "error"
775
- });
776
- }
777
- tableNames.add(table.name);
778
- }
779
- errors.push(...this.validateReferences(schema));
780
- }
781
- return {
782
- valid: errors.filter((e) => e.severity === "error").length === 0,
783
- errors
784
- };
785
- }
786
- /**
787
- * Validate a table
788
- */
789
- validateTable(table, tableIndex) {
790
- const errors = [];
791
- const basePath = `schema.tables[${tableIndex}]`;
792
- if (!table.name || table.name.trim() === "") {
793
- errors.push({
794
- path: `${basePath}.name`,
795
- message: "Table name is required",
796
- severity: "error"
797
- });
798
- }
799
- if (table.name && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table.name)) {
800
- errors.push({
801
- path: `${basePath}.name`,
802
- message: `Invalid table name '${table.name}': must start with letter or underscore and contain only alphanumeric characters and underscores`,
803
- severity: "error"
804
- });
805
- }
806
- if (!table.fields || table.fields.length === 0) {
807
- errors.push({
808
- path: `${basePath}.fields`,
809
- message: `Table '${table.name}' must have at least one field`,
810
- severity: "error"
811
- });
812
- }
813
- if (table.fields) {
814
- const fieldNames = /* @__PURE__ */ new Set();
815
- for (let i = 0; i < table.fields.length; i++) {
816
- const field = table.fields[i];
817
- const fieldErrors = this.validateField(field, basePath, i);
818
- errors.push(...fieldErrors);
819
- if (fieldNames.has(field.name)) {
820
- errors.push({
821
- path: `${basePath}.fields[${i}].name`,
822
- message: `Duplicate field name: ${field.name}`,
823
- severity: "error"
824
- });
825
- }
826
- fieldNames.add(field.name);
827
- }
828
- }
829
- return errors;
830
- }
831
- /**
832
- * Validate a field
833
- */
834
- validateField(field, tablePath, fieldIndex) {
835
- const errors = [];
836
- const basePath = `${tablePath}.fields[${fieldIndex}]`;
837
- if (!field.name || field.name.trim() === "") {
838
- errors.push({
839
- path: `${basePath}.name`,
840
- message: "Field name is required",
841
- severity: "error"
842
- });
843
- }
844
- if (field.name && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field.name)) {
845
- errors.push({
846
- path: `${basePath}.name`,
847
- message: `Invalid field name '${field.name}': must start with letter or underscore and contain only alphanumeric characters and underscores`,
848
- severity: "error"
849
- });
850
- }
851
- if (!field.type) {
852
- errors.push({
853
- path: `${basePath}.type`,
854
- message: "Field type is required",
855
- severity: "error"
856
- });
857
- }
858
- if (field.type === "enum") {
859
- if (!field.enumValues || field.enumValues.length === 0) {
860
- errors.push({
861
- path: `${basePath}.enumValues`,
862
- message: "Enum type requires enumValues array",
863
- severity: "error"
864
- });
865
- }
866
- }
867
- if (field.type === "string" && field.length && field.length <= 0) {
868
- errors.push({
869
- path: `${basePath}.length`,
870
- message: "String length must be greater than 0",
871
- severity: "error"
872
- });
873
- }
874
- return errors;
875
- }
876
- /**
877
- * Validate foreign key references
878
- */
879
- validateReferences(schema) {
880
- const errors = [];
881
- const tableNames = new Set(schema.tables.map((t) => t.name));
882
- for (let i = 0; i < schema.tables.length; i++) {
883
- const table = schema.tables[i];
884
- for (let j = 0; j < table.fields.length; j++) {
885
- const field = table.fields[j];
886
- if (field.references) {
887
- const refTable = field.references.table;
888
- const refField = field.references.field;
889
- if (!tableNames.has(refTable)) {
890
- errors.push({
891
- path: `schema.tables[${i}].fields[${j}].references.table`,
892
- message: `Referenced table '${refTable}' does not exist`,
893
- severity: "error"
894
- });
895
- } else {
896
- const referencedTable = schema.tables.find((t) => t.name === refTable);
897
- if (referencedTable) {
898
- const referencedField = referencedTable.fields.find((f) => f.name === refField);
899
- if (!referencedField) {
900
- errors.push({
901
- path: `schema.tables[${i}].fields[${j}].references.field`,
902
- message: `Referenced field '${refField}' does not exist in table '${refTable}'`,
903
- severity: "error"
904
- });
905
- }
906
- }
907
- }
908
- }
909
- }
910
- }
911
- return errors;
912
- }
913
- };
914
- var defaultValidator = new SchemaValidator();
915
- var VALID_BASE_COLUMN_TYPES = [
916
- "uuid",
917
- "varchar",
918
- "text",
919
- "int",
920
- "bigint",
921
- "boolean",
922
- "timestamptz",
923
- "date"
924
- ];
925
- function isValidColumnType(type) {
926
- const normalizedType = type.toLowerCase().trim().replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*,\s*/g, ",").replace(/\s*\)\s*/g, ")");
927
- if (VALID_BASE_COLUMN_TYPES.includes(normalizedType)) {
928
- return true;
929
- }
930
- return /^varchar\(\d+\)$/.test(normalizedType) || /^numeric\(\d+,\d+\)$/.test(normalizedType);
214
+ async function validateSchemaChanges(previousState, currentSchema) {
215
+ const core = await loadCore();
216
+ return core.validateSchemaChanges(previousState, currentSchema);
931
217
  }
932
- function validateSchema(schema) {
933
- validateDuplicateTables(schema);
934
- for (const tableName in schema.tables) {
935
- const table = schema.tables[tableName];
936
- validateTableColumns(tableName, table, schema.tables);
937
- }
218
+ async function toValidationReport(findings) {
219
+ const core = await loadCore();
220
+ return core.toValidationReport(findings);
938
221
  }
939
- function validateDuplicateTables(schema) {
940
- const tableNames = Object.keys(schema.tables);
941
- const seen = /* @__PURE__ */ new Set();
942
- for (const tableName of tableNames) {
943
- if (seen.has(tableName)) {
944
- throw new Error(`Duplicate table: '${tableName}'`);
945
- }
946
- seen.add(tableName);
947
- }
222
+ async function parseMigrationSql(sql) {
223
+ const core = await loadCore();
224
+ return core.parseMigrationSql(sql);
948
225
  }
949
- function validateTableColumns(tableName, table, allTables) {
950
- const columnNames = /* @__PURE__ */ new Set();
951
- let primaryKeyCount = 0;
952
- for (const column of table.columns) {
953
- if (columnNames.has(column.name)) {
954
- throw new Error(`Table '${tableName}': duplicate column '${column.name}'`);
955
- }
956
- columnNames.add(column.name);
957
- if (column.primaryKey) {
958
- primaryKeyCount++;
959
- }
960
- if (!isValidColumnType(column.type)) {
961
- throw new Error(
962
- `Table '${tableName}', column '${column.name}': type '${column.type}' is not valid. Supported types: ${VALID_BASE_COLUMN_TYPES.join(", ")}, varchar(n), numeric(p,s)`
963
- );
964
- }
965
- if (column.foreignKey) {
966
- const fkTable = column.foreignKey.table;
967
- const fkColumn = column.foreignKey.column;
968
- if (!allTables[fkTable]) {
969
- throw new Error(
970
- `Table '${tableName}', column '${column.name}': referenced table '${fkTable}' does not exist`
971
- );
972
- }
973
- const referencedTable = allTables[fkTable];
974
- const columnExists = referencedTable.columns.some((col) => col.name === fkColumn);
975
- if (!columnExists) {
976
- throw new Error(
977
- `Table '${tableName}', column '${column.name}': table '${fkTable}' does not have column '${fkColumn}'`
978
- );
979
- }
980
- }
981
- }
982
- if (primaryKeyCount > 1) {
983
- throw new Error(`Table '${tableName}': can only have one primary key (found ${primaryKeyCount})`);
984
- }
226
+ async function applySqlOps(ops) {
227
+ const core = await loadCore();
228
+ return core.applySqlOps(ops);
985
229
  }
986
-
987
- // src/generator/sql-generator.ts
988
- function generateSql(diff, provider, sqlConfig) {
989
- const statements = [];
990
- for (const operation of diff.operations) {
991
- const sql = generateOperation(operation, provider, sqlConfig);
992
- if (sql) {
993
- statements.push(sql);
994
- }
995
- }
996
- return statements.join("\n\n");
230
+ async function schemaToDsl(schema) {
231
+ const core = await loadCore();
232
+ return core.schemaToDsl(schema);
997
233
  }
998
- function generateOperation(operation, provider, sqlConfig) {
999
- switch (operation.kind) {
1000
- case "create_table":
1001
- return generateCreateTable(operation.table, provider, sqlConfig);
1002
- case "drop_table":
1003
- return generateDropTable(operation.tableName);
1004
- case "column_type_changed":
1005
- return generateAlterColumnType(
1006
- operation.tableName,
1007
- operation.columnName,
1008
- operation.toType
1009
- );
1010
- case "add_column":
1011
- return generateAddColumn(operation.tableName, operation.column, provider, sqlConfig);
1012
- case "drop_column":
1013
- return generateDropColumn(operation.tableName, operation.columnName);
1014
- }
234
+ async function loadMigrationSqlInput(inputPath) {
235
+ const core = await loadCore();
236
+ return core.loadMigrationSqlInput(inputPath);
1015
237
  }
1016
- function generateCreateTable(table, provider, sqlConfig) {
1017
- const columnDefs = table.columns.map(
1018
- (col) => generateColumnDefinition(col, provider, sqlConfig)
1019
- );
1020
- const lines = ["CREATE TABLE " + table.name + " ("];
1021
- columnDefs.forEach((colDef, index) => {
1022
- const isLast = index === columnDefs.length - 1;
1023
- lines.push(" " + colDef + (isLast ? "" : ","));
1024
- });
1025
- lines.push(");");
1026
- return lines.join("\n");
238
+ async function createSchemaValidationError(message) {
239
+ const core = await loadCore();
240
+ return new core.SchemaValidationError(message);
1027
241
  }
1028
- function generateColumnDefinition(column, provider, sqlConfig) {
1029
- const parts = [column.name, column.type];
1030
- if (column.foreignKey) {
1031
- parts.push(
1032
- `references ${column.foreignKey.table}(${column.foreignKey.column})`
1033
- );
1034
- }
1035
- if (column.primaryKey) {
1036
- parts.push("primary key");
1037
- }
1038
- if (column.unique) {
1039
- parts.push("unique");
1040
- }
1041
- if (column.nullable === false) {
1042
- parts.push("not null");
242
+ async function isSchemaValidationError(error2) {
243
+ const core = await loadCore();
244
+ return error2 instanceof core.SchemaValidationError;
245
+ }
246
+
247
+ // src/utils/output.ts
248
+ var import_boxen = __toESM(require("boxen"));
249
+ var import_chalk = require("chalk");
250
+ var isInteractive = Boolean(process.stdout?.isTTY);
251
+ var colorsEnabled = isInteractive && process.env.FORCE_COLOR !== "0" && !("NO_COLOR" in process.env);
252
+ var color = new import_chalk.Chalk({ level: colorsEnabled ? 3 : 0 });
253
+ var theme = {
254
+ primary: color.cyanBright,
255
+ success: color.hex("#00FF88"),
256
+ warning: color.hex("#FFD166"),
257
+ error: color.hex("#EF476F"),
258
+ accent: color.magentaBright
259
+ };
260
+ function success(message) {
261
+ const text = theme.success(`[OK] ${message}`);
262
+ if (!isInteractive) {
263
+ console.log(text);
264
+ return;
1043
265
  }
1044
- if (column.default !== void 0) {
1045
- parts.push("default " + column.default);
1046
- } else if (column.type === "uuid" && column.primaryKey && provider === "supabase") {
1047
- parts.push("default gen_random_uuid()");
266
+ try {
267
+ console.log(
268
+ (0, import_boxen.default)(text, {
269
+ padding: 1,
270
+ borderColor: "cyan",
271
+ borderStyle: "round"
272
+ })
273
+ );
274
+ } catch {
275
+ console.log(text);
1048
276
  }
1049
- return parts.join(" ");
1050
- }
1051
- function generateDropTable(tableName) {
1052
- return `DROP TABLE ${tableName};`;
1053
277
  }
1054
- function generateAddColumn(tableName, column, provider, sqlConfig) {
1055
- const colDef = generateColumnDefinition(column, provider, sqlConfig);
1056
- return `ALTER TABLE ${tableName} ADD COLUMN ${colDef};`;
278
+ function info(message) {
279
+ console.log(theme.primary(message));
1057
280
  }
1058
- function generateDropColumn(tableName, columnName) {
1059
- return `ALTER TABLE ${tableName} DROP COLUMN ${columnName};`;
281
+ function warning(message) {
282
+ console.warn(theme.warning(`[WARN] ${message}`));
1060
283
  }
1061
- function generateAlterColumnType(tableName, columnName, newType) {
1062
- return `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} TYPE ${newType} USING ${columnName}::${newType};`;
284
+ function error(message) {
285
+ console.error(theme.error(`[ERROR] ${message}`));
1063
286
  }
1064
287
 
1065
288
  // src/commands/diff.ts
1066
289
  var REQUIRED_CONFIG_FIELDS = ["schemaFile", "stateFile"];
1067
290
  function resolveConfigPath(root, targetPath) {
1068
- return import_path4.default.isAbsolute(targetPath) ? targetPath : import_path4.default.join(root, targetPath);
291
+ return import_path3.default.isAbsolute(targetPath) ? targetPath : import_path3.default.join(root, targetPath);
1069
292
  }
1070
293
  async function runDiff() {
1071
294
  const root = getProjectRoot();
@@ -1082,33 +305,30 @@ async function runDiff() {
1082
305
  }
1083
306
  const schemaPath = resolveConfigPath(root, config.schemaFile);
1084
307
  const statePath = resolveConfigPath(root, config.stateFile);
1085
- if (config.provider && config.provider !== "postgres" && config.provider !== "supabase") {
1086
- throw new Error(`Unsupported provider '${config.provider}'.`);
1087
- }
1088
- const provider = config.provider ?? "postgres";
308
+ const { provider } = resolveProvider(config.provider);
1089
309
  const schemaSource = await readTextFile(schemaPath);
1090
- const schema = parseSchema(schemaSource);
310
+ const schema = await parseSchema(schemaSource);
1091
311
  try {
1092
- validateSchema(schema);
312
+ await validateSchema(schema);
1093
313
  } catch (error2) {
1094
314
  if (error2 instanceof Error) {
1095
- throw new SchemaValidationError(error2.message);
315
+ throw await createSchemaValidationError(error2.message);
1096
316
  }
1097
317
  throw error2;
1098
318
  }
1099
319
  const previousState = await loadState(statePath);
1100
- const diff = diffSchemas(previousState, schema);
320
+ const diff = await diffSchemas(previousState, schema);
1101
321
  if (diff.operations.length === 0) {
1102
322
  success("No changes detected");
1103
323
  return;
1104
324
  }
1105
- const sql = generateSql(diff, provider, config.sql);
325
+ const sql = await generateSql(diff, provider, config.sql);
1106
326
  console.log(sql);
1107
327
  }
1108
328
 
1109
329
  // src/commands/generate.ts
1110
330
  var import_commander2 = require("commander");
1111
- var import_path5 = __toESM(require("path"));
331
+ var import_path4 = __toESM(require("path"));
1112
332
 
1113
333
  // src/core/utils.ts
1114
334
  function nowTimestamp() {
@@ -1127,7 +347,7 @@ var REQUIRED_CONFIG_FIELDS2 = [
1127
347
  "outputDir"
1128
348
  ];
1129
349
  function resolveConfigPath2(root, targetPath) {
1130
- return import_path5.default.isAbsolute(targetPath) ? targetPath : import_path5.default.join(root, targetPath);
350
+ return import_path4.default.isAbsolute(targetPath) ? targetPath : import_path4.default.join(root, targetPath);
1131
351
  }
1132
352
  async function runGenerate(options) {
1133
353
  const root = getProjectRoot();
@@ -1145,44 +365,92 @@ async function runGenerate(options) {
1145
365
  const schemaPath = resolveConfigPath2(root, config.schemaFile);
1146
366
  const statePath = resolveConfigPath2(root, config.stateFile);
1147
367
  const outputDir = resolveConfigPath2(root, config.outputDir);
1148
- if (config.provider && config.provider !== "postgres" && config.provider !== "supabase") {
1149
- throw new Error(`Unsupported provider '${config.provider}'.`);
1150
- }
1151
- const provider = config.provider ?? "postgres";
1152
- if (!config.provider) {
368
+ const { provider, usedDefault } = resolveProvider(config.provider);
369
+ if (usedDefault) {
1153
370
  info("Provider not set; defaulting to postgres.");
1154
371
  }
1155
372
  info("Generating SQL...");
1156
373
  const schemaSource = await readTextFile(schemaPath);
1157
- const schema = parseSchema(schemaSource);
374
+ const schema = await parseSchema(schemaSource);
1158
375
  try {
1159
- validateSchema(schema);
376
+ await validateSchema(schema);
1160
377
  } catch (error2) {
1161
378
  if (error2 instanceof Error) {
1162
- throw new SchemaValidationError(error2.message);
379
+ throw await createSchemaValidationError(error2.message);
1163
380
  }
1164
381
  throw error2;
1165
382
  }
1166
383
  const previousState = await loadState(statePath);
1167
- const diff = diffSchemas(previousState, schema);
384
+ const diff = await diffSchemas(previousState, schema);
1168
385
  if (diff.operations.length === 0) {
1169
386
  info("No changes detected");
1170
387
  return;
1171
388
  }
1172
- const sql = generateSql(diff, provider, config.sql);
389
+ const sql = await generateSql(diff, provider, config.sql);
1173
390
  const timestamp = nowTimestamp();
1174
391
  const slug = slugifyName(options.name ?? "migration");
1175
392
  const fileName = `${timestamp}-${slug}.sql`;
1176
393
  await ensureDir(outputDir);
1177
- const migrationPath = import_path5.default.join(outputDir, fileName);
394
+ const migrationPath = import_path4.default.join(outputDir, fileName);
1178
395
  await writeTextFile(migrationPath, sql + "\n");
1179
396
  const nextState = await schemaToState(schema);
1180
397
  await saveState(statePath, nextState);
1181
398
  success(`SQL generated successfully: ${migrationPath}`);
1182
399
  }
1183
400
 
1184
- // src/commands/init.ts
401
+ // src/commands/import.ts
1185
402
  var import_commander3 = require("commander");
403
+ var import_path5 = __toESM(require("path"));
404
+ function resolveConfigPath3(root, targetPath) {
405
+ return import_path5.default.isAbsolute(targetPath) ? targetPath : import_path5.default.join(root, targetPath);
406
+ }
407
+ async function runImport(inputPath, options = {}) {
408
+ const root = getProjectRoot();
409
+ const absoluteInputPath = resolveConfigPath3(root, inputPath);
410
+ const inputs = await loadMigrationSqlInput(absoluteInputPath);
411
+ if (inputs.length === 0) {
412
+ throw new Error(`No .sql migration files found in: ${absoluteInputPath}`);
413
+ }
414
+ const allOps = [];
415
+ const parseWarnings = [];
416
+ for (const input of inputs) {
417
+ const result = await parseMigrationSql(input.sql);
418
+ allOps.push(...result.ops);
419
+ parseWarnings.push(...result.warnings.map((item) => ({
420
+ statement: `[${import_path5.default.basename(input.filePath)}] ${item.statement}`,
421
+ reason: item.reason
422
+ })));
423
+ }
424
+ const applied = await applySqlOps(allOps);
425
+ const dsl = await schemaToDsl(applied.schema);
426
+ let targetPath = options.out;
427
+ if (!targetPath) {
428
+ const configPath = getConfigPath(root);
429
+ if (await fileExists(configPath)) {
430
+ const config = await readJsonFile(configPath, {});
431
+ if (typeof config.schemaFile === "string" && config.schemaFile.length > 0) {
432
+ targetPath = config.schemaFile;
433
+ }
434
+ }
435
+ }
436
+ const schemaPath = targetPath ? resolveConfigPath3(root, targetPath) : getSchemaFilePath(root);
437
+ await writeTextFile(schemaPath, dsl);
438
+ success(`Imported ${inputs.length} migration file(s) into ${schemaPath}`);
439
+ info(`Parsed ${allOps.length} supported DDL operation(s)`);
440
+ const warnings = [...parseWarnings, ...applied.warnings];
441
+ if (warnings.length > 0) {
442
+ warning(`Ignored ${warnings.length} unsupported item(s)`);
443
+ for (const item of warnings.slice(0, 10)) {
444
+ warning(`${item.reason}: ${item.statement}`);
445
+ }
446
+ if (warnings.length > 10) {
447
+ warning(`...and ${warnings.length - 10} more`);
448
+ }
449
+ }
450
+ }
451
+
452
+ // src/commands/init.ts
453
+ var import_commander4 = require("commander");
1186
454
  async function runInit() {
1187
455
  const root = getProjectRoot();
1188
456
  const schemaForgeDir = getSchemaForgeDir(root);
@@ -1219,8 +487,8 @@ table users {
1219
487
  await writeTextFile(schemaFilePath, schemaContent);
1220
488
  success(`Created ${schemaFilePath}`);
1221
489
  const config = {
1222
- provider: "supabase",
1223
- outputDir: "supabase/migrations",
490
+ provider: "postgres",
491
+ outputDir: "migrations",
1224
492
  schemaFile: "schemaforge/schema.sf",
1225
493
  stateFile: "schemaforge/state.json",
1226
494
  sql: {
@@ -1236,7 +504,7 @@ table users {
1236
504
  };
1237
505
  await writeJsonFile(statePath, state);
1238
506
  success(`Created ${statePath}`);
1239
- const outputDir = "supabase/migrations";
507
+ const outputDir = "migrations";
1240
508
  await ensureDir(outputDir);
1241
509
  success(`Created ${outputDir}`);
1242
510
  success("Project initialized successfully");
@@ -1245,11 +513,73 @@ table users {
1245
513
  info(" 2. Run: schema-forge generate");
1246
514
  }
1247
515
 
516
+ // src/commands/validate.ts
517
+ var import_commander5 = require("commander");
518
+ var import_path6 = __toESM(require("path"));
519
+ var REQUIRED_CONFIG_FIELDS3 = ["schemaFile", "stateFile"];
520
+ function resolveConfigPath4(root, targetPath) {
521
+ return import_path6.default.isAbsolute(targetPath) ? targetPath : import_path6.default.join(root, targetPath);
522
+ }
523
+ async function runValidate(options = {}) {
524
+ const root = getProjectRoot();
525
+ const configPath = getConfigPath(root);
526
+ if (!await fileExists(configPath)) {
527
+ throw new Error('SchemaForge project not initialized. Run "schema-forge init" first.');
528
+ }
529
+ const config = await readJsonFile(configPath, {});
530
+ for (const field of REQUIRED_CONFIG_FIELDS3) {
531
+ const value = config[field];
532
+ if (!value || typeof value !== "string") {
533
+ throw new Error(`Invalid config: '${field}' is required`);
534
+ }
535
+ }
536
+ const schemaPath = resolveConfigPath4(root, config.schemaFile);
537
+ const statePath = resolveConfigPath4(root, config.stateFile);
538
+ const schemaSource = await readTextFile(schemaPath);
539
+ const schema = await parseSchema(schemaSource);
540
+ try {
541
+ await validateSchema(schema);
542
+ } catch (error2) {
543
+ if (error2 instanceof Error) {
544
+ throw await createSchemaValidationError(error2.message);
545
+ }
546
+ throw error2;
547
+ }
548
+ const previousState = await loadState(statePath);
549
+ const findings = await validateSchemaChanges(previousState, schema);
550
+ const report = await toValidationReport(findings);
551
+ if (options.json) {
552
+ console.log(JSON.stringify(report, null, 2));
553
+ process.exitCode = report.hasErrors ? 1 : 0;
554
+ return;
555
+ }
556
+ if (findings.length === 0) {
557
+ success("No destructive changes detected");
558
+ process.exitCode = 0;
559
+ return;
560
+ }
561
+ console.log(
562
+ `Validation Summary: ${report.errors.length} error(s), ${report.warnings.length} warning(s)`
563
+ );
564
+ const tableOrder = Array.from(new Set(findings.map((finding) => finding.table)));
565
+ for (const tableName of tableOrder) {
566
+ console.log(tableName);
567
+ for (const finding of findings.filter((entry) => entry.table === tableName)) {
568
+ const target = finding.column ? `${finding.table}.${finding.column}` : finding.table;
569
+ const typeRange = finding.from && finding.to ? ` (${finding.from} -> ${finding.to})` : "";
570
+ console.log(
571
+ `${finding.severity.toUpperCase()}: ${finding.code} ${target}${typeRange} - ${finding.message}`
572
+ );
573
+ }
574
+ }
575
+ process.exitCode = report.hasErrors ? 1 : 0;
576
+ }
577
+
1248
578
  // src/cli.ts
1249
- var program = new import_commander4.Command();
579
+ var program = new import_commander6.Command();
1250
580
  program.name("schema-forge").description("CLI tool for schema management and SQL generation").version(package_default.version);
1251
- function handleError(error2) {
1252
- if (error2 instanceof SchemaValidationError) {
581
+ async function handleError(error2) {
582
+ if (await isSchemaValidationError(error2) && error2 instanceof Error) {
1253
583
  error(error2.message);
1254
584
  process.exitCode = 2;
1255
585
  return;
@@ -1265,21 +595,35 @@ program.command("init").description("Initialize a new schema project").action(as
1265
595
  try {
1266
596
  await runInit();
1267
597
  } catch (error2) {
1268
- handleError(error2);
598
+ await handleError(error2);
1269
599
  }
1270
600
  });
1271
601
  program.command("generate").description("Generate SQL from schema files").option("--name <string>", "Schema name to generate").action(async (options) => {
1272
602
  try {
1273
603
  await runGenerate(options);
1274
604
  } catch (error2) {
1275
- handleError(error2);
605
+ await handleError(error2);
1276
606
  }
1277
607
  });
1278
608
  program.command("diff").description("Compare two schema versions and generate migration SQL").action(async () => {
1279
609
  try {
1280
610
  await runDiff();
1281
611
  } catch (error2) {
1282
- handleError(error2);
612
+ await handleError(error2);
613
+ }
614
+ });
615
+ program.command("import").description("Import schema from SQL migrations").argument("<path>", "Path to .sql file or migrations directory").option("--out <path>", "Output schema file path").action(async (targetPath, options) => {
616
+ try {
617
+ await runImport(targetPath, options);
618
+ } catch (error2) {
619
+ await handleError(error2);
620
+ }
621
+ });
622
+ program.command("validate").description("Detect destructive or risky schema changes").option("--json", "Output structured JSON").action(async (options) => {
623
+ try {
624
+ await runValidate(options);
625
+ } catch (error2) {
626
+ await handleError(error2);
1283
627
  }
1284
628
  });
1285
629
  program.parse(process.argv);