@xubylele/schema-forge 1.8.1 → 1.10.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
@@ -2,7 +2,7 @@
2
2
 
3
3
  A modern CLI tool for database schema management with a clean DSL and automatic SQL migration generation.
4
4
 
5
- **Website:** [schemaforge.xuby.cl](https://schemaforge.xuby.cl/) · **npm package:** [@xubylele/schema-forge](https://www.npmjs.com/package/@xubylele/schema-forge)
5
+ **Website:** [schemaforge.xuby.cl](https://schemaforge.xuby.cl/) · **npm package:** [@xubylele/schema-forge](https://www.npmjs.com/package/@xubylele/schema-forge) · **Roadmap:** [ROADMAP.md](ROADMAP.md)
6
6
 
7
7
  ## Features
8
8
 
@@ -40,7 +40,7 @@ const result = await generate({ name: 'MyMigration' });
40
40
  if (result.exitCode !== EXIT_CODES.SUCCESS) process.exit(result.exitCode);
41
41
  ```
42
42
 
43
- **Exports:** `init`, `generate`, `diff`, `doctor`, `validate`, `introspect`, `importSchema` (each returns `Promise<RunResult>`), `RunResult` (`{ exitCode: number }`), `EXIT_CODES`, and option types (`GenerateOptions`, `DiffOptions`, etc.). Entrypoint: `@xubylele/schema-forge/api`. Exit code semantics: [docs/exit-codes.json](docs/exit-codes.json).
43
+ **Exports:** `init`, `generate`, `diff`, `doctor`, `validate`, `introspect`, `importSchema` (each returns `Promise<RunResult>`), `RunResult` (`{ exitCode: number }`), `EXIT_CODES`, and option types (`InitOptions`, `GenerateOptions`, `DiffOptions`, etc.). Entrypoint: `@xubylele/schema-forge/api`. Exit code semantics: [docs/exit-codes.json](docs/exit-codes.json).
44
44
 
45
45
  ## Development
46
46
 
@@ -88,15 +88,20 @@ Here's a quick walkthrough to get started with SchemaForge:
88
88
  ### 1. Initialize a new project
89
89
 
90
90
  ```bash
91
- schema-forge init
91
+ schema-forge init [provider]
92
92
  ```
93
93
 
94
+ Optional `provider`: `postgres` (default) or `supabase`. You can also use `--provider <provider>`.
95
+
96
+ - **postgres** – Creates `migrations/` at the project root and sets `outputDir` to `migrations`.
97
+ - **supabase** – Uses `supabase/migrations/` for migrations. If `supabase/` does not exist, it is created; if it already exists (e.g. from `supabase init`), SchemaForge config is set to use `supabase/migrations/`.
98
+
94
99
  This creates:
95
100
 
96
101
  - `schemaforge/schema.sf` - Your schema definition file
97
- - `schemaforge/config.json` - Project configuration
102
+ - `schemaforge/config.json` - Project configuration (includes `provider` and `outputDir`)
98
103
  - `schemaforge/state.json` - State tracking file
99
- - `supabase/migrations/` - Directory for generated migrations
104
+ - `migrations/` or `supabase/migrations/` - Directory for generated migrations (depends on provider)
100
105
 
101
106
  ### 2. Define your schema
102
107
 
@@ -186,9 +191,18 @@ For common function-style defaults, comparisons are normalized to avoid obvious
186
191
  Initialize a new SchemaForge project in the current directory.
187
192
 
188
193
  ```bash
189
- schema-forge init
194
+ schema-forge init [provider]
195
+ schema-forge init --provider <provider>
190
196
  ```
191
197
 
198
+ - **`[provider]`** (optional) – `postgres` or `supabase`. Default is `postgres`.
199
+ - **`--provider <provider>`** – Same as above; overrides the positional argument if both are given.
200
+
201
+ **Behavior:**
202
+
203
+ - **postgres** (default): Creates `migrations/` at the project root. Config gets `provider: "postgres"` and `outputDir: "migrations"`.
204
+ - **supabase**: Uses `supabase/migrations/` for generated migrations. If the `supabase/` folder does not exist, it is created along with `supabase/migrations/`. If `supabase/` already exists (e.g. from an existing Supabase project), only `supabase/migrations/` is ensured and config is set to use it. Config gets `provider: "supabase"` and `outputDir: "supabase/migrations"`.
205
+
192
206
  Creates the necessary directory structure and configuration files.
193
207
 
194
208
  ### `schema-forge generate`
@@ -578,6 +592,8 @@ table profiles {
578
592
 
579
593
  ## Project Structure
580
594
 
595
+ With **postgres** (default), migrations live in `migrations/` at the project root. With **supabase**, they live in `supabase/migrations/`. Example for a Supabase-backed project:
596
+
581
597
  ```bash
582
598
  your-project/
583
599
  +-- schemaforge/
@@ -585,14 +601,16 @@ your-project/
585
601
  | +-- config.json # Project configuration
586
602
  | \-- state.json # State tracking (auto-generated)
587
603
  \-- supabase/
588
- \-- migrations/ # Generated SQL migrations
604
+ \-- migrations/ # Generated SQL migrations (when provider is supabase)
589
605
  +-- 20240101120000-initial.sql
590
606
  \-- 20240101120100-add-user-avatar.sql
591
607
  ```
592
608
 
609
+ For postgres, replace `supabase/migrations/` with a top-level `migrations/` directory.
610
+
593
611
  ## Configuration
594
612
 
595
- The `schemaforge/config.json` file contains project configuration:
613
+ The `schemaforge/config.json` file contains project configuration. The `provider` and `outputDir` values are set by `schema-forge init` based on the provider you choose:
596
614
 
597
615
  ```json
598
616
  {
@@ -607,18 +625,21 @@ The `schemaforge/config.json` file contains project configuration:
607
625
  }
608
626
  ```
609
627
 
628
+ - **postgres**: `provider: "postgres"`, `outputDir: "migrations"`.
629
+ - **supabase**: `provider: "supabase"`, `outputDir: "supabase/migrations"`.
630
+
610
631
  ## Supported Databases
611
632
 
612
633
  Currently supports:
613
634
 
614
- - PostgreSQL (`postgres`)
615
- - Supabase (`supabase`)
635
+ - PostgreSQL (`postgres`) – default; migrations in `migrations/`
636
+ - Supabase (`supabase`) – migrations in `supabase/migrations/`; choose at init with `schema-forge init supabase`
616
637
 
617
638
  ## Development Workflow
618
639
 
619
640
  A typical development workflow looks like this:
620
641
 
621
- 1. **Initialize** - `schema-forge init` (one time)
642
+ 1. **Initialize** - `schema-forge init` or `schema-forge init supabase` (one time)
622
643
  2. **Edit schema** - Modify `schemaforge/schema.sf`
623
644
  3. **Preview changes** - `schema-forge diff` (optional)
624
645
  4. **Generate migration** - `schema-forge generate --name "description"`
package/dist/api.d.ts CHANGED
@@ -21,6 +21,10 @@ interface ImportOptions {
21
21
  out?: string;
22
22
  }
23
23
 
24
+ interface InitOptions {
25
+ provider?: string;
26
+ }
27
+
24
28
  interface IntrospectOptions {
25
29
  url?: string;
26
30
  schema?: string;
@@ -32,6 +36,7 @@ interface ValidateOptions {
32
36
  json?: boolean;
33
37
  url?: string;
34
38
  schema?: string;
39
+ force?: boolean;
35
40
  }
36
41
 
37
42
  /**
@@ -70,8 +75,9 @@ interface RunResult {
70
75
  }
71
76
  /**
72
77
  * Initialize a new schema project in the current directory.
78
+ * @param options.provider - Database provider: 'postgres' (default) or 'supabase'. Supabase uses supabase/migrations for output.
73
79
  */
74
- declare function init(): Promise<RunResult>;
80
+ declare function init(options?: InitOptions): Promise<RunResult>;
75
81
  /**
76
82
  * Generate SQL migration from schema files.
77
83
  */
@@ -98,4 +104,4 @@ declare function introspect(options?: IntrospectOptions): Promise<RunResult>;
98
104
  */
99
105
  declare function importSchema(inputPath: string, options?: ImportOptions): Promise<RunResult>;
100
106
 
101
- export { type DiffOptions, type DoctorOptions, EXIT_CODES, type GenerateOptions, type ImportOptions, type IntrospectOptions, type RunResult, type ValidateOptions, diff, doctor, generate, importSchema, init, introspect, validate };
107
+ export { type DiffOptions, type DoctorOptions, EXIT_CODES, type GenerateOptions, type ImportOptions, type InitOptions, type IntrospectOptions, type RunResult, type ValidateOptions, diff, doctor, generate, importSchema, init, introspect, validate };
package/dist/api.js CHANGED
@@ -2950,6 +2950,9 @@ var EXIT_CODES = {
2950
2950
  /** Destructive operation detected in CI environment without --force */
2951
2951
  CI_DESTRUCTIVE: 3
2952
2952
  };
2953
+ function shouldFailCIDestructive(isCIEnvironment, hasDestructiveFindings2, isForceEnabled) {
2954
+ return isCIEnvironment && hasDestructiveFindings2 && !isForceEnabled;
2955
+ }
2953
2956
 
2954
2957
  // src/utils/output.ts
2955
2958
  var import_boxen = __toESM(require("boxen"));
@@ -3389,7 +3392,21 @@ async function runImport(inputPath, options = {}) {
3389
3392
 
3390
3393
  // src/commands/init.ts
3391
3394
  var import_commander5 = require("commander");
3392
- async function runInit() {
3395
+ var import_path11 = __toESM(require("path"));
3396
+ var ALLOWED_PROVIDERS = ["postgres", "supabase"];
3397
+ function resolveInitProvider(provider) {
3398
+ if (!provider) {
3399
+ return "postgres";
3400
+ }
3401
+ const normalized = provider.trim().toLowerCase();
3402
+ if (ALLOWED_PROVIDERS.includes(normalized)) {
3403
+ return normalized;
3404
+ }
3405
+ throw new Error(
3406
+ `Invalid provider "${provider}". Allowed values: ${ALLOWED_PROVIDERS.join(", ")}.`
3407
+ );
3408
+ }
3409
+ async function runInit(options) {
3393
3410
  const root = getProjectRoot();
3394
3411
  const schemaForgeDir = getSchemaForgeDir(root);
3395
3412
  if (await fileExists(schemaForgeDir)) {
@@ -3407,6 +3424,7 @@ async function runInit() {
3407
3424
  if (await fileExists(statePath)) {
3408
3425
  throw new Error(`${statePath} already exists`);
3409
3426
  }
3427
+ const provider = resolveInitProvider(options?.provider);
3410
3428
  info("Initializing schema project...");
3411
3429
  await ensureDir(schemaForgeDir);
3412
3430
  const schemaContent = `# SchemaForge schema definition
@@ -3419,9 +3437,26 @@ table users {
3419
3437
  `;
3420
3438
  await writeTextFile(schemaFilePath, schemaContent);
3421
3439
  success(`Created ${schemaFilePath}`);
3440
+ let outputDir;
3441
+ if (provider === "supabase") {
3442
+ const supabaseDir = import_path11.default.join(root, "supabase");
3443
+ const migrationsDir = import_path11.default.join(root, "supabase", "migrations");
3444
+ if (!await fileExists(supabaseDir)) {
3445
+ await ensureDir(migrationsDir);
3446
+ success(`Created supabase/migrations`);
3447
+ } else {
3448
+ await ensureDir(migrationsDir);
3449
+ success(`Using existing supabase/; migrations at supabase/migrations`);
3450
+ }
3451
+ outputDir = "supabase/migrations";
3452
+ } else {
3453
+ outputDir = "migrations";
3454
+ await ensureDir(import_path11.default.join(root, outputDir));
3455
+ success(`Created ${outputDir}`);
3456
+ }
3422
3457
  const config = {
3423
- provider: "postgres",
3424
- outputDir: "migrations",
3458
+ provider,
3459
+ outputDir,
3425
3460
  schemaFile: "schemaforge/schema.sf",
3426
3461
  stateFile: "schemaforge/state.json",
3427
3462
  sql: {
@@ -3437,9 +3472,6 @@ table users {
3437
3472
  };
3438
3473
  await writeJsonFile(statePath, state);
3439
3474
  success(`Created ${statePath}`);
3440
- const outputDir = "migrations";
3441
- await ensureDir(outputDir);
3442
- success(`Created ${outputDir}`);
3443
3475
  success("Project initialized successfully");
3444
3476
  info("Next steps:");
3445
3477
  info(" 1. Edit schemaforge/schema.sf to define your schema");
@@ -3449,9 +3481,9 @@ table users {
3449
3481
 
3450
3482
  // src/commands/introspect.ts
3451
3483
  var import_commander6 = require("commander");
3452
- var import_path11 = __toESM(require("path"));
3484
+ var import_path12 = __toESM(require("path"));
3453
3485
  function resolveOutputPath(root, outputPath) {
3454
- return import_path11.default.isAbsolute(outputPath) ? outputPath : import_path11.default.join(root, outputPath);
3486
+ return import_path12.default.isAbsolute(outputPath) ? outputPath : import_path12.default.join(root, outputPath);
3455
3487
  }
3456
3488
  async function runIntrospect(options = {}) {
3457
3489
  const connectionString = resolvePostgresConnectionString({ url: options.url });
@@ -3478,9 +3510,9 @@ async function runIntrospect(options = {}) {
3478
3510
 
3479
3511
  // src/commands/validate.ts
3480
3512
  var import_commander7 = require("commander");
3481
- var import_path12 = __toESM(require("path"));
3513
+ var import_path13 = __toESM(require("path"));
3482
3514
  function resolveConfigPath5(root, targetPath) {
3483
- return import_path12.default.isAbsolute(targetPath) ? targetPath : import_path12.default.join(root, targetPath);
3515
+ return import_path13.default.isAbsolute(targetPath) ? targetPath : import_path13.default.join(root, targetPath);
3484
3516
  }
3485
3517
  async function runValidate(options = {}) {
3486
3518
  const root = getProjectRoot();
@@ -3554,7 +3586,7 @@ async function runValidate(options = {}) {
3554
3586
  }
3555
3587
  const findings = await validateSchemaChanges2(previousState, schema);
3556
3588
  const report = await toValidationReport2(findings);
3557
- if (isCI() && hasDestructiveFindings(findings)) {
3589
+ if (shouldFailCIDestructive(isCI(), hasDestructiveFindings(findings), Boolean(options.force))) {
3558
3590
  process.exitCode = EXIT_CODES.CI_DESTRUCTIVE;
3559
3591
  } else {
3560
3592
  process.exitCode = report.hasErrors ? EXIT_CODES.VALIDATION_ERROR : EXIT_CODES.SUCCESS;
@@ -3598,8 +3630,8 @@ async function runWithResult(fn) {
3598
3630
  return { exitCode: EXIT_CODES.VALIDATION_ERROR };
3599
3631
  }
3600
3632
  }
3601
- async function init() {
3602
- return runWithResult(() => runInit());
3633
+ async function init(options = {}) {
3634
+ return runWithResult(() => runInit(options));
3603
3635
  }
3604
3636
  async function generate(options = {}) {
3605
3637
  return runWithResult(() => runGenerate(options));
package/dist/cli.js CHANGED
@@ -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.8.1",
2736
+ version: "1.10.0",
2737
2737
  description: "Universal migration generator from schema DSL",
2738
2738
  main: "dist/cli.js",
2739
2739
  type: "commonjs",
@@ -3015,6 +3015,9 @@ var EXIT_CODES = {
3015
3015
  /** Destructive operation detected in CI environment without --force */
3016
3016
  CI_DESTRUCTIVE: 3
3017
3017
  };
3018
+ function shouldFailCIDestructive(isCIEnvironment, hasDestructiveFindings2, isForceEnabled) {
3019
+ return isCIEnvironment && hasDestructiveFindings2 && !isForceEnabled;
3020
+ }
3018
3021
 
3019
3022
  // src/utils/output.ts
3020
3023
  var import_boxen = __toESM(require("boxen"));
@@ -3454,7 +3457,21 @@ async function runImport(inputPath, options = {}) {
3454
3457
 
3455
3458
  // src/commands/init.ts
3456
3459
  var import_commander5 = require("commander");
3457
- async function runInit() {
3460
+ var import_path11 = __toESM(require("path"));
3461
+ var ALLOWED_PROVIDERS = ["postgres", "supabase"];
3462
+ function resolveInitProvider(provider) {
3463
+ if (!provider) {
3464
+ return "postgres";
3465
+ }
3466
+ const normalized = provider.trim().toLowerCase();
3467
+ if (ALLOWED_PROVIDERS.includes(normalized)) {
3468
+ return normalized;
3469
+ }
3470
+ throw new Error(
3471
+ `Invalid provider "${provider}". Allowed values: ${ALLOWED_PROVIDERS.join(", ")}.`
3472
+ );
3473
+ }
3474
+ async function runInit(options) {
3458
3475
  const root = getProjectRoot();
3459
3476
  const schemaForgeDir = getSchemaForgeDir(root);
3460
3477
  if (await fileExists(schemaForgeDir)) {
@@ -3472,6 +3489,7 @@ async function runInit() {
3472
3489
  if (await fileExists(statePath)) {
3473
3490
  throw new Error(`${statePath} already exists`);
3474
3491
  }
3492
+ const provider = resolveInitProvider(options?.provider);
3475
3493
  info("Initializing schema project...");
3476
3494
  await ensureDir(schemaForgeDir);
3477
3495
  const schemaContent = `# SchemaForge schema definition
@@ -3484,9 +3502,26 @@ table users {
3484
3502
  `;
3485
3503
  await writeTextFile(schemaFilePath, schemaContent);
3486
3504
  success(`Created ${schemaFilePath}`);
3505
+ let outputDir;
3506
+ if (provider === "supabase") {
3507
+ const supabaseDir = import_path11.default.join(root, "supabase");
3508
+ const migrationsDir = import_path11.default.join(root, "supabase", "migrations");
3509
+ if (!await fileExists(supabaseDir)) {
3510
+ await ensureDir(migrationsDir);
3511
+ success(`Created supabase/migrations`);
3512
+ } else {
3513
+ await ensureDir(migrationsDir);
3514
+ success(`Using existing supabase/; migrations at supabase/migrations`);
3515
+ }
3516
+ outputDir = "supabase/migrations";
3517
+ } else {
3518
+ outputDir = "migrations";
3519
+ await ensureDir(import_path11.default.join(root, outputDir));
3520
+ success(`Created ${outputDir}`);
3521
+ }
3487
3522
  const config = {
3488
- provider: "postgres",
3489
- outputDir: "migrations",
3523
+ provider,
3524
+ outputDir,
3490
3525
  schemaFile: "schemaforge/schema.sf",
3491
3526
  stateFile: "schemaforge/state.json",
3492
3527
  sql: {
@@ -3502,9 +3537,6 @@ table users {
3502
3537
  };
3503
3538
  await writeJsonFile(statePath, state);
3504
3539
  success(`Created ${statePath}`);
3505
- const outputDir = "migrations";
3506
- await ensureDir(outputDir);
3507
- success(`Created ${outputDir}`);
3508
3540
  success("Project initialized successfully");
3509
3541
  info("Next steps:");
3510
3542
  info(" 1. Edit schemaforge/schema.sf to define your schema");
@@ -3514,9 +3546,9 @@ table users {
3514
3546
 
3515
3547
  // src/commands/introspect.ts
3516
3548
  var import_commander6 = require("commander");
3517
- var import_path11 = __toESM(require("path"));
3549
+ var import_path12 = __toESM(require("path"));
3518
3550
  function resolveOutputPath(root, outputPath) {
3519
- return import_path11.default.isAbsolute(outputPath) ? outputPath : import_path11.default.join(root, outputPath);
3551
+ return import_path12.default.isAbsolute(outputPath) ? outputPath : import_path12.default.join(root, outputPath);
3520
3552
  }
3521
3553
  async function runIntrospect(options = {}) {
3522
3554
  const connectionString = resolvePostgresConnectionString({ url: options.url });
@@ -3543,9 +3575,9 @@ async function runIntrospect(options = {}) {
3543
3575
 
3544
3576
  // src/commands/validate.ts
3545
3577
  var import_commander7 = require("commander");
3546
- var import_path12 = __toESM(require("path"));
3578
+ var import_path13 = __toESM(require("path"));
3547
3579
  function resolveConfigPath5(root, targetPath) {
3548
- return import_path12.default.isAbsolute(targetPath) ? targetPath : import_path12.default.join(root, targetPath);
3580
+ return import_path13.default.isAbsolute(targetPath) ? targetPath : import_path13.default.join(root, targetPath);
3549
3581
  }
3550
3582
  async function runValidate(options = {}) {
3551
3583
  const root = getProjectRoot();
@@ -3619,7 +3651,7 @@ async function runValidate(options = {}) {
3619
3651
  }
3620
3652
  const findings = await validateSchemaChanges2(previousState, schema);
3621
3653
  const report = await toValidationReport2(findings);
3622
- if (isCI() && hasDestructiveFindings(findings)) {
3654
+ if (shouldFailCIDestructive(isCI(), hasDestructiveFindings(findings), Boolean(options.force))) {
3623
3655
  process.exitCode = EXIT_CODES.CI_DESTRUCTIVE;
3624
3656
  } else {
3625
3657
  process.exitCode = report.hasErrors ? EXIT_CODES.VALIDATION_ERROR : EXIT_CODES.SUCCESS;
@@ -3710,9 +3742,12 @@ async function handleError(error2) {
3710
3742
  }
3711
3743
  process.exitCode = EXIT_CODES.VALIDATION_ERROR;
3712
3744
  }
3713
- program.command("init").description("Initialize a new schema project").action(async () => {
3745
+ program.command("init").description(
3746
+ "Initialize a new schema project. Optional provider: postgres (default) or supabase. Supabase uses supabase/migrations for output."
3747
+ ).argument("[provider]", "Database provider: postgres or supabase").option("--provider <provider>", "Database provider: postgres or supabase (overrides argument)").action(async (providerArg, options) => {
3714
3748
  try {
3715
- await runInit();
3749
+ const provider = options.provider ?? providerArg;
3750
+ await runInit({ provider });
3716
3751
  } catch (error2) {
3717
3752
  await handleError(error2);
3718
3753
  }
@@ -3756,9 +3791,11 @@ program.command("import").description("Import schema from SQL migrations").argum
3756
3791
  await handleError(error2);
3757
3792
  }
3758
3793
  });
3759
- program.command("validate").description("Detect destructive or risky schema changes. In CI environments (CI=true), exits with code 3 if destructive operations are detected.").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) => {
3794
+ 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) => {
3760
3795
  try {
3761
- await runValidate(options);
3796
+ const globalOptions = program.opts();
3797
+ validateFlagExclusivity(globalOptions);
3798
+ await runValidate({ ...options, ...globalOptions });
3762
3799
  } catch (error2) {
3763
3800
  await handleError(error2);
3764
3801
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xubylele/schema-forge",
3
- "version": "1.8.1",
3
+ "version": "1.10.0",
4
4
  "description": "Universal migration generator from schema DSL",
5
5
  "main": "dist/cli.js",
6
6
  "type": "commonjs",