@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.
- package/dist/commands/dev.d.ts +8 -0
- package/dist/commands/dev.js +120 -11
- package/dist/commands/doctor.js +20 -0
- package/dist/commands/migrate.js +81 -0
- package/package.json +43 -43
package/dist/commands/dev.d.ts
CHANGED
|
@@ -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: {
|
package/dist/commands/dev.js
CHANGED
|
@@ -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()
|
|
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,
|
|
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,
|
|
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 = `${
|
|
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
|
-
|
|
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(`${
|
|
445
|
-
console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${
|
|
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
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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)
|
package/dist/commands/migrate.js
CHANGED
|
@@ -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.
|
|
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.
|
|
92
|
-
"@stacksjs/ai": "^0.70.
|
|
93
|
-
"@stacksjs/alias": "^0.70.
|
|
94
|
-
"@stacksjs/arrays": "^0.70.
|
|
95
|
-
"@stacksjs/auth": "^0.70.
|
|
96
|
-
"@stacksjs/build": "^0.70.
|
|
97
|
-
"@stacksjs/cache": "^0.70.
|
|
98
|
-
"@stacksjs/cli": "^0.70.
|
|
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.
|
|
101
|
-
"@stacksjs/collections": "^0.70.
|
|
102
|
-
"@stacksjs/config": "^0.70.
|
|
103
|
-
"@stacksjs/database": "^0.70.
|
|
104
|
-
"@stacksjs/desktop-build": "^0.70.
|
|
105
|
-
"@stacksjs/dns": "^0.70.
|
|
106
|
-
"@stacksjs/email": "^0.70.
|
|
107
|
-
"@stacksjs/enums": "^0.70.
|
|
108
|
-
"@stacksjs/error-handling": "^0.70.
|
|
109
|
-
"@stacksjs/events": "^0.70.
|
|
110
|
-
"@stacksjs/git": "^0.70.
|
|
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.
|
|
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.
|
|
116
|
-
"@stacksjs/lint": "^0.70.
|
|
117
|
-
"@stacksjs/logging": "^0.70.
|
|
118
|
-
"@stacksjs/notifications": "^0.70.
|
|
119
|
-
"@stacksjs/objects": "^0.70.
|
|
120
|
-
"@stacksjs/orm": "^0.70.
|
|
121
|
-
"@stacksjs/path": "^0.70.
|
|
122
|
-
"@stacksjs/skills": "^0.70.
|
|
123
|
-
"@stacksjs/payments": "^0.70.
|
|
124
|
-
"@stacksjs/realtime": "^0.70.
|
|
125
|
-
"@stacksjs/router": "^0.70.
|
|
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.
|
|
128
|
-
"@stacksjs/security": "^0.70.
|
|
129
|
-
"@stacksjs/server": "^0.70.
|
|
130
|
-
"@stacksjs/storage": "^0.70.
|
|
131
|
-
"@stacksjs/strings": "^0.70.
|
|
132
|
-
"@stacksjs/testing": "^0.70.
|
|
133
|
-
"@stacksjs/tunnel": "^0.70.
|
|
134
|
-
"@stacksjs/types": "^0.70.
|
|
135
|
-
"@stacksjs/ui": "^0.70.
|
|
136
|
-
"@stacksjs/utils": "^0.70.
|
|
137
|
-
"@stacksjs/validation": "^0.70.
|
|
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",
|