@stacksjs/buddy 0.70.251 → 0.70.252

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.
@@ -7,6 +7,14 @@ export declare function shouldUsePrettyDevUrls(input: {
7
7
  proxyManagedExternally: boolean
8
8
  systemAuthorized: boolean
9
9
  }): boolean;
10
+ export declare function normalizeDevelopmentEntryPath(path: string | undefined): string;
11
+ export declare function resolveDevelopmentEntryPath(input?: {
12
+ root?: string
13
+ site?: boolean
14
+ configuredPath?: string
15
+ }): string;
16
+ export declare function developmentUrl(baseUrl: string, entryPath: string): string;
17
+ export declare function developmentBrowserCommand(url: string, platform?: unknown): string[];
10
18
  export declare function dev(buddy: CLI): void;
11
19
  export declare function startDevelopmentServer(_options: DevOptions, _startTime?: number): Promise<void>;
12
20
  export declare function setupPrettyDevEnvironment(input: {
@@ -72,6 +72,98 @@ export function resolvePrettyDevDomain(appUrl, nativeMode = !1) {
72
72
  export function shouldUsePrettyDevUrls(input) {
73
73
  return input.domain !== null && !input.localhostOnly && (input.proxyManagedExternally || input.systemAuthorized);
74
74
  }
75
+ const APPLICATION_VIEW_CANDIDATES = [
76
+ "app/index",
77
+ "app",
78
+ "dashboard/index",
79
+ "dashboard",
80
+ "home/index",
81
+ "home",
82
+ "composer",
83
+ "workspace/index",
84
+ "workspace",
85
+ "feed",
86
+ "portal",
87
+ "admin",
88
+ "account/index",
89
+ "account",
90
+ "profile/index",
91
+ "profile"
92
+ ], SITE_LAYOUT_NAMES = new Set(["default", "guest", "marketing", "site"]);
93
+ export function normalizeDevelopmentEntryPath(path) {
94
+ const value = path?.trim();
95
+ if (!value || value === "/")
96
+ return "/";
97
+ try {
98
+ const parsed = new URL(value, "http://stacks.localhost");
99
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`;
100
+ } catch {
101
+ return "/";
102
+ }
103
+ }
104
+ function configuredApplicationPath(root) {
105
+ if (process.env.APP_PATH)
106
+ return process.env.APP_PATH;
107
+ try {
108
+ return readFileSync(join(root, "config/app.ts"), "utf8").match(/^[ \t]*appPath\s*:\s*(['"`])([^'"`]+)\1/m)?.[2];
109
+ } catch {
110
+ return;
111
+ }
112
+ }
113
+ function viewSource(root, route) {
114
+ const base = join(root, "resources/views", route);
115
+ for (const extension of ["stx", "vue", "html"]) {
116
+ const path = `${base}.${extension}`;
117
+ if (existsSync(path))
118
+ try {
119
+ return readFileSync(path, "utf8");
120
+ } catch {
121
+ return "";
122
+ }
123
+ }
124
+ return;
125
+ }
126
+ function usesApplicationLayout(source) {
127
+ const layout = source.match(/@extends\(\s*['"]layouts\/([^'"]+)['"]\s*\)/)?.[1] ?? source.match(/@layout\(\s*['"]layouts\/([^'"]+)['"]\s*\)/)?.[1];
128
+ if (!layout)
129
+ return !1;
130
+ return !SITE_LAYOUT_NAMES.has(layout.split("/").at(-1) ?? layout);
131
+ }
132
+ export function resolveDevelopmentEntryPath(input = {}) {
133
+ if (input.site)
134
+ return "/";
135
+ const root = input.root ?? projectPath(), configuredPath = input.configuredPath ?? configuredApplicationPath(root);
136
+ if (configuredPath)
137
+ return normalizeDevelopmentEntryPath(configuredPath);
138
+ const loginExists = viewSource(root, "login") !== void 0 || viewSource(root, "auth/login") !== void 0, candidates = APPLICATION_VIEW_CANDIDATES.map((route) => ({ route, source: viewSource(root, route) })).filter((candidate) => candidate.source !== void 0), appCandidate = candidates.find((candidate) => usesApplicationLayout(candidate.source)) ?? (loginExists ? candidates[0] : void 0);
139
+ if (appCandidate)
140
+ return normalizeDevelopmentEntryPath(`/${appCandidate.route.replace(/\/index$/, "")}`);
141
+ return loginExists ? "/login" : "/";
142
+ }
143
+ export function developmentUrl(baseUrl, entryPath) {
144
+ const normalized = normalizeDevelopmentEntryPath(entryPath);
145
+ if (normalized === "/")
146
+ return baseUrl.replace(/\/$/, "");
147
+ return new URL(normalized, `${baseUrl.replace(/\/$/, "")}/`).toString();
148
+ }
149
+ export function developmentBrowserCommand(url, platform = process.platform) {
150
+ if (platform === "darwin")
151
+ return ["open", url];
152
+ if (platform === "win32")
153
+ return ["cmd", "/c", "start", "", url];
154
+ return ["xdg-open", url];
155
+ }
156
+ function openDevelopmentBrowser(url) {
157
+ if (!process.stdout.isTTY || process.env.CI || process.env.STACKS_DEV_NO_OPEN === "1")
158
+ return;
159
+ try {
160
+ Bun.spawn(developmentBrowserCommand(url), {
161
+ stdin: "ignore",
162
+ stdout: "ignore",
163
+ stderr: "ignore"
164
+ }).unref();
165
+ } catch {}
166
+ }
75
167
  async function canStartPrettyDevProxy() {
76
168
  if (await waitForHttpsProxy(443, 150))
77
169
  return !0;
@@ -96,11 +188,14 @@ export function dev(buddy) {
96
188
  interactive: "Get asked which development server to start",
97
189
  select: "Which development server are you trying to start?",
98
190
  withLocalhost: "Include the localhost URL in the output",
191
+ site: "Open the marketing site instead of the application",
99
192
  project: "Target a specific project",
100
193
  verbose: "Enable verbose output"
101
194
  };
102
- buddy.command("dev [server]", descriptions.dev).option("-f, --frontend", descriptions.frontend).option("-a, --api", descriptions.api).option("-c, --components", descriptions.components).option("-d, --dashboard", descriptions.dashboard).option("-k, --desktop", descriptions.desktop).option("-n, --native", descriptions.native).option("-o, --docs", descriptions.docs).option("-s, --system-tray", descriptions.systemTray).option("-i, --interactive", descriptions.interactive, { default: !1 }).option("-l, --with-localhost", descriptions.withLocalhost, { default: !1 }).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (server, options) => {
103
- const perf = Bun.nanoseconds(), target = server || (options.frontend ? "frontend" : void 0) || (options.api ? "api" : void 0) || (options.components ? "components" : void 0) || (options.dashboard ? "dashboard" : void 0) || (options.desktop ? "desktop" : void 0) || (options.native ? "native" : void 0) || (options.systemTray || options["system-tray"] ? "system-tray" : void 0) || (options.docs ? "docs" : void 0);
195
+ buddy.command("dev [server]", descriptions.dev).option("-f, --frontend", descriptions.frontend).option("-a, --api", descriptions.api).option("-c, --components", descriptions.components).option("-d, --dashboard", descriptions.dashboard).option("-k, --desktop", descriptions.desktop).option("-n, --native", descriptions.native).option("-o, --docs", descriptions.docs).option("-s, --system-tray", descriptions.systemTray).option("-i, --interactive", descriptions.interactive, { default: !1 }).option("-l, --with-localhost", descriptions.withLocalhost, { default: !1 }).option("--site", descriptions.site, { default: !1 }).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (server, options) => {
196
+ const perf = Bun.nanoseconds();
197
+ process.env.STACKS_DEV_ENTRY_PATH = resolveDevelopmentEntryPath({ site: options.site });
198
+ const target = server || (options.frontend ? "frontend" : void 0) || (options.api ? "api" : void 0) || (options.components ? "components" : void 0) || (options.dashboard ? "dashboard" : void 0) || (options.desktop ? "desktop" : void 0) || (options.native ? "native" : void 0) || (options.systemTray || options["system-tray"] ? "system-tray" : void 0) || (options.docs ? "docs" : void 0);
104
199
  if (target) {
105
200
  const serverOptions = { ...options }, a = await actions();
106
201
  switch (target) {
@@ -203,7 +298,8 @@ export function dev(buddy) {
203
298
  });
204
299
  await a.runApiDevServer(options);
205
300
  });
206
- buddy.command("dev:frontend", descriptions.frontend).alias("dev:pages").alias("dev:views").option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
301
+ buddy.command("dev:frontend", descriptions.frontend).alias("dev:pages").alias("dev:views").option("-p, --project [project]", descriptions.project, { default: !1 }).option("--site", descriptions.site, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
302
+ process.env.STACKS_DEV_ENTRY_PATH = resolveDevelopmentEntryPath({ site: options.site });
207
303
  await (await actions()).runFrontendDevServer(options);
208
304
  });
209
305
  buddy.command("dev:dashboard", descriptions.dashboard).alias("dev:admin").option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
@@ -215,7 +311,9 @@ export function dev(buddy) {
215
311
  onUnknownSubcommand(buddy, "dev");
216
312
  }
217
313
  export async function startDevelopmentServer(_options, _startTime) {
218
- const options = _options, startedAt = _startTime, appUrl = process.env.APP_URL ?? "stacks.localhost", nativeMode = options.native === !0, proxyManagedExternally = process.env.STACKS_PROXY_MANAGED === "1", preferredFrontendPort = Number(process.env.PORT) || 3000, preferredApiPort = Number(process.env.PORT_API) || 3008, preferredDocsPort = Number(process.env.PORT_DOCS) || 3006, preferredDashboardPort = Number(process.env.PORT_ADMIN) || 3002;
314
+ const options = _options, startedAt = _startTime, appUrl = process.env.APP_URL ?? "stacks.localhost", nativeMode = options.native === !0, entryPath = resolveDevelopmentEntryPath({ site: options.site });
315
+ process.env.STACKS_DEV_ENTRY_PATH = entryPath;
316
+ const proxyManagedExternally = process.env.STACKS_PROXY_MANAGED === "1", preferredFrontendPort = Number(process.env.PORT) || 3000, preferredApiPort = Number(process.env.PORT_API) || 3008, preferredDocsPort = Number(process.env.PORT_DOCS) || 3006, preferredDashboardPort = Number(process.env.PORT_ADMIN) || 3002;
219
317
  if (process.env.STACKS_DEV_NO_KILL !== "1")
220
318
  await cleanupStaleDevProcesses([
221
319
  preferredFrontendPort,
@@ -240,7 +338,7 @@ export async function startDevelopmentServer(_options, _startTime) {
240
338
  localhostOnly,
241
339
  proxyManagedExternally,
242
340
  systemAuthorized
243
- }), prettySetupRequired = prettyUrlsRequested && !usePrettyUrls, hasCustomDomain = usePrettyUrls && !proxyManagedExternally, displayedDomain = usePrettyUrls ? domain : null, dashboardDomain = displayedDomain ? `dashboard.${displayedDomain}` : null, frontendUrl = displayedDomain ? `https://${displayedDomain}` : `http://localhost:${frontendPort}`, apiUrl = displayedDomain ? `https://${displayedDomain}/api` : `http://localhost:${apiPort}`, docsUrl = displayedDomain ? `https://${displayedDomain}/docs` : `http://localhost:${docsPort}`, dashboardUrl = dashboardDomain ? `https://${dashboardDomain}` : `http://localhost:${dashboardPort}`;
341
+ }), prettySetupRequired = prettyUrlsRequested && !usePrettyUrls, hasCustomDomain = usePrettyUrls && !proxyManagedExternally, displayedDomain = usePrettyUrls ? domain : null, dashboardDomain = displayedDomain ? `dashboard.${displayedDomain}` : null, frontendBaseUrl = displayedDomain ? `https://${displayedDomain}` : `http://localhost:${frontendPort}`, frontendUrl = developmentUrl(frontendBaseUrl, entryPath), apiUrl = displayedDomain ? `https://${displayedDomain}/api` : `http://localhost:${apiPort}`, docsUrl = displayedDomain ? `https://${displayedDomain}/docs` : `http://localhost:${docsPort}`, dashboardUrl = dashboardDomain ? `https://${dashboardDomain}` : `http://localhost:${dashboardPort}`;
244
342
  process.env.STACKS_PROXY_MANAGED = "1";
245
343
  process.env.STACKS_DEV_QUIET = "1";
246
344
  console.log();
@@ -297,7 +395,7 @@ export async function startDevelopmentServer(_options, _startTime) {
297
395
  { name: "API", port: apiPort }
298
396
  ], readinessTimeoutMs = 30000;
299
397
  let readyAnnounced = !1;
300
- const nativeUrl = `http://localhost:${frontendPort}`, nativeWindowReady = nativeMode ? waitForPort(frontendPort, readinessTimeoutMs).then(async (ready) => {
398
+ const nativeUrl = developmentUrl(`http://localhost:${frontendPort}`, entryPath), nativeWindowReady = nativeMode ? waitForPort(frontendPort, readinessTimeoutMs).then(async (ready) => {
301
399
  if (!ready) {
302
400
  log.warn(`Native window skipped because the frontend did not answer within ${readinessTimeoutMs / 1000}s`);
303
401
  return;
@@ -340,6 +438,8 @@ export async function startDevelopmentServer(_options, _startTime) {
340
438
  hasCustomDomain: !!appLooksCustom && !localhostOnly,
341
439
  proxyReachable: proxyManagedExternally ? !0 : proxyReachable,
342
440
  frontendUrl,
441
+ frontendBaseUrl,
442
+ entryPath,
343
443
  apiUrl,
344
444
  docsUrl,
345
445
  dashboardUrl,
@@ -351,6 +451,10 @@ export async function startDevelopmentServer(_options, _startTime) {
351
451
  dashboardPort,
352
452
  dashboardDomain
353
453
  });
454
+ if (!nativeMode) {
455
+ const browserUrl = hasCustomDomain && (proxyManagedExternally || proxyReachable) ? frontendUrl : developmentUrl(`http://localhost:${frontendPort}`, entryPath);
456
+ openDevelopmentBrowser(browserUrl);
457
+ }
354
458
  if (startedAt) {
355
459
  const elapsedMs = (Bun.nanoseconds() - startedAt) / 1e6, summary = failed.length ? `ready in ${(elapsedMs / 1000).toFixed(1)}s \u2014 ${failed.join(", ")} did not bind within ${readinessTimeoutMs / 1000}s` : `ready in ${(elapsedMs / 1000).toFixed(1)}s`;
356
460
  console.log(` ${dim(summary)}`);
@@ -409,6 +513,8 @@ function printDevReadyBanner(input) {
409
513
  hasCustomDomain,
410
514
  proxyReachable,
411
515
  frontendUrl,
516
+ frontendBaseUrl,
517
+ entryPath,
412
518
  apiUrl,
413
519
  docsUrl,
414
520
  dashboardUrl,
@@ -419,9 +525,12 @@ function printDevReadyBanner(input) {
419
525
  domain,
420
526
  dashboardPort,
421
527
  dashboardDomain
422
- } = input, verbose = options.verbose ?? !1, useProxy = hasCustomDomain && proxyReachable, feUrl = useProxy ? frontendUrl : `http://localhost:${frontendPort}`, apUrl = useProxy ? apiUrl : `http://localhost:${apiPort}`, dcUrl = useProxy ? docsUrl : `http://localhost:${docsPort}`, dbUrl = useProxy ? dashboardUrl : `http://localhost:${dashboardPort}`, blogUrl = `${feUrl}/blog`;
528
+ } = input, verbose = options.verbose ?? !1, useProxy = hasCustomDomain && proxyReachable, feUrl = useProxy ? frontendUrl : developmentUrl(`http://localhost:${frontendPort}`, entryPath), feBaseUrl = useProxy ? frontendBaseUrl : `http://localhost:${frontendPort}`, apUrl = useProxy ? apiUrl : `http://localhost:${apiPort}`, dcUrl = useProxy ? docsUrl : `http://localhost:${docsPort}`, dbUrl = useProxy ? dashboardUrl : `http://localhost:${dashboardPort}`, blogUrl = `${feBaseUrl}/blog`;
423
529
  console.log();
424
- console.log(` ${green("\u279C")} ${bold("Frontend")}: ${cyan(feUrl)}`);
530
+ const frontendLabel = entryPath === "/" ? "Site" : "App";
531
+ console.log(` ${green("\u279C")} ${bold(frontendLabel.padEnd(8))}: ${cyan(feUrl)}`);
532
+ if (entryPath !== "/")
533
+ console.log(` ${dim("\u279C")} ${dim("Site")}: ${dim(feBaseUrl)}`);
425
534
  if (nativeMode)
426
535
  console.log(` ${green("\u279C")} ${bold("Native")}: ${cyan(`Craft \u2192 ${feUrl}`)}`);
427
536
  console.log(` ${green("\u279C")} ${bold("API")}: ${cyan(apUrl)}`);
@@ -431,7 +540,7 @@ function printDevReadyBanner(input) {
431
540
  if (includeDashboard)
432
541
  console.log(` ${green("\u279C")} ${bold("Dashboard")}: ${cyan(dbUrl)}`);
433
542
  if (useProxy) {
434
- console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`http://localhost:${frontendPort} (bypasses the proxy)`)}`);
543
+ console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`${developmentUrl(`http://localhost:${frontendPort}`, entryPath)} (bypasses the proxy)`)}`);
435
544
  if (includeDashboard && dashboardDomain)
436
545
  console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`http://localhost:${dashboardPort} (dashboard, bypasses the proxy)`)}`);
437
546
  }
@@ -441,8 +550,8 @@ function printDevReadyBanner(input) {
441
550
  }
442
551
  if (verbose && domain) {
443
552
  console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${frontendPort} \u2192 ${domain}`)}`);
444
- console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendUrl}/api \u2192 localhost:${apiPort}`)}`);
445
- console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendUrl}/docs \u2192 localhost:${docsPort}`)}`);
553
+ console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendBaseUrl}/api \u2192 localhost:${apiPort}`)}`);
554
+ console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendBaseUrl}/docs \u2192 localhost:${docsPort}`)}`);
446
555
  if (includeDashboard)
447
556
  console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${dashboardPort} \u2192 ${dashboardDomain}`)}`);
448
557
  }
@@ -177,6 +177,26 @@ export function doctor(buddy) {
177
177
  const example = `CREATE UNIQUE INDEX IF NOT EXISTS "${first.table}_${first.columns.join("_")}_unique" ON "${first.table}" ("${first.columns.join('", "')}")`;
178
178
  throw Error(`${result.missing.length}/${result.declared.length} declared unique constraints have no UNIQUE index: ${sample}${more}. Run \`buddy migrate\` (re-queues missing unique-index migrations, #1952) \u2014 dedupe duplicate rows first or migrate hard-fails; if no migration file exists run \`buddy generate:migrations\`, or create manually: ${example}`);
179
179
  }, 1e4);
180
+ await probe(checks, "Migration ledger", async () => {
181
+ const { auditMigrationLedger } = await import("@stacksjs/database"), result = await auditMigrationLedger();
182
+ if (!result.supported)
183
+ return "Dialect not audited (skipped)";
184
+ if (result.entries.length === 0)
185
+ return "No migration files";
186
+ const { counts, orphans } = result;
187
+ if (!result.drift)
188
+ return `${result.entries.length} migrations, ledger consistent with the schema`;
189
+ const parts = [];
190
+ if (counts.stranded > 0)
191
+ parts.push(`${counts.stranded} applied but unrecorded (would re-run)`);
192
+ if (counts.partial > 0)
193
+ parts.push(`${counts.partial} half-applied`);
194
+ if (counts.reverted > 0)
195
+ parts.push(`${counts.reverted} recorded but missing from the schema`);
196
+ if (orphans.length > 0)
197
+ parts.push(`${orphans.length} ledger row(s) with no file on disk`);
198
+ throw Error(`Migration ledger has drifted: ${parts.join(", ")}. Inspect with \`buddy migrate:status\`, repair with \`buddy migrate:status --reconcile\`.`);
199
+ }, 1e4);
180
200
  await probe(checks, "FK orphans", async () => {
181
201
  const { findFkOrphans } = await import("@stacksjs/database"), result = await findFkOrphans();
182
202
  if (!result.supported)
@@ -553,9 +553,90 @@ export function migrate(buddy) {
553
553
  process.exit(ExitCode.FatalError);
554
554
  }
555
555
  log.success(`Wrote ${result.value.files.length} ${target} migration file(s) to database/migrations.`);
556
+ if (applied > 0) {
557
+ const { reconcileMigrationLedger } = await import("@stacksjs/database"), fixed = await reconcileMigrationLedger();
558
+ if (fixed.remapped.length > 0)
559
+ log.info(`Repointed ${fixed.remapped.length} ledger row(s) at their renumbered file.`);
560
+ if (fixed.recorded.length > 0)
561
+ log.info(`Recorded ${fixed.recorded.length} migration(s) already present in the schema.`);
562
+ if (fixed.skipped.length > 0)
563
+ log.warn(`${fixed.skipped.length} ledger entr(ies) need a look \u2014 run \`./buddy migrate:status\`.`);
564
+ }
556
565
  log.info("Review the change with `git diff`, then run `./buddy migrate`.");
557
566
  await outro("Regenerated.", { startTime: perf, useSeconds: !0 });
558
567
  process.exit(ExitCode.Success);
559
568
  });
569
+ 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) => {
570
+ const perf = options.json ? void 0 : await intro("buddy migrate:status"), { auditMigrationLedger, reconcileMigrationLedger } = await import("@stacksjs/database"), audit = await auditMigrationLedger();
571
+ if (options.json) {
572
+ console.log(JSON.stringify(audit, (_k, v) => v instanceof Set ? [...v] : v, 2));
573
+ process.exit(audit.drift ? ExitCode.FatalError : ExitCode.Success);
574
+ }
575
+ if (!audit.supported) {
576
+ log.info(`Dialect "${audit.dialect}" is not audited. Nothing to compare.`);
577
+ await outro("Skipped.", { startTime: perf, useSeconds: !0 });
578
+ process.exit(ExitCode.Success);
579
+ }
580
+ const { counts, entries, orphans } = audit, list = (status) => entries.filter((e) => e.status === status).map((e) => e.file), report = [], section = (heading, files) => {
581
+ if (files.length === 0)
582
+ return;
583
+ if (heading)
584
+ report.push(` ${heading}`);
585
+ for (const file of files.slice(0, 8))
586
+ report.push(` ${file}`);
587
+ if (files.length > 8)
588
+ report.push(` \u2026 +${files.length - 8} more`);
589
+ report.push("");
590
+ };
591
+ report.push("");
592
+ report.push(` Migration status: ${audit.dialect}`);
593
+ 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");
594
+ report.push(` ${entries.length} file(s) on disk \xB7 ${audit.recordedCount} recorded in the ledger`);
595
+ report.push("");
596
+ if (counts.applied > 0)
597
+ report.push(` ${counts.applied} applied - recorded, and present in the schema.`, "");
598
+ if (counts.unverifiable > 0)
599
+ report.push(` ${counts.unverifiable} unverifiable - data migrations with no schema trace to check.`, "");
600
+ section(`${counts.pending} pending - not applied yet, will run on the next \`buddy migrate\`:`, list("pending"));
601
+ if (counts.stranded > 0) {
602
+ report.push(` ${counts.stranded} STRANDED - already applied to the schema, but missing from the ledger.`);
603
+ report.push(" These re-run on the next `buddy migrate`, which is unsafe for anything not idempotent.");
604
+ section("", list("stranded"));
605
+ }
606
+ section(`${counts.partial} PARTIAL - some effects present, some missing. Needs a human:`, list("partial"));
607
+ section(`${counts.reverted} REVERTED - recorded as applied, but the effects are gone from the schema:`, list("reverted"));
608
+ if (orphans.length > 0)
609
+ 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?)"}`));
610
+ console.log(report.join(`
611
+ `));
612
+ if (!audit.drift) {
613
+ await outro("Ledger matches the corpus and the schema.", { startTime: perf, useSeconds: !0 });
614
+ process.exit(ExitCode.Success);
615
+ }
616
+ if (!options.reconcile) {
617
+ console.log(`
618
+ Repair with: ./buddy migrate:status --reconcile
619
+ That repoints renumbered ledger rows and records migrations the schema
620
+ already proves. It never runs SQL from a migration file.
621
+ `);
622
+ await outro("Drift detected.", { startTime: perf, useSeconds: !0 });
623
+ process.exit(ExitCode.FatalError);
624
+ }
625
+ const fixed = await reconcileMigrationLedger({ includePartial: options.includePartial });
626
+ if (fixed.remapped.length > 0)
627
+ log.success(`Repointed ${fixed.remapped.length} ledger row(s) at their renumbered file.`);
628
+ if (fixed.recorded.length > 0)
629
+ log.success(`Recorded ${fixed.recorded.length} migration(s) the schema already reflects.`);
630
+ if (fixed.skipped.length > 0) {
631
+ log.warn(`Left ${fixed.skipped.length} entr(ies) alone:`);
632
+ console.log(fixed.skipped.slice(0, 8).map((s) => ` ${s.file} \u2014 ${s.reason}`).join(`
633
+ `) + (fixed.skipped.length > 8 ? `
634
+ \u2026 +${fixed.skipped.length - 8} more` : ""));
635
+ }
636
+ if (fixed.remapped.length === 0 && fixed.recorded.length === 0)
637
+ log.info("Nothing could be repaired automatically.");
638
+ await outro("Reconciled.", { startTime: perf, useSeconds: !0 });
639
+ process.exit(ExitCode.Success);
640
+ });
560
641
  onUnknownSubcommand(buddy, "migrate");
561
642
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/buddy",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.251",
5
+ "version": "0.70.252",
6
6
  "description": "Meet Buddy. The Stacks runtime.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -88,53 +88,53 @@
88
88
  "prepublishOnly": "bun run build"
89
89
  },
90
90
  "dependencies": {
91
- "@stacksjs/actions": "^0.70.251",
92
- "@stacksjs/ai": "^0.70.251",
93
- "@stacksjs/alias": "^0.70.251",
94
- "@stacksjs/arrays": "^0.70.251",
95
- "@stacksjs/auth": "^0.70.251",
96
- "@stacksjs/build": "^0.70.251",
97
- "@stacksjs/cache": "^0.70.251",
98
- "@stacksjs/cli": "^0.70.251",
91
+ "@stacksjs/actions": "^0.70.252",
92
+ "@stacksjs/ai": "^0.70.252",
93
+ "@stacksjs/alias": "^0.70.252",
94
+ "@stacksjs/arrays": "^0.70.252",
95
+ "@stacksjs/auth": "^0.70.252",
96
+ "@stacksjs/build": "^0.70.252",
97
+ "@stacksjs/cache": "^0.70.252",
98
+ "@stacksjs/cli": "^0.70.252",
99
99
  "@stacksjs/clapp": "^0.2.12",
100
- "@stacksjs/cloud": "^0.70.251",
101
- "@stacksjs/collections": "^0.70.251",
102
- "@stacksjs/config": "^0.70.251",
103
- "@stacksjs/database": "^0.70.251",
104
- "@stacksjs/desktop-build": "^0.70.251",
105
- "@stacksjs/dns": "^0.70.251",
106
- "@stacksjs/email": "^0.70.251",
107
- "@stacksjs/enums": "^0.70.251",
108
- "@stacksjs/error-handling": "^0.70.251",
109
- "@stacksjs/events": "^0.70.251",
110
- "@stacksjs/git": "^0.70.251",
100
+ "@stacksjs/cloud": "^0.70.252",
101
+ "@stacksjs/collections": "^0.70.252",
102
+ "@stacksjs/config": "^0.70.252",
103
+ "@stacksjs/database": "^0.70.252",
104
+ "@stacksjs/desktop-build": "^0.70.252",
105
+ "@stacksjs/dns": "^0.70.252",
106
+ "@stacksjs/email": "^0.70.252",
107
+ "@stacksjs/enums": "^0.70.252",
108
+ "@stacksjs/error-handling": "^0.70.252",
109
+ "@stacksjs/events": "^0.70.252",
110
+ "@stacksjs/git": "^0.70.252",
111
111
  "@stacksjs/gitit": "^0.2.5",
112
- "@stacksjs/health": "^0.70.251",
112
+ "@stacksjs/health": "^0.70.252",
113
113
  "@stacksjs/dnsx": "^0.2.3",
114
114
  "@stacksjs/httx": "^0.1.10",
115
- "@stacksjs/image": "^0.70.251",
116
- "@stacksjs/lint": "^0.70.251",
117
- "@stacksjs/logging": "^0.70.251",
118
- "@stacksjs/notifications": "^0.70.251",
119
- "@stacksjs/objects": "^0.70.251",
120
- "@stacksjs/orm": "^0.70.251",
121
- "@stacksjs/path": "^0.70.251",
122
- "@stacksjs/skills": "^0.70.251",
123
- "@stacksjs/payments": "^0.70.251",
124
- "@stacksjs/realtime": "^0.70.251",
125
- "@stacksjs/router": "^0.70.251",
115
+ "@stacksjs/image": "^0.70.252",
116
+ "@stacksjs/lint": "^0.70.252",
117
+ "@stacksjs/logging": "^0.70.252",
118
+ "@stacksjs/notifications": "^0.70.252",
119
+ "@stacksjs/objects": "^0.70.252",
120
+ "@stacksjs/orm": "^0.70.252",
121
+ "@stacksjs/path": "^0.70.252",
122
+ "@stacksjs/skills": "^0.70.252",
123
+ "@stacksjs/payments": "^0.70.252",
124
+ "@stacksjs/realtime": "^0.70.252",
125
+ "@stacksjs/router": "^0.70.252",
126
126
  "@stacksjs/rpx": "^0.11.42",
127
- "@stacksjs/search-engine": "^0.70.251",
128
- "@stacksjs/security": "^0.70.251",
129
- "@stacksjs/server": "^0.70.251",
130
- "@stacksjs/storage": "^0.70.251",
131
- "@stacksjs/strings": "^0.70.251",
132
- "@stacksjs/testing": "^0.70.251",
133
- "@stacksjs/tunnel": "^0.70.251",
134
- "@stacksjs/types": "^0.70.251",
135
- "@stacksjs/ui": "^0.70.251",
136
- "@stacksjs/utils": "^0.70.251",
137
- "@stacksjs/validation": "^0.70.251",
127
+ "@stacksjs/search-engine": "^0.70.252",
128
+ "@stacksjs/security": "^0.70.252",
129
+ "@stacksjs/server": "^0.70.252",
130
+ "@stacksjs/storage": "^0.70.252",
131
+ "@stacksjs/strings": "^0.70.252",
132
+ "@stacksjs/testing": "^0.70.252",
133
+ "@stacksjs/tunnel": "^0.70.252",
134
+ "@stacksjs/types": "^0.70.252",
135
+ "@stacksjs/ui": "^0.70.252",
136
+ "@stacksjs/utils": "^0.70.252",
137
+ "@stacksjs/validation": "^0.70.252",
138
138
  "@stacksjs/ts-cloud": "^0.7.92",
139
139
  "ajv": "^8.20.0",
140
140
  "ajv-formats": "^3.0.1",