@stacksjs/buddy 0.70.258 → 0.70.260
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/dist/cli.d.ts +11 -1
- package/dist/cli.js +5 -164
- package/dist/commands/about.js +1 -35
- package/dist/commands/add.js +1 -25
- package/dist/commands/ai-context.js +4 -27
- package/dist/commands/auth.js +1 -68
- package/dist/commands/build.js +1 -221
- package/dist/commands/cd.js +8 -39
- package/dist/commands/changelog.js +1 -32
- package/dist/commands/clean.js +1 -44
- package/dist/commands/cloud.js +2 -519
- package/dist/commands/commit.js +1 -16
- package/dist/commands/completion.js +6 -42
- package/dist/commands/config-migrate.js +6 -94
- package/dist/commands/configure.js +2 -45
- package/dist/commands/create.js +1 -162
- package/dist/commands/deploy.js +13 -1676
- package/dist/commands/desktop-apple.js +12 -399
- package/dist/commands/dev.js +6 -1083
- package/dist/commands/dns.js +3 -103
- package/dist/commands/docs/buddy-commands.js +10 -132
- package/dist/commands/docs/buddy-commands.test.js +1 -38
- package/dist/commands/docs/framework-repo.js +1 -14
- package/dist/commands/docs/framework-repo.test.js +1 -68
- package/dist/commands/docs/generated-artifacts.js +3 -70
- package/dist/commands/docs/generated-artifacts.test.js +1 -19
- package/dist/commands/docs/links.js +2 -85
- package/dist/commands/docs/links.test.js +4 -66
- package/dist/commands/docs.js +1 -24
- package/dist/commands/doctor.js +2 -478
- package/dist/commands/domains.js +2 -152
- package/dist/commands/email.js +31 -503
- package/dist/commands/env.js +1 -312
- package/dist/commands/extension.js +1 -239
- package/dist/commands/features.js +9 -345
- package/dist/commands/fresh.js +1 -46
- package/dist/commands/generate.js +2 -152
- package/dist/commands/http.js +1 -33
- package/dist/commands/index.js +1 -57
- package/dist/commands/install.js +1 -22
- package/dist/commands/key.js +1 -23
- package/dist/commands/link.js +2 -147
- package/dist/commands/lint.js +2 -84
- package/dist/commands/list.js +1 -140
- package/dist/commands/mail.js +15 -1477
- package/dist/commands/maintenance.js +1 -141
- package/dist/commands/make.js +1 -392
- package/dist/commands/migrate-project.js +1 -49
- package/dist/commands/migrate.js +27 -625
- package/dist/commands/outdated.js +1 -19
- package/dist/commands/package.js +1 -17
- package/dist/commands/phone.js +17 -181
- package/dist/commands/ports.js +2 -118
- package/dist/commands/prepublish.js +1 -17
- package/dist/commands/projects.js +1 -29
- package/dist/commands/protocol/craft-evidence.js +4 -98
- package/dist/commands/protocol/craft-evidence.test.js +1 -20
- package/dist/commands/protocol/desktop-lifecycle-report.js +3 -57
- package/dist/commands/protocol/desktop-lifecycle-report.test.js +1 -32
- package/dist/commands/protocol/desktop-support.js +2 -32
- package/dist/commands/protocol/desktop-workflow.test.js +3 -35
- package/dist/commands/protocol/driver-registry.js +2 -71
- package/dist/commands/protocol/pantry-evidence.js +4 -144
- package/dist/commands/protocol/pantry-evidence.test.js +1 -29
- package/dist/commands/protocol/release-manifest.js +3 -102
- package/dist/commands/protocol/release-manifest.test.js +2 -34
- package/dist/commands/protocol/run-conformance.js +4 -346
- package/dist/commands/protocol/run-conformance.test.js +1 -52
- package/dist/commands/protocol/run-driver-contracts.js +4 -83
- package/dist/commands/protocol/run-driver-contracts.test.js +1 -18
- package/dist/commands/protocol/source-manifest.js +3 -113
- package/dist/commands/protocol/sync-suite.js +3 -136
- package/dist/commands/protocol/sync-suite.test.js +1 -44
- package/dist/commands/protocol.js +1 -60
- package/dist/commands/publish.js +8 -500
- package/dist/commands/queue.js +1 -248
- package/dist/commands/release.js +1 -42
- package/dist/commands/route.js +1 -21
- package/dist/commands/run-tool.js +1 -9
- package/dist/commands/saas.js +1 -25
- package/dist/commands/schedule.js +1 -61
- package/dist/commands/search.js +1 -84
- package/dist/commands/seed.js +1 -47
- package/dist/commands/serve.js +1 -55
- package/dist/commands/setup-ai.js +2 -121
- package/dist/commands/setup.js +2 -305
- package/dist/commands/share.js +1 -214
- package/dist/commands/sms.js +32 -319
- package/dist/commands/stacks.js +1 -72
- package/dist/commands/telemetry.js +1 -74
- package/dist/commands/test.js +1 -130
- package/dist/commands/tinker.js +1 -37
- package/dist/commands/types.js +1 -18
- package/dist/commands/upgrade.js +1 -98
- package/dist/commands/user.js +3 -84
- package/dist/commands/version.js +1 -14
- package/dist/config.js +1 -231
- package/dist/custom-cli.js +1 -23
- package/dist/database-preflight.js +1 -88
- package/dist/global-options.js +1 -4
- package/dist/index.js +1 -1
- package/dist/lazy-commands.js +1 -241
- package/dist/migrators/index.js +2 -62
- package/dist/migrators/laravel/index.js +1 -148
- package/dist/migrators/laravel/migrations.js +4 -231
- package/dist/migrators/laravel/models.js +8 -111
- package/dist/migrators/rails/index.js +1 -11
- package/dist/production-server.js +1 -257
- package/dist/project-setup.js +1 -36
- package/dist/serve-entry.js +1 -5
- package/dist/version-info.js +1 -5
- package/package.json +53 -46
package/dist/commands/migrate.js
CHANGED
|
@@ -1,501 +1,33 @@
|
|
|
1
|
-
import { closeSync, existsSync,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { Action } from "@stacksjs/enums";
|
|
5
|
-
import { hasTTY, isCI } from "@stacksjs/env";
|
|
6
|
-
import { appPath, frameworkPath, frameworkRuntimePath, projectPath } from "@stacksjs/path";
|
|
7
|
-
import { ExitCode } from "@stacksjs/types";
|
|
8
|
-
import { preflightDatabase } from "../database-preflight";
|
|
9
|
-
import { DDL_CONSTRAINT_OVERRIDE_ENV, DIALECT_OVERRIDE_ENV, auditDdlConstraints, auditMigrationCorpus, dialectCapabilities, formatDdlConstraintError, formatMigrationDialectError, stripSqlNoise } from "@stacksjs/database";
|
|
10
|
-
let _runAction;
|
|
11
|
-
async function runAction(...args) {
|
|
12
|
-
if (!_runAction)
|
|
13
|
-
_runAction = (await import("@stacksjs/actions")).runAction;
|
|
14
|
-
return _runAction(...args);
|
|
15
|
-
}
|
|
16
|
-
function acquireMigrationLock() {
|
|
17
|
-
const lockDir = frameworkRuntimePath(), lockFile = `${lockDir}/migrations.lock`;
|
|
18
|
-
try {
|
|
19
|
-
if (!existsSync(lockDir))
|
|
20
|
-
mkdirSync(lockDir, { recursive: !0 });
|
|
21
|
-
const fd = openSync(lockFile, "wx");
|
|
22
|
-
try {
|
|
23
|
-
closeSync(fd);
|
|
24
|
-
} catch {}
|
|
25
|
-
return {
|
|
26
|
-
acquired: !0,
|
|
27
|
-
release: () => {
|
|
28
|
-
try {
|
|
29
|
-
rmSync(lockFile, { force: !0 });
|
|
30
|
-
} catch {}
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
} catch {
|
|
34
|
-
return {
|
|
35
|
-
acquired: !1,
|
|
36
|
-
release: () => {}
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
async function ensureDatabaseOrExit() {
|
|
41
|
-
try {
|
|
42
|
-
const { ensureDatabaseReady } = await import("@stacksjs/database");
|
|
43
|
-
await ensureDatabaseReady();
|
|
44
|
-
} catch (error) {
|
|
45
|
-
log.syncError(error instanceof Error ? error.message : String(error));
|
|
46
|
-
process.exit(ExitCode.FatalError);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
function readMigrateMarker() {
|
|
50
|
-
const file = frameworkRuntimePath("last-migrate-result.json");
|
|
51
|
-
if (!existsSync(file))
|
|
52
|
-
return null;
|
|
53
|
-
try {
|
|
54
|
-
const raw = readFileSync(file, "utf8"), parsed = JSON.parse(raw), n = typeof parsed.appliedCount === "number" ? parsed.appliedCount : null;
|
|
55
|
-
if (n === null || !Number.isFinite(n))
|
|
56
|
-
return null;
|
|
57
|
-
return { appliedCount: Math.max(0, Math.floor(n)) };
|
|
58
|
-
} catch {
|
|
59
|
-
return null;
|
|
60
|
-
} finally {
|
|
61
|
-
try {
|
|
62
|
-
rmSync(file, { force: !0 });
|
|
63
|
-
} catch {}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
function countModelFiles(dir) {
|
|
67
|
-
if (!existsSync(dir))
|
|
68
|
-
return 0;
|
|
69
|
-
let count = 0;
|
|
70
|
-
const entries = readdirSync(dir, { withFileTypes: !0 });
|
|
71
|
-
for (const entry of entries)
|
|
72
|
-
if (entry.isDirectory())
|
|
73
|
-
count += countModelFiles(`${dir}/${entry.name}`);
|
|
74
|
-
else if (entry.name.endsWith(".ts") && !entry.name.startsWith(".") && !entry.name.startsWith("index"))
|
|
75
|
-
count++;
|
|
76
|
-
return count;
|
|
77
|
-
}
|
|
78
|
-
function validateModelsExist() {
|
|
79
|
-
const userModelsPath = appPath("Models"), defaultModelsPath = frameworkPath("defaults/app/Models"), userModelCount = countModelFiles(userModelsPath), defaultModelCount = countModelFiles(defaultModelsPath);
|
|
80
|
-
if (userModelCount === 0 && defaultModelCount === 0)
|
|
81
|
-
return {
|
|
82
|
-
valid: !1,
|
|
83
|
-
error: "No models found. Please create models in app/Models or ensure framework defaults exist."
|
|
84
|
-
};
|
|
85
|
-
return { valid: !0 };
|
|
86
|
-
}
|
|
87
|
-
function validateMigrationDialect() {
|
|
88
|
-
const driver = String(process.env.DB_CONNECTION || "sqlite").toLowerCase(), dir = projectPath("database/migrations");
|
|
89
|
-
if (process.env[DIALECT_OVERRIDE_ENV] !== "1") {
|
|
90
|
-
const caps = dialectCapabilities(driver), target = caps.wire === "mysql" ? "mysql" : caps.wire === "postgres" ? "postgres" : "sqlite";
|
|
91
|
-
if (driver === "sqlite" || driver === "postgres" || caps.wire === "mysql") {
|
|
92
|
-
const audit = auditMigrationCorpus({ dir, target });
|
|
93
|
-
if (!audit.empty && audit.incompatible.length > 0)
|
|
94
|
-
return { valid: !1, error: formatMigrationDialectError(audit, target, "database/migrations") };
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (process.env[DDL_CONSTRAINT_OVERRIDE_ENV] !== "1") {
|
|
98
|
-
const constraints = auditDdlConstraints({ dir, dialect: driver });
|
|
99
|
-
if (!constraints.empty && constraints.violations.length > 0)
|
|
100
|
-
return { valid: !1, error: formatDdlConstraintError(constraints, driver, "database/migrations") };
|
|
101
|
-
}
|
|
102
|
-
return { valid: !0 };
|
|
103
|
-
}
|
|
104
|
-
async function reportMissingForeignKeys() {
|
|
105
|
-
try {
|
|
106
|
-
const { auditForeignKeys } = await import("@stacksjs/database"), result = await auditForeignKeys();
|
|
107
|
-
if (result.missing.length === 0)
|
|
108
|
-
return;
|
|
109
|
-
const sample = result.missing.slice(0, 5).map((fk) => ` \u2022 ${fk.fromTable}.${fk.fromColumn} \u2192 ${fk.toTable}.${fk.toColumn} (${fk.model})`).join(`
|
|
110
|
-
`), more = result.missing.length > 5 ? `
|
|
111
|
-
+ ${result.missing.length - 5} more \u2014 run \`./buddy doctor\` for the full list.` : "";
|
|
112
|
-
log.warn(`${result.missing.length} of ${result.declared.length} declared foreign keys are missing from the live schema:
|
|
1
|
+
import{closeSync,existsSync,mkdirSync,openSync,readdirSync,readFileSync,rmSync}from"node:fs";import process from"node:process";import{confirm,intro,log,onUnknownSubcommand,outro,text}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{hasTTY,isCI}from"@stacksjs/env";import{appPath,frameworkPath,frameworkRuntimePath,projectPath}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{preflightDatabase}from"../database-preflight";import{DDL_CONSTRAINT_OVERRIDE_ENV,DIALECT_OVERRIDE_ENV,auditDdlConstraints,auditMigrationCorpus,dialectCapabilities,formatDdlConstraintError,formatMigrationDialectError,stripSqlNoise}from"@stacksjs/database";let _runAction;async function runAction(...args){if(!_runAction)_runAction=(await import("@stacksjs/actions")).runAction;return _runAction(...args)}function acquireMigrationLock(){const lockDir=frameworkRuntimePath(),lockFile=`${lockDir}/migrations.lock`;try{if(!existsSync(lockDir))mkdirSync(lockDir,{recursive:!0});const fd=openSync(lockFile,"wx");try{closeSync(fd)}catch{}return{acquired:!0,release:()=>{try{rmSync(lockFile,{force:!0})}catch{}}}}catch{return{acquired:!1,release:()=>{}}}}async function ensureDatabaseOrExit(){try{const{ensureDatabaseReady}=await import("@stacksjs/database");await ensureDatabaseReady()}catch(error){log.syncError(error instanceof Error?error.message:String(error));process.exit(ExitCode.FatalError)}}function readMigrateMarker(){const file=frameworkRuntimePath("last-migrate-result.json");if(!existsSync(file))return null;try{const raw=readFileSync(file,"utf8"),parsed=JSON.parse(raw),n=typeof parsed.appliedCount==="number"?parsed.appliedCount:null;if(n===null||!Number.isFinite(n))return null;return{appliedCount:Math.max(0,Math.floor(n))}}catch{return null}finally{try{rmSync(file,{force:!0})}catch{}}}function countModelFiles(dir){if(!existsSync(dir))return 0;let count=0;const entries=readdirSync(dir,{withFileTypes:!0});for(const entry of entries)if(entry.isDirectory())count+=countModelFiles(`${dir}/${entry.name}`);else if(entry.name.endsWith(".ts")&&!entry.name.startsWith(".")&&!entry.name.startsWith("index"))count++;return count}function validateModelsExist(){const userModelsPath=appPath("Models"),defaultModelsPath=frameworkPath("defaults/app/Models"),userModelCount=countModelFiles(userModelsPath),defaultModelCount=countModelFiles(defaultModelsPath);if(userModelCount===0&&defaultModelCount===0)return{valid:!1,error:"No models found. Please create models in app/Models or ensure framework defaults exist."};return{valid:!0}}function validateMigrationDialect(){const driver=String(process.env.DB_CONNECTION||"sqlite").toLowerCase(),dir=projectPath("database/migrations");if(process.env[DIALECT_OVERRIDE_ENV]!=="1"){const caps=dialectCapabilities(driver),target=caps.wire==="mysql"?"mysql":caps.wire==="postgres"?"postgres":"sqlite";if(driver==="sqlite"||driver==="postgres"||caps.wire==="mysql"){const audit=auditMigrationCorpus({dir,target});if(!audit.empty&&audit.incompatible.length>0)return{valid:!1,error:formatMigrationDialectError(audit,target,"database/migrations")}}}if(process.env[DDL_CONSTRAINT_OVERRIDE_ENV]!=="1"){const constraints=auditDdlConstraints({dir,dialect:driver});if(!constraints.empty&&constraints.violations.length>0)return{valid:!1,error:formatDdlConstraintError(constraints,driver,"database/migrations")}}return{valid:!0}}async function reportMissingForeignKeys(){try{const{auditForeignKeys}=await import("@stacksjs/database"),result=await auditForeignKeys();if(result.missing.length===0)return;const sample=result.missing.slice(0,5).map((fk)=>` \u2022 ${fk.fromTable}.${fk.fromColumn} \u2192 ${fk.toTable}.${fk.toColumn} (${fk.model})`).join(`
|
|
2
|
+
`),more=result.missing.length>5?`
|
|
3
|
+
+ ${result.missing.length-5} more \u2014 run \`./buddy doctor\` for the full list.`:"";log.warn(`${result.missing.length} of ${result.declared.length} declared foreign keys are missing from the live schema:
|
|
113
4
|
${sample}${more}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
async function reportFkOrphans() {
|
|
120
|
-
try {
|
|
121
|
-
const { findFkOrphans } = await import("@stacksjs/database"), result = await findFkOrphans();
|
|
122
|
-
if (!result.supported || result.total === 0)
|
|
123
|
-
return;
|
|
124
|
-
const sample = result.orphans.slice(0, 5).map((o) => ` \u2022 ${o.table}.${o.column} \u2192 ${o.parent} (${o.count} row${o.count === 1 ? "" : "s"})`).join(`
|
|
125
|
-
`), more = result.orphans.length > 5 ? `
|
|
126
|
-
+ ${result.orphans.length - 5} more \u2014 run \`./buddy doctor\` for the full list.` : "", first = result.orphans[0];
|
|
127
|
-
log.warn(`${result.total} row${result.total === 1 ? "" : "s"} violate foreign keys (orphaned parents), now that SQLite enforces \`foreign_keys = ON\`:
|
|
5
|
+
`+"The model-backed create migrations may be stale for this database \u2014 run `./buddy migrate:fresh` to regenerate and replay them from the model attributes (this resets data).")}catch(err){log.debug(`[migrate] FK integrity check skipped: ${err instanceof Error?err.message:String(err)}`)}}async function reportFkOrphans(){try{const{findFkOrphans}=await import("@stacksjs/database"),result=await findFkOrphans();if(!result.supported||result.total===0)return;const sample=result.orphans.slice(0,5).map((o)=>` \u2022 ${o.table}.${o.column} \u2192 ${o.parent} (${o.count} row${o.count===1?"":"s"})`).join(`
|
|
6
|
+
`),more=result.orphans.length>5?`
|
|
7
|
+
+ ${result.orphans.length-5} more \u2014 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\`:
|
|
128
8
|
${sample}${more}
|
|
129
|
-
|
|
130
|
-
} catch (err) {
|
|
131
|
-
log.debug(`[migrate] FK orphan scan skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
function describeOp(op) {
|
|
135
|
-
switch (op.kind) {
|
|
136
|
-
case "drop_table":
|
|
137
|
-
return `drop table "${op.table}" (all rows lost)`;
|
|
138
|
-
case "drop_column":
|
|
139
|
-
return `drop column "${op.table}"."${op.column}" (column data lost)`;
|
|
140
|
-
case "modify_column":
|
|
141
|
-
return `change type of "${op.table}"."${op.column}" (possible data loss)`;
|
|
142
|
-
case "rebuild_table":
|
|
143
|
-
return `rebuild table "${op.table}" (type/constraint change)`;
|
|
144
|
-
case "rename_column":
|
|
145
|
-
return `rename "${op.table}"."${op.from}" \u2192 "${op.to}"`;
|
|
146
|
-
case "rename_table":
|
|
147
|
-
return `rename table "${op.from}" \u2192 "${op.to}"`;
|
|
148
|
-
default:
|
|
149
|
-
return `${op.kind} on "${op.table}"${op.column ? `."${op.column}"` : ""}`;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
async function confirmDestructiveMigrations(opts) {
|
|
153
|
-
let operations = [];
|
|
154
|
-
try {
|
|
155
|
-
const { previewPendingMigrations } = await import("@stacksjs/database");
|
|
156
|
-
operations = await previewPendingMigrations({ fromDb: opts.fromDb, applyRenames: opts.applyRenames });
|
|
157
|
-
} catch (error) {
|
|
158
|
-
log.debug(`Migration preview unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
159
|
-
return !0;
|
|
160
|
-
}
|
|
161
|
-
const renames = operations.filter((o) => o.kind === "rename_column" || o.kind === "rename_table");
|
|
162
|
-
for (const r of renames)
|
|
163
|
-
log.info(`Detected ${describeOp(r)} \u2014 applying as a rename (data preserved). Use --no-rename to drop + add instead.`);
|
|
164
|
-
const destructive = operations.filter((o) => o.destructive);
|
|
165
|
-
if (destructive.length === 0)
|
|
166
|
-
return !0;
|
|
167
|
-
log.warn(`This migration includes ${destructive.length} potentially destructive change${destructive.length === 1 ? "" : "s"}:`);
|
|
168
|
-
for (const op of destructive)
|
|
169
|
-
log.warn(` \u2022 ${describeOp(op)}`);
|
|
170
|
-
if (opts.force)
|
|
171
|
-
return !0;
|
|
172
|
-
if (isCI || !hasTTY) {
|
|
173
|
-
log.error("Refusing to apply destructive changes in a non-interactive environment. Re-run with --force to proceed.");
|
|
174
|
-
return !1;
|
|
175
|
-
}
|
|
176
|
-
return confirm({ message: "Apply these destructive changes?", initial: !1 });
|
|
177
|
-
}
|
|
178
|
-
function parseGuardBool(raw) {
|
|
179
|
-
if (raw == null || raw === "")
|
|
180
|
-
return;
|
|
181
|
-
const v = raw.toLowerCase().trim();
|
|
182
|
-
if (v === "1" || v === "true" || v === "yes" || v === "on")
|
|
183
|
-
return !0;
|
|
184
|
-
if (v === "0" || v === "false" || v === "no" || v === "off")
|
|
185
|
-
return !1;
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
function parseFreshGuard(raw) {
|
|
189
|
-
const v = raw?.toLowerCase().trim();
|
|
190
|
-
return v === "allow" || v === "confirm" || v === "disabled" ? v : void 0;
|
|
191
|
-
}
|
|
192
|
-
async function resolveMigrationGuards() {
|
|
193
|
-
const isProd = /^prod/i.test(process.env.APP_ENV || "local");
|
|
194
|
-
let cfg = {};
|
|
195
|
-
try {
|
|
196
|
-
const { awaitConfig } = await import("@stacksjs/config");
|
|
197
|
-
cfg = (await awaitConfig()).database?.safety ?? {};
|
|
198
|
-
} catch (error) {
|
|
199
|
-
log.debug(`[migrate] safety config unavailable, using defaults: ${error instanceof Error ? error.message : String(error)}`);
|
|
200
|
-
}
|
|
201
|
-
const confirmMigrate = parseGuardBool(process.env.DB_MIGRATE_CONFIRM) ?? cfg.confirmMigrate ?? !0, migrateFresh = parseFreshGuard(process.env.DB_MIGRATE_FRESH) ?? cfg.migrateFresh ?? (isProd ? "disabled" : "allow");
|
|
202
|
-
return { confirmMigrate, migrateFresh };
|
|
203
|
-
}
|
|
204
|
-
function currentDatabaseLabel() {
|
|
205
|
-
if ((process.env.DB_CONNECTION || "sqlite").toLowerCase() === "sqlite")
|
|
206
|
-
return process.env.DB_DATABASE_PATH || "database/stacks.sqlite";
|
|
207
|
-
return process.env.DB_DATABASE || "stacks";
|
|
208
|
-
}
|
|
209
|
-
export function migrate(buddy) {
|
|
210
|
-
const descriptions = {
|
|
211
|
-
migrate: "Migrates your database",
|
|
212
|
-
fresh: "Drop all tables and re-run every migration (destroys all data)",
|
|
213
|
-
project: "Target a specific project",
|
|
214
|
-
verbose: "Enable verbose output",
|
|
215
|
-
auth: "Also migrate auth tables (oauth_clients, oauth_access_tokens, oauth_refresh_tokens, password_resets)",
|
|
216
|
-
force: "Apply destructive changes (drop column/table, lossy type change) without confirmation",
|
|
217
|
-
fromDb: "Diff against the live database schema instead of the snapshot (self-heal drift)",
|
|
218
|
-
noRename: "Treat renamed columns as drop + add instead of a data-preserving rename"
|
|
219
|
-
};
|
|
220
|
-
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("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
221
|
-
log.debug("Running `buddy migrate` ...", options);
|
|
222
|
-
const perf = await intro("buddy migrate"), validation = validateModelsExist();
|
|
223
|
-
if (!validation.valid) {
|
|
224
|
-
console.error(`
|
|
9
|
+
`+"These were written under the old `foreign_keys = OFF` default (#1951). Review and clean up manually \u2014 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}" (type/constraint change)`;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 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)} \u2014 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)}`);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"};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("--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(`
|
|
225
10
|
\u274C Error: ${validation.error}
|
|
226
|
-
`);
|
|
227
|
-
process.exit(ExitCode.FatalError);
|
|
228
|
-
}
|
|
229
|
-
const dialectCheck = validateMigrationDialect();
|
|
230
|
-
if (!dialectCheck.valid) {
|
|
231
|
-
console.error(`
|
|
11
|
+
`);process.exit(ExitCode.FatalError)}const dialectCheck=validateMigrationDialect();if(!dialectCheck.valid){console.error(`
|
|
232
12
|
\u274C Error: ${dialectCheck.error}
|
|
233
|
-
`);
|
|
234
|
-
process.exit(ExitCode.FatalError);
|
|
235
|
-
}
|
|
236
|
-
const applyRenames = options.rename === !1 ? !1 : void 0;
|
|
237
|
-
if (options.fromDb)
|
|
238
|
-
process.env.STACKS_MIGRATE_FROM_DB = "1";
|
|
239
|
-
if (applyRenames === !1)
|
|
240
|
-
process.env.STACKS_MIGRATE_NO_RENAME = "1";
|
|
241
|
-
if (options.diff) {
|
|
242
|
-
try {
|
|
243
|
-
const { previewPendingMigrations } = await import("@stacksjs/database"), ops = await previewPendingMigrations({ fromDb: options.fromDb, applyRenames });
|
|
244
|
-
if (ops.length === 0)
|
|
245
|
-
log.info("No pending schema changes \u2014 your models match the database.");
|
|
246
|
-
else {
|
|
247
|
-
log.info(`${ops.length} pending change${ops.length === 1 ? "" : "s"}:`);
|
|
248
|
-
for (const op of ops)
|
|
249
|
-
log.info(` \u2022 ${describeOp(op)}${op.destructive ? " [destructive]" : ""}`);
|
|
250
|
-
}
|
|
251
|
-
} catch (error) {
|
|
252
|
-
await log.error("Failed to preview migrations:", error);
|
|
253
|
-
}
|
|
254
|
-
await outro("Diff complete \u2014 no changes applied.", { startTime: perf, useSeconds: !0 });
|
|
255
|
-
process.exit(ExitCode.Success);
|
|
256
|
-
}
|
|
257
|
-
await preflightDatabase({ createDatabase: options.createDatabase, command: "migrate" });
|
|
258
|
-
if ((await resolveMigrationGuards()).confirmMigrate && !options.force)
|
|
259
|
-
if (isCI || !hasTTY)
|
|
260
|
-
log.debug("[migrate] confirmMigrate guard skipped \u2014 non-interactive environment.");
|
|
261
|
-
else {
|
|
262
|
-
const APP_ENV = process.env.APP_ENV || "local";
|
|
263
|
-
await log.flush();
|
|
264
|
-
if (!await confirm({
|
|
265
|
-
message: `Run migrations against the ${APP_ENV} database "${currentDatabaseLabel()}"?`,
|
|
266
|
-
initial: !0
|
|
267
|
-
})) {
|
|
268
|
-
await outro("Migration cancelled \u2014 no changes applied.", { startTime: perf, useSeconds: !0 });
|
|
269
|
-
process.exit(ExitCode.Success);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
await ensureDatabaseOrExit();
|
|
273
|
-
const lock = acquireMigrationLock();
|
|
274
|
-
if (!lock.acquired) {
|
|
275
|
-
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.");
|
|
276
|
-
process.exit(ExitCode.FatalError);
|
|
277
|
-
}
|
|
278
|
-
if (!await confirmDestructiveMigrations({ force: options.force, fromDb: options.fromDb, applyRenames })) {
|
|
279
|
-
lock.release();
|
|
280
|
-
await outro("Migration cancelled \u2014 no changes applied.", { startTime: perf, useSeconds: !0 });
|
|
281
|
-
process.exit(ExitCode.Success);
|
|
282
|
-
}
|
|
283
|
-
if (options.auth !== !1) {
|
|
284
|
-
log.debug("Migrating auth tables...");
|
|
285
|
-
try {
|
|
286
|
-
const { migrateAuthTables } = await import("@stacksjs/database"), authResult = await migrateAuthTables({ verbose: options.verbose });
|
|
287
|
-
if (!authResult.success)
|
|
288
|
-
log.error(`Failed to migrate auth tables: ${authResult.error}`);
|
|
289
|
-
} catch (error) {
|
|
290
|
-
log.error("Failed to migrate auth tables:", error);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
const result = await runAction(Action.Migrate, options).finally(() => lock.release());
|
|
294
|
-
if (result.isErr)
|
|
295
|
-
log.error("Model migrations failed \u2014 applying notification/RBAC table guarantees before exiting.");
|
|
296
|
-
if (options.auth !== !1)
|
|
297
|
-
try {
|
|
298
|
-
const { ensureUtcDatetimeColumns, migrateNotificationTables, migrateRbacTables, migrateTraitTables } = await import("@stacksjs/database"), notifResult = await migrateNotificationTables({ verbose: options.verbose });
|
|
299
|
-
if (!notifResult.success)
|
|
300
|
-
log.error(`Failed to migrate notification tables: ${notifResult.error}`);
|
|
301
|
-
const rbacResult = await migrateRbacTables({ verbose: options.verbose });
|
|
302
|
-
if (!rbacResult.success)
|
|
303
|
-
log.error(`Failed to migrate RBAC tables: ${rbacResult.error}`);
|
|
304
|
-
const traitResult = await migrateTraitTables({ verbose: options.verbose });
|
|
305
|
-
if (!traitResult.success)
|
|
306
|
-
log.error(`Failed to migrate polymorphic trait tables: ${traitResult.error}`);
|
|
307
|
-
const datetimeResult = await ensureUtcDatetimeColumns({ verbose: options.verbose });
|
|
308
|
-
if (!datetimeResult.success)
|
|
309
|
-
log.error(`Failed to convert TIMESTAMP columns to DATETIME: ${datetimeResult.error}`);
|
|
310
|
-
} catch (error) {
|
|
311
|
-
log.error("Failed to migrate notification/RBAC/trait tables:", error);
|
|
312
|
-
}
|
|
313
|
-
if (result.isErr) {
|
|
314
|
-
await outro("While running the migrate command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
315
|
-
process.exit(ExitCode.FatalError);
|
|
316
|
-
}
|
|
317
|
-
if (options.auth !== !1)
|
|
318
|
-
try {
|
|
319
|
-
const { ensureUsersAuthColumns, sqlHelpers } = await import("@stacksjs/database"), driver = process.env.DB_CONNECTION || "sqlite";
|
|
320
|
-
await ensureUsersAuthColumns(sqlHelpers(driver), { verbose: options.verbose });
|
|
321
|
-
} catch (error) {
|
|
322
|
-
log.error("Failed to ensure users auth columns post-migration:", error);
|
|
323
|
-
}
|
|
324
|
-
try {
|
|
325
|
-
const { ensureUuidColumns, sqlHelpers } = await import("@stacksjs/database"), driver = process.env.DB_CONNECTION || "sqlite";
|
|
326
|
-
await ensureUuidColumns(sqlHelpers(driver), { verbose: options.verbose });
|
|
327
|
-
} catch (error) {
|
|
328
|
-
log.error("Failed to ensure uuid columns post-migration:", error);
|
|
329
|
-
}
|
|
330
|
-
await reportMissingForeignKeys();
|
|
331
|
-
await reportFkOrphans();
|
|
332
|
-
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 \u2014 your ${APP_ENV} database is already up to date.${authSuffix}` : `Applied ${marker.appliedCount} migration${marker.appliedCount === 1 ? "" : "s"} to your ${APP_ENV} database.${authSuffix}`;
|
|
333
|
-
await outro(outroMessage, {
|
|
334
|
-
startTime: perf,
|
|
335
|
-
useSeconds: !0
|
|
336
|
-
});
|
|
337
|
-
process.exit(ExitCode.Success);
|
|
338
|
-
});
|
|
339
|
-
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) => {
|
|
340
|
-
log.debug("Running `buddy migrate:fresh` ...", options);
|
|
341
|
-
const perf = await intro("buddy migrate:fresh"), validation = validateModelsExist();
|
|
342
|
-
if (!validation.valid) {
|
|
343
|
-
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.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 \u2014 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 \u2014 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 \u2014 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 \u2014 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)}if(!await confirmDestructiveMigrations({force:options.force,fromDb:options.fromDb,applyRenames})){lock.release();await outro("Migration cancelled \u2014 no changes applied.",{startTime:perf,useSeconds:!0});process.exit(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(result.isErr)log.error("Model migrations failed \u2014 applying notification/RBAC table guarantees before exiting.");if(options.auth!==!1)try{const{ensureUtcDatetimeColumns,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}`)}catch(error){log.error("Failed to migrate notification/RBAC/trait tables:",error)}if(result.isErr){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();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 \u2014 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(`
|
|
344
14
|
\u274C Error: ${validation.error}
|
|
345
|
-
`);
|
|
346
|
-
process.exit(ExitCode.FatalError);
|
|
347
|
-
}
|
|
348
|
-
const dialectCheck = validateMigrationDialect();
|
|
349
|
-
if (!dialectCheck.valid) {
|
|
350
|
-
console.error(`
|
|
15
|
+
`);process.exit(ExitCode.FatalError)}const dialectCheck=validateMigrationDialect();if(!dialectCheck.valid){console.error(`
|
|
351
16
|
\u274C Error: ${dialectCheck.error}
|
|
352
|
-
`);
|
|
353
|
-
process.exit(ExitCode.FatalError);
|
|
354
|
-
}
|
|
355
|
-
const guards = await resolveMigrationGuards(), dbLabel = currentDatabaseLabel(), APP_ENV = process.env.APP_ENV || "local";
|
|
356
|
-
if (guards.migrateFresh === "disabled") {
|
|
357
|
-
await log.error(`\`buddy migrate:fresh\` is disabled by your migration safety guards (it DROPS every table).
|
|
17
|
+
`);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).
|
|
358
18
|
Target: ${APP_ENV} database "${dbLabel}"
|
|
359
19
|
To allow it, set database.safety.migrateFresh to 'allow' in config/database.ts,
|
|
360
|
-
or run once with: DB_MIGRATE_FRESH=allow ./buddy migrate:fresh`);
|
|
361
|
-
await outro('migrate:fresh refused \u2014 the migrateFresh guard is set to "disabled".', { startTime: perf, useSeconds: !0 });
|
|
362
|
-
process.exit(ExitCode.FatalError);
|
|
363
|
-
}
|
|
364
|
-
if (!(guards.migrateFresh === "allow" && options.force === !0)) {
|
|
365
|
-
if (isCI || !hasTTY) {
|
|
366
|
-
const hint = guards.migrateFresh === "confirm" ? 'Guard is "confirm": migrate:fresh must be run interactively.' : "Re-run with --force to drop the database non-interactively.";
|
|
367
|
-
await log.error(`Refusing to drop the ${APP_ENV} database "${dbLabel}" in a non-interactive environment. ${hint}`);
|
|
368
|
-
await outro("migrate:fresh cancelled.", { startTime: perf, useSeconds: !0 });
|
|
369
|
-
process.exit(ExitCode.FatalError);
|
|
370
|
-
}
|
|
371
|
-
log.warn(`This will DROP ALL TABLES in the ${APP_ENV} database "${dbLabel}" and rebuild them from scratch. All data will be lost.`);
|
|
372
|
-
await log.flush();
|
|
373
|
-
if ((await text({ message: `Type the database name "${dbLabel}" to confirm (blank to cancel):` })).trim() !== dbLabel) {
|
|
374
|
-
await outro("migrate:fresh cancelled \u2014 confirmation did not match.", { startTime: perf, useSeconds: !0 });
|
|
375
|
-
process.exit(ExitCode.Success);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
await preflightDatabase({ createDatabase: options.createDatabase, command: "migrate:fresh" });
|
|
379
|
-
await ensureDatabaseOrExit();
|
|
380
|
-
const result = await runAction(Action.MigrateFresh, options);
|
|
381
|
-
if (result.isErr)
|
|
382
|
-
log.error("Model migrations failed \u2014 applying auth/notification/RBAC table guarantees before exiting.");
|
|
383
|
-
if (options.auth !== !1) {
|
|
384
|
-
log.debug("Migrating auth tables...");
|
|
385
|
-
try {
|
|
386
|
-
const { migrateAuthTables, migrateNotificationTables, migrateRbacTables, migrateTraitTables } = await import("@stacksjs/database"), authResult = await migrateAuthTables({ verbose: options.verbose });
|
|
387
|
-
if (!authResult.success)
|
|
388
|
-
log.error(`Failed to migrate auth tables: ${authResult.error}`);
|
|
389
|
-
const notifResult = await migrateNotificationTables({ verbose: options.verbose });
|
|
390
|
-
if (!notifResult.success)
|
|
391
|
-
log.error(`Failed to migrate notification tables: ${notifResult.error}`);
|
|
392
|
-
const rbacResult = await migrateRbacTables({ verbose: options.verbose });
|
|
393
|
-
if (!rbacResult.success)
|
|
394
|
-
log.error(`Failed to migrate RBAC tables: ${rbacResult.error}`);
|
|
395
|
-
const traitResult = await migrateTraitTables({ verbose: options.verbose });
|
|
396
|
-
if (!traitResult.success)
|
|
397
|
-
log.error(`Failed to migrate polymorphic trait tables: ${traitResult.error}`);
|
|
398
|
-
} catch (error) {
|
|
399
|
-
log.error("Failed to migrate auth/notification/RBAC/trait tables:", error);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
try {
|
|
403
|
-
const { ensureUuidColumns, sqlHelpers } = await import("@stacksjs/database"), driver = process.env.DB_CONNECTION || "sqlite";
|
|
404
|
-
await ensureUuidColumns(sqlHelpers(driver), { verbose: options.verbose });
|
|
405
|
-
} catch (error) {
|
|
406
|
-
log.error("Failed to ensure uuid columns post-migration:", error);
|
|
407
|
-
}
|
|
408
|
-
if (result.isErr) {
|
|
409
|
-
await outro("While running the migrate:fresh command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
410
|
-
process.exit(ExitCode.FatalError);
|
|
411
|
-
}
|
|
412
|
-
await reportMissingForeignKeys();
|
|
413
|
-
if (options.seed) {
|
|
414
|
-
log.info("Running database seeders...");
|
|
415
|
-
try {
|
|
416
|
-
const { seed } = await import("@stacksjs/database"), seedResult = await seed({ verbose: options.verbose, fresh: !0 });
|
|
417
|
-
if (seedResult.failed > 0) {
|
|
418
|
-
log.warn(`Seeding completed with ${seedResult.failed} failure(s)`);
|
|
419
|
-
for (const r of seedResult.results)
|
|
420
|
-
if (!r.success)
|
|
421
|
-
log.error(` - ${r.model}: ${r.error}`);
|
|
422
|
-
} else
|
|
423
|
-
log.success(`Seeded ${seedResult.successful} model(s)`);
|
|
424
|
-
} catch (error) {
|
|
425
|
-
log.error("Failed to run seeders:", error);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
const parts = [];
|
|
429
|
-
if (options.auth !== !1)
|
|
430
|
-
parts.push("auth tables");
|
|
431
|
-
if (options.seed)
|
|
432
|
-
parts.push("seeded");
|
|
433
|
-
const suffix = parts.length > 0 ? ` & ${parts.join(" & ")}` : "", marker = readMigrateMarker(), countPhrase = marker == null ? "" : marker.appliedCount === 0 ? " (0 applied \u2014 no migration files found?)" : ` (${marker.appliedCount} migration${marker.appliedCount === 1 ? "" : "s"} applied)`;
|
|
434
|
-
await outro(`All tables dropped successfully & migrated successfully${countPhrase}${suffix}`, {
|
|
435
|
-
startTime: perf,
|
|
436
|
-
useSeconds: !0
|
|
437
|
-
});
|
|
438
|
-
process.exit(ExitCode.Success);
|
|
439
|
-
});
|
|
440
|
-
buddy.command("migrate:dns", descriptions.migrate).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
441
|
-
log.debug("Running `buddy migrate:dns` ...", options);
|
|
442
|
-
const perf = await intro("buddy migrate:dns"), result = await runAction(Action.MigrateDns, { ...options });
|
|
443
|
-
if (result.isErr) {
|
|
444
|
-
await outro("While running the migrate:dns command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
445
|
-
process.exit(ExitCode.FatalError);
|
|
446
|
-
}
|
|
447
|
-
const APP_URL = process.env.APP_URL || "undefined";
|
|
448
|
-
await outro(`Migrated your ${APP_URL} DNS.`, {
|
|
449
|
-
startTime: perf,
|
|
450
|
-
useSeconds: !0
|
|
451
|
-
});
|
|
452
|
-
process.exit(ExitCode.Success);
|
|
453
|
-
});
|
|
454
|
-
buddy.command("migrate:switch <driver>", "Pre-flight check + plan for switching DB_CONNECTION between sqlite / mysql / postgres").action(async (driver) => {
|
|
455
|
-
log.debug(`Running \`buddy migrate:switch ${driver}\` ...`);
|
|
456
|
-
const perf = await intro("buddy migrate:switch"), target = driver.toLowerCase();
|
|
457
|
-
if (!new Set(["sqlite", "mysql", "postgres"]).has(target)) {
|
|
458
|
-
console.log(`
|
|
20
|
+
or run once with: DB_MIGRATE_FRESH=allow ./buddy migrate:fresh`);await outro('migrate:fresh refused \u2014 the migrateFresh guard is set to "disabled".',{startTime:perf,useSeconds:!0});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});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 \u2014 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(result.isErr)log.error("Model migrations failed \u2014 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(result.isErr){await outro("While running the migrate:fresh command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await reportMissingForeignKeys();if(options.seed){log.info("Running database seeders...");try{const{seed}=await import("@stacksjs/database"),seedResult=await seed({verbose:options.verbose,fresh:!0});if(seedResult.failed>0){log.warn(`Seeding completed with ${seedResult.failed} failure(s)`);for(const r of seedResult.results)if(!r.success)log.error(` - ${r.model}: ${r.error}`)}else log.success(`Seeded ${seedResult.successful} model(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 \u2014 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:dns",descriptions.migrate).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy migrate:dns` ...",options);const perf=await intro("buddy migrate:dns"),result=await runAction(Action.MigrateDns,{...options});if(result.isErr){await outro("While running the migrate:dns command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}const APP_URL=process.env.APP_URL||"undefined";await outro(`Migrated your ${APP_URL} DNS.`,{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:switch <driver>","Pre-flight check + plan for switching DB_CONNECTION between sqlite / mysql / 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","postgres"]).has(target)){console.log(`
|
|
459
21
|
Unknown target driver "${driver}". Allowed: sqlite, mysql, postgres.
|
|
460
|
-
`);
|
|
461
|
-
await outro("Aborted.", { startTime: perf, useSeconds: !0 });
|
|
462
|
-
process.exit(ExitCode.FatalError);
|
|
463
|
-
}
|
|
464
|
-
const current = (process.env.DB_CONNECTION || "sqlite").toLowerCase();
|
|
465
|
-
if (current === target) {
|
|
466
|
-
console.log(`
|
|
22
|
+
`);await outro("Aborted.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.FatalError)}const current=(process.env.DB_CONNECTION||"sqlite").toLowerCase();if(current===target){console.log(`
|
|
467
23
|
DB_CONNECTION is already "${target}". Nothing to switch.
|
|
468
|
-
`);
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
postgres: ["DB_HOST", "DB_PORT", "DB_USERNAME", "DB_PASSWORD", "DB_DATABASE"]
|
|
476
|
-
}[target] ?? []).filter((k) => !process.env[k]);
|
|
477
|
-
let alterCount = 0, uniqueIdxCount = 0;
|
|
478
|
-
try {
|
|
479
|
-
const migrationsDir = projectPath("database/migrations");
|
|
480
|
-
if (existsSync(migrationsDir)) {
|
|
481
|
-
const files = readdirSync(migrationsDir).filter((f) => f.endsWith(".sql"));
|
|
482
|
-
for (const f of files) {
|
|
483
|
-
const content = stripSqlNoise(readFileSync(`${migrationsDir}/${f}`, "utf-8")).toLowerCase();
|
|
484
|
-
if (/alter\s+table[\s\S]*add\s+constraint/.test(content))
|
|
485
|
-
alterCount++;
|
|
486
|
-
if (/^\s*create\s+unique\s+index/m.test(content))
|
|
487
|
-
uniqueIdxCount++;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
} catch {}
|
|
491
|
-
const switchAudit = auditMigrationCorpus({ dir: projectPath("database/migrations"), target }), switchBlocked = switchAudit.incompatible.length > 0, blockedFiles = new Set(switchAudit.incompatible.map((m) => m.file)).size, dialectNote = switchBlocked ? `
|
|
492
|
-
\u26D4 ${blockedFiles} of ${switchAudit.total} migration files are ${switchAudit.inferred ?? "another dialect"}-flavoured and will NOT run on ${target}.
|
|
493
|
-
Regenerate them against ${target} before migrating, or this switch will fail on the first file.` : "", sqliteFkNote = current === "sqlite" && alterCount > 0 ? `
|
|
494
|
-
(These were skipped on SQLite per stacksjs/stacks#1916 and survive on disk for replay.)` : "", boolNote = current === "sqlite" && (target === "postgres" || target === "mysql") ? `
|
|
495
|
-
\u2022 Booleans land as 0/1 on SQLite; ${target} stores them as ${target === "postgres" ? "true/false" : "0/1 (compatible)"}.` : "", tzNote = target === "postgres" ? `
|
|
496
|
-
\u2022 PostgreSQL uses timestamptz (with TZ) where SQLite/MySQL store plain TIMESTAMP. Existing rows do NOT auto-upgrade \u2014 they ride the column's stored type.` : "", envExtras = target !== "sqlite" ? ", plus DB_HOST / DB_PORT / DB_USERNAME / DB_PASSWORD / DB_DATABASE" : "", missingNote = missingEnv.length > 0 ? `
|
|
497
|
-
\u26A0 Missing env vars for ${target}: ${missingEnv.join(", ")}` : "";
|
|
498
|
-
console.log(`
|
|
24
|
+
`);await outro("No-op.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}const missingEnv=({sqlite:["DB_DATABASE"],mysql:["DB_HOST","DB_PORT","DB_USERNAME","DB_PASSWORD","DB_DATABASE"],postgres:["DB_HOST","DB_PORT","DB_USERNAME","DB_PASSWORD","DB_DATABASE"]}[target]??[]).filter((k)=>!process.env[k]);let alterCount=0,uniqueIdxCount=0;try{const migrationsDir=projectPath("database/migrations");if(existsSync(migrationsDir)){const files=readdirSync(migrationsDir).filter((f)=>f.endsWith(".sql"));for(const f of files){const content=stripSqlNoise(readFileSync(`${migrationsDir}/${f}`,"utf-8")).toLowerCase();if(/alter\s+table[\s\S]*add\s+constraint/.test(content))alterCount++;if(/^\s*create\s+unique\s+index/m.test(content))uniqueIdxCount++}}}catch{}const switchAudit=auditMigrationCorpus({dir:projectPath("database/migrations"),target}),switchBlocked=switchAudit.incompatible.length>0,blockedFiles=new Set(switchAudit.incompatible.map((m)=>m.file)).size,dialectNote=switchBlocked?`
|
|
25
|
+
\u26D4 ${blockedFiles} of ${switchAudit.total} migration files are ${switchAudit.inferred??"another dialect"}-flavoured and will NOT run on ${target}.
|
|
26
|
+
Regenerate them against ${target} before migrating, or this switch will fail on the first file.`:"",sqliteFkNote=current==="sqlite"&&alterCount>0?`
|
|
27
|
+
(These were skipped on SQLite per stacksjs/stacks#1916 and survive on disk for replay.)`:"",boolNote=current==="sqlite"&&(target==="postgres"||target==="mysql")?`
|
|
28
|
+
\u2022 Booleans land as 0/1 on SQLite; ${target} stores them as ${target==="postgres"?"true/false":"0/1 (compatible)"}.`:"",tzNote=target==="postgres"?`
|
|
29
|
+
\u2022 PostgreSQL uses timestamptz (with TZ) where SQLite/MySQL store plain TIMESTAMP. Existing rows do NOT auto-upgrade \u2014 they ride the column's stored type.`:"",envExtras=target!=="sqlite"?", plus DB_HOST / DB_PORT / DB_USERNAME / DB_PASSWORD / DB_DATABASE":"",missingNote=missingEnv.length>0?`
|
|
30
|
+
\u26A0 Missing env vars for ${target}: ${missingEnv.join(", ")}`:"";console.log(`
|
|
499
31
|
Switch plan: ${current} \u2192 ${target}
|
|
500
32
|
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
501
33
|
\u2022 ${alterCount} ALTER TABLE ADD CONSTRAINT migration(s) will be applied against ${target}.${sqliteFkNote}
|
|
@@ -507,42 +39,9 @@ export function migrate(buddy) {
|
|
|
507
39
|
Next steps:
|
|
508
40
|
1. Update .env: DB_CONNECTION=${target}${envExtras}
|
|
509
41
|
2. (Optional) Export data from the current ${current} database.
|
|
510
|
-
3. ${switchBlocked
|
|
42
|
+
3. ${switchBlocked?`Regenerate the migration files for ${target} FIRST \u2014 \`./buddy migrate\` will refuse until then.`:"Run `./buddy migrate` (or `migrate:fresh` to start clean)."}
|
|
511
43
|
4. The post-migrate FK audit will report any constraints that didn't replay.
|
|
512
|
-
`);
|
|
513
|
-
await outro("Plan rendered. Re-run after updating .env to actually switch.", {
|
|
514
|
-
startTime: perf,
|
|
515
|
-
useSeconds: !0
|
|
516
|
-
});
|
|
517
|
-
process.exit(ExitCode.Success);
|
|
518
|
-
});
|
|
519
|
-
buddy.command("migrate:regenerate [dialect]", "Rebuild database/migrations from your models for a given dialect").option("--dry-run", "Show what would change without writing anything", { default: !1 }).option("-f, --force", "Regenerate even though the database already has migrations recorded", { default: !1 }).action(async (dialect, options) => {
|
|
520
|
-
const perf = await intro("buddy migrate:regenerate"), target = (dialect || process.env.DB_CONNECTION || "sqlite").toLowerCase();
|
|
521
|
-
if (!new Set(["sqlite", "mysql", "postgres", "singlestore"]).has(target)) {
|
|
522
|
-
log.syncError(`Unknown dialect "${target}". Allowed: sqlite, mysql, postgres, singlestore.`);
|
|
523
|
-
process.exit(ExitCode.FatalError);
|
|
524
|
-
}
|
|
525
|
-
const { countAppliedMigrations, regenerateMigrationCorpus } = await import("@stacksjs/database");
|
|
526
|
-
let applied = 0;
|
|
527
|
-
try {
|
|
528
|
-
applied = await countAppliedMigrations();
|
|
529
|
-
} catch {
|
|
530
|
-
applied = 0;
|
|
531
|
-
}
|
|
532
|
-
if (applied > 0 && !options.force && !options.dryRun) {
|
|
533
|
-
log.syncError(`This database already has ${applied} migration(s) recorded.`);
|
|
534
|
-
log.syncError("Regenerating renumbers every file, and the migrations table keys on the filename,");
|
|
535
|
-
log.syncError("so already-applied migrations would look pending and run a second time.");
|
|
536
|
-
log.syncError(" Point at an empty database, or re-run with --force if you know it is safe.");
|
|
537
|
-
process.exit(ExitCode.FatalError);
|
|
538
|
-
}
|
|
539
|
-
const plan = await regenerateMigrationCorpus({ dialect: target, dryRun: !0 });
|
|
540
|
-
if (plan.isErr) {
|
|
541
|
-
log.syncError(plan.error.message);
|
|
542
|
-
process.exit(ExitCode.FatalError);
|
|
543
|
-
}
|
|
544
|
-
const { files, removed, models } = plan.value;
|
|
545
|
-
console.log(`
|
|
44
|
+
`);await outro("Plan rendered. Re-run after updating .env to actually switch.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:regenerate [dialect]","Rebuild database/migrations from your models for a given dialect").option("--dry-run","Show what would change without writing anything",{default:!1}).option("-f, --force","Regenerate even though the database already has migrations recorded",{default:!1}).action(async(dialect,options)=>{const perf=await intro("buddy migrate:regenerate"),target=(dialect||process.env.DB_CONNECTION||"sqlite").toLowerCase();if(!new Set(["sqlite","mysql","postgres","singlestore"]).has(target)){log.syncError(`Unknown dialect "${target}". Allowed: sqlite, mysql, postgres, singlestore.`);process.exit(ExitCode.FatalError)}const{countAppliedMigrations,regenerateMigrationCorpus}=await import("@stacksjs/database");let applied=0;try{applied=await countAppliedMigrations()}catch{applied=0}if(applied>0&&!options.force&&!options.dryRun){log.syncError(`This database already has ${applied} migration(s) recorded.`);log.syncError("Regenerating renumbers every file, and the migrations table keys on the filename,");log.syncError("so already-applied migrations would look pending and run a second time.");log.syncError(" Point at an empty database, or re-run with --force if you know it is safe.");process.exit(ExitCode.FatalError)}const plan=await regenerateMigrationCorpus({dialect:target,dryRun:!0});if(plan.isErr){log.syncError(plan.error.message);process.exit(ExitCode.FatalError)}const{files,removed,models}=plan.value;console.log(`
|
|
546
45
|
Regenerate plan: ${target}
|
|
547
46
|
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
548
47
|
\u2022 ${models} model(s) read from app/Models and the framework defaults
|
|
@@ -550,108 +49,11 @@ export function migrate(buddy) {
|
|
|
550
49
|
\u2022 ${removed.length} existing file(s) will be removed
|
|
551
50
|
\u2022 These files are tracked in git, so review with \`git diff\` afterwards
|
|
552
51
|
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
553
|
-
`);
|
|
554
|
-
|
|
555
|
-
await outro("Dry run. Nothing was written.", { startTime: perf, useSeconds: !0 });
|
|
556
|
-
process.exit(ExitCode.Success);
|
|
557
|
-
}
|
|
558
|
-
if (!isCI && hasTTY && process.stdin.isTTY) {
|
|
559
|
-
await log.flush();
|
|
560
|
-
if (!await confirm({ message: `Replace ${removed.length} migration file(s) with ${files.length} generated for ${target}?`, initial: !1 })) {
|
|
561
|
-
await outro("Cancelled. Nothing was written.", { startTime: perf, useSeconds: !0 });
|
|
562
|
-
process.exit(ExitCode.Success);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
const result = await regenerateMigrationCorpus({ dialect: target });
|
|
566
|
-
if (result.isErr) {
|
|
567
|
-
log.syncError(result.error.message);
|
|
568
|
-
process.exit(ExitCode.FatalError);
|
|
569
|
-
}
|
|
570
|
-
log.success(`Wrote ${result.value.files.length} ${target} migration file(s) to database/migrations.`);
|
|
571
|
-
if (applied > 0) {
|
|
572
|
-
const { reconcileMigrationLedger } = await import("@stacksjs/database"), fixed = await reconcileMigrationLedger();
|
|
573
|
-
if (fixed.remapped.length > 0)
|
|
574
|
-
log.info(`Repointed ${fixed.remapped.length} ledger row(s) at their renumbered file.`);
|
|
575
|
-
if (fixed.recorded.length > 0)
|
|
576
|
-
log.info(`Recorded ${fixed.recorded.length} migration(s) already present in the schema.`);
|
|
577
|
-
if (fixed.skipped.length > 0)
|
|
578
|
-
log.warn(`${fixed.skipped.length} ledger entr(ies) need a look \u2014 run \`./buddy migrate:status\`.`);
|
|
579
|
-
}
|
|
580
|
-
log.info("Review the change with `git diff`, then run `./buddy migrate`.");
|
|
581
|
-
await outro("Regenerated.", { startTime: perf, useSeconds: !0 });
|
|
582
|
-
process.exit(ExitCode.Success);
|
|
583
|
-
});
|
|
584
|
-
buddy.command("migrate:status", "Compare database/migrations, the migrations ledger, and the live schema").option("--reconcile", "Repair the ledger where the schema proves what happened", { default: !1 }).option("--include-partial", "With --reconcile, also record half-applied migrations", { default: !1 }).option("--json", "Emit the audit as JSON", { default: !1 }).action(async (options) => {
|
|
585
|
-
const perf = options.json ? void 0 : await intro("buddy migrate:status"), { auditMigrationLedger, reconcileMigrationLedger } = await import("@stacksjs/database"), audit = await auditMigrationLedger();
|
|
586
|
-
if (options.json) {
|
|
587
|
-
console.log(JSON.stringify(audit, (_k, v) => v instanceof Set ? [...v] : v, 2));
|
|
588
|
-
process.exit(audit.drift ? ExitCode.FatalError : ExitCode.Success);
|
|
589
|
-
}
|
|
590
|
-
if (!audit.supported) {
|
|
591
|
-
log.info(`Dialect "${audit.dialect}" is not audited. Nothing to compare.`);
|
|
592
|
-
await outro("Skipped.", { startTime: perf, useSeconds: !0 });
|
|
593
|
-
process.exit(ExitCode.Success);
|
|
594
|
-
}
|
|
595
|
-
const { counts, entries, orphans } = audit, list = (status) => entries.filter((e) => e.status === status).map((e) => e.file), report = [], section = (heading, files) => {
|
|
596
|
-
if (files.length === 0)
|
|
597
|
-
return;
|
|
598
|
-
if (heading)
|
|
599
|
-
report.push(` ${heading}`);
|
|
600
|
-
for (const file of files.slice(0, 8))
|
|
601
|
-
report.push(` ${file}`);
|
|
602
|
-
if (files.length > 8)
|
|
603
|
-
report.push(` \u2026 +${files.length - 8} more`);
|
|
604
|
-
report.push("");
|
|
605
|
-
};
|
|
606
|
-
report.push("");
|
|
607
|
-
report.push(` Migration status: ${audit.dialect}`);
|
|
608
|
-
report.push(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
609
|
-
report.push(` ${entries.length} file(s) on disk \xB7 ${audit.recordedCount} recorded in the ledger`);
|
|
610
|
-
report.push("");
|
|
611
|
-
if (counts.applied > 0)
|
|
612
|
-
report.push(` ${counts.applied} applied - recorded, and present in the schema.`, "");
|
|
613
|
-
if (counts.unverifiable > 0)
|
|
614
|
-
report.push(` ${counts.unverifiable} unverifiable - data migrations with no schema trace to check.`, "");
|
|
615
|
-
section(`${counts.pending} pending - not applied yet, will run on the next \`buddy migrate\`:`, list("pending"));
|
|
616
|
-
if (counts.stranded > 0) {
|
|
617
|
-
report.push(` ${counts.stranded} STRANDED - already applied to the schema, but missing from the ledger.`);
|
|
618
|
-
report.push(" These re-run on the next `buddy migrate`, which is unsafe for anything not idempotent.");
|
|
619
|
-
section("", list("stranded"));
|
|
620
|
-
}
|
|
621
|
-
section(`${counts.partial} PARTIAL - some effects present, some missing. Needs a human:`, list("partial"));
|
|
622
|
-
section(`${counts.reverted} REVERTED - recorded as applied, but the effects are gone from the schema:`, list("reverted"));
|
|
623
|
-
if (orphans.length > 0)
|
|
624
|
-
section(`${orphans.length} orphaned ledger row(s) - recorded, but no such file on disk:`, orphans.map((o) => `${o.migration}${o.renamedTo ? ` -> renumbered to ${o.renamedTo}` : " (no counterpart; migration deleted?)"}`));
|
|
625
|
-
console.log(report.join(`
|
|
626
|
-
`));
|
|
627
|
-
if (!audit.drift) {
|
|
628
|
-
await outro("Ledger matches the corpus and the schema.", { startTime: perf, useSeconds: !0 });
|
|
629
|
-
process.exit(ExitCode.Success);
|
|
630
|
-
}
|
|
631
|
-
if (!options.reconcile) {
|
|
632
|
-
console.log(`
|
|
52
|
+
`);if(options.dryRun){await outro("Dry run. Nothing was written.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}if(!isCI&&hasTTY&&process.stdin.isTTY){await log.flush();if(!await confirm({message:`Replace ${removed.length} migration file(s) with ${files.length} generated for ${target}?`,initial:!1})){await outro("Cancelled. Nothing was written.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}}const result=await regenerateMigrationCorpus({dialect:target});if(result.isErr){log.syncError(result.error.message);process.exit(ExitCode.FatalError)}log.success(`Wrote ${result.value.files.length} ${target} migration file(s) to database/migrations.`);if(applied>0){const{reconcileMigrationLedger}=await import("@stacksjs/database"),fixed=await reconcileMigrationLedger();if(fixed.remapped.length>0)log.info(`Repointed ${fixed.remapped.length} ledger row(s) at their renumbered file.`);if(fixed.recorded.length>0)log.info(`Recorded ${fixed.recorded.length} migration(s) already present in the schema.`);if(fixed.skipped.length>0)log.warn(`${fixed.skipped.length} ledger entr(ies) need a look \u2014 run \`./buddy migrate:status\`.`)}log.info("Review the change with `git diff`, then run `./buddy migrate`.");await outro("Regenerated.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:status","Compare database/migrations, the migrations ledger, and the live schema").option("--reconcile","Repair the ledger where the schema proves what happened",{default:!1}).option("--include-partial","With --reconcile, also record half-applied migrations",{default:!1}).option("--json","Emit the audit as JSON",{default:!1}).action(async(options)=>{const perf=options.json?void 0:await intro("buddy migrate:status"),{auditMigrationLedger,reconcileMigrationLedger}=await import("@stacksjs/database"),audit=await auditMigrationLedger();if(options.json){console.log(JSON.stringify(audit,(_k,v)=>v instanceof Set?[...v]:v,2));process.exit(audit.drift?ExitCode.FatalError:ExitCode.Success)}if(!audit.supported){log.info(`Dialect "${audit.dialect}" is not audited. Nothing to compare.`);await outro("Skipped.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}const{counts,entries,orphans}=audit,list=(status)=>entries.filter((e)=>e.status===status).map((e)=>e.file),report=[],section=(heading,files)=>{if(files.length===0)return;if(heading)report.push(` ${heading}`);for(const file of files.slice(0,8))report.push(` ${file}`);if(files.length>8)report.push(` \u2026 +${files.length-8} more`);report.push("")};report.push("");report.push(` Migration status: ${audit.dialect}`);report.push(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");report.push(` ${entries.length} file(s) on disk \xB7 ${audit.recordedCount} recorded in the ledger`);report.push("");if(counts.applied>0)report.push(` ${counts.applied} applied - recorded, and present in the schema.`,"");if(counts.unverifiable>0)report.push(` ${counts.unverifiable} unverifiable - data migrations with no schema trace to check.`,"");section(`${counts.pending} pending - not applied yet, will run on the next \`buddy migrate\`:`,list("pending"));if(counts.stranded>0){report.push(` ${counts.stranded} STRANDED - already applied to the schema, but missing from the ledger.`);report.push(" These re-run on the next `buddy migrate`, which is unsafe for anything not idempotent.");section("",list("stranded"))}section(`${counts.partial} PARTIAL - some effects present, some missing. Needs a human:`,list("partial"));section(`${counts.reverted} REVERTED - recorded as applied, but the effects are gone from the schema:`,list("reverted"));if(orphans.length>0)section(`${orphans.length} orphaned ledger row(s) - recorded, but no such file on disk:`,orphans.map((o)=>`${o.migration}${o.renamedTo?` -> renumbered to ${o.renamedTo}`:" (no counterpart; migration deleted?)"}`));console.log(report.join(`
|
|
53
|
+
`));if(!audit.drift){await outro("Ledger matches the corpus and the schema.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}if(!options.reconcile){console.log(`
|
|
633
54
|
Repair with: ./buddy migrate:status --reconcile
|
|
634
55
|
That repoints renumbered ledger rows and records migrations the schema
|
|
635
56
|
already proves. It never runs SQL from a migration file.
|
|
636
|
-
`);
|
|
637
|
-
|
|
638
|
-
process.exit(ExitCode.
|
|
639
|
-
}
|
|
640
|
-
const fixed = await reconcileMigrationLedger({ includePartial: options.includePartial });
|
|
641
|
-
if (fixed.remapped.length > 0)
|
|
642
|
-
log.success(`Repointed ${fixed.remapped.length} ledger row(s) at their renumbered file.`);
|
|
643
|
-
if (fixed.recorded.length > 0)
|
|
644
|
-
log.success(`Recorded ${fixed.recorded.length} migration(s) the schema already reflects.`);
|
|
645
|
-
if (fixed.skipped.length > 0) {
|
|
646
|
-
log.warn(`Left ${fixed.skipped.length} entr(ies) alone:`);
|
|
647
|
-
console.log(fixed.skipped.slice(0, 8).map((s) => ` ${s.file} \u2014 ${s.reason}`).join(`
|
|
648
|
-
`) + (fixed.skipped.length > 8 ? `
|
|
649
|
-
\u2026 +${fixed.skipped.length - 8} more` : ""));
|
|
650
|
-
}
|
|
651
|
-
if (fixed.remapped.length === 0 && fixed.recorded.length === 0)
|
|
652
|
-
log.info("Nothing could be repaired automatically.");
|
|
653
|
-
await outro("Reconciled.", { startTime: perf, useSeconds: !0 });
|
|
654
|
-
process.exit(ExitCode.Success);
|
|
655
|
-
});
|
|
656
|
-
onUnknownSubcommand(buddy, "migrate");
|
|
657
|
-
}
|
|
57
|
+
`);await outro("Drift detected.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.FatalError)}const fixed=await reconcileMigrationLedger({includePartial:options.includePartial});if(fixed.remapped.length>0)log.success(`Repointed ${fixed.remapped.length} ledger row(s) at their renumbered file.`);if(fixed.recorded.length>0)log.success(`Recorded ${fixed.recorded.length} migration(s) the schema already reflects.`);if(fixed.skipped.length>0){log.warn(`Left ${fixed.skipped.length} entr(ies) alone:`);console.log(fixed.skipped.slice(0,8).map((s)=>` ${s.file} \u2014 ${s.reason}`).join(`
|
|
58
|
+
`)+(fixed.skipped.length>8?`
|
|
59
|
+
\u2026 +${fixed.skipped.length-8} more`:""))}if(fixed.remapped.length===0&&fixed.recorded.length===0)log.info("Nothing could be repaired automatically.");await outro("Reconciled.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});onUnknownSubcommand(buddy,"migrate")}
|