@stacksjs/buddy 0.70.88 → 0.70.90
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 +1 -0
- package/dist/cli.js +165 -0
- package/dist/commands/about.d.ts +2 -0
- package/dist/commands/about.js +35 -0
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +33 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +68 -0
- package/dist/commands/build.d.ts +2 -0
- package/dist/commands/build.js +153 -0
- package/dist/commands/cd.d.ts +20 -0
- package/dist/commands/cd.js +39 -0
- package/dist/commands/changelog.d.ts +2 -0
- package/dist/commands/changelog.js +32 -0
- package/dist/commands/clean.d.ts +2 -0
- package/dist/commands/clean.js +37 -0
- package/dist/commands/cloud.d.ts +2 -0
- package/dist/commands/cloud.js +503 -0
- package/dist/commands/commit.d.ts +2 -0
- package/dist/commands/commit.js +16 -0
- package/dist/commands/completion.d.ts +2 -0
- package/dist/commands/completion.js +143 -0
- package/dist/commands/config-migrate.d.ts +26 -0
- package/dist/commands/config-migrate.js +94 -0
- package/dist/commands/configure.d.ts +2 -0
- package/dist/commands/configure.js +48 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.js +141 -0
- package/dist/commands/deploy.d.ts +107 -0
- package/dist/commands/deploy.js +1439 -0
- package/dist/commands/dev.d.ts +10 -0
- package/dist/commands/dev.js +907 -0
- package/dist/commands/dns.d.ts +2 -0
- package/dist/commands/dns.js +102 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +285 -0
- package/dist/commands/domains.d.ts +2 -0
- package/dist/commands/domains.js +159 -0
- package/dist/commands/email.d.ts +2 -0
- package/dist/commands/email.js +524 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/commands/env.js +246 -0
- package/dist/commands/extension.d.ts +8 -0
- package/dist/commands/extension.js +78 -0
- package/dist/commands/features.d.ts +180 -0
- package/dist/commands/features.js +422 -0
- package/dist/commands/fresh.d.ts +2 -0
- package/dist/commands/fresh.js +39 -0
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.js +104 -0
- package/dist/commands/http.d.ts +2 -0
- package/dist/commands/http.js +33 -0
- package/dist/commands/index.d.ts +53 -0
- package/dist/commands/index.js +53 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +22 -0
- package/dist/commands/key.d.ts +2 -0
- package/dist/commands/key.js +23 -0
- package/dist/commands/lint.d.ts +2 -0
- package/dist/commands/lint.js +46 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.js +108 -0
- package/dist/commands/mail.d.ts +2 -0
- package/dist/commands/mail.js +693 -0
- package/dist/commands/maintenance.d.ts +2 -0
- package/dist/commands/maintenance.js +141 -0
- package/dist/commands/make.d.ts +2 -0
- package/dist/commands/make.js +375 -0
- package/dist/commands/migrate-project.d.ts +2 -0
- package/dist/commands/migrate-project.js +49 -0
- package/dist/commands/migrate.d.ts +4 -0
- package/dist/commands/migrate.js +455 -0
- package/dist/commands/outdated.d.ts +2 -0
- package/dist/commands/outdated.js +19 -0
- package/dist/commands/package.d.ts +2 -0
- package/dist/commands/package.js +17 -0
- package/dist/commands/phone.d.ts +2 -0
- package/dist/commands/phone.js +188 -0
- package/dist/commands/ports.d.ts +2 -0
- package/dist/commands/ports.js +118 -0
- package/dist/commands/prepublish.d.ts +2 -0
- package/dist/commands/prepublish.js +17 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +29 -0
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +172 -0
- package/dist/commands/queue.d.ts +2 -0
- package/dist/commands/queue.js +248 -0
- package/dist/commands/release.d.ts +2 -0
- package/dist/commands/release.js +30 -0
- package/dist/commands/route.d.ts +2 -0
- package/dist/commands/route.js +21 -0
- package/dist/commands/saas.d.ts +2 -0
- package/dist/commands/saas.js +25 -0
- package/dist/commands/schedule.d.ts +2 -0
- package/dist/commands/schedule.js +61 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +84 -0
- package/dist/commands/seed.d.ts +2 -0
- package/dist/commands/seed.js +71 -0
- package/dist/commands/serve.d.ts +25 -0
- package/dist/commands/serve.js +182 -0
- package/dist/commands/setup.d.ts +12 -0
- package/dist/commands/setup.js +215 -0
- package/dist/commands/share.d.ts +2 -0
- package/dist/commands/share.js +214 -0
- package/dist/commands/sms.d.ts +2 -0
- package/dist/commands/sms.js +328 -0
- package/dist/commands/stacks.d.ts +2 -0
- package/dist/commands/stacks.js +69 -0
- package/dist/commands/telemetry.d.ts +2 -0
- package/dist/commands/telemetry.js +74 -0
- package/dist/commands/test.d.ts +2 -0
- package/dist/commands/test.js +130 -0
- package/dist/commands/tinker.d.ts +2 -0
- package/dist/commands/tinker.js +37 -0
- package/dist/commands/types.d.ts +2 -0
- package/dist/commands/types.js +18 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +98 -0
- package/dist/commands/version.d.ts +2 -0
- package/dist/commands/version.js +16 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +223 -0
- package/dist/custom-cli.d.ts +1 -0
- package/dist/custom-cli.js +23 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lazy-commands.d.ts +61 -0
- package/dist/lazy-commands.js +188 -0
- package/dist/migrators/index.d.ts +9 -0
- package/dist/migrators/index.js +62 -0
- package/dist/migrators/laravel/index.d.ts +2 -0
- package/dist/migrators/laravel/index.js +148 -0
- package/dist/migrators/laravel/migrations.d.ts +52 -0
- package/dist/migrators/laravel/migrations.js +232 -0
- package/dist/migrators/laravel/models.d.ts +34 -0
- package/dist/migrators/laravel/models.js +134 -0
- package/dist/migrators/rails/index.d.ts +2 -0
- package/dist/migrators/rails/index.js +11 -0
- package/dist/migrators/types.d.ts +44 -0
- package/dist/migrators/types.js +0 -0
- package/package.json +2 -2
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { confirm, intro, log, onUnknownSubcommand, outro, text } from "@stacksjs/cli";
|
|
4
|
+
import { Action } from "@stacksjs/enums";
|
|
5
|
+
import { hasTTY, isCI } from "@stacksjs/env";
|
|
6
|
+
import { appPath, frameworkPath, projectPath } from "@stacksjs/path";
|
|
7
|
+
import { ExitCode } from "@stacksjs/types";
|
|
8
|
+
let _runAction;
|
|
9
|
+
async function runAction(...args) {
|
|
10
|
+
if (!_runAction)
|
|
11
|
+
_runAction = (await import("@stacksjs/actions")).runAction;
|
|
12
|
+
return _runAction(...args);
|
|
13
|
+
}
|
|
14
|
+
function acquireMigrationLock() {
|
|
15
|
+
const lockDir = projectPath(".stacks"), lockFile = `${lockDir}/migrations.lock`;
|
|
16
|
+
try {
|
|
17
|
+
if (!existsSync(lockDir))
|
|
18
|
+
mkdirSync(lockDir, { recursive: !0 });
|
|
19
|
+
const fd = openSync(lockFile, "wx");
|
|
20
|
+
try {
|
|
21
|
+
closeSync(fd);
|
|
22
|
+
} catch {}
|
|
23
|
+
return {
|
|
24
|
+
acquired: !0,
|
|
25
|
+
release: () => {
|
|
26
|
+
try {
|
|
27
|
+
rmSync(lockFile, { force: !0 });
|
|
28
|
+
} catch {}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
} catch {
|
|
32
|
+
return {
|
|
33
|
+
acquired: !1,
|
|
34
|
+
release: () => {}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function readMigrateMarker() {
|
|
39
|
+
const file = projectPath(".stacks/last-migrate-result.json");
|
|
40
|
+
if (!existsSync(file))
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
const raw = readFileSync(file, "utf8"), parsed = JSON.parse(raw), n = typeof parsed.appliedCount === "number" ? parsed.appliedCount : null;
|
|
44
|
+
if (n === null || !Number.isFinite(n))
|
|
45
|
+
return null;
|
|
46
|
+
return { appliedCount: Math.max(0, Math.floor(n)) };
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
} finally {
|
|
50
|
+
try {
|
|
51
|
+
rmSync(file, { force: !0 });
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function countModelFiles(dir) {
|
|
56
|
+
if (!existsSync(dir))
|
|
57
|
+
return 0;
|
|
58
|
+
let count = 0;
|
|
59
|
+
const entries = readdirSync(dir, { withFileTypes: !0 });
|
|
60
|
+
for (const entry of entries)
|
|
61
|
+
if (entry.isDirectory())
|
|
62
|
+
count += countModelFiles(`${dir}/${entry.name}`);
|
|
63
|
+
else if (entry.name.endsWith(".ts") && !entry.name.startsWith(".") && !entry.name.startsWith("index"))
|
|
64
|
+
count++;
|
|
65
|
+
return count;
|
|
66
|
+
}
|
|
67
|
+
function validateModelsExist() {
|
|
68
|
+
const userModelsPath = appPath("Models"), defaultModelsPath = frameworkPath("defaults/app/Models"), userModelCount = countModelFiles(userModelsPath), defaultModelCount = countModelFiles(defaultModelsPath);
|
|
69
|
+
if (userModelCount === 0 && defaultModelCount === 0)
|
|
70
|
+
return {
|
|
71
|
+
valid: !1,
|
|
72
|
+
error: "No models found. Please create models in app/Models or ensure framework defaults exist."
|
|
73
|
+
};
|
|
74
|
+
return { valid: !0 };
|
|
75
|
+
}
|
|
76
|
+
async function reportMissingForeignKeys() {
|
|
77
|
+
try {
|
|
78
|
+
const { auditForeignKeys } = await import("@stacksjs/database"), result = await auditForeignKeys();
|
|
79
|
+
if (result.missing.length === 0)
|
|
80
|
+
return;
|
|
81
|
+
const sample = result.missing.slice(0, 5).map((fk) => ` \u2022 ${fk.fromTable}.${fk.fromColumn} \u2192 ${fk.toTable}.${fk.toColumn} (${fk.model})`).join(`
|
|
82
|
+
`), more = result.missing.length > 5 ? `
|
|
83
|
+
+ ${result.missing.length - 5} more \u2014 run \`./buddy doctor\` for the full list.` : "";
|
|
84
|
+
log.warn(`${result.missing.length} of ${result.declared.length} declared foreign keys are missing from the live schema:
|
|
85
|
+
${sample}${more}
|
|
86
|
+
` + "If you just flipped DB_CONNECTION, the FK ALTER migrations may be sitting on disk but unapplied \u2014 run `./buddy migrate:fresh` against the new database (will reset data) or replay the alter-*.sql files manually.");
|
|
87
|
+
} catch (err) {
|
|
88
|
+
log.debug(`[migrate] FK integrity check skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function reportFkOrphans() {
|
|
92
|
+
try {
|
|
93
|
+
const { findFkOrphans } = await import("@stacksjs/database"), result = await findFkOrphans();
|
|
94
|
+
if (!result.supported || result.total === 0)
|
|
95
|
+
return;
|
|
96
|
+
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(`
|
|
97
|
+
`), more = result.orphans.length > 5 ? `
|
|
98
|
+
+ ${result.orphans.length - 5} more \u2014 run \`./buddy doctor\` for the full list.` : "", first = result.orphans[0];
|
|
99
|
+
log.warn(`${result.total} row${result.total === 1 ? "" : "s"} violate foreign keys (orphaned parents), now that SQLite enforces \`foreign_keys = ON\`:
|
|
100
|
+
${sample}${more}
|
|
101
|
+
` + "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.`);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
log.debug(`[migrate] FK orphan scan skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function describeOp(op) {
|
|
107
|
+
switch (op.kind) {
|
|
108
|
+
case "drop_table":
|
|
109
|
+
return `drop table "${op.table}" (all rows lost)`;
|
|
110
|
+
case "drop_column":
|
|
111
|
+
return `drop column "${op.table}"."${op.column}" (column data lost)`;
|
|
112
|
+
case "modify_column":
|
|
113
|
+
return `change type of "${op.table}"."${op.column}" (possible data loss)`;
|
|
114
|
+
case "rebuild_table":
|
|
115
|
+
return `rebuild table "${op.table}" (type/constraint change)`;
|
|
116
|
+
case "rename_column":
|
|
117
|
+
return `rename "${op.table}"."${op.from}" \u2192 "${op.to}"`;
|
|
118
|
+
case "rename_table":
|
|
119
|
+
return `rename table "${op.from}" \u2192 "${op.to}"`;
|
|
120
|
+
default:
|
|
121
|
+
return `${op.kind} on "${op.table}"${op.column ? `."${op.column}"` : ""}`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function confirmDestructiveMigrations(opts) {
|
|
125
|
+
let operations = [];
|
|
126
|
+
try {
|
|
127
|
+
const { previewPendingMigrations } = await import("@stacksjs/database");
|
|
128
|
+
operations = await previewPendingMigrations({ fromDb: opts.fromDb, applyRenames: opts.applyRenames });
|
|
129
|
+
} catch (error) {
|
|
130
|
+
log.debug(`Migration preview unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
131
|
+
return !0;
|
|
132
|
+
}
|
|
133
|
+
const renames = operations.filter((o) => o.kind === "rename_column" || o.kind === "rename_table");
|
|
134
|
+
for (const r of renames)
|
|
135
|
+
log.info(`Detected ${describeOp(r)} \u2014 applying as a rename (data preserved). Use --no-rename to drop + add instead.`);
|
|
136
|
+
const destructive = operations.filter((o) => o.destructive);
|
|
137
|
+
if (destructive.length === 0)
|
|
138
|
+
return !0;
|
|
139
|
+
log.warn(`This migration includes ${destructive.length} potentially destructive change${destructive.length === 1 ? "" : "s"}:`);
|
|
140
|
+
for (const op of destructive)
|
|
141
|
+
log.warn(` \u2022 ${describeOp(op)}`);
|
|
142
|
+
if (opts.force)
|
|
143
|
+
return !0;
|
|
144
|
+
if (isCI || !hasTTY) {
|
|
145
|
+
log.error("Refusing to apply destructive changes in a non-interactive environment. Re-run with --force to proceed.");
|
|
146
|
+
return !1;
|
|
147
|
+
}
|
|
148
|
+
return confirm({ message: "Apply these destructive changes?", initial: !1 });
|
|
149
|
+
}
|
|
150
|
+
function parseGuardBool(raw) {
|
|
151
|
+
if (raw == null || raw === "")
|
|
152
|
+
return;
|
|
153
|
+
const v = raw.toLowerCase().trim();
|
|
154
|
+
if (v === "1" || v === "true" || v === "yes" || v === "on")
|
|
155
|
+
return !0;
|
|
156
|
+
if (v === "0" || v === "false" || v === "no" || v === "off")
|
|
157
|
+
return !1;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
function parseFreshGuard(raw) {
|
|
161
|
+
const v = raw?.toLowerCase().trim();
|
|
162
|
+
return v === "allow" || v === "confirm" || v === "disabled" ? v : void 0;
|
|
163
|
+
}
|
|
164
|
+
async function resolveMigrationGuards() {
|
|
165
|
+
const isProd = /^prod/i.test(process.env.APP_ENV || "local");
|
|
166
|
+
let cfg = {};
|
|
167
|
+
try {
|
|
168
|
+
const { awaitConfig } = await import("@stacksjs/config");
|
|
169
|
+
cfg = (await awaitConfig()).database?.safety ?? {};
|
|
170
|
+
} catch (error) {
|
|
171
|
+
log.debug(`[migrate] safety config unavailable, using defaults: ${error instanceof Error ? error.message : String(error)}`);
|
|
172
|
+
}
|
|
173
|
+
const confirmMigrate = parseGuardBool(process.env.DB_MIGRATE_CONFIRM) ?? cfg.confirmMigrate ?? !0, migrateFresh = parseFreshGuard(process.env.DB_MIGRATE_FRESH) ?? cfg.migrateFresh ?? (isProd ? "disabled" : "allow");
|
|
174
|
+
return { confirmMigrate, migrateFresh };
|
|
175
|
+
}
|
|
176
|
+
function currentDatabaseLabel() {
|
|
177
|
+
if ((process.env.DB_CONNECTION || "sqlite").toLowerCase() === "sqlite")
|
|
178
|
+
return process.env.DB_DATABASE_PATH || "database/stacks.sqlite";
|
|
179
|
+
return process.env.DB_DATABASE || "stacks";
|
|
180
|
+
}
|
|
181
|
+
export function migrate(buddy) {
|
|
182
|
+
const descriptions = {
|
|
183
|
+
migrate: "Migrates your database",
|
|
184
|
+
project: "Target a specific project",
|
|
185
|
+
verbose: "Enable verbose output",
|
|
186
|
+
auth: "Also migrate auth tables (oauth_clients, oauth_access_tokens, oauth_refresh_tokens, password_resets)",
|
|
187
|
+
force: "Apply destructive changes (drop column/table, lossy type change) without confirmation",
|
|
188
|
+
fromDb: "Diff against the live database schema instead of the snapshot (self-heal drift)",
|
|
189
|
+
noRename: "Treat renamed columns as drop + add instead of a data-preserving rename"
|
|
190
|
+
};
|
|
191
|
+
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("--from-db", descriptions.fromDb, { default: !1 }).option("--no-rename", descriptions.noRename).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
192
|
+
log.debug("Running `buddy migrate` ...", options);
|
|
193
|
+
const perf = await intro("buddy migrate"), validation = validateModelsExist();
|
|
194
|
+
if (!validation.valid) {
|
|
195
|
+
console.error(`
|
|
196
|
+
\u274C Error: ${validation.error}
|
|
197
|
+
`);
|
|
198
|
+
process.exit(ExitCode.FatalError);
|
|
199
|
+
}
|
|
200
|
+
const applyRenames = options.rename === !1 ? !1 : void 0;
|
|
201
|
+
if (options.fromDb)
|
|
202
|
+
process.env.STACKS_MIGRATE_FROM_DB = "1";
|
|
203
|
+
if (applyRenames === !1)
|
|
204
|
+
process.env.STACKS_MIGRATE_NO_RENAME = "1";
|
|
205
|
+
if (options.diff) {
|
|
206
|
+
try {
|
|
207
|
+
const { previewPendingMigrations } = await import("@stacksjs/database"), ops = await previewPendingMigrations({ fromDb: options.fromDb, applyRenames });
|
|
208
|
+
if (ops.length === 0)
|
|
209
|
+
log.info("No pending schema changes \u2014 your models match the database.");
|
|
210
|
+
else {
|
|
211
|
+
log.info(`${ops.length} pending change${ops.length === 1 ? "" : "s"}:`);
|
|
212
|
+
for (const op of ops)
|
|
213
|
+
log.info(` \u2022 ${describeOp(op)}${op.destructive ? " [destructive]" : ""}`);
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
log.error("Failed to preview migrations:", error);
|
|
217
|
+
}
|
|
218
|
+
await outro("Diff complete \u2014 no changes applied.", { startTime: perf, useSeconds: !0 });
|
|
219
|
+
process.exit(ExitCode.Success);
|
|
220
|
+
}
|
|
221
|
+
if ((await resolveMigrationGuards()).confirmMigrate && !options.force)
|
|
222
|
+
if (isCI || !hasTTY)
|
|
223
|
+
log.debug("[migrate] confirmMigrate guard skipped \u2014 non-interactive environment.");
|
|
224
|
+
else {
|
|
225
|
+
const APP_ENV = process.env.APP_ENV || "local";
|
|
226
|
+
if (!await confirm({
|
|
227
|
+
message: `Run migrations against the ${APP_ENV} database "${currentDatabaseLabel()}"?`,
|
|
228
|
+
initial: !0
|
|
229
|
+
})) {
|
|
230
|
+
await outro("Migration cancelled \u2014 no changes applied.", { startTime: perf, useSeconds: !0 });
|
|
231
|
+
process.exit(ExitCode.Success);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const lock = acquireMigrationLock();
|
|
235
|
+
if (!lock.acquired) {
|
|
236
|
+
log.error("Another migration is already running (.stacks/migrations.lock exists). Wait for it to finish, or remove the lockfile if it is stale.");
|
|
237
|
+
process.exit(ExitCode.FatalError);
|
|
238
|
+
}
|
|
239
|
+
if (!await confirmDestructiveMigrations({ force: options.force, fromDb: options.fromDb, applyRenames })) {
|
|
240
|
+
lock.release();
|
|
241
|
+
await outro("Migration cancelled \u2014 no changes applied.", { startTime: perf, useSeconds: !0 });
|
|
242
|
+
process.exit(ExitCode.Success);
|
|
243
|
+
}
|
|
244
|
+
if (options.auth !== !1) {
|
|
245
|
+
log.debug("Migrating auth tables...");
|
|
246
|
+
try {
|
|
247
|
+
const { migrateAuthTables, migrateNotificationTables, migrateRbacTables } = await import("@stacksjs/database"), authResult = await migrateAuthTables({ verbose: options.verbose });
|
|
248
|
+
if (!authResult.success)
|
|
249
|
+
log.error(`Failed to migrate auth tables: ${authResult.error}`);
|
|
250
|
+
const notifResult = await migrateNotificationTables({ verbose: options.verbose });
|
|
251
|
+
if (!notifResult.success)
|
|
252
|
+
log.error(`Failed to migrate notification tables: ${notifResult.error}`);
|
|
253
|
+
const rbacResult = await migrateRbacTables({ verbose: options.verbose });
|
|
254
|
+
if (!rbacResult.success)
|
|
255
|
+
log.error(`Failed to migrate RBAC tables: ${rbacResult.error}`);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
log.error("Failed to migrate auth/notification/RBAC tables:", error);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const result = await runAction(Action.Migrate, options).finally(() => lock.release());
|
|
261
|
+
if (result.isErr)
|
|
262
|
+
log.error("Model migrations failed.");
|
|
263
|
+
if (result.isErr) {
|
|
264
|
+
await outro("While running the migrate command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
265
|
+
process.exit(ExitCode.FatalError);
|
|
266
|
+
}
|
|
267
|
+
if (options.auth !== !1)
|
|
268
|
+
try {
|
|
269
|
+
const { ensureUsersAuthColumns, sqlHelpers } = await import("@stacksjs/database"), driver = process.env.DB_CONNECTION || "sqlite";
|
|
270
|
+
await ensureUsersAuthColumns(sqlHelpers(driver), { verbose: options.verbose });
|
|
271
|
+
} catch (error) {
|
|
272
|
+
log.error("Failed to ensure users auth columns post-migration:", error);
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const { ensureUuidColumns, sqlHelpers } = await import("@stacksjs/database"), driver = process.env.DB_CONNECTION || "sqlite";
|
|
276
|
+
await ensureUuidColumns(sqlHelpers(driver), { verbose: options.verbose });
|
|
277
|
+
} catch (error) {
|
|
278
|
+
log.error("Failed to ensure uuid columns post-migration:", error);
|
|
279
|
+
}
|
|
280
|
+
await reportMissingForeignKeys();
|
|
281
|
+
await reportFkOrphans();
|
|
282
|
+
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}`;
|
|
283
|
+
await outro(outroMessage, {
|
|
284
|
+
startTime: perf,
|
|
285
|
+
useSeconds: !0
|
|
286
|
+
});
|
|
287
|
+
process.exit(ExitCode.Success);
|
|
288
|
+
});
|
|
289
|
+
buddy.command("migrate:fresh", descriptions.migrate).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("-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) => {
|
|
290
|
+
log.debug("Running `buddy migrate:fresh` ...", options);
|
|
291
|
+
const perf = await intro("buddy migrate:fresh"), validation = validateModelsExist();
|
|
292
|
+
if (!validation.valid) {
|
|
293
|
+
console.error(`
|
|
294
|
+
\u274C Error: ${validation.error}
|
|
295
|
+
`);
|
|
296
|
+
process.exit(ExitCode.FatalError);
|
|
297
|
+
}
|
|
298
|
+
const guards = await resolveMigrationGuards(), dbLabel = currentDatabaseLabel(), APP_ENV = process.env.APP_ENV || "local";
|
|
299
|
+
if (guards.migrateFresh === "disabled") {
|
|
300
|
+
log.error(`\`buddy migrate:fresh\` is disabled by your migration safety guards (it DROPS every table).
|
|
301
|
+
Target: ${APP_ENV} database "${dbLabel}"
|
|
302
|
+
To allow it, set database.safety.migrateFresh to 'allow' in config/database.ts,
|
|
303
|
+
or run once with: DB_MIGRATE_FRESH=allow ./buddy migrate:fresh`);
|
|
304
|
+
await outro('migrate:fresh refused \u2014 the migrateFresh guard is set to "disabled".', { startTime: perf, useSeconds: !0 });
|
|
305
|
+
process.exit(ExitCode.FatalError);
|
|
306
|
+
}
|
|
307
|
+
if (!(guards.migrateFresh === "allow" && options.force === !0)) {
|
|
308
|
+
if (isCI || !hasTTY) {
|
|
309
|
+
const hint = guards.migrateFresh === "confirm" ? 'Guard is "confirm": migrate:fresh must be run interactively.' : "Re-run with --force to drop the database non-interactively.";
|
|
310
|
+
log.error(`Refusing to drop the ${APP_ENV} database "${dbLabel}" in a non-interactive environment. ${hint}`);
|
|
311
|
+
await outro("migrate:fresh cancelled.", { startTime: perf, useSeconds: !0 });
|
|
312
|
+
process.exit(ExitCode.FatalError);
|
|
313
|
+
}
|
|
314
|
+
log.warn(`This will DROP ALL TABLES in the ${APP_ENV} database "${dbLabel}" and rebuild them from scratch. All data will be lost.`);
|
|
315
|
+
if ((await text({ message: `Type the database name "${dbLabel}" to confirm (blank to cancel):` })).trim() !== dbLabel) {
|
|
316
|
+
await outro("migrate:fresh cancelled \u2014 confirmation did not match.", { startTime: perf, useSeconds: !0 });
|
|
317
|
+
process.exit(ExitCode.Success);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const result = await runAction(Action.MigrateFresh, options);
|
|
321
|
+
if (result.isErr)
|
|
322
|
+
log.error("Model migrations failed \u2014 applying auth/notification/RBAC table guarantees before exiting.");
|
|
323
|
+
if (options.auth !== !1) {
|
|
324
|
+
log.debug("Migrating auth tables...");
|
|
325
|
+
try {
|
|
326
|
+
const { migrateAuthTables, migrateNotificationTables, migrateRbacTables } = await import("@stacksjs/database"), authResult = await migrateAuthTables({ verbose: options.verbose });
|
|
327
|
+
if (!authResult.success)
|
|
328
|
+
log.error(`Failed to migrate auth tables: ${authResult.error}`);
|
|
329
|
+
const notifResult = await migrateNotificationTables({ verbose: options.verbose });
|
|
330
|
+
if (!notifResult.success)
|
|
331
|
+
log.error(`Failed to migrate notification tables: ${notifResult.error}`);
|
|
332
|
+
const rbacResult = await migrateRbacTables({ verbose: options.verbose });
|
|
333
|
+
if (!rbacResult.success)
|
|
334
|
+
log.error(`Failed to migrate RBAC tables: ${rbacResult.error}`);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
log.error("Failed to migrate auth/notification/RBAC tables:", error);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
const { ensureUuidColumns, sqlHelpers } = await import("@stacksjs/database"), driver = process.env.DB_CONNECTION || "sqlite";
|
|
341
|
+
await ensureUuidColumns(sqlHelpers(driver), { verbose: options.verbose });
|
|
342
|
+
} catch (error) {
|
|
343
|
+
log.error("Failed to ensure uuid columns post-migration:", error);
|
|
344
|
+
}
|
|
345
|
+
if (result.isErr) {
|
|
346
|
+
await outro("While running the migrate:fresh command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
347
|
+
process.exit(ExitCode.FatalError);
|
|
348
|
+
}
|
|
349
|
+
await reportMissingForeignKeys();
|
|
350
|
+
if (options.seed) {
|
|
351
|
+
log.info("Running database seeders...");
|
|
352
|
+
try {
|
|
353
|
+
const { seed } = await import("@stacksjs/database"), seedResult = await seed({ verbose: options.verbose, fresh: !0 });
|
|
354
|
+
if (seedResult.failed > 0) {
|
|
355
|
+
log.warn(`Seeding completed with ${seedResult.failed} failure(s)`);
|
|
356
|
+
for (const r of seedResult.results)
|
|
357
|
+
if (!r.success)
|
|
358
|
+
log.error(` - ${r.model}: ${r.error}`);
|
|
359
|
+
} else
|
|
360
|
+
log.success(`Seeded ${seedResult.successful} model(s)`);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
log.error("Failed to run seeders:", error);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const parts = [];
|
|
366
|
+
if (options.auth !== !1)
|
|
367
|
+
parts.push("auth tables");
|
|
368
|
+
if (options.seed)
|
|
369
|
+
parts.push("seeded");
|
|
370
|
+
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)`;
|
|
371
|
+
await outro(`All tables dropped successfully & migrated successfully${countPhrase}${suffix}`, {
|
|
372
|
+
startTime: perf,
|
|
373
|
+
useSeconds: !0
|
|
374
|
+
});
|
|
375
|
+
process.exit(ExitCode.Success);
|
|
376
|
+
});
|
|
377
|
+
buddy.command("migrate:dns", descriptions.migrate).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
378
|
+
log.debug("Running `buddy migrate:dns` ...", options);
|
|
379
|
+
const perf = await intro("buddy migrate:dns"), result = await runAction(Action.MigrateDns, { ...options });
|
|
380
|
+
if (result.isErr) {
|
|
381
|
+
await outro("While running the migrate:dns command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
382
|
+
process.exit(ExitCode.FatalError);
|
|
383
|
+
}
|
|
384
|
+
const APP_URL = process.env.APP_URL || "undefined";
|
|
385
|
+
await outro(`Migrated your ${APP_URL} DNS.`, {
|
|
386
|
+
startTime: perf,
|
|
387
|
+
useSeconds: !0
|
|
388
|
+
});
|
|
389
|
+
process.exit(ExitCode.Success);
|
|
390
|
+
});
|
|
391
|
+
buddy.command("migrate:switch <driver>", "Pre-flight check + plan for switching DB_CONNECTION between sqlite / mysql / postgres").action(async (driver) => {
|
|
392
|
+
log.debug(`Running \`buddy migrate:switch ${driver}\` ...`);
|
|
393
|
+
const perf = await intro("buddy migrate:switch"), target = driver.toLowerCase();
|
|
394
|
+
if (!new Set(["sqlite", "mysql", "postgres"]).has(target)) {
|
|
395
|
+
console.log(`
|
|
396
|
+
Unknown target driver "${driver}". Allowed: sqlite, mysql, postgres.
|
|
397
|
+
`);
|
|
398
|
+
await outro("Aborted.", { startTime: perf, useSeconds: !0 });
|
|
399
|
+
process.exit(ExitCode.FatalError);
|
|
400
|
+
}
|
|
401
|
+
const current = (process.env.DB_CONNECTION || "sqlite").toLowerCase();
|
|
402
|
+
if (current === target) {
|
|
403
|
+
console.log(`
|
|
404
|
+
DB_CONNECTION is already "${target}". Nothing to switch.
|
|
405
|
+
`);
|
|
406
|
+
await outro("No-op.", { startTime: perf, useSeconds: !0 });
|
|
407
|
+
process.exit(ExitCode.Success);
|
|
408
|
+
}
|
|
409
|
+
const missingEnv = ({
|
|
410
|
+
sqlite: ["DB_DATABASE"],
|
|
411
|
+
mysql: ["DB_HOST", "DB_PORT", "DB_USERNAME", "DB_PASSWORD", "DB_DATABASE"],
|
|
412
|
+
postgres: ["DB_HOST", "DB_PORT", "DB_USERNAME", "DB_PASSWORD", "DB_DATABASE"]
|
|
413
|
+
}[target] ?? []).filter((k) => !process.env[k]);
|
|
414
|
+
let alterCount = 0, uniqueIdxCount = 0;
|
|
415
|
+
try {
|
|
416
|
+
const migrationsDir = projectPath("database/migrations");
|
|
417
|
+
if (existsSync(migrationsDir)) {
|
|
418
|
+
const files = readdirSync(migrationsDir).filter((f) => f.endsWith(".sql"));
|
|
419
|
+
for (const f of files) {
|
|
420
|
+
const content = readFileSync(`${migrationsDir}/${f}`, "utf-8").toLowerCase();
|
|
421
|
+
if (/alter\s+table[\s\S]*add\s+constraint/.test(content))
|
|
422
|
+
alterCount++;
|
|
423
|
+
if (/^\s*create\s+unique\s+index/m.test(content))
|
|
424
|
+
uniqueIdxCount++;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
} catch {}
|
|
428
|
+
const sqliteFkNote = current === "sqlite" && alterCount > 0 ? `
|
|
429
|
+
(These were skipped on SQLite per stacksjs/stacks#1916 and survive on disk for replay.)` : "", boolNote = current === "sqlite" && (target === "postgres" || target === "mysql") ? `
|
|
430
|
+
\u2022 Booleans land as 0/1 on SQLite; ${target} stores them as ${target === "postgres" ? "true/false" : "0/1 (compatible)"}.` : "", tzNote = target === "postgres" ? `
|
|
431
|
+
\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 ? `
|
|
432
|
+
\u26A0 Missing env vars for ${target}: ${missingEnv.join(", ")}` : "";
|
|
433
|
+
console.log(`
|
|
434
|
+
Switch plan: ${current} \u2192 ${target}
|
|
435
|
+
\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
|
|
436
|
+
\u2022 ${alterCount} ALTER TABLE ADD CONSTRAINT migration(s) will be applied against ${target}.${sqliteFkNote}
|
|
437
|
+
\u2022 ${uniqueIdxCount} CREATE UNIQUE INDEX migration(s) will be applied against ${target}.
|
|
438
|
+
\u2022 Auth tables (oauth_clients, oauth_access_tokens, oauth_refresh_tokens, password_resets) will be CREATE TABLE IF NOT EXISTS \u2014 they re-create cleanly under the new dialect.${boolNote}${tzNote}
|
|
439
|
+
\u2022 Existing row data does NOT auto-migrate. Use \`mysqldump\` / \`pg_dump\` (or your own export) to move it.${missingNote}
|
|
440
|
+
\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
|
|
441
|
+
|
|
442
|
+
Next steps:
|
|
443
|
+
1. Update .env: DB_CONNECTION=${target}${envExtras}
|
|
444
|
+
2. (Optional) Export data from the current ${current} database.
|
|
445
|
+
3. Run \`./buddy migrate\` (or \`migrate:fresh\` to start clean).
|
|
446
|
+
4. The post-migrate FK audit will report any constraints that didn't replay.
|
|
447
|
+
`);
|
|
448
|
+
await outro("Plan rendered. Re-run after updating .env to actually switch.", {
|
|
449
|
+
startTime: perf,
|
|
450
|
+
useSeconds: !0
|
|
451
|
+
});
|
|
452
|
+
process.exit(ExitCode.Success);
|
|
453
|
+
});
|
|
454
|
+
onUnknownSubcommand(buddy, "migrate");
|
|
455
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { $ } from "bun";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { log } from "@stacksjs/logging";
|
|
4
|
+
import { projectPath } from "@stacksjs/path";
|
|
5
|
+
import { onUnknownSubcommand } from "@stacksjs/cli";
|
|
6
|
+
export function outdated(buddy) {
|
|
7
|
+
const descriptions = {
|
|
8
|
+
outdated: "List all the outdated project dependencies",
|
|
9
|
+
project: "Target a specific project",
|
|
10
|
+
verbose: "Enable verbose output"
|
|
11
|
+
};
|
|
12
|
+
buddy.command("outdated", descriptions.outdated).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async () => {
|
|
13
|
+
log.debug("Running `buddy outdated` ...");
|
|
14
|
+
$.cwd(projectPath());
|
|
15
|
+
const response = await $`bun outdated`.text();
|
|
16
|
+
console.log(response);
|
|
17
|
+
});
|
|
18
|
+
onUnknownSubcommand(buddy, "outdated");
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { intro, log, onUnknownSubcommand, outro } from "@stacksjs/cli";
|
|
3
|
+
import { ExitCode } from "@stacksjs/types";
|
|
4
|
+
export function packageCommands(buddy) {
|
|
5
|
+
buddy.command("package:discover", "Discover Stacks-compatible packages from pantry").option("--verbose", "Enable verbose output", { default: !1 }).action(async () => {
|
|
6
|
+
const perf = await intro("buddy package:discover"), { discoverPackages } = await import("@stacksjs/actions"), manifest = await discoverPackages(), count = Object.keys(manifest.packages).length;
|
|
7
|
+
if (count > 0) {
|
|
8
|
+
log.success(`Discovered ${count} Stacks package${count === 1 ? "" : "s"}:`);
|
|
9
|
+
for (const name of Object.keys(manifest.packages))
|
|
10
|
+
log.info(` - ${name}`);
|
|
11
|
+
} else
|
|
12
|
+
log.info("No Stacks-compatible packages found in pantry");
|
|
13
|
+
await outro("Package discovery complete", { startTime: perf, useSeconds: !0 });
|
|
14
|
+
process.exit(ExitCode.Success);
|
|
15
|
+
});
|
|
16
|
+
onUnknownSubcommand(buddy, "package");
|
|
17
|
+
}
|