@stacksjs/buddy 0.70.251 → 0.70.253
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/deploy.js +6 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.js +148 -21
- package/dist/commands/doctor.js +20 -0
- package/dist/commands/migrate.js +81 -0
- package/package.json +43 -43
package/dist/commands/deploy.js
CHANGED
|
@@ -1535,6 +1535,12 @@ export function deploy(buddy) {
|
|
|
1535
1535
|
};
|
|
1536
1536
|
buddy.command("deploy [env]", descriptions.deploy).option("--domain", descriptions.domain, { default: void 0 }).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--prod", descriptions.production, { default: !0 }).option("--dev", descriptions.development, { default: !1 }).option("--yes", descriptions.yes, { default: !1 }).option("--site <name>", "Deploy only this one site to the existing server (multi-tenant surgical add)", { default: void 0 }).option("--staging", descriptions.staging, { default: !1 }).option("--docker", "Also build an OCI image with pantry (native, no Docker daemon) and push it to the pantry registry", { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (envArg, options) => {
|
|
1537
1537
|
log.debug("Running `buddy deploy` ...", options);
|
|
1538
|
+
if (process.argv.includes("--dry-run") || options.dryRun === !0) {
|
|
1539
|
+
log.error("`buddy deploy --dry-run` is not supported.");
|
|
1540
|
+
log.info("Deploy has no preview mode: provisioning, release shipping, and DNS all mutate real infrastructure.");
|
|
1541
|
+
log.info("To see what would ship without touching the server, inspect `config/cloud.ts` sites, or run `buddy deploy --site <name>` to narrow a real deploy to one site.");
|
|
1542
|
+
process.exit(ExitCode.FatalError);
|
|
1543
|
+
}
|
|
1538
1544
|
await ensureDeployPrerequisites(options.verbose === !0);
|
|
1539
1545
|
const deployEnv = envArg || (options.staging ? "staging" : options.dev ? "development" : "production"), deployEnvName = deployEnv === "prod" ? "production" : deployEnv === "dev" ? "development" : deployEnv;
|
|
1540
1546
|
process.env.APP_ENV = deployEnvName;
|
package/dist/commands/dev.d.ts
CHANGED
|
@@ -7,6 +7,21 @@ 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 resolveDevelopmentLaunch(input?: {
|
|
12
|
+
root?: string
|
|
13
|
+
browser?: boolean
|
|
14
|
+
native?: boolean
|
|
15
|
+
site?: boolean
|
|
16
|
+
configuredLaunch?: 'browser' | 'native'
|
|
17
|
+
}): 'browser' | 'native';
|
|
18
|
+
export declare function resolveDevelopmentEntryPath(input?: {
|
|
19
|
+
root?: string
|
|
20
|
+
site?: boolean
|
|
21
|
+
configuredPath?: string
|
|
22
|
+
}): string;
|
|
23
|
+
export declare function developmentUrl(baseUrl: string, entryPath: string): string;
|
|
24
|
+
export declare function developmentBrowserCommand(url: string, platform?: unknown): string[];
|
|
10
25
|
export declare function dev(buddy: CLI): void;
|
|
11
26
|
export declare function startDevelopmentServer(_options: DevOptions, _startTime?: number): Promise<void>;
|
|
12
27
|
export declare function setupPrettyDevEnvironment(input: {
|
package/dist/commands/dev.js
CHANGED
|
@@ -72,6 +72,117 @@ 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 configuredDevelopmentLaunch(root) {
|
|
114
|
+
const environmentValue = process.env.STACKS_DEV_LAUNCH?.trim().toLowerCase();
|
|
115
|
+
if (environmentValue === "browser" || environmentValue === "native")
|
|
116
|
+
return environmentValue;
|
|
117
|
+
try {
|
|
118
|
+
const value = readFileSync(join(root, "config/app.ts"), "utf8").match(/\bdevLaunch\s*:\s*(['"`])(browser|native)\1/)?.[2];
|
|
119
|
+
return value === "browser" || value === "native" ? value : void 0;
|
|
120
|
+
} catch {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function resolveDevelopmentLaunch(input = {}) {
|
|
125
|
+
if (input.browser || input.site)
|
|
126
|
+
return "browser";
|
|
127
|
+
if (input.native)
|
|
128
|
+
return "native";
|
|
129
|
+
const root = input.root ?? projectPath();
|
|
130
|
+
return input.configuredLaunch ?? configuredDevelopmentLaunch(root) ?? "browser";
|
|
131
|
+
}
|
|
132
|
+
function viewSource(root, route) {
|
|
133
|
+
const base = join(root, "resources/views", route);
|
|
134
|
+
for (const extension of ["stx", "vue", "html"]) {
|
|
135
|
+
const path = `${base}.${extension}`;
|
|
136
|
+
if (existsSync(path))
|
|
137
|
+
try {
|
|
138
|
+
return readFileSync(path, "utf8");
|
|
139
|
+
} catch {
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
function usesApplicationLayout(source) {
|
|
146
|
+
const layout = source.match(/@extends\(\s*['"]layouts\/([^'"]+)['"]\s*\)/)?.[1] ?? source.match(/@layout\(\s*['"]layouts\/([^'"]+)['"]\s*\)/)?.[1];
|
|
147
|
+
if (!layout)
|
|
148
|
+
return !1;
|
|
149
|
+
return !SITE_LAYOUT_NAMES.has(layout.split("/").at(-1) ?? layout);
|
|
150
|
+
}
|
|
151
|
+
export function resolveDevelopmentEntryPath(input = {}) {
|
|
152
|
+
if (input.site)
|
|
153
|
+
return "/";
|
|
154
|
+
const root = input.root ?? projectPath(), configuredPath = input.configuredPath ?? configuredApplicationPath(root);
|
|
155
|
+
if (configuredPath)
|
|
156
|
+
return normalizeDevelopmentEntryPath(configuredPath);
|
|
157
|
+
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);
|
|
158
|
+
if (appCandidate)
|
|
159
|
+
return normalizeDevelopmentEntryPath(`/${appCandidate.route.replace(/\/index$/, "")}`);
|
|
160
|
+
return loginExists ? "/login" : "/";
|
|
161
|
+
}
|
|
162
|
+
export function developmentUrl(baseUrl, entryPath) {
|
|
163
|
+
const normalized = normalizeDevelopmentEntryPath(entryPath);
|
|
164
|
+
if (normalized === "/")
|
|
165
|
+
return baseUrl.replace(/\/$/, "");
|
|
166
|
+
return new URL(normalized, `${baseUrl.replace(/\/$/, "")}/`).toString();
|
|
167
|
+
}
|
|
168
|
+
export function developmentBrowserCommand(url, platform = process.platform) {
|
|
169
|
+
if (platform === "darwin")
|
|
170
|
+
return ["open", url];
|
|
171
|
+
if (platform === "win32")
|
|
172
|
+
return ["cmd", "/c", "start", "", url];
|
|
173
|
+
return ["xdg-open", url];
|
|
174
|
+
}
|
|
175
|
+
function openDevelopmentBrowser(url) {
|
|
176
|
+
if (!process.stdout.isTTY || process.env.CI || process.env.STACKS_DEV_NO_OPEN === "1")
|
|
177
|
+
return;
|
|
178
|
+
try {
|
|
179
|
+
Bun.spawn(developmentBrowserCommand(url), {
|
|
180
|
+
stdin: "ignore",
|
|
181
|
+
stdout: "ignore",
|
|
182
|
+
stderr: "ignore"
|
|
183
|
+
}).unref();
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
75
186
|
async function canStartPrettyDevProxy() {
|
|
76
187
|
if (await waitForHttpsProxy(443, 150))
|
|
77
188
|
return !0;
|
|
@@ -96,11 +207,15 @@ export function dev(buddy) {
|
|
|
96
207
|
interactive: "Get asked which development server to start",
|
|
97
208
|
select: "Which development server are you trying to start?",
|
|
98
209
|
withLocalhost: "Include the localhost URL in the output",
|
|
210
|
+
browser: "Open the application in a browser instead of its configured native window",
|
|
211
|
+
site: "Open the marketing site instead of the application",
|
|
99
212
|
project: "Target a specific project",
|
|
100
213
|
verbose: "Enable verbose output"
|
|
101
214
|
};
|
|
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()
|
|
215
|
+
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("--browser", descriptions.browser, { 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) => {
|
|
216
|
+
const perf = Bun.nanoseconds();
|
|
217
|
+
process.env.STACKS_DEV_ENTRY_PATH = resolveDevelopmentEntryPath({ site: options.site });
|
|
218
|
+
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
219
|
if (target) {
|
|
105
220
|
const serverOptions = { ...options }, a = await actions();
|
|
106
221
|
switch (target) {
|
|
@@ -120,7 +235,7 @@ export function dev(buddy) {
|
|
|
120
235
|
await a.runDashboardDevServer(serverOptions);
|
|
121
236
|
break;
|
|
122
237
|
case "desktop":
|
|
123
|
-
await
|
|
238
|
+
await startDevelopmentServer({ ...serverOptions, native: !0 }, perf);
|
|
124
239
|
break;
|
|
125
240
|
case "system-tray":
|
|
126
241
|
await a.runSystemTrayDevServer(serverOptions);
|
|
@@ -144,7 +259,7 @@ export function dev(buddy) {
|
|
|
144
259
|
frontend: () => a.runFrontendDevServer(options),
|
|
145
260
|
api: () => a.runApiDevServer(options),
|
|
146
261
|
dashboard: () => a.runDashboardDevServer(options),
|
|
147
|
-
desktop: () =>
|
|
262
|
+
desktop: () => startDevelopmentServer({ ...options, native: !0 }, perf),
|
|
148
263
|
native: () => startDevelopmentServer({ ...options, native: !0 }, perf),
|
|
149
264
|
components: () => a.runComponentsDevServer(options),
|
|
150
265
|
docs: () => a.runDocsDevServer(options)
|
|
@@ -186,14 +301,8 @@ export function dev(buddy) {
|
|
|
186
301
|
await startDevelopmentServer({ ...options, native: !0 }, perf);
|
|
187
302
|
});
|
|
188
303
|
buddy.command("dev:desktop", descriptions.desktop).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
189
|
-
const perf = await intro("buddy dev:desktop")
|
|
190
|
-
|
|
191
|
-
await outro("While running the dev:desktop command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
192
|
-
process.exit(ExitCode.FatalError);
|
|
193
|
-
}
|
|
194
|
-
console.log("");
|
|
195
|
-
await outro("Exited", { startTime: perf, useSeconds: !0 });
|
|
196
|
-
process.exit(ExitCode.Success);
|
|
304
|
+
const perf = await intro("buddy dev:desktop");
|
|
305
|
+
await startDevelopmentServer({ ...options, native: !0 }, perf);
|
|
197
306
|
});
|
|
198
307
|
buddy.command("dev:api", descriptions.api).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--no-watch-types", "Skip the model/config type-regeneration watcher", { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
199
308
|
const a = await actions();
|
|
@@ -203,7 +312,8 @@ export function dev(buddy) {
|
|
|
203
312
|
});
|
|
204
313
|
await a.runApiDevServer(options);
|
|
205
314
|
});
|
|
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) => {
|
|
315
|
+
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) => {
|
|
316
|
+
process.env.STACKS_DEV_ENTRY_PATH = resolveDevelopmentEntryPath({ site: options.site });
|
|
207
317
|
await (await actions()).runFrontendDevServer(options);
|
|
208
318
|
});
|
|
209
319
|
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 +325,13 @@ export function dev(buddy) {
|
|
|
215
325
|
onUnknownSubcommand(buddy, "dev");
|
|
216
326
|
}
|
|
217
327
|
export async function startDevelopmentServer(_options, _startTime) {
|
|
218
|
-
const options = _options, startedAt = _startTime, appUrl = process.env.APP_URL ?? "stacks.localhost", nativeMode =
|
|
328
|
+
const options = _options, startedAt = _startTime, appUrl = process.env.APP_URL ?? "stacks.localhost", nativeMode = resolveDevelopmentLaunch({
|
|
329
|
+
browser: options.browser,
|
|
330
|
+
native: options.native,
|
|
331
|
+
site: options.site
|
|
332
|
+
}) === "native", entryPath = resolveDevelopmentEntryPath({ site: options.site });
|
|
333
|
+
process.env.STACKS_DEV_ENTRY_PATH = entryPath;
|
|
334
|
+
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
335
|
if (process.env.STACKS_DEV_NO_KILL !== "1")
|
|
220
336
|
await cleanupStaleDevProcesses([
|
|
221
337
|
preferredFrontendPort,
|
|
@@ -240,7 +356,7 @@ export async function startDevelopmentServer(_options, _startTime) {
|
|
|
240
356
|
localhostOnly,
|
|
241
357
|
proxyManagedExternally,
|
|
242
358
|
systemAuthorized
|
|
243
|
-
}), prettySetupRequired = prettyUrlsRequested && !usePrettyUrls, hasCustomDomain = usePrettyUrls && !proxyManagedExternally, displayedDomain = usePrettyUrls ? domain : null, dashboardDomain = displayedDomain ? `dashboard.${displayedDomain}` : null,
|
|
359
|
+
}), 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
360
|
process.env.STACKS_PROXY_MANAGED = "1";
|
|
245
361
|
process.env.STACKS_DEV_QUIET = "1";
|
|
246
362
|
console.log();
|
|
@@ -297,7 +413,7 @@ export async function startDevelopmentServer(_options, _startTime) {
|
|
|
297
413
|
{ name: "API", port: apiPort }
|
|
298
414
|
], readinessTimeoutMs = 30000;
|
|
299
415
|
let readyAnnounced = !1;
|
|
300
|
-
const nativeUrl = `http://localhost:${frontendPort}`, nativeWindowReady = nativeMode ? waitForPort(frontendPort, readinessTimeoutMs).then(async (ready) => {
|
|
416
|
+
const nativeUrl = developmentUrl(`http://localhost:${frontendPort}`, entryPath), nativeWindowReady = nativeMode ? waitForPort(frontendPort, readinessTimeoutMs).then(async (ready) => {
|
|
301
417
|
if (!ready) {
|
|
302
418
|
log.warn(`Native window skipped because the frontend did not answer within ${readinessTimeoutMs / 1000}s`);
|
|
303
419
|
return;
|
|
@@ -340,6 +456,8 @@ export async function startDevelopmentServer(_options, _startTime) {
|
|
|
340
456
|
hasCustomDomain: !!appLooksCustom && !localhostOnly,
|
|
341
457
|
proxyReachable: proxyManagedExternally ? !0 : proxyReachable,
|
|
342
458
|
frontendUrl,
|
|
459
|
+
frontendBaseUrl,
|
|
460
|
+
entryPath,
|
|
343
461
|
apiUrl,
|
|
344
462
|
docsUrl,
|
|
345
463
|
dashboardUrl,
|
|
@@ -351,6 +469,10 @@ export async function startDevelopmentServer(_options, _startTime) {
|
|
|
351
469
|
dashboardPort,
|
|
352
470
|
dashboardDomain
|
|
353
471
|
});
|
|
472
|
+
if (!nativeMode) {
|
|
473
|
+
const browserUrl = hasCustomDomain && (proxyManagedExternally || proxyReachable) ? frontendUrl : developmentUrl(`http://localhost:${frontendPort}`, entryPath);
|
|
474
|
+
openDevelopmentBrowser(browserUrl);
|
|
475
|
+
}
|
|
354
476
|
if (startedAt) {
|
|
355
477
|
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
478
|
console.log(` ${dim(summary)}`);
|
|
@@ -409,6 +531,8 @@ function printDevReadyBanner(input) {
|
|
|
409
531
|
hasCustomDomain,
|
|
410
532
|
proxyReachable,
|
|
411
533
|
frontendUrl,
|
|
534
|
+
frontendBaseUrl,
|
|
535
|
+
entryPath,
|
|
412
536
|
apiUrl,
|
|
413
537
|
docsUrl,
|
|
414
538
|
dashboardUrl,
|
|
@@ -419,9 +543,12 @@ function printDevReadyBanner(input) {
|
|
|
419
543
|
domain,
|
|
420
544
|
dashboardPort,
|
|
421
545
|
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 = `${
|
|
546
|
+
} = 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
547
|
console.log();
|
|
424
|
-
|
|
548
|
+
const frontendLabel = entryPath === "/" ? "Site" : "App";
|
|
549
|
+
console.log(` ${green("\u279C")} ${bold(frontendLabel.padEnd(8))}: ${cyan(feUrl)}`);
|
|
550
|
+
if (entryPath !== "/")
|
|
551
|
+
console.log(` ${dim("\u279C")} ${dim("Site")}: ${dim(feBaseUrl)}`);
|
|
425
552
|
if (nativeMode)
|
|
426
553
|
console.log(` ${green("\u279C")} ${bold("Native")}: ${cyan(`Craft \u2192 ${feUrl}`)}`);
|
|
427
554
|
console.log(` ${green("\u279C")} ${bold("API")}: ${cyan(apUrl)}`);
|
|
@@ -431,7 +558,7 @@ function printDevReadyBanner(input) {
|
|
|
431
558
|
if (includeDashboard)
|
|
432
559
|
console.log(` ${green("\u279C")} ${bold("Dashboard")}: ${cyan(dbUrl)}`);
|
|
433
560
|
if (useProxy) {
|
|
434
|
-
console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`http://localhost:${frontendPort} (bypasses the proxy)`)}`);
|
|
561
|
+
console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`${developmentUrl(`http://localhost:${frontendPort}`, entryPath)} (bypasses the proxy)`)}`);
|
|
435
562
|
if (includeDashboard && dashboardDomain)
|
|
436
563
|
console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`http://localhost:${dashboardPort} (dashboard, bypasses the proxy)`)}`);
|
|
437
564
|
}
|
|
@@ -441,8 +568,8 @@ function printDevReadyBanner(input) {
|
|
|
441
568
|
}
|
|
442
569
|
if (verbose && domain) {
|
|
443
570
|
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(`${
|
|
571
|
+
console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendBaseUrl}/api \u2192 localhost:${apiPort}`)}`);
|
|
572
|
+
console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendBaseUrl}/docs \u2192 localhost:${docsPort}`)}`);
|
|
446
573
|
if (includeDashboard)
|
|
447
574
|
console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${dashboardPort} \u2192 ${dashboardDomain}`)}`);
|
|
448
575
|
}
|
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.253",
|
|
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.253",
|
|
92
|
+
"@stacksjs/ai": "^0.70.253",
|
|
93
|
+
"@stacksjs/alias": "^0.70.253",
|
|
94
|
+
"@stacksjs/arrays": "^0.70.253",
|
|
95
|
+
"@stacksjs/auth": "^0.70.253",
|
|
96
|
+
"@stacksjs/build": "^0.70.253",
|
|
97
|
+
"@stacksjs/cache": "^0.70.253",
|
|
98
|
+
"@stacksjs/cli": "^0.70.253",
|
|
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.253",
|
|
101
|
+
"@stacksjs/collections": "^0.70.253",
|
|
102
|
+
"@stacksjs/config": "^0.70.253",
|
|
103
|
+
"@stacksjs/database": "^0.70.253",
|
|
104
|
+
"@stacksjs/desktop-build": "^0.70.253",
|
|
105
|
+
"@stacksjs/dns": "^0.70.253",
|
|
106
|
+
"@stacksjs/email": "^0.70.253",
|
|
107
|
+
"@stacksjs/enums": "^0.70.253",
|
|
108
|
+
"@stacksjs/error-handling": "^0.70.253",
|
|
109
|
+
"@stacksjs/events": "^0.70.253",
|
|
110
|
+
"@stacksjs/git": "^0.70.253",
|
|
111
111
|
"@stacksjs/gitit": "^0.2.5",
|
|
112
|
-
"@stacksjs/health": "^0.70.
|
|
112
|
+
"@stacksjs/health": "^0.70.253",
|
|
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.253",
|
|
116
|
+
"@stacksjs/lint": "^0.70.253",
|
|
117
|
+
"@stacksjs/logging": "^0.70.253",
|
|
118
|
+
"@stacksjs/notifications": "^0.70.253",
|
|
119
|
+
"@stacksjs/objects": "^0.70.253",
|
|
120
|
+
"@stacksjs/orm": "^0.70.253",
|
|
121
|
+
"@stacksjs/path": "^0.70.253",
|
|
122
|
+
"@stacksjs/skills": "^0.70.253",
|
|
123
|
+
"@stacksjs/payments": "^0.70.253",
|
|
124
|
+
"@stacksjs/realtime": "^0.70.253",
|
|
125
|
+
"@stacksjs/router": "^0.70.253",
|
|
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.253",
|
|
128
|
+
"@stacksjs/security": "^0.70.253",
|
|
129
|
+
"@stacksjs/server": "^0.70.253",
|
|
130
|
+
"@stacksjs/storage": "^0.70.253",
|
|
131
|
+
"@stacksjs/strings": "^0.70.253",
|
|
132
|
+
"@stacksjs/testing": "^0.70.253",
|
|
133
|
+
"@stacksjs/tunnel": "^0.70.253",
|
|
134
|
+
"@stacksjs/types": "^0.70.253",
|
|
135
|
+
"@stacksjs/ui": "^0.70.253",
|
|
136
|
+
"@stacksjs/utils": "^0.70.253",
|
|
137
|
+
"@stacksjs/validation": "^0.70.253",
|
|
138
138
|
"@stacksjs/ts-cloud": "^0.7.92",
|
|
139
139
|
"ajv": "^8.20.0",
|
|
140
140
|
"ajv-formats": "^3.0.1",
|