@stacksjs/buddy 0.70.258 → 0.70.260
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +11 -1
- package/dist/cli.js +5 -164
- package/dist/commands/about.js +1 -35
- package/dist/commands/add.js +1 -25
- package/dist/commands/ai-context.js +4 -27
- package/dist/commands/auth.js +1 -68
- package/dist/commands/build.js +1 -221
- package/dist/commands/cd.js +8 -39
- package/dist/commands/changelog.js +1 -32
- package/dist/commands/clean.js +1 -44
- package/dist/commands/cloud.js +2 -519
- package/dist/commands/commit.js +1 -16
- package/dist/commands/completion.js +6 -42
- package/dist/commands/config-migrate.js +6 -94
- package/dist/commands/configure.js +2 -45
- package/dist/commands/create.js +1 -162
- package/dist/commands/deploy.js +13 -1676
- package/dist/commands/desktop-apple.js +12 -399
- package/dist/commands/dev.js +6 -1083
- package/dist/commands/dns.js +3 -103
- package/dist/commands/docs/buddy-commands.js +10 -132
- package/dist/commands/docs/buddy-commands.test.js +1 -38
- package/dist/commands/docs/framework-repo.js +1 -14
- package/dist/commands/docs/framework-repo.test.js +1 -68
- package/dist/commands/docs/generated-artifacts.js +3 -70
- package/dist/commands/docs/generated-artifacts.test.js +1 -19
- package/dist/commands/docs/links.js +2 -85
- package/dist/commands/docs/links.test.js +4 -66
- package/dist/commands/docs.js +1 -24
- package/dist/commands/doctor.js +2 -478
- package/dist/commands/domains.js +2 -152
- package/dist/commands/email.js +31 -503
- package/dist/commands/env.js +1 -312
- package/dist/commands/extension.js +1 -239
- package/dist/commands/features.js +9 -345
- package/dist/commands/fresh.js +1 -46
- package/dist/commands/generate.js +2 -152
- package/dist/commands/http.js +1 -33
- package/dist/commands/index.js +1 -57
- package/dist/commands/install.js +1 -22
- package/dist/commands/key.js +1 -23
- package/dist/commands/link.js +2 -147
- package/dist/commands/lint.js +2 -84
- package/dist/commands/list.js +1 -140
- package/dist/commands/mail.js +15 -1477
- package/dist/commands/maintenance.js +1 -141
- package/dist/commands/make.js +1 -392
- package/dist/commands/migrate-project.js +1 -49
- package/dist/commands/migrate.js +27 -625
- package/dist/commands/outdated.js +1 -19
- package/dist/commands/package.js +1 -17
- package/dist/commands/phone.js +17 -181
- package/dist/commands/ports.js +2 -118
- package/dist/commands/prepublish.js +1 -17
- package/dist/commands/projects.js +1 -29
- package/dist/commands/protocol/craft-evidence.js +4 -98
- package/dist/commands/protocol/craft-evidence.test.js +1 -20
- package/dist/commands/protocol/desktop-lifecycle-report.js +3 -57
- package/dist/commands/protocol/desktop-lifecycle-report.test.js +1 -32
- package/dist/commands/protocol/desktop-support.js +2 -32
- package/dist/commands/protocol/desktop-workflow.test.js +3 -35
- package/dist/commands/protocol/driver-registry.js +2 -71
- package/dist/commands/protocol/pantry-evidence.js +4 -144
- package/dist/commands/protocol/pantry-evidence.test.js +1 -29
- package/dist/commands/protocol/release-manifest.js +3 -102
- package/dist/commands/protocol/release-manifest.test.js +2 -34
- package/dist/commands/protocol/run-conformance.js +4 -346
- package/dist/commands/protocol/run-conformance.test.js +1 -52
- package/dist/commands/protocol/run-driver-contracts.js +4 -83
- package/dist/commands/protocol/run-driver-contracts.test.js +1 -18
- package/dist/commands/protocol/source-manifest.js +3 -113
- package/dist/commands/protocol/sync-suite.js +3 -136
- package/dist/commands/protocol/sync-suite.test.js +1 -44
- package/dist/commands/protocol.js +1 -60
- package/dist/commands/publish.js +8 -500
- package/dist/commands/queue.js +1 -248
- package/dist/commands/release.js +1 -42
- package/dist/commands/route.js +1 -21
- package/dist/commands/run-tool.js +1 -9
- package/dist/commands/saas.js +1 -25
- package/dist/commands/schedule.js +1 -61
- package/dist/commands/search.js +1 -84
- package/dist/commands/seed.js +1 -47
- package/dist/commands/serve.js +1 -55
- package/dist/commands/setup-ai.js +2 -121
- package/dist/commands/setup.js +2 -305
- package/dist/commands/share.js +1 -214
- package/dist/commands/sms.js +32 -319
- package/dist/commands/stacks.js +1 -72
- package/dist/commands/telemetry.js +1 -74
- package/dist/commands/test.js +1 -130
- package/dist/commands/tinker.js +1 -37
- package/dist/commands/types.js +1 -18
- package/dist/commands/upgrade.js +1 -98
- package/dist/commands/user.js +3 -84
- package/dist/commands/version.js +1 -14
- package/dist/config.js +1 -231
- package/dist/custom-cli.js +1 -23
- package/dist/database-preflight.js +1 -88
- package/dist/global-options.js +1 -4
- package/dist/index.js +1 -1
- package/dist/lazy-commands.js +1 -241
- package/dist/migrators/index.js +2 -62
- package/dist/migrators/laravel/index.js +1 -148
- package/dist/migrators/laravel/migrations.js +4 -231
- package/dist/migrators/laravel/models.js +8 -111
- package/dist/migrators/rails/index.js +1 -11
- package/dist/production-server.js +1 -257
- package/dist/project-setup.js +1 -36
- package/dist/serve-entry.js +1 -5
- package/dist/version-info.js +1 -5
- package/package.json +53 -46
package/dist/commands/dev.js
CHANGED
|
@@ -1,1083 +1,6 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import process
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import { dirname, join, relative } from "node:path";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { Action } from "@stacksjs/enums";
|
|
10
|
-
import { libsPath, projectPath, stxPath } from "@stacksjs/path";
|
|
11
|
-
import { ExitCode } from "@stacksjs/types";
|
|
12
|
-
import { version } from "../../package.json";
|
|
13
|
-
const DEV_BOOT_STARTING_LINE_COUNT = 3;
|
|
14
|
-
function eraseDevBootStartingLines() {
|
|
15
|
-
if (!process.stdout.isTTY)
|
|
16
|
-
return;
|
|
17
|
-
for (let i = 0;i < DEV_BOOT_STARTING_LINE_COUNT; i++) {
|
|
18
|
-
readline.moveCursor(process.stdout, 0, -1);
|
|
19
|
-
readline.clearLine(process.stdout, 0);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
let developmentRpx;
|
|
23
|
-
function resolveRpxEntryPath() {
|
|
24
|
-
return [
|
|
25
|
-
projectPath("node_modules/@stacksjs/rpx/dist/index.js"),
|
|
26
|
-
join(homedir(), "Code/Tools/rpx/packages/rpx/dist/index.js"),
|
|
27
|
-
projectPath("pantry/@stacksjs/rpx/dist/src/index.js")
|
|
28
|
-
].find((entry) => existsSync(entry)) ?? null;
|
|
29
|
-
}
|
|
30
|
-
async function importDevelopmentRpx() {
|
|
31
|
-
if (developmentRpx)
|
|
32
|
-
return developmentRpx;
|
|
33
|
-
const entry = resolveRpxEntryPath();
|
|
34
|
-
developmentRpx = entry ? await import(entry) : await import("@stacksjs/rpx");
|
|
35
|
-
return developmentRpx;
|
|
36
|
-
}
|
|
37
|
-
const activeRpxRegistryIds = [];
|
|
38
|
-
let _actions;
|
|
39
|
-
async function actions() {
|
|
40
|
-
if (!_actions)
|
|
41
|
-
_actions = await import("@stacksjs/actions");
|
|
42
|
-
return _actions;
|
|
43
|
-
}
|
|
44
|
-
export const interactiveDevChoices = [
|
|
45
|
-
{ value: "all", title: "All" },
|
|
46
|
-
{ value: "frontend", title: "Frontend" },
|
|
47
|
-
{ value: "api", title: "Backend" },
|
|
48
|
-
{ value: "dashboard", title: "Dashboard" },
|
|
49
|
-
{ value: "desktop", title: "Desktop" },
|
|
50
|
-
{ value: "native", title: "Native App" },
|
|
51
|
-
{ value: "components", title: "Components" },
|
|
52
|
-
{ value: "docs", title: "Documentation" }
|
|
53
|
-
];
|
|
54
|
-
export async function dispatchInteractiveDevSelection(selection, runners) {
|
|
55
|
-
if (!Object.hasOwn(runners, selection))
|
|
56
|
-
return !1;
|
|
57
|
-
await runners[selection]();
|
|
58
|
-
return !0;
|
|
59
|
-
}
|
|
60
|
-
export function resolvePrettyDevDomain(appUrl, nativeMode = !1) {
|
|
61
|
-
if (!appUrl || nativeMode)
|
|
62
|
-
return null;
|
|
63
|
-
try {
|
|
64
|
-
const hostname = new URL(/^https?:\/\//i.test(appUrl) ? appUrl : `https://${appUrl}`).hostname.toLowerCase();
|
|
65
|
-
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0")
|
|
66
|
-
return null;
|
|
67
|
-
return hostname;
|
|
68
|
-
} catch {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
export function shouldUsePrettyDevUrls(input) {
|
|
73
|
-
return input.domain !== null && !input.localhostOnly && (input.proxyManagedExternally || input.systemAuthorized);
|
|
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
|
-
}
|
|
186
|
-
async function canStartPrettyDevProxy() {
|
|
187
|
-
if (await waitForHttpsProxy(443, 150))
|
|
188
|
-
return !0;
|
|
189
|
-
try {
|
|
190
|
-
const { authorizeSystemAccess } = await importDevelopmentRpx();
|
|
191
|
-
return authorizeSystemAccess({ interactive: !1 });
|
|
192
|
-
} catch {
|
|
193
|
-
return !1;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
export function dev(buddy) {
|
|
197
|
-
const descriptions = {
|
|
198
|
-
dev: "Start development server",
|
|
199
|
-
frontend: "Start the frontend development server",
|
|
200
|
-
components: "Start the Components development server",
|
|
201
|
-
desktop: "Start the Desktop App development server",
|
|
202
|
-
native: "Start the app in a native Craft window",
|
|
203
|
-
dashboard: "Start the Dashboard development server",
|
|
204
|
-
api: "Start the local API development server",
|
|
205
|
-
docs: "Start the Documentation development server",
|
|
206
|
-
systemTray: "Start the System Tray development server",
|
|
207
|
-
interactive: "Get asked which development server to start",
|
|
208
|
-
select: "Which development server are you trying to start?",
|
|
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",
|
|
212
|
-
project: "Target a specific project",
|
|
213
|
-
verbose: "Enable verbose output"
|
|
214
|
-
};
|
|
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);
|
|
219
|
-
if (target) {
|
|
220
|
-
const serverOptions = { ...options }, a = await actions();
|
|
221
|
-
switch (target) {
|
|
222
|
-
case "native":
|
|
223
|
-
await startDevelopmentServer({ ...serverOptions, native: !0 }, perf);
|
|
224
|
-
break;
|
|
225
|
-
case "frontend":
|
|
226
|
-
await a.runFrontendDevServer(serverOptions);
|
|
227
|
-
break;
|
|
228
|
-
case "api":
|
|
229
|
-
await a.runApiDevServer(serverOptions);
|
|
230
|
-
break;
|
|
231
|
-
case "components":
|
|
232
|
-
await a.runComponentsDevServer(serverOptions);
|
|
233
|
-
break;
|
|
234
|
-
case "dashboard":
|
|
235
|
-
await a.runDashboardDevServer(serverOptions);
|
|
236
|
-
break;
|
|
237
|
-
case "desktop":
|
|
238
|
-
await startDevelopmentServer({ ...serverOptions, native: !0 }, perf);
|
|
239
|
-
break;
|
|
240
|
-
case "system-tray":
|
|
241
|
-
await a.runSystemTrayDevServer(serverOptions);
|
|
242
|
-
break;
|
|
243
|
-
case "docs":
|
|
244
|
-
await a.runDocsDevServer(serverOptions);
|
|
245
|
-
break;
|
|
246
|
-
default:
|
|
247
|
-
log.error(`Unknown server: ${target}`);
|
|
248
|
-
process.exit(ExitCode.InvalidArgument);
|
|
249
|
-
}
|
|
250
|
-
} else if (wantsInteractive(options)) {
|
|
251
|
-
const selectedValue = (await prompts({
|
|
252
|
-
type: "select",
|
|
253
|
-
name: "value",
|
|
254
|
-
message: descriptions.select,
|
|
255
|
-
choices: interactiveDevChoices
|
|
256
|
-
})).value, a = await actions();
|
|
257
|
-
if (!await dispatchInteractiveDevSelection(selectedValue, {
|
|
258
|
-
all: () => startDevelopmentServer(options, perf),
|
|
259
|
-
frontend: () => a.runFrontendDevServer(options),
|
|
260
|
-
api: () => a.runApiDevServer(options),
|
|
261
|
-
dashboard: () => a.runDashboardDevServer(options),
|
|
262
|
-
desktop: () => startDevelopmentServer({ ...options, native: !0 }, perf),
|
|
263
|
-
native: () => startDevelopmentServer({ ...options, native: !0 }, perf),
|
|
264
|
-
components: () => a.runComponentsDevServer(options),
|
|
265
|
-
docs: () => a.runDocsDevServer(options)
|
|
266
|
-
})) {
|
|
267
|
-
log.error("Invalid option during interactive mode");
|
|
268
|
-
process.exit(ExitCode.InvalidArgument);
|
|
269
|
-
}
|
|
270
|
-
} else
|
|
271
|
-
await startDevelopmentServer(options, perf);
|
|
272
|
-
outro("Exited", { startTime: perf, useSeconds: !0 });
|
|
273
|
-
process.exit(ExitCode.Success);
|
|
274
|
-
});
|
|
275
|
-
buddy.command("dev:components", descriptions.components).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
276
|
-
const perf = await intro("buddy dev:components"), result = await runCommand("bun run dev", {
|
|
277
|
-
cwd: libsPath("components/stx")
|
|
278
|
-
});
|
|
279
|
-
if (options.verbose)
|
|
280
|
-
log.info("buddy dev:components result", result);
|
|
281
|
-
if (result.isErr) {
|
|
282
|
-
await outro("While running the dev:components command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
283
|
-
process.exit(ExitCode.FatalError);
|
|
284
|
-
}
|
|
285
|
-
console.log("");
|
|
286
|
-
await outro("Exited", { startTime: perf, useSeconds: !0 });
|
|
287
|
-
process.exit(ExitCode.Success);
|
|
288
|
-
});
|
|
289
|
-
buddy.command("dev:docs", descriptions.docs).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
290
|
-
const perf = await intro("buddy dev:docs"), result = await (await actions()).runAction(Action.DevDocs, options);
|
|
291
|
-
if (result.isErr) {
|
|
292
|
-
await outro("While running the dev:docs command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
293
|
-
process.exit(ExitCode.FatalError);
|
|
294
|
-
}
|
|
295
|
-
console.log("");
|
|
296
|
-
await outro("Exited", { startTime: perf, useSeconds: !0 });
|
|
297
|
-
process.exit(ExitCode.Success);
|
|
298
|
-
});
|
|
299
|
-
buddy.command("dev:native", descriptions.native).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
300
|
-
const perf = await intro("buddy dev:native");
|
|
301
|
-
await startDevelopmentServer({ ...options, native: !0 }, perf);
|
|
302
|
-
});
|
|
303
|
-
buddy.command("dev:desktop", descriptions.desktop).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
304
|
-
const perf = await intro("buddy dev:desktop");
|
|
305
|
-
await startDevelopmentServer({ ...options, native: !0 }, perf);
|
|
306
|
-
});
|
|
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) => {
|
|
308
|
-
const a = await actions();
|
|
309
|
-
if (options.watchTypes !== !1)
|
|
310
|
-
a.watchTypes(options).catch((err) => {
|
|
311
|
-
log.warn("[dev:api] type watcher exited:", err);
|
|
312
|
-
});
|
|
313
|
-
await a.runApiDevServer(options);
|
|
314
|
-
});
|
|
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 });
|
|
317
|
-
await (await actions()).runFrontendDevServer(options);
|
|
318
|
-
});
|
|
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) => {
|
|
320
|
-
await (await actions()).runDashboardDevServer(options);
|
|
321
|
-
});
|
|
322
|
-
buddy.command("dev:system-tray", descriptions.systemTray).alias("dev:tray").option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
323
|
-
await (await actions()).runSystemTrayDevServer(options);
|
|
324
|
-
});
|
|
325
|
-
onUnknownSubcommand(buddy, "dev");
|
|
326
|
-
}
|
|
327
|
-
export async function startDevelopmentServer(_options, _startTime) {
|
|
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;
|
|
335
|
-
if (process.env.STACKS_DEV_NO_KILL !== "1")
|
|
336
|
-
await cleanupStaleDevProcesses([
|
|
337
|
-
preferredFrontendPort,
|
|
338
|
-
preferredApiPort,
|
|
339
|
-
preferredDocsPort,
|
|
340
|
-
preferredDashboardPort
|
|
341
|
-
]);
|
|
342
|
-
const portShifts = [], claimPort = async (label, preferred) => {
|
|
343
|
-
if (proxyManagedExternally)
|
|
344
|
-
return preferred;
|
|
345
|
-
const port = await findAvailablePort(preferred);
|
|
346
|
-
if (port !== preferred)
|
|
347
|
-
portShifts.push(`${label} :${preferred} \u2192 :${port}`);
|
|
348
|
-
return port;
|
|
349
|
-
}, frontendPort = await claimPort("Frontend", preferredFrontendPort), apiPort = await claimPort("API", preferredApiPort), docsPort = await claimPort("Docs", preferredDocsPort), dashboardPort = await claimPort("Dashboard", preferredDashboardPort);
|
|
350
|
-
process.env.PORT = String(frontendPort);
|
|
351
|
-
process.env.PORT_API = String(apiPort);
|
|
352
|
-
process.env.PORT_DOCS = String(docsPort);
|
|
353
|
-
process.env.PORT_ADMIN = String(dashboardPort);
|
|
354
|
-
const includeDashboard = process.env.STACKS_DEV_DASHBOARD === "1", domain = resolvePrettyDevDomain(appUrl, nativeMode), appLooksCustom = domain !== null, localhostOnly = process.env.STACKS_DEV_LOCALHOST === "1", prettyUrlsRequested = appLooksCustom && !localhostOnly, systemAuthorized = !prettyUrlsRequested || proxyManagedExternally ? !0 : await canStartPrettyDevProxy(), usePrettyUrls = shouldUsePrettyDevUrls({
|
|
355
|
-
domain,
|
|
356
|
-
localhostOnly,
|
|
357
|
-
proxyManagedExternally,
|
|
358
|
-
systemAuthorized
|
|
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}`;
|
|
360
|
-
process.env.STACKS_PROXY_MANAGED = "1";
|
|
361
|
-
process.env.STACKS_DEV_QUIET = "1";
|
|
362
|
-
console.log();
|
|
363
|
-
if (prettySetupRequired) {
|
|
364
|
-
console.log(` ${yellow("\u26A0")} ${yellow("Pretty URL setup required")} ${dim("- using localhost for this session")}`);
|
|
365
|
-
console.log(` ${dim(" ")}${dim("Run `./buddy setup:ssl` once, then restart `./buddy dev`.")}`);
|
|
366
|
-
console.log();
|
|
367
|
-
}
|
|
368
|
-
if (portShifts.length > 0) {
|
|
369
|
-
console.log(` ${yellow("\u26A0")} ${yellow("Ports in use by another process")} ${dim(`- ${portShifts.join(", ")}`)}`);
|
|
370
|
-
console.log();
|
|
371
|
-
}
|
|
372
|
-
console.log(` ${bold(cyan("stacks"))} ${dim(`v${version}`)} ${dim("starting\u2026")}`);
|
|
373
|
-
console.log();
|
|
374
|
-
const rpxTlsPreflight = hasCustomDomain && domain ? prepareRpxTlsForDev({ domain, includeDashboard, options }) : Promise.resolve();
|
|
375
|
-
if (localhostOnly && domain && !proxyManagedExternally)
|
|
376
|
-
removeStalePublicDomainOverrides(domain, includeDashboard, options.verbose ?? !1).catch(() => {});
|
|
377
|
-
rpxTlsPreflight.catch(() => {});
|
|
378
|
-
let isExiting = !1, closeNativeApp;
|
|
379
|
-
const SHUTDOWN_GRACE_MS = 1500, cleanup = () => {
|
|
380
|
-
if (isExiting)
|
|
381
|
-
return;
|
|
382
|
-
isExiting = !0;
|
|
383
|
-
closeNativeApp?.();
|
|
384
|
-
const rpxTeardown = (async () => {
|
|
385
|
-
try {
|
|
386
|
-
await unregisterRpxProxies(activeRpxRegistryIds);
|
|
387
|
-
activeRpxRegistryIds.length = 0;
|
|
388
|
-
if (hasCustomDomain && domain)
|
|
389
|
-
await removeStalePublicDomainOverrides(domain, includeDashboard, options.verbose ?? !1);
|
|
390
|
-
} catch {}
|
|
391
|
-
})(), teardownDeadline = new Promise((resolve) => setTimeout(resolve, SHUTDOWN_GRACE_MS));
|
|
392
|
-
Promise.race([rpxTeardown, teardownDeadline]).finally(() => {
|
|
393
|
-
try {
|
|
394
|
-
process.kill(-process.pid, "SIGTERM");
|
|
395
|
-
} catch {
|
|
396
|
-
try {
|
|
397
|
-
process.kill(0, "SIGTERM");
|
|
398
|
-
} catch {}
|
|
399
|
-
}
|
|
400
|
-
setTimeout(() => {
|
|
401
|
-
try {
|
|
402
|
-
process.kill(0, "SIGKILL");
|
|
403
|
-
} catch {
|
|
404
|
-
process.exit(1);
|
|
405
|
-
}
|
|
406
|
-
}, SHUTDOWN_GRACE_MS).unref();
|
|
407
|
-
});
|
|
408
|
-
};
|
|
409
|
-
process.on("SIGINT", cleanup);
|
|
410
|
-
process.on("SIGTERM", cleanup);
|
|
411
|
-
process.on("SIGHUP", cleanup);
|
|
412
|
-
const quietOpts = { ...options, quiet: !0 }, a = await actions(), ports = [
|
|
413
|
-
{ name: "API", port: apiPort }
|
|
414
|
-
], readinessTimeoutMs = 30000;
|
|
415
|
-
let readyAnnounced = !1;
|
|
416
|
-
const nativeUrl = developmentUrl(`http://localhost:${frontendPort}`, entryPath), nativeWindowReady = nativeMode ? waitForPort(frontendPort, readinessTimeoutMs).then(async (ready) => {
|
|
417
|
-
if (!ready) {
|
|
418
|
-
log.warn(`Native window skipped because the frontend did not answer within ${readinessTimeoutMs / 1000}s`);
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
closeNativeApp = await launchNativeAppWindow(nativeUrl, options);
|
|
422
|
-
}) : Promise.resolve();
|
|
423
|
-
Promise.all(ports.map((p) => waitForPort(p.port, readinessTimeoutMs))).then(async (results) => {
|
|
424
|
-
if (readyAnnounced)
|
|
425
|
-
return;
|
|
426
|
-
readyAnnounced = !0;
|
|
427
|
-
const failed = results.map((ok, i) => ok ? null : ports[i]?.name ?? null).filter((x) => x !== null);
|
|
428
|
-
let proxyReachable = !1;
|
|
429
|
-
if (hasCustomDomain && domain) {
|
|
430
|
-
(async () => {
|
|
431
|
-
try {
|
|
432
|
-
await rpxTlsPreflight;
|
|
433
|
-
await registerRpxProxiesForDomain({
|
|
434
|
-
domain,
|
|
435
|
-
frontendPort,
|
|
436
|
-
apiPort,
|
|
437
|
-
docsPort,
|
|
438
|
-
dashboardPort,
|
|
439
|
-
includeDashboard,
|
|
440
|
-
options
|
|
441
|
-
});
|
|
442
|
-
} catch {}
|
|
443
|
-
})();
|
|
444
|
-
proxyReachable = await waitForHttpsProxy(443, 16000);
|
|
445
|
-
if (!proxyReachable) {
|
|
446
|
-
console.log(` ${yellow("\u26A0")} ${yellow("HTTPS proxy not reachable on :443")} \u2014 serving ${cyan(`http://localhost:${frontendPort}`)} instead`);
|
|
447
|
-
console.log(` ${dim(" ")}${dim("rpx needs a valid SUDO_PASSWORD in .env to bind :443; trust the local CA, then restart `./buddy dev`.")}`);
|
|
448
|
-
if (options.verbose)
|
|
449
|
-
console.log(` ${dim(" ")}${dim(`Trust CA: sh ${join(RPX_SSL_DIR, "trust-rpx-cert.sh")}`)}`);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
eraseDevBootStartingLines();
|
|
453
|
-
printDevReadyBanner({
|
|
454
|
-
options,
|
|
455
|
-
nativeMode,
|
|
456
|
-
hasCustomDomain: !!appLooksCustom && !localhostOnly,
|
|
457
|
-
proxyReachable: proxyManagedExternally ? !0 : proxyReachable,
|
|
458
|
-
frontendUrl,
|
|
459
|
-
frontendBaseUrl,
|
|
460
|
-
entryPath,
|
|
461
|
-
apiUrl,
|
|
462
|
-
docsUrl,
|
|
463
|
-
dashboardUrl,
|
|
464
|
-
frontendPort,
|
|
465
|
-
apiPort,
|
|
466
|
-
docsPort,
|
|
467
|
-
includeDashboard,
|
|
468
|
-
domain,
|
|
469
|
-
dashboardPort,
|
|
470
|
-
dashboardDomain
|
|
471
|
-
});
|
|
472
|
-
if (!nativeMode) {
|
|
473
|
-
const browserUrl = hasCustomDomain && (proxyManagedExternally || proxyReachable) ? frontendUrl : developmentUrl(`http://localhost:${frontendPort}`, entryPath);
|
|
474
|
-
openDevelopmentBrowser(browserUrl);
|
|
475
|
-
}
|
|
476
|
-
if (startedAt) {
|
|
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`;
|
|
478
|
-
console.log(` ${dim(summary)}`);
|
|
479
|
-
console.log();
|
|
480
|
-
printDevEngineNotes();
|
|
481
|
-
console.log();
|
|
482
|
-
}
|
|
483
|
-
if (process.env.STACKS_PRINT_ROUTES === "1")
|
|
484
|
-
await printRegisteredRoutes(apiPort).catch(() => {});
|
|
485
|
-
}).catch(() => {});
|
|
486
|
-
await Promise.all([
|
|
487
|
-
a.runFrontendDevServer(quietOpts).catch((error) => {
|
|
488
|
-
if (options.verbose)
|
|
489
|
-
log.error(`Frontend: ${error}`);
|
|
490
|
-
}),
|
|
491
|
-
a.runApiDevServer(quietOpts).catch((error) => {
|
|
492
|
-
if (options.verbose)
|
|
493
|
-
log.error(`API: ${error}`);
|
|
494
|
-
}),
|
|
495
|
-
a.runDocsDevServer(quietOpts).catch((error) => {
|
|
496
|
-
if (options.verbose)
|
|
497
|
-
log.error(`Docs: ${error}`);
|
|
498
|
-
}),
|
|
499
|
-
includeDashboard ? a.runDashboardDevServer(quietOpts).catch((error) => {
|
|
500
|
-
if (options.verbose)
|
|
501
|
-
log.error(`Dashboard: ${error}`);
|
|
502
|
-
}) : Promise.resolve(),
|
|
503
|
-
nativeWindowReady.catch((error) => {
|
|
504
|
-
if (options.verbose)
|
|
505
|
-
log.warn(`Native window: ${error}`);
|
|
506
|
-
})
|
|
507
|
-
]);
|
|
508
|
-
}
|
|
509
|
-
function isComingSoonMode() {
|
|
510
|
-
if (existsSync(projectPath("storage/framework/coming-soon")))
|
|
511
|
-
return !0;
|
|
512
|
-
try {
|
|
513
|
-
const idx = projectPath("resources/views/index.stx");
|
|
514
|
-
return existsSync(idx) && /siteMode\s*=\s*['"]coming-soon['"]/.test(readFileSync(idx, "utf8"));
|
|
515
|
-
} catch {
|
|
516
|
-
return !1;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
function blogIsConfigured() {
|
|
520
|
-
try {
|
|
521
|
-
const dir = projectPath("content/blog");
|
|
522
|
-
return existsSync(dir) && readdirSync(dir).some((f) => f.endsWith(".md"));
|
|
523
|
-
} catch {
|
|
524
|
-
return !1;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
function printDevReadyBanner(input) {
|
|
528
|
-
const {
|
|
529
|
-
options,
|
|
530
|
-
nativeMode,
|
|
531
|
-
hasCustomDomain,
|
|
532
|
-
proxyReachable,
|
|
533
|
-
frontendUrl,
|
|
534
|
-
frontendBaseUrl,
|
|
535
|
-
entryPath,
|
|
536
|
-
apiUrl,
|
|
537
|
-
docsUrl,
|
|
538
|
-
dashboardUrl,
|
|
539
|
-
frontendPort,
|
|
540
|
-
apiPort,
|
|
541
|
-
docsPort,
|
|
542
|
-
includeDashboard,
|
|
543
|
-
domain,
|
|
544
|
-
dashboardPort,
|
|
545
|
-
dashboardDomain
|
|
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`;
|
|
547
|
-
console.log();
|
|
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)}`);
|
|
552
|
-
if (nativeMode)
|
|
553
|
-
console.log(` ${green("\u279C")} ${bold("Native")}: ${cyan(`Craft \u2192 ${feUrl}`)}`);
|
|
554
|
-
console.log(` ${green("\u279C")} ${bold("API")}: ${cyan(apUrl)}`);
|
|
555
|
-
console.log(` ${green("\u279C")} ${bold("Docs")}: ${cyan(dcUrl)}`);
|
|
556
|
-
if (blogIsConfigured())
|
|
557
|
-
console.log(` ${green("\u279C")} ${bold("Blog")}: ${cyan(blogUrl)}`);
|
|
558
|
-
if (includeDashboard)
|
|
559
|
-
console.log(` ${green("\u279C")} ${bold("Dashboard")}: ${cyan(dbUrl)}`);
|
|
560
|
-
if (useProxy) {
|
|
561
|
-
console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`${developmentUrl(`http://localhost:${frontendPort}`, entryPath)} (bypasses the proxy)`)}`);
|
|
562
|
-
if (includeDashboard && dashboardDomain)
|
|
563
|
-
console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`http://localhost:${dashboardPort} (dashboard, bypasses the proxy)`)}`);
|
|
564
|
-
}
|
|
565
|
-
if (isComingSoonMode()) {
|
|
566
|
-
console.log();
|
|
567
|
-
console.log(` ${yellow("\u25CF")} ${bold(yellow("Coming soon mode"))} ${dim("\u2014 visitors see the holding page; bypass with the coming-soon secret.")}`);
|
|
568
|
-
}
|
|
569
|
-
if (verbose && domain) {
|
|
570
|
-
console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${frontendPort} \u2192 ${domain}`)}`);
|
|
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}`)}`);
|
|
573
|
-
if (includeDashboard)
|
|
574
|
-
console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${dashboardPort} \u2192 ${dashboardDomain}`)}`);
|
|
575
|
-
}
|
|
576
|
-
console.log();
|
|
577
|
-
}
|
|
578
|
-
function printDevEngineNotes() {
|
|
579
|
-
const routesFile = stxPath("routes.ts"), routesLabel = relative(projectPath(), routesFile), crosswindConfig = join(projectPath(), "config/crosswind.ts");
|
|
580
|
-
if (existsSync(crosswindConfig) || existsSync(join(projectPath(), "crosswind.config.ts")))
|
|
581
|
-
console.log(` ${green("[Crosswind]")} ${dim("CSS engine loaded")}`);
|
|
582
|
-
if (existsSync(routesFile))
|
|
583
|
-
try {
|
|
584
|
-
const routeCount = (readFileSync(routesFile, "utf8").match(/pattern:/g) ?? []).length;
|
|
585
|
-
if (routeCount > 0)
|
|
586
|
-
console.log(` ${green("[stx]")} ${dim(`Generated ${routeCount} routes \u2192 ${routesLabel}`)}`);
|
|
587
|
-
} catch {
|
|
588
|
-
console.log(` ${green("[stx]")} ${dim(`Routes manifest \u2192 ${routesLabel}`)}`);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
async function launchNativeAppWindow(url, options) {
|
|
592
|
-
let createApp;
|
|
593
|
-
try {
|
|
594
|
-
createApp = (await importCraftSdk()).createApp;
|
|
595
|
-
} catch (error) {
|
|
596
|
-
if (options.verbose)
|
|
597
|
-
log.warn(`Native window unavailable: ${error}`);
|
|
598
|
-
console.log(` ${dim("Native window unavailable. Install craft-native or set CRAFT_BIN to a Craft binary.")}
|
|
599
|
-
`);
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
if (!createApp) {
|
|
603
|
-
console.log(` ${dim("Native window unavailable. The Craft SDK did not export createApp.")}
|
|
604
|
-
`);
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
const craftBinaryPath = resolveCraftBinaryPath();
|
|
608
|
-
if (!craftBinaryPath) {
|
|
609
|
-
console.log(` ${dim("Native window unavailable. Set CRAFT_BIN to a Craft binary, or install Craft in ~/Code/Tools/craft.")}
|
|
610
|
-
`);
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
const appIconPath = resolveNativeAppIconPath(), app = createApp({
|
|
614
|
-
url,
|
|
615
|
-
quiet: !options.verbose,
|
|
616
|
-
craftPath: craftBinaryPath,
|
|
617
|
-
window: {
|
|
618
|
-
title: await resolveNativeAppTitle(),
|
|
619
|
-
width: 1280,
|
|
620
|
-
height: 860,
|
|
621
|
-
titlebarHidden: !0,
|
|
622
|
-
webSidebarMaterial: !0,
|
|
623
|
-
webSidebarWidth: 286,
|
|
624
|
-
webSidebarMaterialOpacity: 0.78,
|
|
625
|
-
...appIconPath && { icon: appIconPath }
|
|
626
|
-
}
|
|
627
|
-
});
|
|
628
|
-
app.show().catch((error) => {
|
|
629
|
-
if (options.verbose)
|
|
630
|
-
log.warn(`Native window exited: ${error}`);
|
|
631
|
-
});
|
|
632
|
-
return () => app.close();
|
|
633
|
-
}
|
|
634
|
-
async function importCraftSdk() {
|
|
635
|
-
const localCraftSdk = process.env.HOME ? `${process.env.HOME}/Code/Tools/craft/packages/typescript/src/index.ts` : void 0;
|
|
636
|
-
if (localCraftSdk && existsSync(localCraftSdk))
|
|
637
|
-
return await import(localCraftSdk);
|
|
638
|
-
const packageNames = ["craft-native", "@craft-native/craft", "@stacksjs/ts-craft"];
|
|
639
|
-
let primaryError;
|
|
640
|
-
for (const packageName of packageNames)
|
|
641
|
-
try {
|
|
642
|
-
return await import(packageName);
|
|
643
|
-
} catch (error) {
|
|
644
|
-
primaryError ??= error;
|
|
645
|
-
}
|
|
646
|
-
throw primaryError;
|
|
647
|
-
}
|
|
648
|
-
async function cleanupStaleDevProcesses(ports) {
|
|
649
|
-
const projectRoot = projectPath(), actionDevPath = projectPath("storage/framework/core/actions/src/dev/"), pids = new Set;
|
|
650
|
-
for (const port of ports)
|
|
651
|
-
for (const pid of await listenerPids(port)) {
|
|
652
|
-
if (pid === process.pid)
|
|
653
|
-
continue;
|
|
654
|
-
const command = await commandForPid(pid);
|
|
655
|
-
if (!command.includes(projectRoot))
|
|
656
|
-
continue;
|
|
657
|
-
if (command.includes("storage/framework/core/buddy/src/cli.ts dev") || command.includes("storage/framework/core/actions/src/dev/") || command.includes(actionDevPath))
|
|
658
|
-
pids.add(pid);
|
|
659
|
-
}
|
|
660
|
-
if (pids.size === 0)
|
|
661
|
-
return;
|
|
662
|
-
for (const pid of pids)
|
|
663
|
-
try {
|
|
664
|
-
process.kill(pid, "SIGTERM");
|
|
665
|
-
} catch {}
|
|
666
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
667
|
-
for (const pid of pids)
|
|
668
|
-
try {
|
|
669
|
-
process.kill(pid, "SIGKILL");
|
|
670
|
-
} catch {}
|
|
671
|
-
await new Promise((r) => setTimeout(r, 150));
|
|
672
|
-
}
|
|
673
|
-
async function listenerPids(port) {
|
|
674
|
-
try {
|
|
675
|
-
const proc = Bun.spawn(["lsof", "-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
|
|
676
|
-
stdout: "pipe",
|
|
677
|
-
stderr: "ignore"
|
|
678
|
-
}), output = await new Response(proc.stdout).text();
|
|
679
|
-
await proc.exited;
|
|
680
|
-
return output.split(/\s+/).map((value) => Number(value)).filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
681
|
-
} catch {
|
|
682
|
-
return [];
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
async function commandForPid(pid) {
|
|
686
|
-
try {
|
|
687
|
-
const proc = Bun.spawn(["ps", "-p", String(pid), "-o", "command="], {
|
|
688
|
-
stdout: "pipe",
|
|
689
|
-
stderr: "ignore"
|
|
690
|
-
}), output = await new Response(proc.stdout).text();
|
|
691
|
-
await proc.exited;
|
|
692
|
-
return output.trim();
|
|
693
|
-
} catch {
|
|
694
|
-
return "";
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
function resolveCraftBinaryPath() {
|
|
698
|
-
const home = process.env.HOME;
|
|
699
|
-
return [
|
|
700
|
-
process.env.CRAFT_BIN,
|
|
701
|
-
home ? `${home}/Code/Tools/craft/craft` : void 0,
|
|
702
|
-
home ? `${home}/Code/Tools/craft/bin/craft` : void 0,
|
|
703
|
-
home ? `${home}/Code/Tools/craft/packages/zig/zig-out/bin/craft` : void 0,
|
|
704
|
-
home ? `${home}/Documents/Projects/craft/packages/zig/zig-out/bin/craft` : void 0
|
|
705
|
-
].filter((candidate) => Boolean(candidate)).find((candidate) => existsSync(candidate));
|
|
706
|
-
}
|
|
707
|
-
function resolveNativeAppIconPath() {
|
|
708
|
-
return [
|
|
709
|
-
projectPath("resources/assets/images/app-icon.png"),
|
|
710
|
-
projectPath("resources/assets/images/icon.png"),
|
|
711
|
-
projectPath("public/icon.png")
|
|
712
|
-
].find((candidate) => existsSync(candidate));
|
|
713
|
-
}
|
|
714
|
-
async function resolveNativeAppTitle() {
|
|
715
|
-
try {
|
|
716
|
-
const pkg = await Bun.file(projectPath("package.json")).json(), name = typeof pkg.productName === "string" ? pkg.productName : pkg.name;
|
|
717
|
-
if (typeof name === "string" && name.length > 0)
|
|
718
|
-
return name.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
719
|
-
} catch {}
|
|
720
|
-
return "Stacks App";
|
|
721
|
-
}
|
|
722
|
-
const METHOD_COLORS = {
|
|
723
|
-
GET: cyan,
|
|
724
|
-
POST: green,
|
|
725
|
-
PUT: cyan,
|
|
726
|
-
PATCH: cyan,
|
|
727
|
-
DELETE: cyan,
|
|
728
|
-
OPTIONS: dim
|
|
729
|
-
};
|
|
730
|
-
async function printRegisteredRoutes(apiPort) {
|
|
731
|
-
try {
|
|
732
|
-
const ac = new AbortController, t = setTimeout(() => ac.abort(), 1500), res = await fetch(`http://127.0.0.1:${apiPort}/__routes`, { signal: ac.signal }).catch(() => null);
|
|
733
|
-
clearTimeout(t);
|
|
734
|
-
if (!res || !res.ok)
|
|
735
|
-
return;
|
|
736
|
-
const routes = await res.json();
|
|
737
|
-
if (!Array.isArray(routes) || routes.length === 0)
|
|
738
|
-
return;
|
|
739
|
-
const sorted = [...routes].sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method)), longestPath = Math.max(...sorted.map((r) => r.path.length));
|
|
740
|
-
console.log(` ${dim("Registered routes")}
|
|
741
|
-
`);
|
|
742
|
-
for (const r of sorted) {
|
|
743
|
-
const method = (METHOD_COLORS[r.method.toUpperCase()] ?? dim)(r.method.toUpperCase().padEnd(6)), path = r.path.padEnd(longestPath), named = r.name ? ` ${dim(`(${r.name})`)}` : "";
|
|
744
|
-
console.log(` ${method} ${path}${named}`);
|
|
745
|
-
}
|
|
746
|
-
console.log();
|
|
747
|
-
} catch {}
|
|
748
|
-
}
|
|
749
|
-
async function probeDevServerHttp(port) {
|
|
750
|
-
for (const path of ["/__health", "/"])
|
|
751
|
-
if (await new Promise((resolve) => {
|
|
752
|
-
const ac = new AbortController, t = setTimeout(() => ac.abort(), path === "/" ? 8000 : 800);
|
|
753
|
-
fetch(`http://127.0.0.1:${port}${path}`, { signal: ac.signal, redirect: "manual" }).then(() => {
|
|
754
|
-
clearTimeout(t);
|
|
755
|
-
resolve(!0);
|
|
756
|
-
}).catch(() => {
|
|
757
|
-
clearTimeout(t);
|
|
758
|
-
resolve(!1);
|
|
759
|
-
});
|
|
760
|
-
}))
|
|
761
|
-
return !0;
|
|
762
|
-
return !1;
|
|
763
|
-
}
|
|
764
|
-
async function findAvailablePort(preferred, attempts = 20) {
|
|
765
|
-
for (let port = preferred;port < preferred + attempts; port++)
|
|
766
|
-
if (await portIsFree(port))
|
|
767
|
-
return port;
|
|
768
|
-
return preferred;
|
|
769
|
-
}
|
|
770
|
-
async function portIsFree(port) {
|
|
771
|
-
for (const host of ["127.0.0.1", "::1"])
|
|
772
|
-
if (await someoneIsListening(port, host))
|
|
773
|
-
return !1;
|
|
774
|
-
let server;
|
|
775
|
-
try {
|
|
776
|
-
server = Bun.serve({ port, hostname: "0.0.0.0", reusePort: !1, fetch: () => new Response });
|
|
777
|
-
} catch {
|
|
778
|
-
return !1;
|
|
779
|
-
}
|
|
780
|
-
server.stop(!0);
|
|
781
|
-
return !0;
|
|
782
|
-
}
|
|
783
|
-
async function someoneIsListening(port, host) {
|
|
784
|
-
const { connect } = await import("node:net");
|
|
785
|
-
return new Promise((resolve) => {
|
|
786
|
-
const socket = connect({ port, host }), finish = (listening) => {
|
|
787
|
-
socket.destroy();
|
|
788
|
-
resolve(listening);
|
|
789
|
-
};
|
|
790
|
-
socket.setTimeout(300, () => finish(!1));
|
|
791
|
-
socket.once("connect", () => finish(!0));
|
|
792
|
-
socket.once("error", () => finish(!1));
|
|
793
|
-
});
|
|
794
|
-
}
|
|
795
|
-
async function waitForPort(port, timeoutMs) {
|
|
796
|
-
const deadline = Date.now() + timeoutMs;
|
|
797
|
-
while (Date.now() < deadline) {
|
|
798
|
-
if (await probeDevServerHttp(port))
|
|
799
|
-
return !0;
|
|
800
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
801
|
-
}
|
|
802
|
-
return !1;
|
|
803
|
-
}
|
|
804
|
-
async function waitForHttpsProxy(port, timeoutMs) {
|
|
805
|
-
const { connect } = await import("node:net"), deadline = Date.now() + timeoutMs;
|
|
806
|
-
while (Date.now() < deadline) {
|
|
807
|
-
if (await new Promise((resolve) => {
|
|
808
|
-
const socket = connect({ port, host: "127.0.0.1" }), finish = (v) => {
|
|
809
|
-
try {
|
|
810
|
-
socket.destroy();
|
|
811
|
-
} catch {}
|
|
812
|
-
resolve(v);
|
|
813
|
-
};
|
|
814
|
-
socket.once("connect", () => finish(!0));
|
|
815
|
-
socket.once("error", () => finish(!1));
|
|
816
|
-
setTimeout(() => finish(!1), 800).unref?.();
|
|
817
|
-
}))
|
|
818
|
-
return !0;
|
|
819
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
820
|
-
}
|
|
821
|
-
return !1;
|
|
822
|
-
}
|
|
823
|
-
const RPX_SSL_DIR = join(homedir(), ".stacks", "ssl"), RPX_ROOT_CA_PATH = join(RPX_SSL_DIR, "rpx-root-ca.crt"), RPX_HOST_CERT_PATH = join(RPX_SSL_DIR, "rpx.localhost.crt");
|
|
824
|
-
function buildDevelopmentTlsHostnames(domain, includeDashboard) {
|
|
825
|
-
return [
|
|
826
|
-
domain,
|
|
827
|
-
...includeDashboard ? [`dashboard.${domain}`] : [],
|
|
828
|
-
"rpx.localhost"
|
|
829
|
-
];
|
|
830
|
-
}
|
|
831
|
-
async function ensureRpxDevelopmentHttps(domain, options, includeDashboard, trustCertificate = !0) {
|
|
832
|
-
const verbose = options.verbose ?? !1, {
|
|
833
|
-
buildRegistryTlsProxyOptions,
|
|
834
|
-
certIncludesSanHostnames,
|
|
835
|
-
checkExistingCertificates,
|
|
836
|
-
clearSslConfigCache,
|
|
837
|
-
forceTrustCertificate,
|
|
838
|
-
generateCertificate,
|
|
839
|
-
getRegistryDir,
|
|
840
|
-
isDaemonRunning,
|
|
841
|
-
isRootCaTrustedForSsl,
|
|
842
|
-
readAll,
|
|
843
|
-
readCertCommonName,
|
|
844
|
-
stopDaemon,
|
|
845
|
-
trustRootCaForBrowsers,
|
|
846
|
-
verifyHttpsChain
|
|
847
|
-
} = await importDevelopmentRpx(), hostnames = buildDevelopmentTlsHostnames(domain, includeDashboard), entries = await readAll(getRegistryDir(), !1).catch(() => []);
|
|
848
|
-
for (const entry of entries) {
|
|
849
|
-
const host = entry.to?.trim();
|
|
850
|
-
if (host && !hostnames.includes(host))
|
|
851
|
-
hostnames.push(host);
|
|
852
|
-
}
|
|
853
|
-
const tlsOptions = buildRegistryTlsProxyOptions(hostnames, domain, verbose), hostnameInCert = certIncludesSanHostnames(RPX_HOST_CERT_PATH, hostnames), cnMatchesApp = readCertCommonName(RPX_HOST_CERT_PATH) === domain;
|
|
854
|
-
let certRegenerated = !1;
|
|
855
|
-
if (!await checkExistingCertificates(tlsOptions) || !hostnameInCert || !cnMatchesApp) {
|
|
856
|
-
clearSslConfigCache();
|
|
857
|
-
await generateCertificate({ ...tlsOptions, forceRegenerate: !0 });
|
|
858
|
-
certRegenerated = !0;
|
|
859
|
-
}
|
|
860
|
-
let trusted = isRootCaTrustedForSsl(RPX_ROOT_CA_PATH, domain, { verbose });
|
|
861
|
-
if (!trusted && trustCertificate) {
|
|
862
|
-
trusted = trustRootCaForBrowsers(RPX_ROOT_CA_PATH, { serverName: domain, verbose }) || await forceTrustCertificate(RPX_ROOT_CA_PATH, { serverName: domain, verbose }) || isRootCaTrustedForSsl(RPX_ROOT_CA_PATH, domain, { verbose });
|
|
863
|
-
if (trusted)
|
|
864
|
-
console.log(` ${green("\u2713")} ${dim("HTTPS")}: ${dim("Local CA trusted \u2014 reload the browser if you still see a warning")}`);
|
|
865
|
-
else
|
|
866
|
-
console.log(` ${yellow("\u26A0")} ${yellow("HTTPS")}: ${yellow(`Local CA not trusted \u2014 run: sh ${join(RPX_SSL_DIR, "trust-rpx-cert.sh")}`)}`);
|
|
867
|
-
} else if (verbose)
|
|
868
|
-
console.log(` ${green("\u2713")} ${dim("HTTPS")}: ${dim("Local certificate trusted for SSL")}`);
|
|
869
|
-
const chainOk = existsSync(RPX_ROOT_CA_PATH) ? await verifyHttpsChain(domain, RPX_ROOT_CA_PATH) : !1;
|
|
870
|
-
if ((certRegenerated || !hostnameInCert || !cnMatchesApp) && await isDaemonRunning()) {
|
|
871
|
-
await stopDaemon({ timeoutMs: 8000 }).catch(() => {});
|
|
872
|
-
if (verbose)
|
|
873
|
-
log.info("Restarting rpx daemon to load TLS certificates for this app");
|
|
874
|
-
} else if (!chainOk && verbose)
|
|
875
|
-
console.log(` ${dim(" ")}${dim("HTTPS chain not yet valid on :443 \u2014 daemon will (re)start after backends are ready")}`);
|
|
876
|
-
}
|
|
877
|
-
function resolveRpxDaemonSpawnCwd() {
|
|
878
|
-
const fromEnv = process.env.RPX_BIN || process.env.STACKS_RPX_BIN;
|
|
879
|
-
if (fromEnv && existsSync(fromEnv))
|
|
880
|
-
return dirname(fromEnv);
|
|
881
|
-
const bootstrap = join(dirname(fileURLToPath(import.meta.url)), "../../scripts/rpx-daemon-bootstrap.js");
|
|
882
|
-
if (existsSync(bootstrap))
|
|
883
|
-
return dirname(bootstrap);
|
|
884
|
-
return homedir();
|
|
885
|
-
}
|
|
886
|
-
async function resolveRpxDaemonSpawnCommand() {
|
|
887
|
-
const bootstrap = join(dirname(fileURLToPath(import.meta.url)), "../../scripts/rpx-daemon-bootstrap.js");
|
|
888
|
-
if (existsSync(bootstrap))
|
|
889
|
-
return [process.execPath, bootstrap];
|
|
890
|
-
const fromEnv = process.env.RPX_BIN || process.env.STACKS_RPX_BIN;
|
|
891
|
-
if (fromEnv && existsSync(fromEnv))
|
|
892
|
-
return [fromEnv, "daemon:start"];
|
|
893
|
-
const pkgUrl = import.meta.resolve("@stacksjs/rpx/package.json"), cli = join(dirname(fileURLToPath(pkgUrl)), "dist/bin/cli.js");
|
|
894
|
-
return [process.execPath, cli, "daemon:start"];
|
|
895
|
-
}
|
|
896
|
-
function buildRpxProxySpecs(input) {
|
|
897
|
-
const { domain, frontendPort, apiPort, docsPort, dashboardPort, includeDashboard } = input, dashboardDomain = `dashboard.${domain}`;
|
|
898
|
-
return [
|
|
899
|
-
{
|
|
900
|
-
id: `${domain}-frontend`,
|
|
901
|
-
from: `localhost:${frontendPort}`,
|
|
902
|
-
to: domain,
|
|
903
|
-
pathRewrites: [
|
|
904
|
-
{ from: "/api", to: `localhost:${apiPort}`, stripPrefix: !1 },
|
|
905
|
-
{ from: "/docs", to: `localhost:${docsPort}`, stripPrefix: !0 }
|
|
906
|
-
]
|
|
907
|
-
},
|
|
908
|
-
...includeDashboard ? [{ id: `${domain}-dashboard`, from: `localhost:${dashboardPort}`, to: dashboardDomain }] : []
|
|
909
|
-
];
|
|
910
|
-
}
|
|
911
|
-
async function unregisterRpxProxies(ids) {
|
|
912
|
-
if (ids.length === 0)
|
|
913
|
-
return;
|
|
914
|
-
try {
|
|
915
|
-
const { removeEntry } = await importDevelopmentRpx();
|
|
916
|
-
for (const id of ids)
|
|
917
|
-
await removeEntry(id).catch(() => {});
|
|
918
|
-
} catch {}
|
|
919
|
-
}
|
|
920
|
-
async function removeStalePublicDomainOverrides(domain, includeDashboard, verbose) {
|
|
921
|
-
const { checkHosts, removeHosts, resolverFilePath, contentLooksLikeRpxResolver } = await importDevelopmentRpx(), hosts = [domain, ...includeDashboard ? [`dashboard.${domain}`] : []], present = await checkHosts(hosts, verbose).catch(() => hosts.map(() => !1)), staleHosts = hosts.filter((_, i) => present[i] === !0);
|
|
922
|
-
if (staleHosts.length > 0) {
|
|
923
|
-
await removeHosts(staleHosts, verbose).catch(() => {});
|
|
924
|
-
if ((await checkHosts(staleHosts, verbose).catch(() => staleHosts.map(() => !0))).some(Boolean))
|
|
925
|
-
log.warn(`Stale /etc/hosts entries still point ${staleHosts.join(", ")} at loopback. Remove them with: sudo nano /etc/hosts`);
|
|
926
|
-
else
|
|
927
|
-
log.info(`Removed stale /etc/hosts entries for ${staleHosts.join(", ")} (left by a previous dev session)`);
|
|
928
|
-
}
|
|
929
|
-
if (process.platform !== "darwin")
|
|
930
|
-
return;
|
|
931
|
-
const labels = domain.split("."), basename = labels.length >= 2 ? labels.slice(-2).join(".") : domain, resolverPath = resolverFilePath(basename);
|
|
932
|
-
let isRpxManaged = !1;
|
|
933
|
-
try {
|
|
934
|
-
isRpxManaged = contentLooksLikeRpxResolver(readFileSync(resolverPath, "utf8"));
|
|
935
|
-
} catch {}
|
|
936
|
-
if (!isRpxManaged)
|
|
937
|
-
return;
|
|
938
|
-
const quotedPath = `'${resolverPath.replace(/'/g, "'\\''")}'`, rm = process.env.SUDO_PASSWORD ? `printf '%s
|
|
939
|
-
' "$SUDO_PASSWORD" | sudo -S rm -f ${quotedPath}` : `sudo -n rm -f ${quotedPath}`;
|
|
940
|
-
try {
|
|
941
|
-
execSync(rm, { stdio: ["pipe", "ignore", "ignore"] });
|
|
942
|
-
log.info(`Removed stale rpx DNS resolver ${resolverPath} (left by a previous dev session)`);
|
|
943
|
-
execSync("sudo -n dscacheutil -flushcache; sudo -n killall -HUP mDNSResponder; true", { stdio: ["pipe", "ignore", "ignore"] });
|
|
944
|
-
} catch {
|
|
945
|
-
log.warn(`A stale rpx DNS resolver still hijacks ${domain}. Remove it with: sudo rm ${resolverPath}`);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
async function prepareRpxTlsForDev(input) {
|
|
949
|
-
const { domain, includeDashboard, options, skipHosts = !1, trustCertificate = !0 } = input, verbose = options.verbose ?? !1, hosts = [
|
|
950
|
-
domain,
|
|
951
|
-
...includeDashboard ? [`dashboard.${domain}`] : []
|
|
952
|
-
], hostsNeedingFile = hosts.filter((host) => {
|
|
953
|
-
const h = host.trim().toLowerCase();
|
|
954
|
-
return h !== "localhost" && !h.endsWith(".localhost") && !h.endsWith(".localhost.");
|
|
955
|
-
}), { addHosts, setupDevelopmentDns } = await importDevelopmentRpx(), dnsDomains = !skipHosts && hostsNeedingFile.length > 0 ? hosts : [];
|
|
956
|
-
if (dnsDomains.length > 0) {
|
|
957
|
-
if (!await setupDevelopmentDns({ domains: dnsDomains, verbose }).catch(() => !1) && verbose)
|
|
958
|
-
log.warn(`Dev DNS not configured for ${dnsDomains.join(", ")} \u2014 falling back to /etc/hosts`);
|
|
959
|
-
}
|
|
960
|
-
if (!skipHosts && hostsNeedingFile.length > 0)
|
|
961
|
-
await addHosts(hostsNeedingFile, verbose).catch((err) => {
|
|
962
|
-
log.warn(`Could not update /etc/hosts for ${hostsNeedingFile.join(", ")}: ${err.message}`);
|
|
963
|
-
log.warn("Add 127.0.0.1 entries manually or set SUDO_PASSWORD in .env");
|
|
964
|
-
});
|
|
965
|
-
await ensureRpxDevelopmentHttps(domain, options, includeDashboard, trustCertificate);
|
|
966
|
-
}
|
|
967
|
-
export async function setupPrettyDevEnvironment(input) {
|
|
968
|
-
const domain = resolvePrettyDevDomain(input.domain ?? process.env.APP_URL ?? "stacks.localhost");
|
|
969
|
-
if (!domain) {
|
|
970
|
-
log.info("APP_URL already uses localhost; no pretty URL setup is needed");
|
|
971
|
-
return !0;
|
|
972
|
-
}
|
|
973
|
-
const rpx = await importDevelopmentRpx();
|
|
974
|
-
if (!rpx.authorizeSystemAccess({ interactive: process.stdin.isTTY === !0 && process.stdout.isTTY === !0 })) {
|
|
975
|
-
log.error("Pretty URL setup needs administrator authorization to bind ports 80 and 443 and trust the local CA");
|
|
976
|
-
return !1;
|
|
977
|
-
}
|
|
978
|
-
await prepareRpxTlsForDev({
|
|
979
|
-
domain,
|
|
980
|
-
includeDashboard: process.env.STACKS_DEV_DASHBOARD === "1",
|
|
981
|
-
options: { verbose: input.verbose },
|
|
982
|
-
skipHosts: input.skipHosts,
|
|
983
|
-
trustCertificate: !input.skipTrust
|
|
984
|
-
});
|
|
985
|
-
const spawnEnv = {};
|
|
986
|
-
if (process.env.SUDO_PASSWORD)
|
|
987
|
-
spawnEnv.SUDO_PASSWORD = process.env.SUDO_PASSWORD;
|
|
988
|
-
if (input.verbose)
|
|
989
|
-
spawnEnv.RPX_VERBOSE = "1";
|
|
990
|
-
const rpxEntry = resolveRpxEntryPath();
|
|
991
|
-
if (rpxEntry)
|
|
992
|
-
spawnEnv.RPX_MODULE = rpxEntry;
|
|
993
|
-
await startRpxDaemonIfNeeded({
|
|
994
|
-
spawnCommand: await resolveRpxDaemonSpawnCommand(),
|
|
995
|
-
spawnEnv,
|
|
996
|
-
verbose: input.verbose ?? !1,
|
|
997
|
-
stopRpx: rpx.stopDaemon
|
|
998
|
-
});
|
|
999
|
-
const ready = await waitForHttpsProxy(443, 5000);
|
|
1000
|
-
if (ready)
|
|
1001
|
-
log.success(`Pretty URLs are ready at https://${domain}`);
|
|
1002
|
-
else
|
|
1003
|
-
log.error("rpx did not become reachable on port 443");
|
|
1004
|
-
return ready;
|
|
1005
|
-
}
|
|
1006
|
-
async function startRpxDaemonIfNeeded(input) {
|
|
1007
|
-
const { ensureDaemonRunning, isDaemonRunning: isRpxUp } = await importDevelopmentRpx();
|
|
1008
|
-
if (await isRpxUp()) {
|
|
1009
|
-
if (await waitForHttpsProxy(443, 1500))
|
|
1010
|
-
return;
|
|
1011
|
-
await input.stopRpx({ timeoutMs: 5000 }).catch(() => {});
|
|
1012
|
-
}
|
|
1013
|
-
const spawnOpts = {
|
|
1014
|
-
spawnCommand: input.spawnCommand,
|
|
1015
|
-
spawnCwd: resolveRpxDaemonSpawnCwd(),
|
|
1016
|
-
startupTimeoutMs: 45000,
|
|
1017
|
-
spawnEnv: input.spawnEnv,
|
|
1018
|
-
verbose: input.verbose
|
|
1019
|
-
};
|
|
1020
|
-
try {
|
|
1021
|
-
await ensureDaemonRunning(spawnOpts);
|
|
1022
|
-
} catch (firstError) {
|
|
1023
|
-
await input.stopRpx({ timeoutMs: 5000 }).catch(() => {});
|
|
1024
|
-
await ensureDaemonRunning(spawnOpts).catch(() => {
|
|
1025
|
-
throw firstError;
|
|
1026
|
-
});
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
async function registerRpxProxiesForDomain(input) {
|
|
1030
|
-
const { domain, frontendPort, apiPort, docsPort, dashboardPort, includeDashboard, options } = input, verbose = options.verbose ?? !1, proxies = buildRpxProxySpecs({ domain, frontendPort, apiPort, docsPort, dashboardPort, includeDashboard });
|
|
1031
|
-
await unregisterRpxProxies([`${domain}-api`, `${domain}-docs`]);
|
|
1032
|
-
const {
|
|
1033
|
-
getRegistryDir,
|
|
1034
|
-
readAll,
|
|
1035
|
-
stopDaemon: stopRpx,
|
|
1036
|
-
syncDevelopmentDnsFromRegistry,
|
|
1037
|
-
writeEntry
|
|
1038
|
-
} = await importDevelopmentRpx(), spawnCommand = await resolveRpxDaemonSpawnCommand(), spawnEnv = {};
|
|
1039
|
-
if (process.env.SUDO_PASSWORD)
|
|
1040
|
-
spawnEnv.SUDO_PASSWORD = process.env.SUDO_PASSWORD;
|
|
1041
|
-
if (verbose)
|
|
1042
|
-
spawnEnv.RPX_VERBOSE = "1";
|
|
1043
|
-
const rpxEntry = resolveRpxEntryPath();
|
|
1044
|
-
if (rpxEntry)
|
|
1045
|
-
spawnEnv.RPX_MODULE = rpxEntry;
|
|
1046
|
-
try {
|
|
1047
|
-
await startRpxDaemonIfNeeded({ spawnCommand, spawnEnv, verbose, stopRpx });
|
|
1048
|
-
} catch (error) {
|
|
1049
|
-
const { isDaemonRunning } = await importDevelopmentRpx();
|
|
1050
|
-
if (!await isDaemonRunning())
|
|
1051
|
-
throw error;
|
|
1052
|
-
if (verbose) {
|
|
1053
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1054
|
-
console.log(` ${dim(" ")}${dim(`rpx daemon spawn skipped (${message}); reusing existing daemon`)}`);
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
const createdAt = new Date().toISOString();
|
|
1058
|
-
for (const proxy of proxies) {
|
|
1059
|
-
await writeEntry({
|
|
1060
|
-
id: proxy.id,
|
|
1061
|
-
from: proxy.from,
|
|
1062
|
-
to: proxy.to,
|
|
1063
|
-
cwd: process.cwd(),
|
|
1064
|
-
createdAt,
|
|
1065
|
-
pid: process.pid,
|
|
1066
|
-
pathRewrites: proxy.pathRewrites
|
|
1067
|
-
}, void 0, verbose);
|
|
1068
|
-
if (!activeRpxRegistryIds.includes(proxy.id))
|
|
1069
|
-
activeRpxRegistryIds.push(proxy.id);
|
|
1070
|
-
if (verbose)
|
|
1071
|
-
console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`https://${proxy.to} \u2192 ${proxy.from}`)}`);
|
|
1072
|
-
}
|
|
1073
|
-
const registryDir = getRegistryDir(), entries = await readAll(registryDir, verbose).catch(() => []);
|
|
1074
|
-
await syncDevelopmentDnsFromRegistry(entries, { verbose, ownerPid: process.pid }).catch((err) => {
|
|
1075
|
-
if (verbose) {
|
|
1076
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1077
|
-
console.log(` ${dim(" ")}${dim(`Dev DNS sync: ${message}`)}`);
|
|
1078
|
-
}
|
|
1079
|
-
});
|
|
1080
|
-
}
|
|
1081
|
-
function wantsInteractive(options) {
|
|
1082
|
-
return options.interactive;
|
|
1083
|
-
}
|
|
1
|
+
import{execSync}from"node:child_process";import{existsSync,readdirSync,readFileSync}from"node:fs";import readline from"node:readline";import process from"node:process";import{bold,cyan,dim,green,intro,log,onUnknownSubcommand,outro,prompts,runCommand,yellow}from"@stacksjs/cli";import{homedir}from"node:os";import{dirname,join,relative}from"node:path";import{fileURLToPath}from"node:url";import{Action}from"@stacksjs/enums";import{libsPath,projectPath,stxPath}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{version}from"../../package.json";const DEV_BOOT_STARTING_LINE_COUNT=3;function eraseDevBootStartingLines(){if(!process.stdout.isTTY)return;for(let i=0;i<DEV_BOOT_STARTING_LINE_COUNT;i++){readline.moveCursor(process.stdout,0,-1);readline.clearLine(process.stdout,0)}}let developmentRpx;function resolveRpxEntryPath(){return[projectPath("node_modules/@stacksjs/rpx/dist/index.js"),join(homedir(),"Code/Tools/rpx/packages/rpx/dist/index.js"),projectPath("pantry/@stacksjs/rpx/dist/src/index.js")].find((entry)=>existsSync(entry))??null}async function importDevelopmentRpx(){if(developmentRpx)return developmentRpx;const entry=resolveRpxEntryPath();developmentRpx=entry?await import(entry):await import("@stacksjs/rpx");return developmentRpx}const activeRpxRegistryIds=[];let _actions;async function actions(){if(!_actions)_actions=await import("@stacksjs/actions");return _actions}export const interactiveDevChoices=[{value:"all",title:"All"},{value:"frontend",title:"Frontend"},{value:"api",title:"Backend"},{value:"dashboard",title:"Dashboard"},{value:"desktop",title:"Desktop"},{value:"native",title:"Native App"},{value:"components",title:"Components"},{value:"docs",title:"Documentation"}];export async function dispatchInteractiveDevSelection(selection,runners){if(!Object.hasOwn(runners,selection))return!1;await runners[selection]();return!0}export function resolvePrettyDevDomain(appUrl,nativeMode=!1){if(!appUrl||nativeMode)return null;try{const hostname=new URL(/^https?:\/\//i.test(appUrl)?appUrl:`https://${appUrl}`).hostname.toLowerCase();if(hostname==="localhost"||hostname==="127.0.0.1"||hostname==="::1"||hostname==="0.0.0.0")return null;return hostname}catch{return null}}export function shouldUsePrettyDevUrls(input){return input.domain!==null&&!input.localhostOnly&&(input.proxyManagedExternally||input.systemAuthorized)}const APPLICATION_VIEW_CANDIDATES=["app/index","app","dashboard/index","dashboard","home/index","home","composer","workspace/index","workspace","feed","portal","admin","account/index","account","profile/index","profile"],SITE_LAYOUT_NAMES=new Set(["default","guest","marketing","site"]);export function normalizeDevelopmentEntryPath(path){const value=path?.trim();if(!value||value==="/")return"/";try{const parsed=new URL(value,"http://stacks.localhost");return`${parsed.pathname}${parsed.search}${parsed.hash}`}catch{return"/"}}function configuredApplicationPath(root){if(process.env.APP_PATH)return process.env.APP_PATH;try{return readFileSync(join(root,"config/app.ts"),"utf8").match(/^[ \t]*appPath\s*:\s*(['"`])([^'"`]+)\1/m)?.[2]}catch{return}}function configuredDevelopmentLaunch(root){const environmentValue=process.env.STACKS_DEV_LAUNCH?.trim().toLowerCase();if(environmentValue==="browser"||environmentValue==="native")return environmentValue;try{const value=readFileSync(join(root,"config/app.ts"),"utf8").match(/\bdevLaunch\s*:\s*(['"`])(browser|native)\1/)?.[2];return value==="browser"||value==="native"?value:void 0}catch{return}}export function resolveDevelopmentLaunch(input={}){if(input.browser||input.site)return"browser";if(input.native)return"native";const root=input.root??projectPath();return input.configuredLaunch??configuredDevelopmentLaunch(root)??"browser"}function viewSource(root,route){const base=join(root,"resources/views",route);for(const extension of["stx","vue","html"]){const path=`${base}.${extension}`;if(existsSync(path))try{return readFileSync(path,"utf8")}catch{return""}}return}function usesApplicationLayout(source){const layout=source.match(/@extends\(\s*['"]layouts\/([^'"]+)['"]\s*\)/)?.[1]??source.match(/@layout\(\s*['"]layouts\/([^'"]+)['"]\s*\)/)?.[1];if(!layout)return!1;return!SITE_LAYOUT_NAMES.has(layout.split("/").at(-1)??layout)}export function resolveDevelopmentEntryPath(input={}){if(input.site)return"/";const root=input.root??projectPath(),configuredPath=input.configuredPath??configuredApplicationPath(root);if(configuredPath)return normalizeDevelopmentEntryPath(configuredPath);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);if(appCandidate)return normalizeDevelopmentEntryPath(`/${appCandidate.route.replace(/\/index$/,"")}`);return loginExists?"/login":"/"}export function developmentUrl(baseUrl,entryPath){const normalized=normalizeDevelopmentEntryPath(entryPath);if(normalized==="/")return baseUrl.replace(/\/$/,"");return new URL(normalized,`${baseUrl.replace(/\/$/,"")}/`).toString()}export function developmentBrowserCommand(url,platform=process.platform){if(platform==="darwin")return["open",url];if(platform==="win32")return["cmd","/c","start","",url];return["xdg-open",url]}function openDevelopmentBrowser(url){if(!process.stdout.isTTY||process.env.CI||process.env.STACKS_DEV_NO_OPEN==="1")return;try{Bun.spawn(developmentBrowserCommand(url),{stdin:"ignore",stdout:"ignore",stderr:"ignore"}).unref()}catch{}}async function canStartPrettyDevProxy(){if(await waitForHttpsProxy(443,150))return!0;try{const{authorizeSystemAccess}=await importDevelopmentRpx();return authorizeSystemAccess({interactive:!1})}catch{return!1}}export function dev(buddy){const descriptions={dev:"Start development server",frontend:"Start the frontend development server",components:"Start the Components development server",desktop:"Start the Desktop App development server",native:"Start the app in a native Craft window",dashboard:"Start the Dashboard development server",api:"Start the local API development server",docs:"Start the Documentation development server",systemTray:"Start the System Tray development server",interactive:"Get asked which development server to start",select:"Which development server are you trying to start?",withLocalhost:"Include the localhost URL in the output",browser:"Open the application in a browser instead of its configured native window",site:"Open the marketing site instead of the application",project:"Target a specific project",verbose:"Enable verbose output"};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)=>{const perf=Bun.nanoseconds();process.env.STACKS_DEV_ENTRY_PATH=resolveDevelopmentEntryPath({site:options.site});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);if(target){const serverOptions={...options},a=await actions();switch(target){case"native":await startDevelopmentServer({...serverOptions,native:!0},perf);break;case"frontend":await a.runFrontendDevServer(serverOptions);break;case"api":await a.runApiDevServer(serverOptions);break;case"components":await a.runComponentsDevServer(serverOptions);break;case"dashboard":await a.runDashboardDevServer(serverOptions);break;case"desktop":await startDevelopmentServer({...serverOptions,native:!0},perf);break;case"system-tray":await a.runSystemTrayDevServer(serverOptions);break;case"docs":await a.runDocsDevServer(serverOptions);break;default:log.error(`Unknown server: ${target}`);process.exit(ExitCode.InvalidArgument)}}else if(wantsInteractive(options)){const selectedValue=(await prompts({type:"select",name:"value",message:descriptions.select,choices:interactiveDevChoices})).value,a=await actions();if(!await dispatchInteractiveDevSelection(selectedValue,{all:()=>startDevelopmentServer(options,perf),frontend:()=>a.runFrontendDevServer(options),api:()=>a.runApiDevServer(options),dashboard:()=>a.runDashboardDevServer(options),desktop:()=>startDevelopmentServer({...options,native:!0},perf),native:()=>startDevelopmentServer({...options,native:!0},perf),components:()=>a.runComponentsDevServer(options),docs:()=>a.runDocsDevServer(options)})){log.error("Invalid option during interactive mode");process.exit(ExitCode.InvalidArgument)}}else await startDevelopmentServer(options,perf);outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("dev:components",descriptions.components).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:components"),result=await runCommand("bun run dev",{cwd:libsPath("components/stx")});if(options.verbose)log.info("buddy dev:components result",result);if(result.isErr){await outro("While running the dev:components command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}console.log("");await outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("dev:docs",descriptions.docs).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:docs"),result=await(await actions()).runAction(Action.DevDocs,options);if(result.isErr){await outro("While running the dev:docs command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}console.log("");await outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("dev:native",descriptions.native).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:native");await startDevelopmentServer({...options,native:!0},perf)});buddy.command("dev:desktop",descriptions.desktop).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:desktop");await startDevelopmentServer({...options,native:!0},perf)});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)=>{const a=await actions();if(options.watchTypes!==!1)a.watchTypes(options).catch((err)=>{log.warn("[dev:api] type watcher exited:",err)});await a.runApiDevServer(options)});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)=>{process.env.STACKS_DEV_ENTRY_PATH=resolveDevelopmentEntryPath({site:options.site});await(await actions()).runFrontendDevServer(options)});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)=>{await(await actions()).runDashboardDevServer(options)});buddy.command("dev:system-tray",descriptions.systemTray).alias("dev:tray").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{await(await actions()).runSystemTrayDevServer(options)});onUnknownSubcommand(buddy,"dev")}export async function startDevelopmentServer(_options,_startTime){const options=_options,startedAt=_startTime,appUrl=process.env.APP_URL??"stacks.localhost",nativeMode=resolveDevelopmentLaunch({browser:options.browser,native:options.native,site:options.site})==="native",entryPath=resolveDevelopmentEntryPath({site:options.site});process.env.STACKS_DEV_ENTRY_PATH=entryPath;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;if(process.env.STACKS_DEV_NO_KILL!=="1")await cleanupStaleDevProcesses([preferredFrontendPort,preferredApiPort,preferredDocsPort,preferredDashboardPort]);const portShifts=[],claimPort=async(label,preferred)=>{if(proxyManagedExternally)return preferred;const port=await findAvailablePort(preferred);if(port!==preferred)portShifts.push(`${label} :${preferred} \u2192 :${port}`);return port},frontendPort=await claimPort("Frontend",preferredFrontendPort),apiPort=await claimPort("API",preferredApiPort),docsPort=await claimPort("Docs",preferredDocsPort),dashboardPort=await claimPort("Dashboard",preferredDashboardPort);process.env.PORT=String(frontendPort);process.env.PORT_API=String(apiPort);process.env.PORT_DOCS=String(docsPort);process.env.PORT_ADMIN=String(dashboardPort);const includeDashboard=process.env.STACKS_DEV_DASHBOARD==="1",domain=resolvePrettyDevDomain(appUrl,nativeMode),appLooksCustom=domain!==null,localhostOnly=process.env.STACKS_DEV_LOCALHOST==="1",prettyUrlsRequested=appLooksCustom&&!localhostOnly,systemAuthorized=!prettyUrlsRequested||proxyManagedExternally?!0:await canStartPrettyDevProxy(),usePrettyUrls=shouldUsePrettyDevUrls({domain,localhostOnly,proxyManagedExternally,systemAuthorized}),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}`;process.env.STACKS_PROXY_MANAGED="1";process.env.STACKS_DEV_QUIET="1";console.log();if(prettySetupRequired){console.log(` ${yellow("\u26A0")} ${yellow("Pretty URL setup required")} ${dim("- using localhost for this session")}`);console.log(` ${dim(" ")}${dim("Run `./buddy setup:ssl` once, then restart `./buddy dev`.")}`);console.log()}if(portShifts.length>0){console.log(` ${yellow("\u26A0")} ${yellow("Ports in use by another process")} ${dim(`- ${portShifts.join(", ")}`)}`);console.log()}console.log(` ${bold(cyan("stacks"))} ${dim(`v${version}`)} ${dim("starting\u2026")}`);console.log();const rpxTlsPreflight=hasCustomDomain&&domain?prepareRpxTlsForDev({domain,includeDashboard,options}):Promise.resolve();if(localhostOnly&&domain&&!proxyManagedExternally)removeStalePublicDomainOverrides(domain,includeDashboard,options.verbose??!1).catch(()=>{});rpxTlsPreflight.catch(()=>{});let isExiting=!1,closeNativeApp;const SHUTDOWN_GRACE_MS=1500,cleanup=()=>{if(isExiting)return;isExiting=!0;closeNativeApp?.();const rpxTeardown=(async()=>{try{await unregisterRpxProxies(activeRpxRegistryIds);activeRpxRegistryIds.length=0;if(hasCustomDomain&&domain)await removeStalePublicDomainOverrides(domain,includeDashboard,options.verbose??!1)}catch{}})(),teardownDeadline=new Promise((resolve)=>setTimeout(resolve,SHUTDOWN_GRACE_MS));Promise.race([rpxTeardown,teardownDeadline]).finally(()=>{try{process.kill(-process.pid,"SIGTERM")}catch{try{process.kill(0,"SIGTERM")}catch{}}setTimeout(()=>{try{process.kill(0,"SIGKILL")}catch{process.exit(1)}},SHUTDOWN_GRACE_MS).unref()})};process.on("SIGINT",cleanup);process.on("SIGTERM",cleanup);process.on("SIGHUP",cleanup);const quietOpts={...options,quiet:!0},a=await actions(),ports=[{name:"API",port:apiPort}],readinessTimeoutMs=30000;let readyAnnounced=!1;const nativeUrl=developmentUrl(`http://localhost:${frontendPort}`,entryPath),nativeWindowReady=nativeMode?waitForPort(frontendPort,readinessTimeoutMs).then(async(ready)=>{if(!ready){log.warn(`Native window skipped because the frontend did not answer within ${readinessTimeoutMs/1000}s`);return}closeNativeApp=await launchNativeAppWindow(nativeUrl,options)}):Promise.resolve();Promise.all(ports.map((p)=>waitForPort(p.port,readinessTimeoutMs))).then(async(results)=>{if(readyAnnounced)return;readyAnnounced=!0;const failed=results.map((ok,i)=>ok?null:ports[i]?.name??null).filter((x)=>x!==null);let proxyReachable=!1;if(hasCustomDomain&&domain){(async()=>{try{await rpxTlsPreflight;await registerRpxProxiesForDomain({domain,frontendPort,apiPort,docsPort,dashboardPort,includeDashboard,options})}catch{}})();proxyReachable=await waitForHttpsProxy(443,16000);if(!proxyReachable){console.log(` ${yellow("\u26A0")} ${yellow("HTTPS proxy not reachable on :443")} \u2014 serving ${cyan(`http://localhost:${frontendPort}`)} instead`);console.log(` ${dim(" ")}${dim("rpx needs a valid SUDO_PASSWORD in .env to bind :443; trust the local CA, then restart `./buddy dev`.")}`);if(options.verbose)console.log(` ${dim(" ")}${dim(`Trust CA: sh ${join(RPX_SSL_DIR,"trust-rpx-cert.sh")}`)}`)}}eraseDevBootStartingLines();printDevReadyBanner({options,nativeMode,hasCustomDomain:!!appLooksCustom&&!localhostOnly,proxyReachable:proxyManagedExternally?!0:proxyReachable,frontendUrl,frontendBaseUrl,entryPath,apiUrl,docsUrl,dashboardUrl,frontendPort,apiPort,docsPort,includeDashboard,domain,dashboardPort,dashboardDomain});if(!nativeMode){const browserUrl=hasCustomDomain&&(proxyManagedExternally||proxyReachable)?frontendUrl:developmentUrl(`http://localhost:${frontendPort}`,entryPath);openDevelopmentBrowser(browserUrl)}if(startedAt){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`;console.log(` ${dim(summary)}`);console.log();printDevEngineNotes();console.log()}if(process.env.STACKS_PRINT_ROUTES==="1")await printRegisteredRoutes(apiPort).catch(()=>{})}).catch(()=>{});await Promise.all([a.runFrontendDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`Frontend: ${error}`)}),a.runApiDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`API: ${error}`)}),a.runDocsDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`Docs: ${error}`)}),includeDashboard?a.runDashboardDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`Dashboard: ${error}`)}):Promise.resolve(),nativeWindowReady.catch((error)=>{if(options.verbose)log.warn(`Native window: ${error}`)})])}function isComingSoonMode(){if(existsSync(projectPath("storage/framework/coming-soon")))return!0;try{const idx=projectPath("resources/views/index.stx");return existsSync(idx)&&/siteMode\s*=\s*['"]coming-soon['"]/.test(readFileSync(idx,"utf8"))}catch{return!1}}function blogIsConfigured(){try{const dir=projectPath("content/blog");return existsSync(dir)&&readdirSync(dir).some((f)=>f.endsWith(".md"))}catch{return!1}}function printDevReadyBanner(input){const{options,nativeMode,hasCustomDomain,proxyReachable,frontendUrl,frontendBaseUrl,entryPath,apiUrl,docsUrl,dashboardUrl,frontendPort,apiPort,docsPort,includeDashboard,domain,dashboardPort,dashboardDomain}=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`;console.log();const frontendLabel=entryPath==="/"?"Site":"App";console.log(` ${green("\u279C")} ${bold(frontendLabel.padEnd(8))}: ${cyan(feUrl)}`);if(entryPath!=="/")console.log(` ${dim("\u279C")} ${dim("Site")}: ${dim(feBaseUrl)}`);if(nativeMode)console.log(` ${green("\u279C")} ${bold("Native")}: ${cyan(`Craft \u2192 ${feUrl}`)}`);console.log(` ${green("\u279C")} ${bold("API")}: ${cyan(apUrl)}`);console.log(` ${green("\u279C")} ${bold("Docs")}: ${cyan(dcUrl)}`);if(blogIsConfigured())console.log(` ${green("\u279C")} ${bold("Blog")}: ${cyan(blogUrl)}`);if(includeDashboard)console.log(` ${green("\u279C")} ${bold("Dashboard")}: ${cyan(dbUrl)}`);if(useProxy){console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`${developmentUrl(`http://localhost:${frontendPort}`,entryPath)} (bypasses the proxy)`)}`);if(includeDashboard&&dashboardDomain)console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`http://localhost:${dashboardPort} (dashboard, bypasses the proxy)`)}`)}if(isComingSoonMode()){console.log();console.log(` ${yellow("\u25CF")} ${bold(yellow("Coming soon mode"))} ${dim("\u2014 visitors see the holding page; bypass with the coming-soon secret.")}`)}if(verbose&&domain){console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${frontendPort} \u2192 ${domain}`)}`);console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendBaseUrl}/api \u2192 localhost:${apiPort}`)}`);console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendBaseUrl}/docs \u2192 localhost:${docsPort}`)}`);if(includeDashboard)console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${dashboardPort} \u2192 ${dashboardDomain}`)}`)}console.log()}function printDevEngineNotes(){const routesFile=stxPath("routes.ts"),routesLabel=relative(projectPath(),routesFile),crosswindConfig=join(projectPath(),"config/crosswind.ts");if(existsSync(crosswindConfig)||existsSync(join(projectPath(),"crosswind.config.ts")))console.log(` ${green("[Crosswind]")} ${dim("CSS engine loaded")}`);if(existsSync(routesFile))try{const routeCount=(readFileSync(routesFile,"utf8").match(/pattern:/g)??[]).length;if(routeCount>0)console.log(` ${green("[stx]")} ${dim(`Generated ${routeCount} routes \u2192 ${routesLabel}`)}`)}catch{console.log(` ${green("[stx]")} ${dim(`Routes manifest \u2192 ${routesLabel}`)}`)}}async function launchNativeAppWindow(url,options){let createApp;try{createApp=(await importCraftSdk()).createApp}catch(error){if(options.verbose)log.warn(`Native window unavailable: ${error}`);console.log(` ${dim("Native window unavailable. Install craft-native or set CRAFT_BIN to a Craft binary.")}
|
|
2
|
+
`);return}if(!createApp){console.log(` ${dim("Native window unavailable. The Craft SDK did not export createApp.")}
|
|
3
|
+
`);return}const craftBinaryPath=resolveCraftBinaryPath();if(!craftBinaryPath){console.log(` ${dim("Native window unavailable. Set CRAFT_BIN to a Craft binary, or install Craft in ~/Code/Tools/craft.")}
|
|
4
|
+
`);return}const appIconPath=resolveNativeAppIconPath(),app=createApp({url,quiet:!options.verbose,craftPath:craftBinaryPath,window:{title:await resolveNativeAppTitle(),width:1280,height:860,titlebarHidden:!0,webSidebarMaterial:!0,webSidebarWidth:286,webSidebarMaterialOpacity:0.78,...appIconPath&&{icon:appIconPath}}});app.show().catch((error)=>{if(options.verbose)log.warn(`Native window exited: ${error}`)});return()=>app.close()}async function importCraftSdk(){const localCraftSdk=process.env.HOME?`${process.env.HOME}/Code/Tools/craft/packages/typescript/src/index.ts`:void 0;if(localCraftSdk&&existsSync(localCraftSdk))return await import(localCraftSdk);const packageNames=["craft-native","@craft-native/craft","@stacksjs/ts-craft"];let primaryError;for(const packageName of packageNames)try{return await import(packageName)}catch(error){primaryError??=error}throw primaryError}async function cleanupStaleDevProcesses(ports){const projectRoot=projectPath(),actionDevPath=projectPath("storage/framework/core/actions/src/dev/"),pids=new Set;for(const port of ports)for(const pid of await listenerPids(port)){if(pid===process.pid)continue;const command=await commandForPid(pid);if(!command.includes(projectRoot))continue;if(command.includes("storage/framework/core/buddy/src/cli.ts dev")||command.includes("storage/framework/core/actions/src/dev/")||command.includes(actionDevPath))pids.add(pid)}if(pids.size===0)return;for(const pid of pids)try{process.kill(pid,"SIGTERM")}catch{}await new Promise((r)=>setTimeout(r,250));for(const pid of pids)try{process.kill(pid,"SIGKILL")}catch{}await new Promise((r)=>setTimeout(r,150))}async function listenerPids(port){try{const proc=Bun.spawn(["lsof","-nP",`-iTCP:${port}`,"-sTCP:LISTEN","-t"],{stdout:"pipe",stderr:"ignore"}),output=await new Response(proc.stdout).text();await proc.exited;return output.split(/\s+/).map((value)=>Number(value)).filter((pid)=>Number.isInteger(pid)&&pid>0)}catch{return[]}}async function commandForPid(pid){try{const proc=Bun.spawn(["ps","-p",String(pid),"-o","command="],{stdout:"pipe",stderr:"ignore"}),output=await new Response(proc.stdout).text();await proc.exited;return output.trim()}catch{return""}}function resolveCraftBinaryPath(){const home=process.env.HOME;return[process.env.CRAFT_BIN,home?`${home}/Code/Tools/craft/craft`:void 0,home?`${home}/Code/Tools/craft/bin/craft`:void 0,home?`${home}/Code/Tools/craft/packages/zig/zig-out/bin/craft`:void 0,home?`${home}/Documents/Projects/craft/packages/zig/zig-out/bin/craft`:void 0].filter((candidate)=>Boolean(candidate)).find((candidate)=>existsSync(candidate))}function resolveNativeAppIconPath(){return[projectPath("resources/assets/images/app-icon.png"),projectPath("resources/assets/images/icon.png"),projectPath("public/icon.png")].find((candidate)=>existsSync(candidate))}async function resolveNativeAppTitle(){try{const pkg=await Bun.file(projectPath("package.json")).json(),name=typeof pkg.productName==="string"?pkg.productName:pkg.name;if(typeof name==="string"&&name.length>0)return name.split(/[-_\s]+/).filter(Boolean).map((part)=>part.charAt(0).toUpperCase()+part.slice(1)).join(" ")}catch{}return"Stacks App"}const METHOD_COLORS={GET:cyan,POST:green,PUT:cyan,PATCH:cyan,DELETE:cyan,OPTIONS:dim};async function printRegisteredRoutes(apiPort){try{const ac=new AbortController,t=setTimeout(()=>ac.abort(),1500),res=await fetch(`http://127.0.0.1:${apiPort}/__routes`,{signal:ac.signal}).catch(()=>null);clearTimeout(t);if(!res||!res.ok)return;const routes=await res.json();if(!Array.isArray(routes)||routes.length===0)return;const sorted=[...routes].sort((a,b)=>a.path.localeCompare(b.path)||a.method.localeCompare(b.method)),longestPath=Math.max(...sorted.map((r)=>r.path.length));console.log(` ${dim("Registered routes")}
|
|
5
|
+
`);for(const r of sorted){const method=(METHOD_COLORS[r.method.toUpperCase()]??dim)(r.method.toUpperCase().padEnd(6)),path=r.path.padEnd(longestPath),named=r.name?` ${dim(`(${r.name})`)}`:"";console.log(` ${method} ${path}${named}`)}console.log()}catch{}}async function probeDevServerHttp(port){for(const path of["/__health","/"])if(await new Promise((resolve)=>{const ac=new AbortController,t=setTimeout(()=>ac.abort(),path==="/"?8000:800);fetch(`http://127.0.0.1:${port}${path}`,{signal:ac.signal,redirect:"manual"}).then(()=>{clearTimeout(t);resolve(!0)}).catch(()=>{clearTimeout(t);resolve(!1)})}))return!0;return!1}async function findAvailablePort(preferred,attempts=20){for(let port=preferred;port<preferred+attempts;port++)if(await portIsFree(port))return port;return preferred}async function portIsFree(port){for(const host of["127.0.0.1","::1"])if(await someoneIsListening(port,host))return!1;let server;try{server=Bun.serve({port,hostname:"0.0.0.0",reusePort:!1,fetch:()=>new Response})}catch{return!1}server.stop(!0);return!0}async function someoneIsListening(port,host){const{connect}=await import("node:net");return new Promise((resolve)=>{const socket=connect({port,host}),finish=(listening)=>{socket.destroy();resolve(listening)};socket.setTimeout(300,()=>finish(!1));socket.once("connect",()=>finish(!0));socket.once("error",()=>finish(!1))})}async function waitForPort(port,timeoutMs){const deadline=Date.now()+timeoutMs;while(Date.now()<deadline){if(await probeDevServerHttp(port))return!0;await new Promise((r)=>setTimeout(r,50))}return!1}async function waitForHttpsProxy(port,timeoutMs){const{connect}=await import("node:net"),deadline=Date.now()+timeoutMs;while(Date.now()<deadline){if(await new Promise((resolve)=>{const socket=connect({port,host:"127.0.0.1"}),finish=(v)=>{try{socket.destroy()}catch{}resolve(v)};socket.once("connect",()=>finish(!0));socket.once("error",()=>finish(!1));setTimeout(()=>finish(!1),800).unref?.()}))return!0;await new Promise((r)=>setTimeout(r,200))}return!1}const RPX_SSL_DIR=join(homedir(),".stacks","ssl"),RPX_ROOT_CA_PATH=join(RPX_SSL_DIR,"rpx-root-ca.crt"),RPX_HOST_CERT_PATH=join(RPX_SSL_DIR,"rpx.localhost.crt");function buildDevelopmentTlsHostnames(domain,includeDashboard){return[domain,...includeDashboard?[`dashboard.${domain}`]:[],"rpx.localhost"]}async function ensureRpxDevelopmentHttps(domain,options,includeDashboard,trustCertificate=!0){const verbose=options.verbose??!1,{buildRegistryTlsProxyOptions,certIncludesSanHostnames,checkExistingCertificates,clearSslConfigCache,forceTrustCertificate,generateCertificate,getRegistryDir,isDaemonRunning,isRootCaTrustedForSsl,readAll,readCertCommonName,stopDaemon,trustRootCaForBrowsers,verifyHttpsChain}=await importDevelopmentRpx(),hostnames=buildDevelopmentTlsHostnames(domain,includeDashboard),entries=await readAll(getRegistryDir(),!1).catch(()=>[]);for(const entry of entries){const host=entry.to?.trim();if(host&&!hostnames.includes(host))hostnames.push(host)}const tlsOptions=buildRegistryTlsProxyOptions(hostnames,domain,verbose),hostnameInCert=certIncludesSanHostnames(RPX_HOST_CERT_PATH,hostnames),cnMatchesApp=readCertCommonName(RPX_HOST_CERT_PATH)===domain;let certRegenerated=!1;if(!await checkExistingCertificates(tlsOptions)||!hostnameInCert||!cnMatchesApp){clearSslConfigCache();await generateCertificate({...tlsOptions,forceRegenerate:!0});certRegenerated=!0}let trusted=isRootCaTrustedForSsl(RPX_ROOT_CA_PATH,domain,{verbose});if(!trusted&&trustCertificate){trusted=trustRootCaForBrowsers(RPX_ROOT_CA_PATH,{serverName:domain,verbose})||await forceTrustCertificate(RPX_ROOT_CA_PATH,{serverName:domain,verbose})||isRootCaTrustedForSsl(RPX_ROOT_CA_PATH,domain,{verbose});if(trusted)console.log(` ${green("\u2713")} ${dim("HTTPS")}: ${dim("Local CA trusted \u2014 reload the browser if you still see a warning")}`);else console.log(` ${yellow("\u26A0")} ${yellow("HTTPS")}: ${yellow(`Local CA not trusted \u2014 run: sh ${join(RPX_SSL_DIR,"trust-rpx-cert.sh")}`)}`)}else if(verbose)console.log(` ${green("\u2713")} ${dim("HTTPS")}: ${dim("Local certificate trusted for SSL")}`);const chainOk=existsSync(RPX_ROOT_CA_PATH)?await verifyHttpsChain(domain,RPX_ROOT_CA_PATH):!1;if((certRegenerated||!hostnameInCert||!cnMatchesApp)&&await isDaemonRunning()){await stopDaemon({timeoutMs:8000}).catch(()=>{});if(verbose)log.info("Restarting rpx daemon to load TLS certificates for this app")}else if(!chainOk&&verbose)console.log(` ${dim(" ")}${dim("HTTPS chain not yet valid on :443 \u2014 daemon will (re)start after backends are ready")}`)}function resolveRpxDaemonSpawnCwd(){const fromEnv=process.env.RPX_BIN||process.env.STACKS_RPX_BIN;if(fromEnv&&existsSync(fromEnv))return dirname(fromEnv);const bootstrap=join(dirname(fileURLToPath(import.meta.url)),"../../scripts/rpx-daemon-bootstrap.js");if(existsSync(bootstrap))return dirname(bootstrap);return homedir()}async function resolveRpxDaemonSpawnCommand(){const bootstrap=join(dirname(fileURLToPath(import.meta.url)),"../../scripts/rpx-daemon-bootstrap.js");if(existsSync(bootstrap))return[process.execPath,bootstrap];const fromEnv=process.env.RPX_BIN||process.env.STACKS_RPX_BIN;if(fromEnv&&existsSync(fromEnv))return[fromEnv,"daemon:start"];const pkgUrl=import.meta.resolve("@stacksjs/rpx/package.json"),cli=join(dirname(fileURLToPath(pkgUrl)),"dist/bin/cli.js");return[process.execPath,cli,"daemon:start"]}function buildRpxProxySpecs(input){const{domain,frontendPort,apiPort,docsPort,dashboardPort,includeDashboard}=input,dashboardDomain=`dashboard.${domain}`;return[{id:`${domain}-frontend`,from:`localhost:${frontendPort}`,to:domain,pathRewrites:[{from:"/api",to:`localhost:${apiPort}`,stripPrefix:!1},{from:"/docs",to:`localhost:${docsPort}`,stripPrefix:!0}]},...includeDashboard?[{id:`${domain}-dashboard`,from:`localhost:${dashboardPort}`,to:dashboardDomain}]:[]]}async function unregisterRpxProxies(ids){if(ids.length===0)return;try{const{removeEntry}=await importDevelopmentRpx();for(const id of ids)await removeEntry(id).catch(()=>{})}catch{}}async function removeStalePublicDomainOverrides(domain,includeDashboard,verbose){const{checkHosts,removeHosts,resolverFilePath,contentLooksLikeRpxResolver}=await importDevelopmentRpx(),hosts=[domain,...includeDashboard?[`dashboard.${domain}`]:[]],present=await checkHosts(hosts,verbose).catch(()=>hosts.map(()=>!1)),staleHosts=hosts.filter((_,i)=>present[i]===!0);if(staleHosts.length>0){await removeHosts(staleHosts,verbose).catch(()=>{});if((await checkHosts(staleHosts,verbose).catch(()=>staleHosts.map(()=>!0))).some(Boolean))log.warn(`Stale /etc/hosts entries still point ${staleHosts.join(", ")} at loopback. Remove them with: sudo nano /etc/hosts`);else log.info(`Removed stale /etc/hosts entries for ${staleHosts.join(", ")} (left by a previous dev session)`)}if(process.platform!=="darwin")return;const labels=domain.split("."),basename=labels.length>=2?labels.slice(-2).join("."):domain,resolverPath=resolverFilePath(basename);let isRpxManaged=!1;try{isRpxManaged=contentLooksLikeRpxResolver(readFileSync(resolverPath,"utf8"))}catch{}if(!isRpxManaged)return;const quotedPath=`'${resolverPath.replace(/'/g,"'\\''")}'`,rm=process.env.SUDO_PASSWORD?`printf '%s
|
|
6
|
+
' "$SUDO_PASSWORD" | sudo -S rm -f ${quotedPath}`:`sudo -n rm -f ${quotedPath}`;try{execSync(rm,{stdio:["pipe","ignore","ignore"]});log.info(`Removed stale rpx DNS resolver ${resolverPath} (left by a previous dev session)`);execSync("sudo -n dscacheutil -flushcache; sudo -n killall -HUP mDNSResponder; true",{stdio:["pipe","ignore","ignore"]})}catch{log.warn(`A stale rpx DNS resolver still hijacks ${domain}. Remove it with: sudo rm ${resolverPath}`)}}async function prepareRpxTlsForDev(input){const{domain,includeDashboard,options,skipHosts=!1,trustCertificate=!0}=input,verbose=options.verbose??!1,hosts=[domain,...includeDashboard?[`dashboard.${domain}`]:[]],hostsNeedingFile=hosts.filter((host)=>{const h=host.trim().toLowerCase();return h!=="localhost"&&!h.endsWith(".localhost")&&!h.endsWith(".localhost.")}),{addHosts,setupDevelopmentDns}=await importDevelopmentRpx(),dnsDomains=!skipHosts&&hostsNeedingFile.length>0?hosts:[];if(dnsDomains.length>0){if(!await setupDevelopmentDns({domains:dnsDomains,verbose}).catch(()=>!1)&&verbose)log.warn(`Dev DNS not configured for ${dnsDomains.join(", ")} \u2014 falling back to /etc/hosts`)}if(!skipHosts&&hostsNeedingFile.length>0)await addHosts(hostsNeedingFile,verbose).catch((err)=>{log.warn(`Could not update /etc/hosts for ${hostsNeedingFile.join(", ")}: ${err.message}`);log.warn("Add 127.0.0.1 entries manually or set SUDO_PASSWORD in .env")});await ensureRpxDevelopmentHttps(domain,options,includeDashboard,trustCertificate)}export async function setupPrettyDevEnvironment(input){const domain=resolvePrettyDevDomain(input.domain??process.env.APP_URL??"stacks.localhost");if(!domain){log.info("APP_URL already uses localhost; no pretty URL setup is needed");return!0}const rpx=await importDevelopmentRpx();if(!rpx.authorizeSystemAccess({interactive:process.stdin.isTTY===!0&&process.stdout.isTTY===!0})){log.error("Pretty URL setup needs administrator authorization to bind ports 80 and 443 and trust the local CA");return!1}await prepareRpxTlsForDev({domain,includeDashboard:process.env.STACKS_DEV_DASHBOARD==="1",options:{verbose:input.verbose},skipHosts:input.skipHosts,trustCertificate:!input.skipTrust});const spawnEnv={};if(process.env.SUDO_PASSWORD)spawnEnv.SUDO_PASSWORD=process.env.SUDO_PASSWORD;if(input.verbose)spawnEnv.RPX_VERBOSE="1";const rpxEntry=resolveRpxEntryPath();if(rpxEntry)spawnEnv.RPX_MODULE=rpxEntry;await startRpxDaemonIfNeeded({spawnCommand:await resolveRpxDaemonSpawnCommand(),spawnEnv,verbose:input.verbose??!1,stopRpx:rpx.stopDaemon});const ready=await waitForHttpsProxy(443,5000);if(ready)log.success(`Pretty URLs are ready at https://${domain}`);else log.error("rpx did not become reachable on port 443");return ready}async function startRpxDaemonIfNeeded(input){const{ensureDaemonRunning,isDaemonRunning:isRpxUp}=await importDevelopmentRpx();if(await isRpxUp()){if(await waitForHttpsProxy(443,1500))return;await input.stopRpx({timeoutMs:5000}).catch(()=>{})}const spawnOpts={spawnCommand:input.spawnCommand,spawnCwd:resolveRpxDaemonSpawnCwd(),startupTimeoutMs:45000,spawnEnv:input.spawnEnv,verbose:input.verbose};try{await ensureDaemonRunning(spawnOpts)}catch(firstError){await input.stopRpx({timeoutMs:5000}).catch(()=>{});await ensureDaemonRunning(spawnOpts).catch(()=>{throw firstError})}}async function registerRpxProxiesForDomain(input){const{domain,frontendPort,apiPort,docsPort,dashboardPort,includeDashboard,options}=input,verbose=options.verbose??!1,proxies=buildRpxProxySpecs({domain,frontendPort,apiPort,docsPort,dashboardPort,includeDashboard});await unregisterRpxProxies([`${domain}-api`,`${domain}-docs`]);const{getRegistryDir,readAll,stopDaemon:stopRpx,syncDevelopmentDnsFromRegistry,writeEntry}=await importDevelopmentRpx(),spawnCommand=await resolveRpxDaemonSpawnCommand(),spawnEnv={};if(process.env.SUDO_PASSWORD)spawnEnv.SUDO_PASSWORD=process.env.SUDO_PASSWORD;if(verbose)spawnEnv.RPX_VERBOSE="1";const rpxEntry=resolveRpxEntryPath();if(rpxEntry)spawnEnv.RPX_MODULE=rpxEntry;try{await startRpxDaemonIfNeeded({spawnCommand,spawnEnv,verbose,stopRpx})}catch(error){const{isDaemonRunning}=await importDevelopmentRpx();if(!await isDaemonRunning())throw error;if(verbose){const message=error instanceof Error?error.message:String(error);console.log(` ${dim(" ")}${dim(`rpx daemon spawn skipped (${message}); reusing existing daemon`)}`)}}const createdAt=new Date().toISOString();for(const proxy of proxies){await writeEntry({id:proxy.id,from:proxy.from,to:proxy.to,cwd:process.cwd(),createdAt,pid:process.pid,pathRewrites:proxy.pathRewrites},void 0,verbose);if(!activeRpxRegistryIds.includes(proxy.id))activeRpxRegistryIds.push(proxy.id);if(verbose)console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`https://${proxy.to} \u2192 ${proxy.from}`)}`)}const registryDir=getRegistryDir(),entries=await readAll(registryDir,verbose).catch(()=>[]);await syncDevelopmentDnsFromRegistry(entries,{verbose,ownerPid:process.pid}).catch((err)=>{if(verbose){const message=err instanceof Error?err.message:String(err);console.log(` ${dim(" ")}${dim(`Dev DNS sync: ${message}`)}`)}})}function wantsInteractive(options){return options.interactive}
|