@xubylele/schema-forge 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -210,12 +210,13 @@ Creates the necessary directory structure and configuration files.
210
210
  Generate SQL migration from schema changes.
211
211
 
212
212
  ```bash
213
- schema-forge generate [--name "migration description"] [--safe] [--force]
213
+ schema-forge generate [--name "migration description"] [--migration-format hyphen|underscore] [--safe] [--force]
214
214
  ```
215
215
 
216
216
  **Options:**
217
217
 
218
218
  - `--name` - Optional name for the migration (default: "migration")
219
+ - `--migration-format <format>` - Migration file name: `hyphen` (e.g. `20250315120000-add-users.sql`) or `underscore` (e.g. `20250315120000_add_users.sql`, Supabase CLI style). Overrides `migrationFileNameFormat` in config.
219
220
  - `--safe` - Block execution if destructive operations are detected (exits with error)
220
221
  - `--force` - Bypass safety checks and proceed with destructive operations (shows warning)
221
222
 
@@ -269,6 +270,20 @@ Behavior:
269
270
  - Ignores unsupported SQL safely and prints warnings
270
271
  - Writes a normalized SchemaForge DSL schema file
271
272
 
273
+ ### `schema-forge config`
274
+
275
+ Update `schemaforge/config.json` settings without editing the file.
276
+
277
+ ```bash
278
+ schema-forge config migration-format <format>
279
+ ```
280
+
281
+ **Subcommands:**
282
+
283
+ - **`migration-format <format>`** – Set migration file name format: `hyphen` (e.g. `20250315120000-add-users.sql`) or `underscore` (e.g. `20250315120000_add_users.sql`, Supabase CLI style). Writes `migrationFileNameFormat` to config.
284
+
285
+ Requires an initialized project. Other config keys are left unchanged.
286
+
272
287
  ### `schema-forge validate`
273
288
 
274
289
  Detect destructive or risky schema changes before generating/applying migrations.
@@ -626,7 +641,8 @@ The `schemaforge/config.json` file contains project configuration. The `provider
626
641
  ```
627
642
 
628
643
  - **postgres**: `provider: "postgres"`, `outputDir: "migrations"`.
629
- - **supabase**: `provider: "supabase"`, `outputDir: "supabase/migrations"`.
644
+ - **supabase**: `provider: "supabase"`, `outputDir: "supabase/migrations"`. Init also sets `migrationFileNameFormat: "hyphen"` so migrations are named `timestamp-name.sql` (Supabase CLI uses `timestamp_name.sql` by default; you can set `migrationFileNameFormat: "underscore"` in config or use `--migration-format underscore` to match).
645
+ - **migrationFileNameFormat** (optional): `"hyphen"` (default) or `"underscore"`. Controls generated migration file names: `YYYYMMDDHHmmss-name.sql` vs `YYYYMMDDHHmmss_name.sql`. You can set it via **`schema-forge config migration-format <hyphen|underscore>`** instead of editing the file.
630
646
 
631
647
  ## Supported Databases
632
648
 
package/dist/api.d.ts CHANGED
@@ -11,10 +11,15 @@ interface DoctorOptions {
11
11
  schema?: string;
12
12
  }
13
13
 
14
+ /** Migration file name format: hyphen (timestamp-name.sql) or underscore (timestamp_name.sql, Supabase CLI style). */
15
+ type MigrationFileNameFormat = 'hyphen' | 'underscore';
16
+
14
17
  interface GenerateOptions {
15
18
  name?: string;
16
19
  safe?: boolean;
17
20
  force?: boolean;
21
+ /** Override migration file name format: hyphen (timestamp-name.sql) or underscore (timestamp_name.sql). */
22
+ migrationFormat?: MigrationFileNameFormat;
18
23
  }
19
24
 
20
25
  interface ImportOptions {
package/dist/api.js CHANGED
@@ -3241,6 +3241,10 @@ function nowTimestamp2() {
3241
3241
  function slugifyName2(name) {
3242
3242
  return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "migration";
3243
3243
  }
3244
+ function migrationFileName(timestamp, slug, format = "hyphen") {
3245
+ const sep = format === "underscore" ? "_" : "-";
3246
+ return `${timestamp}${sep}${slug}.sql`;
3247
+ }
3244
3248
 
3245
3249
  // src/commands/generate.ts
3246
3250
  var REQUIRED_CONFIG_FIELDS = [
@@ -3248,6 +3252,19 @@ var REQUIRED_CONFIG_FIELDS = [
3248
3252
  "stateFile",
3249
3253
  "outputDir"
3250
3254
  ];
3255
+ var MIGRATION_FORMATS = ["hyphen", "underscore"];
3256
+ function resolveMigrationFormat(cliFormat, configFormat) {
3257
+ if (cliFormat !== void 0 && cliFormat !== "") {
3258
+ const normalized = cliFormat.trim().toLowerCase();
3259
+ if (MIGRATION_FORMATS.includes(normalized)) {
3260
+ return normalized;
3261
+ }
3262
+ throw new Error(
3263
+ `Invalid --migration-format "${cliFormat}". Allowed: ${MIGRATION_FORMATS.join(", ")}.`
3264
+ );
3265
+ }
3266
+ return configFormat ?? "hyphen";
3267
+ }
3251
3268
  function resolveConfigPath3(root, targetPath) {
3252
3269
  return import_path9.default.isAbsolute(targetPath) ? targetPath : import_path9.default.join(root, targetPath);
3253
3270
  }
@@ -3328,7 +3345,11 @@ Remove --safe flag or modify schema to avoid destructive changes.`
3328
3345
  const sql = await generateSql2(diff2, provider, config.sql);
3329
3346
  const timestamp = nowTimestamp2();
3330
3347
  const slug = slugifyName2(options.name ?? "migration");
3331
- const fileName = `${timestamp}-${slug}.sql`;
3348
+ const format = resolveMigrationFormat(
3349
+ options.migrationFormat,
3350
+ config.migrationFileNameFormat
3351
+ );
3352
+ const fileName = migrationFileName(timestamp, slug, format);
3332
3353
  await ensureDir(outputDir);
3333
3354
  const migrationPath = import_path9.default.join(outputDir, fileName);
3334
3355
  await writeTextFile(migrationPath, sql + "\n");
@@ -3464,6 +3485,9 @@ table users {
3464
3485
  timestampDefault: "now()"
3465
3486
  }
3466
3487
  };
3488
+ if (provider === "supabase") {
3489
+ config.migrationFileNameFormat = "hyphen";
3490
+ }
3467
3491
  await writeJsonFile(configPath, config);
3468
3492
  success(`Created ${configPath}`);
3469
3493
  const state = {
package/dist/cli.js CHANGED
@@ -1130,14 +1130,14 @@ var init_validate = __esm({
1130
1130
  // node_modules/@xubylele/schema-forge-core/dist/core/fs.js
1131
1131
  async function ensureDir2(dirPath) {
1132
1132
  try {
1133
- await import_fs2.promises.mkdir(dirPath, { recursive: true });
1133
+ await import_fs3.promises.mkdir(dirPath, { recursive: true });
1134
1134
  } catch (error2) {
1135
1135
  throw new Error(`Failed to create directory ${dirPath}: ${error2}`);
1136
1136
  }
1137
1137
  }
1138
1138
  async function fileExists2(filePath) {
1139
1139
  try {
1140
- await import_fs2.promises.access(filePath);
1140
+ await import_fs3.promises.access(filePath);
1141
1141
  return true;
1142
1142
  } catch {
1143
1143
  return false;
@@ -1145,7 +1145,7 @@ async function fileExists2(filePath) {
1145
1145
  }
1146
1146
  async function readTextFile2(filePath) {
1147
1147
  try {
1148
- return await import_fs2.promises.readFile(filePath, "utf-8");
1148
+ return await import_fs3.promises.readFile(filePath, "utf-8");
1149
1149
  } catch (error2) {
1150
1150
  throw new Error(`Failed to read file ${filePath}: ${error2}`);
1151
1151
  }
@@ -1154,7 +1154,7 @@ async function writeTextFile2(filePath, content) {
1154
1154
  try {
1155
1155
  const dir = import_path3.default.dirname(filePath);
1156
1156
  await ensureDir2(dir);
1157
- await import_fs2.promises.writeFile(filePath, content, "utf-8");
1157
+ await import_fs3.promises.writeFile(filePath, content, "utf-8");
1158
1158
  } catch (error2) {
1159
1159
  throw new Error(`Failed to write file ${filePath}: ${error2}`);
1160
1160
  }
@@ -1182,7 +1182,7 @@ async function writeJsonFile2(filePath, data) {
1182
1182
  async function findFiles(dirPath, pattern) {
1183
1183
  const results = [];
1184
1184
  try {
1185
- const items = await import_fs2.promises.readdir(dirPath, { withFileTypes: true });
1185
+ const items = await import_fs3.promises.readdir(dirPath, { withFileTypes: true });
1186
1186
  for (const item of items) {
1187
1187
  const fullPath = import_path3.default.join(dirPath, item.name);
1188
1188
  if (item.isDirectory()) {
@@ -1197,11 +1197,11 @@ async function findFiles(dirPath, pattern) {
1197
1197
  }
1198
1198
  return results;
1199
1199
  }
1200
- var import_fs2, import_path3;
1200
+ var import_fs3, import_path3;
1201
1201
  var init_fs = __esm({
1202
1202
  "node_modules/@xubylele/schema-forge-core/dist/core/fs.js"() {
1203
1203
  "use strict";
1204
- import_fs2 = require("fs");
1204
+ import_fs3 = require("fs");
1205
1205
  import_path3 = __toESM(require("path"), 1);
1206
1206
  }
1207
1207
  });
@@ -2204,7 +2204,7 @@ var init_schema_to_dsl = __esm({
2204
2204
 
2205
2205
  // node_modules/@xubylele/schema-forge-core/dist/core/sql/load-migrations.js
2206
2206
  async function loadMigrationSqlInput(inputPath) {
2207
- const stats = await import_fs4.promises.stat(inputPath);
2207
+ const stats = await import_fs5.promises.stat(inputPath);
2208
2208
  if (stats.isFile()) {
2209
2209
  if (!inputPath.toLowerCase().endsWith(".sql")) {
2210
2210
  throw new Error(`Input file must be a .sql file: ${inputPath}`);
@@ -2225,11 +2225,11 @@ async function loadMigrationSqlInput(inputPath) {
2225
2225
  }
2226
2226
  return result;
2227
2227
  }
2228
- var import_fs4, import_path5;
2228
+ var import_fs5, import_path5;
2229
2229
  var init_load_migrations = __esm({
2230
2230
  "node_modules/@xubylele/schema-forge-core/dist/core/sql/load-migrations.js"() {
2231
2231
  "use strict";
2232
- import_fs4 = require("fs");
2232
+ import_fs5 = require("fs");
2233
2233
  import_path5 = __toESM(require("path"), 1);
2234
2234
  init_fs();
2235
2235
  }
@@ -2733,7 +2733,7 @@ var import_commander8 = require("commander");
2733
2733
  // package.json
2734
2734
  var package_default = {
2735
2735
  name: "@xubylele/schema-forge",
2736
- version: "1.10.0",
2736
+ version: "1.11.0",
2737
2737
  description: "Universal migration generator from schema DSL",
2738
2738
  main: "dist/cli.js",
2739
2739
  type: "commonjs",
@@ -2802,10 +2802,6 @@ var package_default = {
2802
2802
  }
2803
2803
  };
2804
2804
 
2805
- // src/commands/diff.ts
2806
- var import_commander = require("commander");
2807
- var import_path7 = __toESM(require("path"));
2808
-
2809
2805
  // src/core/fs.ts
2810
2806
  var import_fs = require("fs");
2811
2807
  var import_path = __toESM(require("path"));
@@ -2884,6 +2880,95 @@ function getStatePath(root, config) {
2884
2880
  return import_path2.default.join(schemaForgeDir, fileName);
2885
2881
  }
2886
2882
 
2883
+ // src/utils/exitCodes.ts
2884
+ var EXIT_CODES = {
2885
+ /** Successful operation */
2886
+ SUCCESS: 0,
2887
+ /** Validation error (invalid DSL syntax, config errors, missing files, etc.) */
2888
+ VALIDATION_ERROR: 1,
2889
+ /** Drift detected - Reserved for future use when comparing actual DB state vs schema */
2890
+ DRIFT_DETECTED: 2,
2891
+ /** Destructive operation detected in CI environment without --force */
2892
+ CI_DESTRUCTIVE: 3
2893
+ };
2894
+ function shouldFailCIDestructive(isCIEnvironment, hasDestructiveFindings2, isForceEnabled) {
2895
+ return isCIEnvironment && hasDestructiveFindings2 && !isForceEnabled;
2896
+ }
2897
+
2898
+ // src/utils/output.ts
2899
+ var import_boxen = __toESM(require("boxen"));
2900
+ var import_chalk = require("chalk");
2901
+ var isInteractive = Boolean(process.stdout?.isTTY);
2902
+ var colorsEnabled = isInteractive && process.env.FORCE_COLOR !== "0" && !("NO_COLOR" in process.env);
2903
+ var color = new import_chalk.Chalk({ level: colorsEnabled ? 3 : 0 });
2904
+ var theme = {
2905
+ primary: color.cyanBright,
2906
+ success: color.hex("#00FF88"),
2907
+ warning: color.hex("#FFD166"),
2908
+ error: color.hex("#EF476F"),
2909
+ accent: color.magentaBright
2910
+ };
2911
+ function success(message) {
2912
+ const text = theme.success(`[OK] ${message}`);
2913
+ if (!isInteractive) {
2914
+ console.log(text);
2915
+ return;
2916
+ }
2917
+ try {
2918
+ console.log(
2919
+ (0, import_boxen.default)(text, {
2920
+ padding: 1,
2921
+ borderColor: "cyan",
2922
+ borderStyle: "round"
2923
+ })
2924
+ );
2925
+ } catch {
2926
+ console.log(text);
2927
+ }
2928
+ }
2929
+ function info(message) {
2930
+ console.log(theme.primary(message));
2931
+ }
2932
+ function warning(message) {
2933
+ console.warn(theme.warning(`[WARN] ${message}`));
2934
+ }
2935
+ function error(message) {
2936
+ console.error(theme.error(`[ERROR] ${message}`));
2937
+ }
2938
+ function forceWarning(message) {
2939
+ console.error(theme.warning(`[FORCE] ${message}`));
2940
+ }
2941
+
2942
+ // src/commands/config.ts
2943
+ var MIGRATION_FORMATS = ["hyphen", "underscore"];
2944
+ async function runConfig(options) {
2945
+ const root = getProjectRoot();
2946
+ const configPath = getConfigPath(root);
2947
+ if (!await fileExists(configPath)) {
2948
+ throw new Error('SchemaForge project not initialized. Run "schema-forge init" first.');
2949
+ }
2950
+ const config = await readJsonFile(configPath, {});
2951
+ if (options.migrationFormat !== void 0) {
2952
+ const format = options.migrationFormat.trim().toLowerCase();
2953
+ if (!MIGRATION_FORMATS.includes(format)) {
2954
+ throw new Error(
2955
+ `Invalid migration format "${options.migrationFormat}". Allowed: ${MIGRATION_FORMATS.join(", ")}.`
2956
+ );
2957
+ }
2958
+ config.migrationFileNameFormat = format;
2959
+ }
2960
+ await writeJsonFile(configPath, config);
2961
+ success(`Updated ${configPath}`);
2962
+ if (options.migrationFormat !== void 0) {
2963
+ success(`migrationFileNameFormat set to "${config.migrationFileNameFormat}" (${config.migrationFileNameFormat === "hyphen" ? "timestamp-name.sql" : "timestamp_name.sql"})`);
2964
+ }
2965
+ process.exitCode = EXIT_CODES.SUCCESS;
2966
+ }
2967
+
2968
+ // src/commands/diff.ts
2969
+ var import_commander = require("commander");
2970
+ var import_path7 = __toESM(require("path"));
2971
+
2887
2972
  // src/core/provider.ts
2888
2973
  var DEFAULT_PROVIDER = "postgres";
2889
2974
  function resolveProvider(provider) {
@@ -3004,65 +3089,6 @@ async function withPostgresQueryExecutor(connectionString, run) {
3004
3089
  }
3005
3090
  }
3006
3091
 
3007
- // src/utils/exitCodes.ts
3008
- var EXIT_CODES = {
3009
- /** Successful operation */
3010
- SUCCESS: 0,
3011
- /** Validation error (invalid DSL syntax, config errors, missing files, etc.) */
3012
- VALIDATION_ERROR: 1,
3013
- /** Drift detected - Reserved for future use when comparing actual DB state vs schema */
3014
- DRIFT_DETECTED: 2,
3015
- /** Destructive operation detected in CI environment without --force */
3016
- CI_DESTRUCTIVE: 3
3017
- };
3018
- function shouldFailCIDestructive(isCIEnvironment, hasDestructiveFindings2, isForceEnabled) {
3019
- return isCIEnvironment && hasDestructiveFindings2 && !isForceEnabled;
3020
- }
3021
-
3022
- // src/utils/output.ts
3023
- var import_boxen = __toESM(require("boxen"));
3024
- var import_chalk = require("chalk");
3025
- var isInteractive = Boolean(process.stdout?.isTTY);
3026
- var colorsEnabled = isInteractive && process.env.FORCE_COLOR !== "0" && !("NO_COLOR" in process.env);
3027
- var color = new import_chalk.Chalk({ level: colorsEnabled ? 3 : 0 });
3028
- var theme = {
3029
- primary: color.cyanBright,
3030
- success: color.hex("#00FF88"),
3031
- warning: color.hex("#FFD166"),
3032
- error: color.hex("#EF476F"),
3033
- accent: color.magentaBright
3034
- };
3035
- function success(message) {
3036
- const text = theme.success(`[OK] ${message}`);
3037
- if (!isInteractive) {
3038
- console.log(text);
3039
- return;
3040
- }
3041
- try {
3042
- console.log(
3043
- (0, import_boxen.default)(text, {
3044
- padding: 1,
3045
- borderColor: "cyan",
3046
- borderStyle: "round"
3047
- })
3048
- );
3049
- } catch {
3050
- console.log(text);
3051
- }
3052
- }
3053
- function info(message) {
3054
- console.log(theme.primary(message));
3055
- }
3056
- function warning(message) {
3057
- console.warn(theme.warning(`[WARN] ${message}`));
3058
- }
3059
- function error(message) {
3060
- console.error(theme.error(`[ERROR] ${message}`));
3061
- }
3062
- function forceWarning(message) {
3063
- console.error(theme.warning(`[FORCE] ${message}`));
3064
- }
3065
-
3066
3092
  // src/utils/prompt.ts
3067
3093
  var import_node_readline = __toESM(require("readline"));
3068
3094
  function isCI() {
@@ -3306,6 +3332,10 @@ function nowTimestamp2() {
3306
3332
  function slugifyName2(name) {
3307
3333
  return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "migration";
3308
3334
  }
3335
+ function migrationFileName(timestamp, slug, format = "hyphen") {
3336
+ const sep = format === "underscore" ? "_" : "-";
3337
+ return `${timestamp}${sep}${slug}.sql`;
3338
+ }
3309
3339
 
3310
3340
  // src/commands/generate.ts
3311
3341
  var REQUIRED_CONFIG_FIELDS = [
@@ -3313,6 +3343,19 @@ var REQUIRED_CONFIG_FIELDS = [
3313
3343
  "stateFile",
3314
3344
  "outputDir"
3315
3345
  ];
3346
+ var MIGRATION_FORMATS2 = ["hyphen", "underscore"];
3347
+ function resolveMigrationFormat(cliFormat, configFormat) {
3348
+ if (cliFormat !== void 0 && cliFormat !== "") {
3349
+ const normalized = cliFormat.trim().toLowerCase();
3350
+ if (MIGRATION_FORMATS2.includes(normalized)) {
3351
+ return normalized;
3352
+ }
3353
+ throw new Error(
3354
+ `Invalid --migration-format "${cliFormat}". Allowed: ${MIGRATION_FORMATS2.join(", ")}.`
3355
+ );
3356
+ }
3357
+ return configFormat ?? "hyphen";
3358
+ }
3316
3359
  function resolveConfigPath3(root, targetPath) {
3317
3360
  return import_path9.default.isAbsolute(targetPath) ? targetPath : import_path9.default.join(root, targetPath);
3318
3361
  }
@@ -3393,7 +3436,11 @@ Remove --safe flag or modify schema to avoid destructive changes.`
3393
3436
  const sql = await generateSql2(diff, provider, config.sql);
3394
3437
  const timestamp = nowTimestamp2();
3395
3438
  const slug = slugifyName2(options.name ?? "migration");
3396
- const fileName = `${timestamp}-${slug}.sql`;
3439
+ const format = resolveMigrationFormat(
3440
+ options.migrationFormat,
3441
+ config.migrationFileNameFormat
3442
+ );
3443
+ const fileName = migrationFileName(timestamp, slug, format);
3397
3444
  await ensureDir(outputDir);
3398
3445
  const migrationPath = import_path9.default.join(outputDir, fileName);
3399
3446
  await writeTextFile(migrationPath, sql + "\n");
@@ -3529,6 +3576,9 @@ table users {
3529
3576
  timestampDefault: "now()"
3530
3577
  }
3531
3578
  };
3579
+ if (provider === "supabase") {
3580
+ config.migrationFileNameFormat = "hyphen";
3581
+ }
3532
3582
  await writeJsonFile(configPath, config);
3533
3583
  success(`Created ${configPath}`);
3534
3584
  const state = {
@@ -3752,7 +3802,7 @@ program.command("init").description(
3752
3802
  await handleError(error2);
3753
3803
  }
3754
3804
  });
3755
- program.command("generate").description("Generate SQL from schema files. In CI environments (CI=true), exits with code 3 if destructive operations are detected unless --force is used.").option("--name <string>", "Schema name to generate").action(async (options) => {
3805
+ program.command("generate").description("Generate SQL from schema files. In CI environments (CI=true), exits with code 3 if destructive operations are detected unless --force is used.").option("--name <string>", "Schema name to generate").option("--migration-format <format>", "Migration file name: hyphen (timestamp-name.sql) or underscore (timestamp_name.sql, Supabase CLI style)").action(async (options) => {
3756
3806
  try {
3757
3807
  const globalOptions = program.opts();
3758
3808
  validateFlagExclusivity(globalOptions);
@@ -3791,6 +3841,13 @@ program.command("import").description("Import schema from SQL migrations").argum
3791
3841
  await handleError(error2);
3792
3842
  }
3793
3843
  });
3844
+ program.command("config").description("Update schemaforge/config.json settings").command("migration-format <format>").description("Set migration file name format: hyphen (timestamp-name.sql) or underscore (timestamp_name.sql)").action(async (format) => {
3845
+ try {
3846
+ await runConfig({ migrationFormat: format });
3847
+ } catch (error2) {
3848
+ await handleError(error2);
3849
+ }
3850
+ });
3794
3851
  program.command("validate").description("Detect destructive or risky schema changes. In CI environments (CI=true), exits with code 3 if destructive operations are detected unless --force is used.").option("--json", "Output structured JSON").option("--url <string>", "PostgreSQL connection URL for live drift validation (defaults to DATABASE_URL)").option("--schema <list>", "Comma-separated schema names to introspect (default: public)").action(async (options) => {
3795
3852
  try {
3796
3853
  const globalOptions = program.opts();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xubylele/schema-forge",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "Universal migration generator from schema DSL",
5
5
  "main": "dist/cli.js",
6
6
  "type": "commonjs",