@stacksjs/buddy 0.70.59 → 0.70.61

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.
Files changed (79) hide show
  1. package/dist/cli.js +164 -4
  2. package/dist/commands/about.js +35 -0
  3. package/dist/commands/add.d.ts +2 -0
  4. package/dist/commands/add.js +33 -0
  5. package/dist/commands/auth.js +68 -0
  6. package/dist/commands/build.js +153 -0
  7. package/dist/commands/cd.js +39 -0
  8. package/dist/commands/changelog.js +32 -0
  9. package/dist/commands/clean.js +37 -0
  10. package/dist/commands/cloud.js +503 -0
  11. package/dist/commands/commit.js +16 -0
  12. package/dist/commands/completion.js +143 -0
  13. package/dist/commands/config-migrate.js +94 -0
  14. package/dist/commands/configure.js +48 -0
  15. package/dist/commands/create.js +137 -0
  16. package/dist/commands/deploy.js +1404 -0
  17. package/dist/commands/dev.js +911 -0
  18. package/dist/commands/dns.js +62 -0
  19. package/dist/commands/doctor.js +280 -0
  20. package/dist/commands/domains.js +159 -0
  21. package/dist/commands/email.js +526 -0
  22. package/dist/commands/env.js +246 -0
  23. package/dist/commands/features.js +422 -0
  24. package/dist/commands/fresh.js +39 -0
  25. package/dist/commands/generate.js +104 -0
  26. package/dist/commands/http.js +30 -0
  27. package/dist/commands/index.js +53 -0
  28. package/dist/commands/install.js +23 -0
  29. package/dist/commands/key.js +23 -0
  30. package/dist/commands/lint.js +50 -0
  31. package/dist/commands/list.js +108 -0
  32. package/dist/commands/mail.js +693 -0
  33. package/dist/commands/maintenance.d.ts +2 -0
  34. package/dist/commands/maintenance.js +141 -0
  35. package/dist/commands/make.js +375 -0
  36. package/dist/commands/migrate-project.js +49 -0
  37. package/dist/commands/migrate.js +373 -0
  38. package/dist/commands/outdated.js +19 -0
  39. package/dist/commands/package.d.ts +2 -0
  40. package/dist/commands/package.js +17 -0
  41. package/dist/commands/phone.js +188 -0
  42. package/dist/commands/ports.js +118 -0
  43. package/dist/commands/prepublish.js +17 -0
  44. package/dist/commands/projects.js +29 -0
  45. package/dist/commands/publish.js +169 -0
  46. package/dist/commands/queue.js +249 -0
  47. package/dist/commands/release.js +30 -0
  48. package/dist/commands/route.js +21 -0
  49. package/dist/commands/saas.js +25 -0
  50. package/dist/commands/schedule.js +61 -0
  51. package/dist/commands/search.js +84 -0
  52. package/dist/commands/seed.js +71 -0
  53. package/dist/commands/serve.js +176 -0
  54. package/dist/commands/setup.js +215 -0
  55. package/dist/commands/share.d.ts +2 -0
  56. package/dist/commands/share.js +209 -0
  57. package/dist/commands/sms.js +328 -0
  58. package/dist/commands/stacks.d.ts +2 -0
  59. package/dist/commands/stacks.js +69 -0
  60. package/dist/commands/telemetry.js +74 -0
  61. package/dist/commands/test.js +130 -0
  62. package/dist/commands/tinker.js +37 -0
  63. package/dist/commands/types.js +18 -0
  64. package/dist/commands/upgrade.js +97 -0
  65. package/dist/commands/version.js +16 -0
  66. package/dist/config.d.ts +43 -0
  67. package/dist/config.js +223 -0
  68. package/dist/custom-cli.d.ts +1 -0
  69. package/dist/custom-cli.js +23 -0
  70. package/dist/index.js +1 -3424
  71. package/dist/lazy-commands.d.ts +61 -0
  72. package/dist/lazy-commands.js +182 -0
  73. package/dist/migrators/index.js +62 -0
  74. package/dist/migrators/laravel/index.js +148 -0
  75. package/dist/migrators/laravel/migrations.js +231 -0
  76. package/dist/migrators/laravel/models.js +132 -0
  77. package/dist/migrators/rails/index.js +11 -0
  78. package/dist/migrators/types.js +0 -0
  79. package/package.json +1 -1
@@ -0,0 +1,373 @@
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 } 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
+ function describeOp(op) {
92
+ switch (op.kind) {
93
+ case "drop_table":
94
+ return `drop table "${op.table}" (all rows lost)`;
95
+ case "drop_column":
96
+ return `drop column "${op.table}"."${op.column}" (column data lost)`;
97
+ case "modify_column":
98
+ return `change type of "${op.table}"."${op.column}" (possible data loss)`;
99
+ case "rebuild_table":
100
+ return `rebuild table "${op.table}" (type/constraint change)`;
101
+ case "rename_column":
102
+ return `rename "${op.table}"."${op.from}" \u2192 "${op.to}"`;
103
+ case "rename_table":
104
+ return `rename table "${op.from}" \u2192 "${op.to}"`;
105
+ default:
106
+ return `${op.kind} on "${op.table}"${op.column ? `."${op.column}"` : ""}`;
107
+ }
108
+ }
109
+ async function confirmDestructiveMigrations(opts) {
110
+ let operations = [];
111
+ try {
112
+ const { previewPendingMigrations } = await import("@stacksjs/database");
113
+ operations = await previewPendingMigrations({ fromDb: opts.fromDb, applyRenames: opts.applyRenames });
114
+ } catch (error) {
115
+ log.debug(`Migration preview unavailable: ${error instanceof Error ? error.message : String(error)}`);
116
+ return !0;
117
+ }
118
+ const renames = operations.filter((o) => o.kind === "rename_column" || o.kind === "rename_table");
119
+ for (const r of renames)
120
+ log.info(`Detected ${describeOp(r)} \u2014 applying as a rename (data preserved). Use --no-rename to drop + add instead.`);
121
+ const destructive = operations.filter((o) => o.destructive);
122
+ if (destructive.length === 0)
123
+ return !0;
124
+ log.warn(`This migration includes ${destructive.length} potentially destructive change${destructive.length === 1 ? "" : "s"}:`);
125
+ for (const op of destructive)
126
+ log.warn(` \u2022 ${describeOp(op)}`);
127
+ if (opts.force)
128
+ return !0;
129
+ if (isCI || !hasTTY) {
130
+ log.error("Refusing to apply destructive changes in a non-interactive environment. Re-run with --force to proceed.");
131
+ return !1;
132
+ }
133
+ return confirm({ message: "Apply these destructive changes?", initial: !1 });
134
+ }
135
+ export function migrate(buddy) {
136
+ const descriptions = {
137
+ migrate: "Migrates your database",
138
+ project: "Target a specific project",
139
+ verbose: "Enable verbose output",
140
+ auth: "Also migrate auth tables (oauth_clients, oauth_access_tokens, oauth_refresh_tokens, password_resets)",
141
+ force: "Apply destructive changes (drop column/table, lossy type change) without confirmation",
142
+ fromDb: "Diff against the live database schema instead of the snapshot (self-heal drift)",
143
+ noRename: "Treat renamed columns as drop + add instead of a data-preserving rename"
144
+ };
145
+ 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) => {
146
+ log.debug("Running `buddy migrate` ...", options);
147
+ const perf = await intro("buddy migrate"), validation = validateModelsExist();
148
+ if (!validation.valid) {
149
+ console.error(`
150
+ \u274C Error: ${validation.error}
151
+ `);
152
+ process.exit(ExitCode.FatalError);
153
+ }
154
+ const applyRenames = options.rename === !1 ? !1 : void 0;
155
+ if (options.fromDb)
156
+ process.env.STACKS_MIGRATE_FROM_DB = "1";
157
+ if (applyRenames === !1)
158
+ process.env.STACKS_MIGRATE_NO_RENAME = "1";
159
+ if (options.diff) {
160
+ try {
161
+ const { previewPendingMigrations } = await import("@stacksjs/database"), ops = await previewPendingMigrations({ fromDb: options.fromDb, applyRenames });
162
+ if (ops.length === 0)
163
+ log.info("No pending schema changes \u2014 your models match the database.");
164
+ else {
165
+ log.info(`${ops.length} pending change${ops.length === 1 ? "" : "s"}:`);
166
+ for (const op of ops)
167
+ log.info(` \u2022 ${describeOp(op)}${op.destructive ? " [destructive]" : ""}`);
168
+ }
169
+ } catch (error) {
170
+ log.error("Failed to preview migrations:", error);
171
+ }
172
+ await outro("Diff complete \u2014 no changes applied.", { startTime: perf, useSeconds: !0 });
173
+ process.exit(ExitCode.Success);
174
+ }
175
+ const lock = acquireMigrationLock();
176
+ if (!lock.acquired) {
177
+ log.error("Another migration is already running (.stacks/migrations.lock exists). Wait for it to finish, or remove the lockfile if it is stale.");
178
+ process.exit(ExitCode.FatalError);
179
+ }
180
+ if (!await confirmDestructiveMigrations({ force: options.force, fromDb: options.fromDb, applyRenames })) {
181
+ lock.release();
182
+ await outro("Migration cancelled \u2014 no changes applied.", { startTime: perf, useSeconds: !0 });
183
+ process.exit(ExitCode.Success);
184
+ }
185
+ if (options.auth !== !1) {
186
+ log.debug("Migrating auth tables...");
187
+ try {
188
+ const { migrateAuthTables, migrateNotificationTables, migrateRbacTables } = await import("@stacksjs/database"), authResult = await migrateAuthTables({ verbose: options.verbose });
189
+ if (!authResult.success)
190
+ log.error(`Failed to migrate auth tables: ${authResult.error}`);
191
+ const notifResult = await migrateNotificationTables({ verbose: options.verbose });
192
+ if (!notifResult.success)
193
+ log.error(`Failed to migrate notification tables: ${notifResult.error}`);
194
+ const rbacResult = await migrateRbacTables({ verbose: options.verbose });
195
+ if (!rbacResult.success)
196
+ log.error(`Failed to migrate RBAC tables: ${rbacResult.error}`);
197
+ } catch (error) {
198
+ log.error("Failed to migrate auth/notification/RBAC tables:", error);
199
+ }
200
+ }
201
+ const result = await runAction(Action.Migrate, options).finally(() => lock.release());
202
+ if (result.isErr)
203
+ log.error("Model migrations failed.");
204
+ if (result.isErr) {
205
+ await outro("While running the migrate command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
206
+ process.exit(ExitCode.FatalError);
207
+ }
208
+ if (options.auth !== !1)
209
+ try {
210
+ const { ensureUsersAuthColumns, sqlHelpers } = await import("@stacksjs/database"), driver = process.env.DB_CONNECTION || "sqlite";
211
+ await ensureUsersAuthColumns(sqlHelpers(driver), { verbose: options.verbose });
212
+ } catch (error) {
213
+ log.error("Failed to ensure users auth columns post-migration:", error);
214
+ }
215
+ try {
216
+ const { ensureUuidColumns, sqlHelpers } = await import("@stacksjs/database"), driver = process.env.DB_CONNECTION || "sqlite";
217
+ await ensureUuidColumns(sqlHelpers(driver), { verbose: options.verbose });
218
+ } catch (error) {
219
+ log.error("Failed to ensure uuid columns post-migration:", error);
220
+ }
221
+ await reportMissingForeignKeys();
222
+ 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}`;
223
+ await outro(outroMessage, {
224
+ startTime: perf,
225
+ useSeconds: !0
226
+ });
227
+ process.exit(ExitCode.Success);
228
+ });
229
+ 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("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
230
+ log.debug("Running `buddy migrate:fresh` ...", options);
231
+ const perf = await intro("buddy migrate:fresh"), validation = validateModelsExist();
232
+ if (!validation.valid) {
233
+ console.error(`
234
+ \u274C Error: ${validation.error}
235
+ `);
236
+ process.exit(ExitCode.FatalError);
237
+ }
238
+ const result = await runAction(Action.MigrateFresh, options);
239
+ if (result.isErr)
240
+ log.error("Model migrations failed \u2014 applying auth/notification/RBAC table guarantees before exiting.");
241
+ if (options.auth !== !1) {
242
+ log.debug("Migrating auth tables...");
243
+ try {
244
+ const { migrateAuthTables, migrateNotificationTables, migrateRbacTables } = await import("@stacksjs/database"), authResult = await migrateAuthTables({ verbose: options.verbose });
245
+ if (!authResult.success)
246
+ log.error(`Failed to migrate auth tables: ${authResult.error}`);
247
+ const notifResult = await migrateNotificationTables({ verbose: options.verbose });
248
+ if (!notifResult.success)
249
+ log.error(`Failed to migrate notification tables: ${notifResult.error}`);
250
+ const rbacResult = await migrateRbacTables({ verbose: options.verbose });
251
+ if (!rbacResult.success)
252
+ log.error(`Failed to migrate RBAC tables: ${rbacResult.error}`);
253
+ } catch (error) {
254
+ log.error("Failed to migrate auth/notification/RBAC tables:", error);
255
+ }
256
+ }
257
+ try {
258
+ const { ensureUuidColumns, sqlHelpers } = await import("@stacksjs/database"), driver = process.env.DB_CONNECTION || "sqlite";
259
+ await ensureUuidColumns(sqlHelpers(driver), { verbose: options.verbose });
260
+ } catch (error) {
261
+ log.error("Failed to ensure uuid columns post-migration:", error);
262
+ }
263
+ if (result.isErr) {
264
+ await outro("While running the migrate:fresh command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
265
+ process.exit(ExitCode.FatalError);
266
+ }
267
+ await reportMissingForeignKeys();
268
+ if (options.seed) {
269
+ log.info("Running database seeders...");
270
+ try {
271
+ const { seed } = await import("@stacksjs/database"), seedResult = await seed({ verbose: options.verbose, fresh: !0 });
272
+ if (seedResult.failed > 0) {
273
+ log.warn(`Seeding completed with ${seedResult.failed} failure(s)`);
274
+ for (const r of seedResult.results)
275
+ if (!r.success)
276
+ log.error(` - ${r.model}: ${r.error}`);
277
+ } else
278
+ log.success(`Seeded ${seedResult.successful} model(s)`);
279
+ } catch (error) {
280
+ log.error("Failed to run seeders:", error);
281
+ }
282
+ }
283
+ const parts = [];
284
+ if (options.auth !== !1)
285
+ parts.push("auth tables");
286
+ if (options.seed)
287
+ parts.push("seeded");
288
+ 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)`;
289
+ await outro(`All tables dropped successfully & migrated successfully${countPhrase}${suffix}`, {
290
+ startTime: perf,
291
+ useSeconds: !0
292
+ });
293
+ process.exit(ExitCode.Success);
294
+ });
295
+ buddy.command("migrate:dns", descriptions.migrate).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
296
+ log.debug("Running `buddy migrate:dns` ...", options);
297
+ const perf = await intro("buddy migrate:dns"), result = await runAction(Action.MigrateDns, { ...options });
298
+ if (result.isErr) {
299
+ await outro("While running the migrate:dns command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
300
+ process.exit(ExitCode.FatalError);
301
+ }
302
+ const APP_URL = process.env.APP_URL || "undefined";
303
+ await outro(`Migrated your ${APP_URL} DNS.`, {
304
+ startTime: perf,
305
+ useSeconds: !0
306
+ });
307
+ process.exit(ExitCode.Success);
308
+ });
309
+ buddy.command("migrate:switch <driver>", "Pre-flight check + plan for switching DB_CONNECTION between sqlite / mysql / postgres").action(async (driver) => {
310
+ log.debug(`Running \`buddy migrate:switch ${driver}\` ...`);
311
+ const perf = await intro("buddy migrate:switch"), target = driver.toLowerCase();
312
+ if (!new Set(["sqlite", "mysql", "postgres"]).has(target)) {
313
+ console.log(`
314
+ Unknown target driver "${driver}". Allowed: sqlite, mysql, postgres.
315
+ `);
316
+ await outro("Aborted.", { startTime: perf, useSeconds: !0 });
317
+ process.exit(ExitCode.FatalError);
318
+ }
319
+ const current = (process.env.DB_CONNECTION || "sqlite").toLowerCase();
320
+ if (current === target) {
321
+ console.log(`
322
+ DB_CONNECTION is already "${target}". Nothing to switch.
323
+ `);
324
+ await outro("No-op.", { startTime: perf, useSeconds: !0 });
325
+ process.exit(ExitCode.Success);
326
+ }
327
+ const missingEnv = ({
328
+ sqlite: ["DB_DATABASE"],
329
+ mysql: ["DB_HOST", "DB_PORT", "DB_USERNAME", "DB_PASSWORD", "DB_DATABASE"],
330
+ postgres: ["DB_HOST", "DB_PORT", "DB_USERNAME", "DB_PASSWORD", "DB_DATABASE"]
331
+ }[target] ?? []).filter((k) => !process.env[k]);
332
+ let alterCount = 0, uniqueIdxCount = 0;
333
+ try {
334
+ const migrationsDir = projectPath("database/migrations");
335
+ if (existsSync(migrationsDir)) {
336
+ const files = readdirSync(migrationsDir).filter((f) => f.endsWith(".sql"));
337
+ for (const f of files) {
338
+ const content = readFileSync(`${migrationsDir}/${f}`, "utf-8").toLowerCase();
339
+ if (/alter\s+table[\s\S]*add\s+constraint/.test(content))
340
+ alterCount++;
341
+ if (/^\s*create\s+unique\s+index/m.test(content))
342
+ uniqueIdxCount++;
343
+ }
344
+ }
345
+ } catch {}
346
+ const sqliteFkNote = current === "sqlite" && alterCount > 0 ? `
347
+ (These were skipped on SQLite per stacksjs/stacks#1916 and survive on disk for replay.)` : "", boolNote = current === "sqlite" && (target === "postgres" || target === "mysql") ? `
348
+ \u2022 Booleans land as 0/1 on SQLite; ${target} stores them as ${target === "postgres" ? "true/false" : "0/1 (compatible)"}.` : "", tzNote = target === "postgres" ? `
349
+ \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 ? `
350
+ \u26A0 Missing env vars for ${target}: ${missingEnv.join(", ")}` : "";
351
+ console.log(`
352
+ Switch plan: ${current} \u2192 ${target}
353
+ \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
354
+ \u2022 ${alterCount} ALTER TABLE ADD CONSTRAINT migration(s) will be applied against ${target}.${sqliteFkNote}
355
+ \u2022 ${uniqueIdxCount} CREATE UNIQUE INDEX migration(s) will be applied against ${target}.
356
+ \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}
357
+ \u2022 Existing row data does NOT auto-migrate. Use \`mysqldump\` / \`pg_dump\` (or your own export) to move it.${missingNote}
358
+ \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
359
+
360
+ Next steps:
361
+ 1. Update .env: DB_CONNECTION=${target}${envExtras}
362
+ 2. (Optional) Export data from the current ${current} database.
363
+ 3. Run \`./buddy migrate\` (or \`migrate:fresh\` to start clean).
364
+ 4. The post-migrate FK audit will report any constraints that didn't replay.
365
+ `);
366
+ await outro("Plan rendered. Re-run after updating .env to actually switch.", {
367
+ startTime: perf,
368
+ useSeconds: !0
369
+ });
370
+ process.exit(ExitCode.Success);
371
+ });
372
+ onUnknownSubcommand(buddy, "migrate");
373
+ }
@@ -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,2 @@
1
+ import type { CLI } from '@stacksjs/types';
2
+ export declare function packageCommands(buddy: CLI): void;
@@ -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
+ }
@@ -0,0 +1,188 @@
1
+ import { getErrorMessage } from "@stacksjs/utils";
2
+ import { readFileSync, existsSync } from "node:fs";
3
+ const TIMEOUT_MS = 30000;
4
+ async function withTimeout(promise, ms = TIMEOUT_MS) {
5
+ let timeoutId;
6
+ const timeoutPromise = new Promise((_, reject) => {
7
+ timeoutId = setTimeout(() => reject(Error(`Operation timed out after ${ms}ms`)), ms);
8
+ });
9
+ try {
10
+ return await Promise.race([promise, timeoutPromise]);
11
+ } finally {
12
+ clearTimeout(timeoutId);
13
+ }
14
+ }
15
+ function loadAwsCredentials() {
16
+ const envPath = ".env.production";
17
+ if (existsSync(envPath)) {
18
+ const content = readFileSync(envPath, "utf-8");
19
+ for (const line of content.split(`
20
+ `)) {
21
+ const eq = line.indexOf("=");
22
+ if (eq > 0 && !line.startsWith("#"))
23
+ process.env[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
24
+ }
25
+ }
26
+ }
27
+ const descriptions = {
28
+ phone: "Phone/voice service management commands",
29
+ status: "Show phone service status",
30
+ numbers: "List phone numbers",
31
+ search: "Search available phone numbers",
32
+ setup: "Initialize phone infrastructure"
33
+ };
34
+ export function phone(buddy) {
35
+ buddy.command("phone", descriptions.phone).action(async () => {
36
+ console.log(`
37
+ \uD83D\uDCDE Phone Service Commands
38
+ `);
39
+ console.log(" buddy phone:status - Show phone service status");
40
+ console.log(" buddy phone:numbers - List phone numbers");
41
+ console.log(" buddy phone:search - Search available phone numbers");
42
+ console.log(" buddy phone:setup - Initialize phone infrastructure");
43
+ console.log("");
44
+ process.exit(0);
45
+ });
46
+ buddy.command("phone:status", descriptions.status).action(async () => {
47
+ console.log(`
48
+ \uD83D\uDCDE Phone Service Status
49
+ `);
50
+ loadAwsCredentials();
51
+ try {
52
+ const { ConnectClient } = await import("@stacksjs/ts-cloud"), connect = new ConnectClient(process.env.AWS_REGION || "us-east-1"), instanceAlias = `${(process.env.APP_NAME || "stacks").toLowerCase().replace(/[^a-z0-9-]/g, "-")}-phone`;
53
+ console.log(`Looking for Connect instance: ${instanceAlias}`);
54
+ const instance = (await withTimeout(connect.listInstances({ MaxResults: 100 }))).InstanceSummaryList?.find((i) => i.InstanceAlias === instanceAlias);
55
+ if (instance) {
56
+ console.log(`
57
+ \u2705 Phone Service Active
58
+ `);
59
+ console.log(` Instance ID: ${instance.Id}`);
60
+ console.log(` Alias: ${instance.InstanceAlias}`);
61
+ console.log(` Status: ${instance.InstanceStatus}`);
62
+ console.log(` Inbound Calls: ${instance.InboundCallsEnabled ? "Enabled" : "Disabled"}`);
63
+ console.log(` Outbound Calls: ${instance.OutboundCallsEnabled ? "Enabled" : "Disabled"}`);
64
+ if (instance.Id)
65
+ try {
66
+ const numbers = await withTimeout(connect.listPhoneNumbers({ InstanceId: instance.Id }));
67
+ if (numbers.ListPhoneNumbersSummaryList && numbers.ListPhoneNumbersSummaryList.length > 0) {
68
+ console.log(`
69
+ Phone Numbers:`);
70
+ for (const num of numbers.ListPhoneNumbersSummaryList)
71
+ console.log(` \uD83D\uDCF1 ${num.PhoneNumber} (${num.PhoneNumberType})`);
72
+ } else
73
+ console.log(`
74
+ No phone numbers claimed yet.`);
75
+ } catch {
76
+ console.log(`
77
+ Could not retrieve phone numbers.`);
78
+ }
79
+ } else {
80
+ console.log(`
81
+ \u274C Phone service not deployed.`);
82
+ console.log(`
83
+ \uD83D\uDCA1 To set up phone service:`);
84
+ console.log(" 1. Enable phone in config/phone.ts");
85
+ console.log(" 2. Run `buddy deploy`");
86
+ }
87
+ } catch (error) {
88
+ console.error("Error checking status:", getErrorMessage(error));
89
+ }
90
+ process.exit(0);
91
+ });
92
+ buddy.command("phone:numbers", descriptions.numbers).action(async () => {
93
+ console.log(`
94
+ \uD83D\uDCDE Phone Numbers
95
+ `);
96
+ loadAwsCredentials();
97
+ try {
98
+ const { ConnectClient } = await import("@stacksjs/ts-cloud"), connect = new ConnectClient(process.env.AWS_REGION || "us-east-1"), instanceAlias = `${(process.env.APP_NAME || "stacks").toLowerCase().replace(/[^a-z0-9-]/g, "-")}-phone`, instance = (await withTimeout(connect.listInstances({ MaxResults: 100 }))).InstanceSummaryList?.find((i) => i.InstanceAlias === instanceAlias);
99
+ if (!instance?.Id) {
100
+ console.log("Phone service not deployed. Run `buddy phone:setup` first.");
101
+ process.exit(0);
102
+ return;
103
+ }
104
+ const numbers = await withTimeout(connect.listPhoneNumbers({ InstanceId: instance.Id }));
105
+ if (!numbers.ListPhoneNumbersSummaryList || numbers.ListPhoneNumbersSummaryList.length === 0) {
106
+ console.log("No phone numbers claimed.");
107
+ console.log("\n\uD83D\uDCA1 Use `buddy phone:search` to find available numbers.");
108
+ process.exit(0);
109
+ return;
110
+ }
111
+ console.log(`Claimed Phone Numbers:
112
+ `);
113
+ for (const num of numbers.ListPhoneNumbersSummaryList) {
114
+ console.log(` \uD83D\uDCF1 ${num.PhoneNumber}`);
115
+ console.log(` Type: ${num.PhoneNumberType}`);
116
+ console.log(` Country: ${num.PhoneNumberCountryCode}`);
117
+ if (num.PhoneNumberDescription)
118
+ console.log(` Description: ${num.PhoneNumberDescription}`);
119
+ console.log("");
120
+ }
121
+ } catch (error) {
122
+ console.error("Error listing numbers:", getErrorMessage(error));
123
+ }
124
+ process.exit(0);
125
+ });
126
+ buddy.command("phone:search [country]", descriptions.search).option("-t, --type <type>", "Phone number type (TOLL_FREE, DID)", { default: "TOLL_FREE" }).action(async (country, options) => {
127
+ const countryCode = country?.toUpperCase() || "US", phoneType = options.type?.toUpperCase() || "TOLL_FREE";
128
+ console.log(`
129
+ \uD83D\uDCDE Searching for ${phoneType} numbers in ${countryCode}...
130
+ `);
131
+ loadAwsCredentials();
132
+ try {
133
+ const { ConnectClient } = await import("@stacksjs/ts-cloud"), connect = new ConnectClient(process.env.AWS_REGION || "us-east-1"), instanceAlias = `${(process.env.APP_NAME || "stacks").toLowerCase().replace(/[^a-z0-9-]/g, "-")}-phone`, instance = (await withTimeout(connect.listInstances({ MaxResults: 100 }))).InstanceSummaryList?.find((i) => i.InstanceAlias === instanceAlias);
134
+ if (!instance?.Arn) {
135
+ console.log("Phone service not deployed. Run `buddy phone:setup` first.");
136
+ process.exit(0);
137
+ return;
138
+ }
139
+ const available = await withTimeout(connect.searchAvailablePhoneNumbers({
140
+ TargetArn: instance.Arn,
141
+ PhoneNumberCountryCode: countryCode,
142
+ PhoneNumberType: phoneType,
143
+ MaxResults: 10
144
+ }));
145
+ if (!available.AvailableNumbersList || available.AvailableNumbersList.length === 0) {
146
+ console.log(`No ${phoneType} numbers available in ${countryCode}.`);
147
+ console.log(`
148
+ \uD83D\uDCA1 Try a different country or number type.`);
149
+ process.exit(0);
150
+ return;
151
+ }
152
+ console.log(`Available Phone Numbers:
153
+ `);
154
+ for (const num of available.AvailableNumbersList)
155
+ console.log(` \uD83D\uDCF1 ${num.PhoneNumber}`);
156
+ console.log("\n\uD83D\uDCA1 To claim a number, use `buddy phone:claim <number>`");
157
+ } catch (error) {
158
+ if (getErrorMessage(error).includes("not authorized")) {
159
+ console.log("Not authorized to search phone numbers.");
160
+ console.log("Make sure your AWS account has Amazon Connect permissions.");
161
+ } else
162
+ console.error("Error searching numbers:", getErrorMessage(error));
163
+ }
164
+ process.exit(0);
165
+ });
166
+ buddy.command("phone:setup", descriptions.setup).action(async () => {
167
+ console.log(`
168
+ \uD83D\uDCDE Phone Service Setup
169
+ `);
170
+ console.log("Amazon Connect setup requires manual configuration.");
171
+ console.log(`
172
+ Steps to set up phone service:
173
+ `);
174
+ console.log("1. Enable phone in config/phone.ts:");
175
+ console.log(" enabled: true");
176
+ console.log("");
177
+ console.log("2. Run `buddy deploy` to create the Connect instance");
178
+ console.log("");
179
+ console.log("3. Access the Connect console to:");
180
+ console.log(" - Claim phone numbers");
181
+ console.log(" - Configure contact flows");
182
+ console.log(" - Set up routing profiles");
183
+ console.log("");
184
+ console.log("4. Use `buddy phone:status` to verify setup");
185
+ console.log("");
186
+ process.exit(0);
187
+ });
188
+ }