@stacksjs/buddy 0.75.12 → 0.75.13

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.
@@ -15,5 +15,14 @@ import type { CLI } from '@stacksjs/types';
15
15
  * and only then discover it has nothing to rebuild them with.
16
16
  */
17
17
  export declare function validateMigrationDialect(cwd?: unknown, options?: { driver?: string }): { valid: boolean, error?: string };
18
+ /**
19
+ * Whether this run is a preview. `--diff` is the migrate flag, but buddy also
20
+ * registers `--dry-run` for every command ("Preview actions without making
21
+ * changes"), and `migrate` used to ignore it: `buddy migrate --dry-run` ran a
22
+ * full migrate, generating files, advancing the model snapshot and applying
23
+ * everything. A flag promising to change nothing has to change nothing.
24
+ * `--pretend` is Laravel's name for the same thing.
25
+ */
26
+ export declare function isMigratePreview(options: { diff?: boolean, dryRun?: boolean, pretend?: boolean }, argv?: readonly string[]): boolean;
18
27
  export declare function migrate(buddy: CLI): void;
19
28
  declare type FreshGuard = 'allow' | 'confirm' | 'disabled';
@@ -6,17 +6,17 @@ The model-backed create migrations may be stale for this database - run \`./budd
6
6
  `),more=result.orphans.length>5?`
7
7
  + ${result.orphans.length-5} more - run \`./buddy doctor\` for the full list.`:"",first=result.orphans[0];log.warn(`${result.total} row${result.total===1?"":"s"} violate foreign keys (orphaned parents), now that SQLite enforces \`foreign_keys = ON\`:
8
8
  ${sample}${more}
9
- These were written under the old \`foreign_keys = OFF\` default (#1951). Review and clean up manually - e.g. DELETE FROM ${first.table} WHERE ${first.column} IS NOT NULL AND ${first.column} NOT IN (SELECT id FROM ${first.parent}). migrate never deletes data.`)}catch(err){log.debug(`[migrate] FK orphan scan skipped: ${err instanceof Error?err.message:String(err)}`)}}function describeOp(op){switch(op.kind){case"drop_table":return`drop table "${op.table}" (all rows lost)`;case"drop_column":return`drop column "${op.table}"."${op.column}" (column data lost)`;case"modify_column":return`change type of "${op.table}"."${op.column}" (possible data loss)`;case"rebuild_table":return`rebuild table "${op.table}" (SQLite rebuilds in place; compare the SQL below against the live schema)`;case"rename_column":return`rename "${op.table}"."${op.from}" \u2192 "${op.to}"`;case"rename_table":return`rename table "${op.from}" \u2192 "${op.to}"`;default:return`${op.kind} on "${op.table}"${op.column?`."${op.column}"`:""}`}}async function missingBaselineNote(){try{const{resolveSnapshotDirectory,snapshotDirectoryIsShared}=await import("@stacksjs/database"),dir=resolveSnapshotDirectory();if(existsSync(dir)&&readdirSync(dir).some((f)=>/^model-snapshot\.\w+\.json$/.test(f)))return;return`No model snapshot in ${dir}, so this change was derived without a baseline and will be proposed again on the next run.${snapshotDirectoryIsShared()?" DB_SNAPSHOT_PATH points there; check the directory exists and is writable.":" If each deploy runs from a new directory, point DB_SNAPSHOT_PATH at a shared one that survives the release."}`}catch(error){log.debug(`[migrate] baseline check skipped: ${error instanceof Error?error.message:String(error)}`);return}}async function confirmDestructiveMigrations(opts){let operations=[];try{const{previewPendingMigrations}=await import("@stacksjs/database");operations=await previewPendingMigrations({fromDb:opts.fromDb,applyRenames:opts.applyRenames})}catch(error){log.debug(`Migration preview unavailable: ${error instanceof Error?error.message:String(error)}`);return!0}const renames=operations.filter((o)=>o.kind==="rename_column"||o.kind==="rename_table");for(const r of renames)log.info(`Detected ${describeOp(r)} - applying as a rename (data preserved). Use --no-rename to drop + add instead.`);const destructive=operations.filter((o)=>o.destructive);if(destructive.length===0)return!0;log.warn(`This migration includes ${destructive.length} potentially destructive change${destructive.length===1?"":"s"}:`);for(const op of destructive)log.warn(` \u2022 ${describeOp(op)}`);const baseline=await missingBaselineNote();if(baseline)log.warn(baseline);if(opts.force)return!0;if(isCI||!hasTTY){log.error("Refusing to apply destructive changes in a non-interactive environment. Re-run with --force to proceed.");return!1}return confirm({message:"Apply these destructive changes?",initial:!1})}function parseGuardBool(raw){if(raw==null||raw==="")return;const v=raw.toLowerCase().trim();if(v==="1"||v==="true"||v==="yes"||v==="on")return!0;if(v==="0"||v==="false"||v==="no"||v==="off")return!1;return}function parseFreshGuard(raw){const v=raw?.toLowerCase().trim();return v==="allow"||v==="confirm"||v==="disabled"?v:void 0}async function resolveMigrationGuards(){const isProd=/^prod/i.test(process.env.APP_ENV||"local");let cfg={};try{const{awaitConfig}=await import("@stacksjs/config");cfg=(await awaitConfig()).database?.safety??{}}catch(error){log.debug(`[migrate] safety config unavailable, using defaults: ${error instanceof Error?error.message:String(error)}`)}const confirmMigrate=parseGuardBool(process.env.DB_MIGRATE_CONFIRM)??cfg.confirmMigrate??!0,migrateFresh=parseFreshGuard(process.env.DB_MIGRATE_FRESH)??cfg.migrateFresh??(isProd?"disabled":"allow");return{confirmMigrate,migrateFresh}}function currentDatabaseLabel(){if((process.env.DB_CONNECTION||"sqlite").toLowerCase()==="sqlite")return process.env.DB_DATABASE_PATH||"database/stacks.sqlite";return process.env.DB_DATABASE||"stacks"}export function migrate(buddy){const descriptions={migrate:"Migrates your database",fresh:"Drop all tables and re-run every migration (destroys all data)",project:"Target a specific project",verbose:"Enable verbose output",auth:"Also migrate auth tables (oauth_clients, oauth_access_tokens, oauth_refresh_tokens, password_resets)",force:"Apply destructive changes (drop column/table, lossy type change) without confirmation",fromDb:"Diff against the live database schema instead of the snapshot (self-heal drift)",noRename:"Treat renamed columns as drop + add instead of a data-preserving rename",noGenerate:"Apply committed migration files only; do not generate new ones from your models"};buddy.command("migrate",descriptions.migrate).alias("db:migrate").option("-d, --diff","Show the SQL that would be run",{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("-a, --auth",descriptions.auth,{default:!0}).option("--no-auth","Skip auth/oauth table migrations").option("-f, --force",descriptions.force,{default:!1}).option("--create-database","Create the database if it does not exist, without asking",{default:!1}).option("--from-db",descriptions.fromDb,{default:!1}).option("--no-rename",descriptions.noRename).option("--no-generate",descriptions.noGenerate).option("--strict","Fail when the schema has drifted from the models or a migration is git-ignored (for CI)",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy migrate` ...",options);const perf=await intro("buddy migrate"),validation=validateModelsExist();if(!validation.valid){console.error(`
9
+ These were written under the old \`foreign_keys = OFF\` default (#1951). Review and clean up manually - e.g. DELETE FROM ${first.table} WHERE ${first.column} IS NOT NULL AND ${first.column} NOT IN (SELECT id FROM ${first.parent}). migrate never deletes data.`)}catch(err){log.debug(`[migrate] FK orphan scan skipped: ${err instanceof Error?err.message:String(err)}`)}}function describeOp(op){switch(op.kind){case"drop_table":return`drop table "${op.table}" (all rows lost)`;case"drop_column":return`drop column "${op.table}"."${op.column}" (column data lost)`;case"modify_column":return`change type of "${op.table}"."${op.column}" (possible data loss)`;case"rebuild_table":return`rebuild table "${op.table}" (SQLite rebuilds in place; compare the SQL below against the live schema)`;case"rename_column":return`rename "${op.table}"."${op.from}" \u2192 "${op.to}"`;case"rename_table":return`rename table "${op.from}" \u2192 "${op.to}"`;default:return`${op.kind} on "${op.table}"${op.column?`."${op.column}"`:""}`}}async function missingBaselineNote(){try{const{resolveSnapshotDirectory,snapshotDirectoryIsShared}=await import("@stacksjs/database"),dir=resolveSnapshotDirectory();if(existsSync(dir)&&readdirSync(dir).some((f)=>/^model-snapshot\.\w+\.json$/.test(f)))return;return`No model snapshot in ${dir}, so this change was derived without a baseline and will be proposed again on the next run.${snapshotDirectoryIsShared()?" DB_SNAPSHOT_PATH points there; check the directory exists and is writable.":" If each deploy runs from a new directory, point DB_SNAPSHOT_PATH at a shared one that survives the release."}`}catch(error){log.debug(`[migrate] baseline check skipped: ${error instanceof Error?error.message:String(error)}`);return}}async function confirmDestructiveMigrations(opts){let operations=[];try{const{previewPendingMigrations}=await import("@stacksjs/database");operations=await previewPendingMigrations({fromDb:opts.fromDb,applyRenames:opts.applyRenames})}catch(error){log.debug(`Migration preview unavailable: ${error instanceof Error?error.message:String(error)}`);return!0}const renames=operations.filter((o)=>o.kind==="rename_column"||o.kind==="rename_table");for(const r of renames)log.info(`Detected ${describeOp(r)} - applying as a rename (data preserved). Use --no-rename to drop + add instead.`);const destructive=operations.filter((o)=>o.destructive);if(destructive.length===0)return!0;log.warn(`This migration includes ${destructive.length} potentially destructive change${destructive.length===1?"":"s"}:`);for(const op of destructive)log.warn(` \u2022 ${describeOp(op)}`);const baseline=await missingBaselineNote();if(baseline)log.warn(baseline);if(opts.force)return!0;if(isCI||!hasTTY){log.error("Refusing to apply destructive changes in a non-interactive environment. Re-run with --force to proceed.");return!1}return confirm({message:"Apply these destructive changes?",initial:!1})}export function isMigratePreview(options,argv=process.argv){return options.diff===!0||options.dryRun===!0||options.pretend===!0||argv.includes("--dry-run")||argv.includes("--pretend")}function parseGuardBool(raw){if(raw==null||raw==="")return;const v=raw.toLowerCase().trim();if(v==="1"||v==="true"||v==="yes"||v==="on")return!0;if(v==="0"||v==="false"||v==="no"||v==="off")return!1;return}function parseFreshGuard(raw){const v=raw?.toLowerCase().trim();return v==="allow"||v==="confirm"||v==="disabled"?v:void 0}async function resolveMigrationGuards(){const isProd=/^prod/i.test(process.env.APP_ENV||"local");let cfg={};try{const{awaitConfig}=await import("@stacksjs/config");cfg=(await awaitConfig()).database?.safety??{}}catch(error){log.debug(`[migrate] safety config unavailable, using defaults: ${error instanceof Error?error.message:String(error)}`)}const confirmMigrate=parseGuardBool(process.env.DB_MIGRATE_CONFIRM)??cfg.confirmMigrate??!0,migrateFresh=parseFreshGuard(process.env.DB_MIGRATE_FRESH)??cfg.migrateFresh??(isProd?"disabled":"allow");return{confirmMigrate,migrateFresh}}function currentDatabaseLabel(){if((process.env.DB_CONNECTION||"sqlite").toLowerCase()==="sqlite")return process.env.DB_DATABASE_PATH||"database/stacks.sqlite";return process.env.DB_DATABASE||"stacks"}export function migrate(buddy){const descriptions={migrate:"Migrates your database",fresh:"Drop all tables and re-run every migration (destroys all data)",project:"Target a specific project",verbose:"Enable verbose output",auth:"Also migrate auth tables (oauth_clients, oauth_access_tokens, oauth_refresh_tokens, password_resets)",force:"Apply destructive changes (drop column/table, lossy type change) without confirmation",fromDb:"Diff against the live database schema instead of the snapshot (self-heal drift)",noRename:"Treat renamed columns as drop + add instead of a data-preserving rename",noGenerate:"Apply committed migration files only; do not generate new ones from your models"};buddy.command("migrate",descriptions.migrate).alias("db:migrate").option("-d, --diff","Show the SQL that would be run",{default:!1}).option("--pretend","Same as --diff (and --dry-run): preview, change nothing",{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("-a, --auth",descriptions.auth,{default:!0}).option("--no-auth","Skip auth/oauth table migrations").option("-f, --force",descriptions.force,{default:!1}).option("--create-database","Create the database if it does not exist, without asking",{default:!1}).option("--from-db",descriptions.fromDb,{default:!1}).option("--no-rename",descriptions.noRename).option("--no-generate",descriptions.noGenerate).option("--strict","Fail when the schema has drifted from the models or a migration is git-ignored (for CI)",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy migrate` ...",options);const perf=await intro("buddy migrate"),validation=validateModelsExist();if(!validation.valid){console.error(`
10
10
  \u274C Error: ${validation.error}
11
11
  `);process.exit(ExitCode.FatalError)}const dialectCheck=validateMigrationDialect();if(!dialectCheck.valid){console.error(`
12
12
  \u274C Error: ${dialectCheck.error}
13
- `);process.exit(ExitCode.FatalError)}const applyRenames=options.rename===!1?!1:void 0;if(options.fromDb)process.env.STACKS_MIGRATE_FROM_DB="1";if(applyRenames===!1)process.env.STACKS_MIGRATE_NO_RENAME="1";if(options.generate===!1)process.env.STACKS_MIGRATE_NO_GENERATE="1";if(options.diff){try{const{previewPendingMigrations}=await import("@stacksjs/database"),ops=await previewPendingMigrations({fromDb:options.fromDb,applyRenames});if(ops.length===0)log.info("No pending schema changes - your models match the database.");else{log.info(`${ops.length} pending change${ops.length===1?"":"s"}:`);for(const op of ops)log.info(` \u2022 ${describeOp(op)}${op.destructive?" [destructive]":""}`)}}catch(error){await log.error("Failed to preview migrations:",error)}await outro("Diff complete - no changes applied.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}await preflightDatabase({createDatabase:options.createDatabase,command:"migrate"});if((await resolveMigrationGuards()).confirmMigrate&&!options.force)if(isCI||!hasTTY)log.debug("[migrate] confirmMigrate guard skipped - non-interactive environment.");else{const APP_ENV=process.env.APP_ENV||"local";await log.flush();if(!await confirm({message:`Run migrations against the ${APP_ENV} database "${currentDatabaseLabel()}"?`,initial:!0})){await outro("Migration cancelled - no changes applied.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}}await ensureDatabaseOrExit();const lock=acquireMigrationLock();if(!lock.acquired){log.syncError("Another migration is already running (storage/framework/runtime/migrations.lock exists). Wait for it to finish, or remove the lockfile if it is stale.");process.exit(ExitCode.FatalError)}try{const{catchUpFrameworkRenames}=await import("@stacksjs/database");await catchUpFrameworkRenames()}catch(error){lock.release();log.syncError(error instanceof Error?error.message:String(error));process.exit(ExitCode.FatalError)}if(options.generate!==!1){if(!await confirmDestructiveMigrations({force:options.force,fromDb:options.fromDb,applyRenames})){lock.release();await outro("Migration cancelled - no changes applied.",{startTime:perf,useSeconds:!0});process.exit(isCI||!hasTTY?ExitCode.FatalError:ExitCode.Success)}}if(options.auth!==!1){log.debug("Migrating auth tables...");try{const{migrateAuthTables}=await import("@stacksjs/database"),authResult=await migrateAuthTables({verbose:options.verbose});if(!authResult.success)log.error(`Failed to migrate auth tables: ${authResult.error}`)}catch(error){log.error("Failed to migrate auth tables:",error)}}const result=await runAction(Action.Migrate,options).finally(()=>lock.release());if(resultFailed(result))log.error("Model migrations failed - applying notification/RBAC table guarantees before exiting.");if(options.auth!==!1)try{const{ensureUtcDatetimeColumns,ensureUtcTimestampDefaults,migrateNotificationTables,migrateRbacTables,migrateTraitTables}=await import("@stacksjs/database"),notifResult=await migrateNotificationTables({verbose:options.verbose});if(!notifResult.success)log.error(`Failed to migrate notification tables: ${notifResult.error}`);const rbacResult=await migrateRbacTables({verbose:options.verbose});if(!rbacResult.success)log.error(`Failed to migrate RBAC tables: ${rbacResult.error}`);const traitResult=await migrateTraitTables({verbose:options.verbose});if(!traitResult.success)log.error(`Failed to migrate polymorphic trait tables: ${traitResult.error}`);const datetimeResult=await ensureUtcDatetimeColumns({verbose:options.verbose});if(!datetimeResult.success)log.error(`Failed to convert TIMESTAMP columns to DATETIME: ${datetimeResult.error}`);const defaultsResult=await ensureUtcTimestampDefaults({verbose:options.verbose});if(!defaultsResult.success)log.error(`Failed to pin timestamp defaults to UTC: ${defaultsResult.error}`)}catch(error){log.error("Failed to migrate notification/RBAC/trait tables:",error)}if(resultFailed(result)){await outro("While running the migrate command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}if(options.auth!==!1)try{const{ensureUsersAuthColumns,sqlHelpers}=await import("@stacksjs/database"),driver=process.env.DB_CONNECTION||"sqlite";await ensureUsersAuthColumns(sqlHelpers(driver),{verbose:options.verbose})}catch(error){log.error("Failed to ensure users auth columns post-migration:",error)}try{const{ensureUuidColumns,sqlHelpers}=await import("@stacksjs/database"),driver=process.env.DB_CONNECTION||"sqlite";await ensureUuidColumns(sqlHelpers(driver),{verbose:options.verbose})}catch(error){log.error("Failed to ensure uuid columns post-migration:",error)}await reportMissingForeignKeys();const schemaMatches=await reportSchemaDrift(),allCommittable=await reportIgnoredMigrations();if(strictMode(options)&&(!schemaMatches||!allCommittable)){console.error(`
13
+ `);process.exit(ExitCode.FatalError)}const applyRenames=options.rename===!1?!1:void 0;if(options.fromDb)process.env.STACKS_MIGRATE_FROM_DB="1";if(applyRenames===!1)process.env.STACKS_MIGRATE_NO_RENAME="1";if(options.generate===!1)process.env.STACKS_MIGRATE_NO_GENERATE="1";if(isMigratePreview(options)){try{const{previewPendingMigrations}=await import("@stacksjs/database"),ops=await previewPendingMigrations({fromDb:options.fromDb,applyRenames});if(ops.length===0)log.info("No pending schema changes - your models match the database.");else{log.info(`${ops.length} pending change${ops.length===1?"":"s"}:`);for(const op of ops)log.info(` \u2022 ${describeOp(op)}${op.destructive?" [destructive]":""}`)}}catch(error){await log.error("Failed to preview migrations:",error)}await outro("Diff complete - no changes applied.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}await preflightDatabase({createDatabase:options.createDatabase,command:"migrate"});if((await resolveMigrationGuards()).confirmMigrate&&!options.force)if(isCI||!hasTTY)log.debug("[migrate] confirmMigrate guard skipped - non-interactive environment.");else{const APP_ENV=process.env.APP_ENV||"local";await log.flush();if(!await confirm({message:`Run migrations against the ${APP_ENV} database "${currentDatabaseLabel()}"?`,initial:!0})){await outro("Migration cancelled - no changes applied.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}}await ensureDatabaseOrExit();const lock=acquireMigrationLock();if(!lock.acquired){log.syncError("Another migration is already running (storage/framework/runtime/migrations.lock exists). Wait for it to finish, or remove the lockfile if it is stale.");process.exit(ExitCode.FatalError)}try{const{catchUpFrameworkRenames}=await import("@stacksjs/database");await catchUpFrameworkRenames()}catch(error){lock.release();log.syncError(error instanceof Error?error.message:String(error));process.exit(ExitCode.FatalError)}if(options.generate!==!1){if(!await confirmDestructiveMigrations({force:options.force,fromDb:options.fromDb,applyRenames})){lock.release();await outro("Migration cancelled - no changes applied.",{startTime:perf,useSeconds:!0});process.exit(isCI||!hasTTY?ExitCode.FatalError:ExitCode.Success)}}if(options.auth!==!1){log.debug("Migrating auth tables...");try{const{migrateAuthTables}=await import("@stacksjs/database"),authResult=await migrateAuthTables({verbose:options.verbose});if(!authResult.success)log.error(`Failed to migrate auth tables: ${authResult.error}`)}catch(error){log.error("Failed to migrate auth tables:",error)}}const result=await runAction(Action.Migrate,options).finally(()=>lock.release());if(resultFailed(result))log.error("Model migrations failed - applying notification/RBAC table guarantees before exiting.");if(options.auth!==!1)try{const{ensureUtcDatetimeColumns,ensureUtcTimestampDefaults,migrateNotificationTables,migrateRbacTables,migrateTraitTables}=await import("@stacksjs/database"),notifResult=await migrateNotificationTables({verbose:options.verbose});if(!notifResult.success)log.error(`Failed to migrate notification tables: ${notifResult.error}`);const rbacResult=await migrateRbacTables({verbose:options.verbose});if(!rbacResult.success)log.error(`Failed to migrate RBAC tables: ${rbacResult.error}`);const traitResult=await migrateTraitTables({verbose:options.verbose});if(!traitResult.success)log.error(`Failed to migrate polymorphic trait tables: ${traitResult.error}`);const datetimeResult=await ensureUtcDatetimeColumns({verbose:options.verbose});if(!datetimeResult.success)log.error(`Failed to convert TIMESTAMP columns to DATETIME: ${datetimeResult.error}`);const defaultsResult=await ensureUtcTimestampDefaults({verbose:options.verbose});if(!defaultsResult.success)log.error(`Failed to pin timestamp defaults to UTC: ${defaultsResult.error}`)}catch(error){log.error("Failed to migrate notification/RBAC/trait tables:",error)}if(resultFailed(result)){await outro("While running the migrate command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}if(options.auth!==!1)try{const{ensureUsersAuthColumns,sqlHelpers}=await import("@stacksjs/database"),driver=process.env.DB_CONNECTION||"sqlite";await ensureUsersAuthColumns(sqlHelpers(driver),{verbose:options.verbose})}catch(error){log.error("Failed to ensure users auth columns post-migration:",error)}try{const{ensureUuidColumns,sqlHelpers}=await import("@stacksjs/database"),driver=process.env.DB_CONNECTION||"sqlite";await ensureUuidColumns(sqlHelpers(driver),{verbose:options.verbose})}catch(error){log.error("Failed to ensure uuid columns post-migration:",error)}await reportMissingForeignKeys();const schemaMatches=await reportSchemaDrift(),allCommittable=await reportIgnoredMigrations();if(strictMode(options)&&(!schemaMatches||!allCommittable)){console.error(`
14
14
  \u274C --strict: ${!allCommittable?"a migration is git-ignored":"the live schema does not match the models"} (see above).
15
- `);process.exit(ExitCode.FatalError)}await reportFkOrphans();const APP_ENV=process.env.APP_ENV||"local",marker=readMigrateMarker(),authSuffix=options.auth!==!1?" (including auth tables)":"",outroMessage=marker==null?`Migrated your ${APP_ENV} database.${authSuffix}`:marker.appliedCount===0?`Nothing to migrate - your ${APP_ENV} database is already up to date.${authSuffix}`:`Applied ${marker.appliedCount} migration${marker.appliedCount===1?"":"s"} to your ${APP_ENV} database.${authSuffix}`;await outro(outroMessage,{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:fresh",descriptions.fresh).alias("db:fresh").option("-d, --diff","Show the SQL that would be run",{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("-s, --seed","Run database seeders after migration",{default:!1}).option("-a, --auth",descriptions.auth,{default:!0}).option("--no-auth","Skip auth/oauth table migrations").option("--create-database","Create the database if it does not exist, without asking",{default:!1}).option("-f, --force",'Skip the drop-database confirmation (only honored when the migrateFresh guard is "allow")',{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy migrate:fresh` ...",options);const perf=await intro("buddy migrate:fresh"),validation=validateModelsExist();if(!validation.valid){console.error(`
15
+ `);process.exit(ExitCode.FatalError)}await reportFkOrphans();const APP_ENV=process.env.APP_ENV||"local",marker=readMigrateMarker(),authSuffix=options.auth!==!1?" (including auth tables)":"",outroMessage=marker==null?`Migrated your ${APP_ENV} database.${authSuffix}`:marker.appliedCount===0?`Nothing to migrate - your ${APP_ENV} database is already up to date.${authSuffix}`:`Applied ${marker.appliedCount} migration${marker.appliedCount===1?"":"s"} to your ${APP_ENV} database.${authSuffix}`;await outro(outroMessage,{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:fresh",descriptions.fresh).alias("db:fresh").option("-d, --diff","Show what would be dropped and replayed, and change nothing",{default:!1}).option("--pretend","Same as --diff (and --dry-run): preview, change nothing",{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("-s, --seed","Run database seeders after migration",{default:!1}).option("-a, --auth",descriptions.auth,{default:!0}).option("--no-auth","Skip auth/oauth table migrations").option("--create-database","Create the database if it does not exist, without asking",{default:!1}).option("-f, --force",'Skip the drop-database confirmation (only honored when the migrateFresh guard is "allow")',{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy migrate:fresh` ...",options);const perf=await intro("buddy migrate:fresh"),validation=validateModelsExist();if(!validation.valid){console.error(`
16
16
  \u274C Error: ${validation.error}
17
17
  `);process.exit(ExitCode.FatalError)}const dialectCheck=validateMigrationDialect();if(!dialectCheck.valid){console.error(`
18
18
  \u274C Error: ${dialectCheck.error}
19
- `);process.exit(ExitCode.FatalError)}const guards=await resolveMigrationGuards(),dbLabel=currentDatabaseLabel(),APP_ENV=process.env.APP_ENV||"local";if(guards.migrateFresh==="disabled"){await log.error(`\`buddy migrate:fresh\` is disabled by your migration safety guards (it DROPS every table).
19
+ `);process.exit(ExitCode.FatalError)}if(isMigratePreview(options)){let files=0;try{const dir=resolveMigrationDirectory(String(process.env.DB_CONNECTION||"sqlite").toLowerCase());files=readdirSync(dir).filter((file)=>file.endsWith(".sql")).length}catch{}log.info(`Would drop every table in the ${process.env.APP_ENV||"local"} database "${currentDatabaseLabel()}" and replay ${files} migration file${files===1?"":"s"}${options.seed?", then seed it":""}.`);await outro("Dry run - nothing was dropped or migrated.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}const guards=await resolveMigrationGuards(),dbLabel=currentDatabaseLabel(),APP_ENV=process.env.APP_ENV||"local";if(guards.migrateFresh==="disabled"){await log.error(`\`buddy migrate:fresh\` is disabled by your migration safety guards (it DROPS every table).
20
20
  Target: ${APP_ENV} database "${dbLabel}"
21
21
  To allow it, set database.safety.migrateFresh to 'allow' in config/database.ts,
22
22
  or run once with: DB_MIGRATE_FRESH=allow ./buddy migrate:fresh`);await outro('migrate:fresh refused - the migrateFresh guard is set to "disabled".',{startTime:perf,useSeconds:!0,type:"error"});process.exit(ExitCode.FatalError)}if(!(guards.migrateFresh==="allow"&&options.force===!0)){if(isCI||!hasTTY){const hint=guards.migrateFresh==="confirm"?'Guard is "confirm": migrate:fresh must be run interactively.':"Re-run with --force to drop the database non-interactively.";await log.error(`Refusing to drop the ${APP_ENV} database "${dbLabel}" in a non-interactive environment. ${hint}`);await outro("migrate:fresh cancelled.",{startTime:perf,useSeconds:!0,type:"warning"});process.exit(ExitCode.FatalError)}log.warn(`This will DROP ALL TABLES in the ${APP_ENV} database "${dbLabel}" and rebuild them from scratch. All data will be lost.`);await log.flush();if((await text({message:`Type the database name "${dbLabel}" to confirm (blank to cancel):`})).trim()!==dbLabel){await outro("migrate:fresh cancelled - confirmation did not match.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}}await preflightDatabase({createDatabase:options.createDatabase,command:"migrate:fresh"});await ensureDatabaseOrExit();const result=await runAction(Action.MigrateFresh,options);if(resultFailed(result))log.error("Model migrations failed - applying auth/notification/RBAC table guarantees before exiting.");if(options.auth!==!1){log.debug("Migrating auth tables...");try{const{migrateAuthTables,migrateNotificationTables,migrateRbacTables,migrateTraitTables}=await import("@stacksjs/database"),authResult=await migrateAuthTables({verbose:options.verbose});if(!authResult.success)log.error(`Failed to migrate auth tables: ${authResult.error}`);const notifResult=await migrateNotificationTables({verbose:options.verbose});if(!notifResult.success)log.error(`Failed to migrate notification tables: ${notifResult.error}`);const rbacResult=await migrateRbacTables({verbose:options.verbose});if(!rbacResult.success)log.error(`Failed to migrate RBAC tables: ${rbacResult.error}`);const traitResult=await migrateTraitTables({verbose:options.verbose});if(!traitResult.success)log.error(`Failed to migrate polymorphic trait tables: ${traitResult.error}`)}catch(error){log.error("Failed to migrate auth/notification/RBAC/trait tables:",error)}}try{const{ensureUuidColumns,sqlHelpers}=await import("@stacksjs/database"),driver=process.env.DB_CONNECTION||"sqlite";await ensureUuidColumns(sqlHelpers(driver),{verbose:options.verbose})}catch(error){log.error("Failed to ensure uuid columns post-migration:",error)}if(resultFailed(result)){await outro("While running the migrate:fresh command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await reportMissingForeignKeys();await reportSchemaDrift();if(options.seed){log.info("Running database seeders...");try{const{runApplicationSeeders,seed}=await import("@stacksjs/database"),seedResult=await seed({verbose:options.verbose,fresh:!0}),applicationSeedResult=await runApplicationSeeders({verbose:options.verbose}),failures=seedResult.failed+applicationSeedResult.failed;if(failures>0){log.warn(`Seeding completed with ${failures} failure(s)`);for(const r of seedResult.results)if(!r.success)log.error(` - ${r.model}: ${r.error}`);for(const r of applicationSeedResult.results)if(!r.success)log.error(` - ${r.seeder}: ${r.error}`)}else log.success(`Seeded ${seedResult.successful} model(s) and ran ${applicationSeedResult.successful} application seeder(s)`)}catch(error){log.error("Failed to run seeders:",error)}}const parts=[];if(options.auth!==!1)parts.push("auth tables");if(options.seed)parts.push("seeded");const suffix=parts.length>0?` & ${parts.join(" & ")}`:"",marker=readMigrateMarker(),countPhrase=marker==null?"":marker.appliedCount===0?" (0 applied - no migration files found?)":` (${marker.appliedCount} migration${marker.appliedCount===1?"":"s"} applied)`;await outro(`All tables dropped successfully & migrated successfully${countPhrase}${suffix}`,{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:switch <driver>","Pre-flight check + plan for switching DB_CONNECTION between sqlite / mysql / vitess / postgres").action(async(driver)=>{log.debug(`Running \`buddy migrate:switch ${driver}\` ...`);const perf=await intro("buddy migrate:switch"),target=driver.toLowerCase();if(!new Set(["sqlite","mysql","vitess","postgres"]).has(target)){console.log(`
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/buddy",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.12",
5
+ "version": "0.75.13",
6
6
  "description": "Meet Buddy. The Stacks runtime.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -95,69 +95,69 @@
95
95
  "prepublishOnly": "bun run build"
96
96
  },
97
97
  "dependencies": {
98
- "@stacksjs/actions": "^0.75.12",
99
- "@stacksjs/ai": "^0.75.12",
100
- "@stacksjs/alias": "^0.75.12",
101
- "@stacksjs/analytics": "^0.75.12",
102
- "@stacksjs/api": "^0.75.12",
103
- "@stacksjs/arrays": "^0.75.12",
104
- "@stacksjs/auth": "^0.75.12",
105
- "@stacksjs/browser-extension": "^0.75.12",
106
- "@stacksjs/build": "^0.75.12",
107
- "@stacksjs/cache": "^0.75.12",
108
- "@stacksjs/chat": "^0.75.12",
98
+ "@stacksjs/actions": "^0.75.13",
99
+ "@stacksjs/ai": "^0.75.13",
100
+ "@stacksjs/alias": "^0.75.13",
101
+ "@stacksjs/analytics": "^0.75.13",
102
+ "@stacksjs/api": "^0.75.13",
103
+ "@stacksjs/arrays": "^0.75.13",
104
+ "@stacksjs/auth": "^0.75.13",
105
+ "@stacksjs/browser-extension": "^0.75.13",
106
+ "@stacksjs/build": "^0.75.13",
107
+ "@stacksjs/cache": "^0.75.13",
108
+ "@stacksjs/chat": "^0.75.13",
109
109
  "@stacksjs/clapp": "^0.2.12",
110
- "@stacksjs/cli": "^0.75.12",
111
- "@stacksjs/cloud": "^0.75.12",
112
- "@stacksjs/cms": "^0.75.12",
113
- "@stacksjs/collections": "^0.75.12",
114
- "@stacksjs/config": "^0.75.12",
115
- "@stacksjs/database": "^0.75.12",
116
- "@stacksjs/desktop-build": "^0.75.12",
117
- "@stacksjs/dns": "^0.75.12",
110
+ "@stacksjs/cli": "^0.75.13",
111
+ "@stacksjs/cloud": "^0.75.13",
112
+ "@stacksjs/cms": "^0.75.13",
113
+ "@stacksjs/collections": "^0.75.13",
114
+ "@stacksjs/config": "^0.75.13",
115
+ "@stacksjs/database": "^0.75.13",
116
+ "@stacksjs/desktop-build": "^0.75.13",
117
+ "@stacksjs/dns": "^0.75.13",
118
118
  "@stacksjs/dnsx": "^0.2.3",
119
- "@stacksjs/email": "^0.75.12",
120
- "@stacksjs/enums": "^0.75.12",
121
- "@stacksjs/env": "^0.75.12",
122
- "@stacksjs/error-handling": "^0.75.12",
123
- "@stacksjs/events": "^0.75.12",
124
- "@stacksjs/features": "^0.75.12",
125
- "@stacksjs/git": "^0.75.12",
119
+ "@stacksjs/email": "^0.75.13",
120
+ "@stacksjs/enums": "^0.75.13",
121
+ "@stacksjs/env": "^0.75.13",
122
+ "@stacksjs/error-handling": "^0.75.13",
123
+ "@stacksjs/events": "^0.75.13",
124
+ "@stacksjs/features": "^0.75.13",
125
+ "@stacksjs/git": "^0.75.13",
126
126
  "@stacksjs/gitit": "^0.2.5",
127
- "@stacksjs/health": "^0.75.12",
127
+ "@stacksjs/health": "^0.75.13",
128
128
  "@stacksjs/httx": "^0.1.10",
129
- "@stacksjs/image": "^0.75.12",
130
- "@stacksjs/lint": "^0.75.12",
131
- "@stacksjs/logging": "^0.75.12",
132
- "@stacksjs/notifications": "^0.75.12",
133
- "@stacksjs/objects": "^0.75.12",
134
- "@stacksjs/orm": "^0.75.12",
135
- "@stacksjs/path": "^0.75.12",
136
- "@stacksjs/payments": "^0.75.12",
137
- "@stacksjs/realtime": "^0.75.12",
138
- "@stacksjs/router": "^0.75.12",
129
+ "@stacksjs/image": "^0.75.13",
130
+ "@stacksjs/lint": "^0.75.13",
131
+ "@stacksjs/logging": "^0.75.13",
132
+ "@stacksjs/notifications": "^0.75.13",
133
+ "@stacksjs/objects": "^0.75.13",
134
+ "@stacksjs/orm": "^0.75.13",
135
+ "@stacksjs/path": "^0.75.13",
136
+ "@stacksjs/payments": "^0.75.13",
137
+ "@stacksjs/realtime": "^0.75.13",
138
+ "@stacksjs/router": "^0.75.13",
139
139
  "@stacksjs/rpx": "^0.11.54",
140
- "@stacksjs/scheduler": "^0.75.12",
141
- "@stacksjs/search-engine": "^0.75.12",
142
- "@stacksjs/security": "^0.75.12",
143
- "@stacksjs/server": "^0.75.12",
144
- "@stacksjs/sites": "^0.75.12",
145
- "@stacksjs/skills": "^0.75.12",
146
- "@stacksjs/storage": "^0.75.12",
147
- "@stacksjs/strings": "^0.75.12",
148
- "@stacksjs/stx": "^0.2.319",
149
- "@stacksjs/testing": "^0.75.12",
150
- "@stacksjs/tinker": "^0.75.12",
140
+ "@stacksjs/scheduler": "^0.75.13",
141
+ "@stacksjs/search-engine": "^0.75.13",
142
+ "@stacksjs/security": "^0.75.13",
143
+ "@stacksjs/server": "^0.75.13",
144
+ "@stacksjs/sites": "^0.75.13",
145
+ "@stacksjs/skills": "^0.75.13",
146
+ "@stacksjs/storage": "^0.75.13",
147
+ "@stacksjs/strings": "^0.75.13",
148
+ "@stacksjs/stx": "^0.2.320",
149
+ "@stacksjs/testing": "^0.75.13",
150
+ "@stacksjs/tinker": "^0.75.13",
151
151
  "@stacksjs/tlsx": "^0.13.19",
152
152
  "@stacksjs/ts-cloud": "^0.16.14",
153
- "@stacksjs/tunnel": "^0.75.12",
154
- "@stacksjs/types": "^0.75.12",
155
- "@stacksjs/ui": "^0.75.12",
156
- "@stacksjs/utils": "^0.75.12",
157
- "@stacksjs/validation": "^0.75.12",
153
+ "@stacksjs/tunnel": "^0.75.13",
154
+ "@stacksjs/types": "^0.75.13",
155
+ "@stacksjs/ui": "^0.75.13",
156
+ "@stacksjs/utils": "^0.75.13",
157
+ "@stacksjs/validation": "^0.75.13",
158
158
  "ajv": "^8.20.0",
159
159
  "ajv-formats": "^3.0.1",
160
- "bun-plugin-stx": "^0.2.319",
160
+ "bun-plugin-stx": "^0.2.320",
161
161
  "ts-pantry": "^0.11.35"
162
162
  },
163
163
  "devDependencies": {