@stacksjs/buddy 0.70.88 → 0.70.90
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 +1 -0
- package/dist/cli.js +165 -0
- package/dist/commands/about.d.ts +2 -0
- package/dist/commands/about.js +35 -0
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +33 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +68 -0
- package/dist/commands/build.d.ts +2 -0
- package/dist/commands/build.js +153 -0
- package/dist/commands/cd.d.ts +20 -0
- package/dist/commands/cd.js +39 -0
- package/dist/commands/changelog.d.ts +2 -0
- package/dist/commands/changelog.js +32 -0
- package/dist/commands/clean.d.ts +2 -0
- package/dist/commands/clean.js +37 -0
- package/dist/commands/cloud.d.ts +2 -0
- package/dist/commands/cloud.js +503 -0
- package/dist/commands/commit.d.ts +2 -0
- package/dist/commands/commit.js +16 -0
- package/dist/commands/completion.d.ts +2 -0
- package/dist/commands/completion.js +143 -0
- package/dist/commands/config-migrate.d.ts +26 -0
- package/dist/commands/config-migrate.js +94 -0
- package/dist/commands/configure.d.ts +2 -0
- package/dist/commands/configure.js +48 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.js +141 -0
- package/dist/commands/deploy.d.ts +107 -0
- package/dist/commands/deploy.js +1439 -0
- package/dist/commands/dev.d.ts +10 -0
- package/dist/commands/dev.js +907 -0
- package/dist/commands/dns.d.ts +2 -0
- package/dist/commands/dns.js +102 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +285 -0
- package/dist/commands/domains.d.ts +2 -0
- package/dist/commands/domains.js +159 -0
- package/dist/commands/email.d.ts +2 -0
- package/dist/commands/email.js +524 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/commands/env.js +246 -0
- package/dist/commands/extension.d.ts +8 -0
- package/dist/commands/extension.js +78 -0
- package/dist/commands/features.d.ts +180 -0
- package/dist/commands/features.js +422 -0
- package/dist/commands/fresh.d.ts +2 -0
- package/dist/commands/fresh.js +39 -0
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.js +104 -0
- package/dist/commands/http.d.ts +2 -0
- package/dist/commands/http.js +33 -0
- package/dist/commands/index.d.ts +53 -0
- package/dist/commands/index.js +53 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +22 -0
- package/dist/commands/key.d.ts +2 -0
- package/dist/commands/key.js +23 -0
- package/dist/commands/lint.d.ts +2 -0
- package/dist/commands/lint.js +46 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.js +108 -0
- package/dist/commands/mail.d.ts +2 -0
- package/dist/commands/mail.js +693 -0
- package/dist/commands/maintenance.d.ts +2 -0
- package/dist/commands/maintenance.js +141 -0
- package/dist/commands/make.d.ts +2 -0
- package/dist/commands/make.js +375 -0
- package/dist/commands/migrate-project.d.ts +2 -0
- package/dist/commands/migrate-project.js +49 -0
- package/dist/commands/migrate.d.ts +4 -0
- package/dist/commands/migrate.js +455 -0
- package/dist/commands/outdated.d.ts +2 -0
- package/dist/commands/outdated.js +19 -0
- package/dist/commands/package.d.ts +2 -0
- package/dist/commands/package.js +17 -0
- package/dist/commands/phone.d.ts +2 -0
- package/dist/commands/phone.js +188 -0
- package/dist/commands/ports.d.ts +2 -0
- package/dist/commands/ports.js +118 -0
- package/dist/commands/prepublish.d.ts +2 -0
- package/dist/commands/prepublish.js +17 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +29 -0
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +172 -0
- package/dist/commands/queue.d.ts +2 -0
- package/dist/commands/queue.js +248 -0
- package/dist/commands/release.d.ts +2 -0
- package/dist/commands/release.js +30 -0
- package/dist/commands/route.d.ts +2 -0
- package/dist/commands/route.js +21 -0
- package/dist/commands/saas.d.ts +2 -0
- package/dist/commands/saas.js +25 -0
- package/dist/commands/schedule.d.ts +2 -0
- package/dist/commands/schedule.js +61 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +84 -0
- package/dist/commands/seed.d.ts +2 -0
- package/dist/commands/seed.js +71 -0
- package/dist/commands/serve.d.ts +25 -0
- package/dist/commands/serve.js +182 -0
- package/dist/commands/setup.d.ts +12 -0
- package/dist/commands/setup.js +215 -0
- package/dist/commands/share.d.ts +2 -0
- package/dist/commands/share.js +214 -0
- package/dist/commands/sms.d.ts +2 -0
- package/dist/commands/sms.js +328 -0
- package/dist/commands/stacks.d.ts +2 -0
- package/dist/commands/stacks.js +69 -0
- package/dist/commands/telemetry.d.ts +2 -0
- package/dist/commands/telemetry.js +74 -0
- package/dist/commands/test.d.ts +2 -0
- package/dist/commands/test.js +130 -0
- package/dist/commands/tinker.d.ts +2 -0
- package/dist/commands/tinker.js +37 -0
- package/dist/commands/types.d.ts +2 -0
- package/dist/commands/types.js +18 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +98 -0
- package/dist/commands/version.d.ts +2 -0
- package/dist/commands/version.js +16 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +223 -0
- package/dist/custom-cli.d.ts +1 -0
- package/dist/custom-cli.js +23 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lazy-commands.d.ts +61 -0
- package/dist/lazy-commands.js +188 -0
- package/dist/migrators/index.d.ts +9 -0
- package/dist/migrators/index.js +62 -0
- package/dist/migrators/laravel/index.d.ts +2 -0
- package/dist/migrators/laravel/index.js +148 -0
- package/dist/migrators/laravel/migrations.d.ts +52 -0
- package/dist/migrators/laravel/migrations.js +232 -0
- package/dist/migrators/laravel/models.d.ts +34 -0
- package/dist/migrators/laravel/models.js +134 -0
- package/dist/migrators/rails/index.d.ts +2 -0
- package/dist/migrators/rails/index.js +11 -0
- package/dist/migrators/types.d.ts +44 -0
- package/dist/migrators/types.js +0 -0
- package/package.json +2 -2
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import readline from "node:readline";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { bold, cyan, dim, green, intro, log, onUnknownSubcommand, outro, prompts, runCommand, yellow } from "@stacksjs/cli";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { Action } from "@stacksjs/enums";
|
|
10
|
+
import { libsPath, projectPath } 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 function dev(buddy) {
|
|
45
|
+
const descriptions = {
|
|
46
|
+
dev: "Start development server",
|
|
47
|
+
frontend: "Start the frontend development server",
|
|
48
|
+
components: "Start the Components development server",
|
|
49
|
+
desktop: "Start the Desktop App development server",
|
|
50
|
+
native: "Start the app in a native Craft window",
|
|
51
|
+
dashboard: "Start the Dashboard development server",
|
|
52
|
+
api: "Start the local API development server",
|
|
53
|
+
email: "Start the Email development server",
|
|
54
|
+
docs: "Start the Documentation development server",
|
|
55
|
+
systemTray: "Start the System Tray development server",
|
|
56
|
+
interactive: "Get asked which development server to start",
|
|
57
|
+
select: "Which development server are you trying to start?",
|
|
58
|
+
withLocalhost: "Include the localhost URL in the output",
|
|
59
|
+
project: "Target a specific project",
|
|
60
|
+
verbose: "Enable verbose output"
|
|
61
|
+
};
|
|
62
|
+
buddy.command("dev [server]", descriptions.dev).option("-f, --frontend", descriptions.frontend).option("-a, --api", descriptions.api).option("-e, --email", descriptions.email).option("-c, --components", descriptions.components).option("-d, --dashboard", descriptions.dashboard).option("-k, --desktop", descriptions.desktop).option("-n, --native", descriptions.native).option("-o, --docs", descriptions.docs).option("-s, --system-tray", descriptions.systemTray).option("-i, --interactive", descriptions.interactive, { default: !1 }).option("-l, --with-localhost", descriptions.withLocalhost, { default: !1 }).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (server, options) => {
|
|
63
|
+
const perf = Bun.nanoseconds(), target = server || (options.frontend ? "frontend" : void 0) || (options.api ? "api" : void 0) || (options.components ? "components" : void 0) || (options.dashboard ? "dashboard" : void 0) || (options.desktop ? "desktop" : void 0) || (options.native ? "native" : void 0) || (options.systemTray || options["system-tray"] ? "system-tray" : void 0) || (options.docs ? "docs" : void 0);
|
|
64
|
+
if (target) {
|
|
65
|
+
const serverOptions = { ...options }, a = await actions();
|
|
66
|
+
switch (target) {
|
|
67
|
+
case "native":
|
|
68
|
+
await startDevelopmentServer({ ...serverOptions, native: !0 }, perf);
|
|
69
|
+
break;
|
|
70
|
+
case "frontend":
|
|
71
|
+
await a.runFrontendDevServer(serverOptions);
|
|
72
|
+
break;
|
|
73
|
+
case "api":
|
|
74
|
+
await a.runApiDevServer(serverOptions);
|
|
75
|
+
break;
|
|
76
|
+
case "components":
|
|
77
|
+
await a.runComponentsDevServer(serverOptions);
|
|
78
|
+
break;
|
|
79
|
+
case "dashboard":
|
|
80
|
+
await a.runDashboardDevServer(serverOptions);
|
|
81
|
+
break;
|
|
82
|
+
case "desktop":
|
|
83
|
+
await a.runDesktopDevServer(serverOptions);
|
|
84
|
+
break;
|
|
85
|
+
case "system-tray":
|
|
86
|
+
await a.runSystemTrayDevServer(serverOptions);
|
|
87
|
+
break;
|
|
88
|
+
case "docs":
|
|
89
|
+
await a.runDocsDevServer(serverOptions);
|
|
90
|
+
break;
|
|
91
|
+
default:
|
|
92
|
+
log.error(`Unknown server: ${target}`);
|
|
93
|
+
process.exit(ExitCode.InvalidArgument);
|
|
94
|
+
}
|
|
95
|
+
} else if (wantsInteractive(options)) {
|
|
96
|
+
const selectedValue = (await prompts({
|
|
97
|
+
type: "select",
|
|
98
|
+
name: "value",
|
|
99
|
+
message: descriptions.select,
|
|
100
|
+
choices: [
|
|
101
|
+
{ value: "all", title: "All" },
|
|
102
|
+
{ value: "frontend", title: "Frontend" },
|
|
103
|
+
{ value: "api", title: "Backend" },
|
|
104
|
+
{ value: "dashboard", title: "Dashboard" },
|
|
105
|
+
{ value: "desktop", title: "Desktop" },
|
|
106
|
+
{ value: "native", title: "Native App" },
|
|
107
|
+
{ value: "email", title: "Email" },
|
|
108
|
+
{ value: "components", title: "Components" },
|
|
109
|
+
{ value: "docs", title: "Documentation" }
|
|
110
|
+
]
|
|
111
|
+
})).value;
|
|
112
|
+
if (selectedValue === "components")
|
|
113
|
+
await (await actions()).runComponentsDevServer(options);
|
|
114
|
+
else if (selectedValue === "api")
|
|
115
|
+
await (await actions()).runApiDevServer(options);
|
|
116
|
+
else if (selectedValue === "dashboard")
|
|
117
|
+
await (await actions()).runDashboardDevServer(options);
|
|
118
|
+
else if (selectedValue === "native")
|
|
119
|
+
await startDevelopmentServer({ ...options, native: !0 }, perf);
|
|
120
|
+
else if (selectedValue === "docs")
|
|
121
|
+
await (await actions()).runDocsDevServer(options);
|
|
122
|
+
else {
|
|
123
|
+
log.error("Invalid option during interactive mode");
|
|
124
|
+
process.exit(ExitCode.InvalidArgument);
|
|
125
|
+
}
|
|
126
|
+
} else
|
|
127
|
+
await startDevelopmentServer(options, perf);
|
|
128
|
+
outro("Exited", { startTime: perf, useSeconds: !0 });
|
|
129
|
+
process.exit(ExitCode.Success);
|
|
130
|
+
});
|
|
131
|
+
buddy.command("dev:components", descriptions.components).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
132
|
+
const perf = await intro("buddy dev:components"), result = await runCommand("bun run dev", {
|
|
133
|
+
cwd: libsPath("components/vue")
|
|
134
|
+
});
|
|
135
|
+
if (options.verbose)
|
|
136
|
+
log.info("buddy dev:components result", result);
|
|
137
|
+
if (result.isErr) {
|
|
138
|
+
await outro("While running the dev:components command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
139
|
+
process.exit(ExitCode.FatalError);
|
|
140
|
+
}
|
|
141
|
+
console.log("");
|
|
142
|
+
await outro("Exited", { startTime: perf, useSeconds: !0 });
|
|
143
|
+
process.exit(ExitCode.Success);
|
|
144
|
+
});
|
|
145
|
+
buddy.command("dev:docs", descriptions.docs).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
146
|
+
const perf = await intro("buddy dev:docs"), result = await (await actions()).runAction(Action.DevDocs, options);
|
|
147
|
+
if (result.isErr) {
|
|
148
|
+
await outro("While running the dev:docs command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
149
|
+
process.exit(ExitCode.FatalError);
|
|
150
|
+
}
|
|
151
|
+
console.log("");
|
|
152
|
+
await outro("Exited", { startTime: perf, useSeconds: !0 });
|
|
153
|
+
process.exit(ExitCode.Success);
|
|
154
|
+
});
|
|
155
|
+
buddy.command("dev:native", descriptions.native).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
156
|
+
const perf = await intro("buddy dev:native");
|
|
157
|
+
await startDevelopmentServer({ ...options, native: !0 }, perf);
|
|
158
|
+
});
|
|
159
|
+
buddy.command("dev:desktop", descriptions.desktop).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
160
|
+
const perf = await intro("buddy dev:desktop"), result = await (await actions()).runAction(Action.DevDesktop, options);
|
|
161
|
+
if (result.isErr) {
|
|
162
|
+
await outro("While running the dev:desktop command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
163
|
+
process.exit(ExitCode.FatalError);
|
|
164
|
+
}
|
|
165
|
+
console.log("");
|
|
166
|
+
await outro("Exited", { startTime: perf, useSeconds: !0 });
|
|
167
|
+
process.exit(ExitCode.Success);
|
|
168
|
+
});
|
|
169
|
+
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) => {
|
|
170
|
+
const a = await actions();
|
|
171
|
+
if (options.watchTypes !== !1)
|
|
172
|
+
a.watchTypes(options).catch((err) => {
|
|
173
|
+
log.warn("[dev:api] type watcher exited:", err);
|
|
174
|
+
});
|
|
175
|
+
await a.runApiDevServer(options);
|
|
176
|
+
});
|
|
177
|
+
buddy.command("dev:frontend", descriptions.frontend).alias("dev:pages").alias("dev:views").option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
178
|
+
await (await actions()).runFrontendDevServer(options);
|
|
179
|
+
});
|
|
180
|
+
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) => {
|
|
181
|
+
await (await actions()).runDashboardDevServer(options);
|
|
182
|
+
});
|
|
183
|
+
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) => {
|
|
184
|
+
await (await actions()).runSystemTrayDevServer(options);
|
|
185
|
+
});
|
|
186
|
+
onUnknownSubcommand(buddy, "dev");
|
|
187
|
+
}
|
|
188
|
+
export async function startDevelopmentServer(_options, _startTime) {
|
|
189
|
+
const options = _options, startedAt = _startTime, appUrl = process.env.APP_URL, nativeMode = options.native === !0, proxyManagedExternally = process.env.STACKS_PROXY_MANAGED === "1", frontendPort = Number(process.env.PORT) || 3000, apiPort = Number(process.env.PORT_API) || 3008, docsPort = Number(process.env.PORT_DOCS) || 3006, dashboardPort = Number(process.env.PORT_ADMIN) || 3002, includeDashboard = process.env.STACKS_DEV_DASHBOARD === "1", appLooksCustom = !nativeMode && appUrl && appUrl !== "localhost" && !appUrl.includes("localhost:"), domain = appLooksCustom ? appUrl.replace(/^https?:\/\//, "") : null, hasCustomDomain = appLooksCustom && !proxyManagedExternally, dashboardDomain = domain ? `dashboard.${domain}` : null, frontendUrl = domain ? `https://${domain}` : `http://localhost:${frontendPort}`, apiUrl = domain ? `https://${domain}/api` : `http://localhost:${apiPort}`, docsUrl = domain ? `https://${domain}/docs` : `http://localhost:${docsPort}`, dashboardUrl = dashboardDomain ? `https://${dashboardDomain}` : `http://localhost:${dashboardPort}`, managedPorts = [
|
|
190
|
+
frontendPort,
|
|
191
|
+
apiPort,
|
|
192
|
+
docsPort,
|
|
193
|
+
...includeDashboard ? [dashboardPort] : []
|
|
194
|
+
];
|
|
195
|
+
process.env.STACKS_PROXY_MANAGED = "1";
|
|
196
|
+
process.env.STACKS_DEV_QUIET = "1";
|
|
197
|
+
console.log();
|
|
198
|
+
console.log(` ${bold(cyan("stacks"))} ${dim(`v${version}`)} ${dim("starting\u2026")}`);
|
|
199
|
+
console.log();
|
|
200
|
+
if (process.env.STACKS_DEV_NO_KILL !== "1")
|
|
201
|
+
await cleanupStaleDevProcesses(managedPorts);
|
|
202
|
+
const rpxTlsPreflight = hasCustomDomain && domain ? prepareRpxTlsForDev({ domain, includeDashboard, options }) : Promise.resolve();
|
|
203
|
+
rpxTlsPreflight.catch(() => {});
|
|
204
|
+
let isExiting = !1, closeNativeApp;
|
|
205
|
+
const SHUTDOWN_GRACE_MS = 1500, cleanup = () => {
|
|
206
|
+
if (isExiting)
|
|
207
|
+
return;
|
|
208
|
+
isExiting = !0;
|
|
209
|
+
closeNativeApp?.();
|
|
210
|
+
unregisterRpxProxies(activeRpxRegistryIds);
|
|
211
|
+
activeRpxRegistryIds.length = 0;
|
|
212
|
+
try {
|
|
213
|
+
process.kill(-process.pid, "SIGTERM");
|
|
214
|
+
} catch {
|
|
215
|
+
try {
|
|
216
|
+
process.kill(0, "SIGTERM");
|
|
217
|
+
} catch {}
|
|
218
|
+
}
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
try {
|
|
221
|
+
process.kill(0, "SIGKILL");
|
|
222
|
+
} catch {
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
}, SHUTDOWN_GRACE_MS).unref();
|
|
226
|
+
};
|
|
227
|
+
process.on("SIGINT", cleanup);
|
|
228
|
+
process.on("SIGTERM", cleanup);
|
|
229
|
+
const quietOpts = { ...options, quiet: !0 }, a = await actions(), ports = [
|
|
230
|
+
{ name: "Frontend", port: frontendPort },
|
|
231
|
+
{ name: "API", port: apiPort }
|
|
232
|
+
], readinessTimeoutMs = 30000;
|
|
233
|
+
let readyAnnounced = !1;
|
|
234
|
+
const nativeUrl = `http://localhost:${frontendPort}`, nativeWindowReady = nativeMode ? waitForPort(frontendPort, readinessTimeoutMs).then(async (ready) => {
|
|
235
|
+
if (!ready) {
|
|
236
|
+
log.warn(`Native window skipped because the frontend did not answer within ${readinessTimeoutMs / 1000}s`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
closeNativeApp = await launchNativeAppWindow(nativeUrl, options);
|
|
240
|
+
}) : Promise.resolve();
|
|
241
|
+
Promise.all(ports.map((p) => waitForPort(p.port, readinessTimeoutMs))).then(async (results) => {
|
|
242
|
+
if (readyAnnounced)
|
|
243
|
+
return;
|
|
244
|
+
readyAnnounced = !0;
|
|
245
|
+
const failed = results.map((ok, i) => ok ? null : ports[i]?.name ?? null).filter((x) => x !== null);
|
|
246
|
+
let proxyReachable = !1;
|
|
247
|
+
if (hasCustomDomain && domain) {
|
|
248
|
+
(async () => {
|
|
249
|
+
try {
|
|
250
|
+
await rpxTlsPreflight;
|
|
251
|
+
await registerRpxProxiesForDomain({
|
|
252
|
+
domain,
|
|
253
|
+
frontendPort,
|
|
254
|
+
apiPort,
|
|
255
|
+
docsPort,
|
|
256
|
+
dashboardPort,
|
|
257
|
+
includeDashboard,
|
|
258
|
+
options
|
|
259
|
+
});
|
|
260
|
+
} catch {}
|
|
261
|
+
})();
|
|
262
|
+
proxyReachable = await waitForHttpsProxy(443, 16000);
|
|
263
|
+
if (!proxyReachable) {
|
|
264
|
+
console.log(` ${yellow("\u26A0")} ${yellow("HTTPS proxy not reachable on :443")} \u2014 serving ${cyan(`http://localhost:${frontendPort}`)} instead`);
|
|
265
|
+
console.log(` ${dim(" ")}${dim("rpx needs a valid SUDO_PASSWORD in .env to bind :443; trust the local CA, then restart `./buddy dev`.")}`);
|
|
266
|
+
if (options.verbose)
|
|
267
|
+
console.log(` ${dim(" ")}${dim(`Trust CA: sh ${join(RPX_SSL_DIR, "trust-rpx-cert.sh")}`)}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
eraseDevBootStartingLines();
|
|
271
|
+
printDevReadyBanner({
|
|
272
|
+
options,
|
|
273
|
+
nativeMode,
|
|
274
|
+
hasCustomDomain: !!appLooksCustom,
|
|
275
|
+
proxyReachable: proxyManagedExternally ? !0 : proxyReachable,
|
|
276
|
+
frontendUrl,
|
|
277
|
+
apiUrl,
|
|
278
|
+
docsUrl,
|
|
279
|
+
dashboardUrl,
|
|
280
|
+
frontendPort,
|
|
281
|
+
apiPort,
|
|
282
|
+
docsPort,
|
|
283
|
+
includeDashboard,
|
|
284
|
+
domain,
|
|
285
|
+
dashboardPort,
|
|
286
|
+
dashboardDomain
|
|
287
|
+
});
|
|
288
|
+
if (startedAt) {
|
|
289
|
+
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`;
|
|
290
|
+
console.log(` ${dim(summary)}`);
|
|
291
|
+
console.log();
|
|
292
|
+
printDevEngineNotes();
|
|
293
|
+
console.log();
|
|
294
|
+
}
|
|
295
|
+
if (process.env.STACKS_PRINT_ROUTES === "1")
|
|
296
|
+
await printRegisteredRoutes(apiPort).catch(() => {});
|
|
297
|
+
}).catch(() => {});
|
|
298
|
+
await Promise.all([
|
|
299
|
+
a.runFrontendDevServer(quietOpts).catch((error) => {
|
|
300
|
+
if (options.verbose)
|
|
301
|
+
log.error(`Frontend: ${error}`);
|
|
302
|
+
}),
|
|
303
|
+
a.runApiDevServer(quietOpts).catch((error) => {
|
|
304
|
+
if (options.verbose)
|
|
305
|
+
log.error(`API: ${error}`);
|
|
306
|
+
}),
|
|
307
|
+
a.runDocsDevServer(quietOpts).catch((error) => {
|
|
308
|
+
if (options.verbose)
|
|
309
|
+
log.error(`Docs: ${error}`);
|
|
310
|
+
}),
|
|
311
|
+
includeDashboard ? a.runDashboardDevServer(quietOpts).catch((error) => {
|
|
312
|
+
if (options.verbose)
|
|
313
|
+
log.error(`Dashboard: ${error}`);
|
|
314
|
+
}) : Promise.resolve(),
|
|
315
|
+
nativeWindowReady.catch((error) => {
|
|
316
|
+
if (options.verbose)
|
|
317
|
+
log.warn(`Native window: ${error}`);
|
|
318
|
+
})
|
|
319
|
+
]);
|
|
320
|
+
}
|
|
321
|
+
function isComingSoonMode() {
|
|
322
|
+
if (existsSync(projectPath("storage/framework/coming-soon")))
|
|
323
|
+
return !0;
|
|
324
|
+
try {
|
|
325
|
+
const idx = projectPath("resources/views/index.stx");
|
|
326
|
+
return existsSync(idx) && /siteMode\s*=\s*['"]coming-soon['"]/.test(readFileSync(idx, "utf8"));
|
|
327
|
+
} catch {
|
|
328
|
+
return !1;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function blogIsConfigured() {
|
|
332
|
+
try {
|
|
333
|
+
const dir = projectPath("content/blog");
|
|
334
|
+
return existsSync(dir) && readdirSync(dir).some((f) => f.endsWith(".md"));
|
|
335
|
+
} catch {
|
|
336
|
+
return !1;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function printDevReadyBanner(input) {
|
|
340
|
+
const {
|
|
341
|
+
options,
|
|
342
|
+
nativeMode,
|
|
343
|
+
hasCustomDomain,
|
|
344
|
+
proxyReachable,
|
|
345
|
+
frontendUrl,
|
|
346
|
+
apiUrl,
|
|
347
|
+
docsUrl,
|
|
348
|
+
dashboardUrl,
|
|
349
|
+
frontendPort,
|
|
350
|
+
apiPort,
|
|
351
|
+
docsPort,
|
|
352
|
+
includeDashboard,
|
|
353
|
+
domain,
|
|
354
|
+
dashboardPort,
|
|
355
|
+
dashboardDomain
|
|
356
|
+
} = input, verbose = options.verbose ?? !1, showLocalUrls = verbose || options.withLocalhost === !0, useProxy = hasCustomDomain && proxyReachable, feUrl = useProxy ? frontendUrl : `http://localhost:${frontendPort}`, apUrl = useProxy ? apiUrl : `http://localhost:${apiPort}`, dcUrl = useProxy ? docsUrl : `http://localhost:${docsPort}`, blogUrl = `${feUrl.replace(/\/$/, "")}/blog`;
|
|
357
|
+
console.log();
|
|
358
|
+
console.log(` ${green("\u279C")} ${bold("Frontend")}: ${cyan(feUrl)}`);
|
|
359
|
+
if (showLocalUrls && useProxy)
|
|
360
|
+
console.log(` ${dim("\u279C")} ${dim("Local")}: ${dim(`http://localhost:${frontendPort}`)}`);
|
|
361
|
+
if (nativeMode)
|
|
362
|
+
console.log(` ${green("\u279C")} ${bold("Native")}: ${cyan(`Craft \u2192 http://localhost:${frontendPort}`)}`);
|
|
363
|
+
console.log(` ${green("\u279C")} ${bold("API")}: ${cyan(apUrl)}`);
|
|
364
|
+
if (showLocalUrls && useProxy)
|
|
365
|
+
console.log(` ${dim("\u279C")} ${dim("Local API")}: ${dim(`http://localhost:${apiPort}`)}`);
|
|
366
|
+
console.log(` ${green("\u279C")} ${bold("Docs")}: ${cyan(dcUrl)}`);
|
|
367
|
+
if (showLocalUrls && useProxy)
|
|
368
|
+
console.log(` ${dim("\u279C")} ${dim("Local docs")}: ${dim(`http://localhost:${docsPort}`)}`);
|
|
369
|
+
if (blogIsConfigured())
|
|
370
|
+
console.log(` ${green("\u279C")} ${bold("Blog")}: ${cyan(blogUrl)}`);
|
|
371
|
+
if (includeDashboard)
|
|
372
|
+
console.log(` ${green("\u279C")} ${bold("Dashboard")}: ${cyan(dashboardUrl)}`);
|
|
373
|
+
if (isComingSoonMode()) {
|
|
374
|
+
console.log();
|
|
375
|
+
console.log(` ${yellow("\u25CF")} ${bold(yellow("Coming soon mode"))} ${dim("\u2014 visitors see the holding page; bypass with the coming-soon secret.")}`);
|
|
376
|
+
}
|
|
377
|
+
if (verbose && domain) {
|
|
378
|
+
console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${frontendPort} \u2192 ${domain}`)}`);
|
|
379
|
+
console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendUrl}/api \u2192 localhost:${apiPort}`)}`);
|
|
380
|
+
console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendUrl}/docs \u2192 localhost:${docsPort}`)}`);
|
|
381
|
+
if (includeDashboard)
|
|
382
|
+
console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${dashboardPort} \u2192 ${dashboardDomain}`)}`);
|
|
383
|
+
}
|
|
384
|
+
console.log();
|
|
385
|
+
}
|
|
386
|
+
function printDevEngineNotes() {
|
|
387
|
+
const routesFile = join(projectPath(), ".stx/routes.ts"), crosswindConfig = join(projectPath(), "config/crosswind.ts");
|
|
388
|
+
if (existsSync(crosswindConfig) || existsSync(join(projectPath(), "crosswind.config.ts")))
|
|
389
|
+
console.log(` ${green("[Crosswind]")} ${dim("CSS engine loaded")}`);
|
|
390
|
+
if (existsSync(routesFile))
|
|
391
|
+
try {
|
|
392
|
+
const routeCount = (readFileSync(routesFile, "utf8").match(/pattern:/g) ?? []).length;
|
|
393
|
+
if (routeCount > 0)
|
|
394
|
+
console.log(` ${green("[stx]")} ${dim(`Generated ${routeCount} routes \u2192 .stx/routes.ts`)}`);
|
|
395
|
+
} catch {
|
|
396
|
+
console.log(` ${green("[stx]")} ${dim("Routes manifest \u2192 .stx/routes.ts")}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async function launchNativeAppWindow(url, options) {
|
|
400
|
+
let createApp;
|
|
401
|
+
try {
|
|
402
|
+
createApp = (await importCraftSdk()).createApp;
|
|
403
|
+
} catch (error) {
|
|
404
|
+
if (options.verbose)
|
|
405
|
+
log.warn(`Native window unavailable: ${error}`);
|
|
406
|
+
console.log(` ${dim("Native window unavailable. Install craft-native or set CRAFT_BIN to a Craft binary.")}
|
|
407
|
+
`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (!createApp) {
|
|
411
|
+
console.log(` ${dim("Native window unavailable. The Craft SDK did not export createApp.")}
|
|
412
|
+
`);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const craftBinaryPath = resolveCraftBinaryPath();
|
|
416
|
+
if (!craftBinaryPath) {
|
|
417
|
+
console.log(` ${dim("Native window unavailable. Set CRAFT_BIN to a Craft binary, or install Craft in ~/Code/Tools/craft.")}
|
|
418
|
+
`);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const appIconPath = resolveNativeAppIconPath(), app = createApp({
|
|
422
|
+
url,
|
|
423
|
+
quiet: !options.verbose,
|
|
424
|
+
craftPath: craftBinaryPath,
|
|
425
|
+
window: {
|
|
426
|
+
title: await resolveNativeAppTitle(),
|
|
427
|
+
width: 1280,
|
|
428
|
+
height: 860,
|
|
429
|
+
titlebarHidden: !0,
|
|
430
|
+
webSidebarMaterial: !0,
|
|
431
|
+
webSidebarWidth: 286,
|
|
432
|
+
webSidebarMaterialOpacity: 0.78,
|
|
433
|
+
...appIconPath && { icon: appIconPath }
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
app.show().catch((error) => {
|
|
437
|
+
if (options.verbose)
|
|
438
|
+
log.warn(`Native window exited: ${error}`);
|
|
439
|
+
});
|
|
440
|
+
return () => app.close();
|
|
441
|
+
}
|
|
442
|
+
async function importCraftSdk() {
|
|
443
|
+
const localCraftSdk = process.env.HOME ? `${process.env.HOME}/Code/Tools/craft/packages/typescript/src/index.ts` : void 0;
|
|
444
|
+
if (localCraftSdk && existsSync(localCraftSdk))
|
|
445
|
+
return await import(localCraftSdk);
|
|
446
|
+
const packageNames = ["craft-native", "@craft-native/craft", "@stacksjs/ts-craft"];
|
|
447
|
+
let primaryError;
|
|
448
|
+
for (const packageName of packageNames)
|
|
449
|
+
try {
|
|
450
|
+
return await import(packageName);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
primaryError ??= error;
|
|
453
|
+
}
|
|
454
|
+
throw primaryError;
|
|
455
|
+
}
|
|
456
|
+
async function cleanupStaleDevProcesses(ports) {
|
|
457
|
+
const projectRoot = projectPath(), actionDevPath = projectPath("storage/framework/core/actions/src/dev/"), pids = new Set;
|
|
458
|
+
for (const port of ports)
|
|
459
|
+
for (const pid of await listenerPids(port)) {
|
|
460
|
+
if (pid === process.pid)
|
|
461
|
+
continue;
|
|
462
|
+
const command = await commandForPid(pid);
|
|
463
|
+
if (!command.includes(projectRoot))
|
|
464
|
+
continue;
|
|
465
|
+
if (command.includes("storage/framework/core/buddy/src/cli.ts dev") || command.includes("storage/framework/core/actions/src/dev/") || command.includes(actionDevPath))
|
|
466
|
+
pids.add(pid);
|
|
467
|
+
}
|
|
468
|
+
if (pids.size === 0)
|
|
469
|
+
return;
|
|
470
|
+
for (const pid of pids)
|
|
471
|
+
try {
|
|
472
|
+
process.kill(pid, "SIGTERM");
|
|
473
|
+
} catch {}
|
|
474
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
475
|
+
for (const pid of pids)
|
|
476
|
+
try {
|
|
477
|
+
process.kill(pid, "SIGKILL");
|
|
478
|
+
} catch {}
|
|
479
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
480
|
+
}
|
|
481
|
+
async function listenerPids(port) {
|
|
482
|
+
try {
|
|
483
|
+
const proc = Bun.spawn(["lsof", "-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
|
|
484
|
+
stdout: "pipe",
|
|
485
|
+
stderr: "ignore"
|
|
486
|
+
}), output = await new Response(proc.stdout).text();
|
|
487
|
+
await proc.exited;
|
|
488
|
+
return output.split(/\s+/).map((value) => Number(value)).filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
489
|
+
} catch {
|
|
490
|
+
return [];
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
async function commandForPid(pid) {
|
|
494
|
+
try {
|
|
495
|
+
const proc = Bun.spawn(["ps", "-p", String(pid), "-o", "command="], {
|
|
496
|
+
stdout: "pipe",
|
|
497
|
+
stderr: "ignore"
|
|
498
|
+
}), output = await new Response(proc.stdout).text();
|
|
499
|
+
await proc.exited;
|
|
500
|
+
return output.trim();
|
|
501
|
+
} catch {
|
|
502
|
+
return "";
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function resolveCraftBinaryPath() {
|
|
506
|
+
const home = process.env.HOME;
|
|
507
|
+
return [
|
|
508
|
+
process.env.CRAFT_BIN,
|
|
509
|
+
home ? `${home}/Code/Tools/craft/craft` : void 0,
|
|
510
|
+
home ? `${home}/Code/Tools/craft/bin/craft` : void 0,
|
|
511
|
+
home ? `${home}/Code/Tools/craft/packages/zig/zig-out/bin/craft` : void 0,
|
|
512
|
+
home ? `${home}/Documents/Projects/craft/packages/zig/zig-out/bin/craft` : void 0
|
|
513
|
+
].filter((candidate) => Boolean(candidate)).find((candidate) => existsSync(candidate));
|
|
514
|
+
}
|
|
515
|
+
function resolveNativeAppIconPath() {
|
|
516
|
+
return [
|
|
517
|
+
projectPath("resources/assets/images/app-icon.png"),
|
|
518
|
+
projectPath("resources/assets/images/icon.png"),
|
|
519
|
+
projectPath("public/icon.png")
|
|
520
|
+
].find((candidate) => existsSync(candidate));
|
|
521
|
+
}
|
|
522
|
+
async function resolveNativeAppTitle() {
|
|
523
|
+
try {
|
|
524
|
+
const pkg = await Bun.file(projectPath("package.json")).json(), name = typeof pkg.productName === "string" ? pkg.productName : pkg.name;
|
|
525
|
+
if (typeof name === "string" && name.length > 0)
|
|
526
|
+
return name.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
527
|
+
} catch {}
|
|
528
|
+
return "Stacks App";
|
|
529
|
+
}
|
|
530
|
+
const METHOD_COLORS = {
|
|
531
|
+
GET: cyan,
|
|
532
|
+
POST: green,
|
|
533
|
+
PUT: cyan,
|
|
534
|
+
PATCH: cyan,
|
|
535
|
+
DELETE: cyan,
|
|
536
|
+
OPTIONS: dim
|
|
537
|
+
};
|
|
538
|
+
async function printRegisteredRoutes(apiPort) {
|
|
539
|
+
try {
|
|
540
|
+
const ac = new AbortController, t = setTimeout(() => ac.abort(), 1500), res = await fetch(`http://127.0.0.1:${apiPort}/__routes`, { signal: ac.signal }).catch(() => null);
|
|
541
|
+
clearTimeout(t);
|
|
542
|
+
if (!res || !res.ok)
|
|
543
|
+
return;
|
|
544
|
+
const routes = await res.json();
|
|
545
|
+
if (!Array.isArray(routes) || routes.length === 0)
|
|
546
|
+
return;
|
|
547
|
+
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));
|
|
548
|
+
console.log(` ${dim("Registered routes")}
|
|
549
|
+
`);
|
|
550
|
+
for (const r of sorted) {
|
|
551
|
+
const method = (METHOD_COLORS[r.method.toUpperCase()] ?? dim)(r.method.toUpperCase().padEnd(6)), path = r.path.padEnd(longestPath), named = r.name ? ` ${dim(`(${r.name})`)}` : "";
|
|
552
|
+
console.log(` ${method} ${path}${named}`);
|
|
553
|
+
}
|
|
554
|
+
console.log();
|
|
555
|
+
} catch {}
|
|
556
|
+
}
|
|
557
|
+
async function probeDevServerHttp(port) {
|
|
558
|
+
for (const path of ["/__health", "/"])
|
|
559
|
+
if (await new Promise((resolve) => {
|
|
560
|
+
const ac = new AbortController, t = setTimeout(() => ac.abort(), path === "/" ? 8000 : 800);
|
|
561
|
+
fetch(`http://127.0.0.1:${port}${path}`, { signal: ac.signal, redirect: "manual" }).then(() => {
|
|
562
|
+
clearTimeout(t);
|
|
563
|
+
resolve(!0);
|
|
564
|
+
}).catch(() => {
|
|
565
|
+
clearTimeout(t);
|
|
566
|
+
resolve(!1);
|
|
567
|
+
});
|
|
568
|
+
}))
|
|
569
|
+
return !0;
|
|
570
|
+
return !1;
|
|
571
|
+
}
|
|
572
|
+
async function waitForPort(port, timeoutMs) {
|
|
573
|
+
const deadline = Date.now() + timeoutMs;
|
|
574
|
+
while (Date.now() < deadline) {
|
|
575
|
+
if (await probeDevServerHttp(port))
|
|
576
|
+
return !0;
|
|
577
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
578
|
+
}
|
|
579
|
+
return !1;
|
|
580
|
+
}
|
|
581
|
+
async function waitForHttpsProxy(port, timeoutMs) {
|
|
582
|
+
const { connect } = await import("node:net"), deadline = Date.now() + timeoutMs;
|
|
583
|
+
while (Date.now() < deadline) {
|
|
584
|
+
if (await new Promise((resolve) => {
|
|
585
|
+
const socket = connect({ port, host: "127.0.0.1" }), finish = (v) => {
|
|
586
|
+
try {
|
|
587
|
+
socket.destroy();
|
|
588
|
+
} catch {}
|
|
589
|
+
resolve(v);
|
|
590
|
+
};
|
|
591
|
+
socket.once("connect", () => finish(!0));
|
|
592
|
+
socket.once("error", () => finish(!1));
|
|
593
|
+
setTimeout(() => finish(!1), 800).unref?.();
|
|
594
|
+
}))
|
|
595
|
+
return !0;
|
|
596
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
597
|
+
}
|
|
598
|
+
return !1;
|
|
599
|
+
}
|
|
600
|
+
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"), LOGIN_KEYCHAIN = join(homedir(), "Library/Keychains/login.keychain-db");
|
|
601
|
+
function normalizeSha256Fingerprint(raw) {
|
|
602
|
+
return (raw.includes("=") ? raw.split("=").pop() : raw).replace(/SHA-256\s+hash:\s*/gi, "").replace(/:/g, "").trim().toUpperCase();
|
|
603
|
+
}
|
|
604
|
+
function readCertFingerprint(certPath) {
|
|
605
|
+
try {
|
|
606
|
+
const out = execSync(`openssl x509 -noout -fingerprint -sha256 -in "${certPath}"`, { encoding: "utf8" });
|
|
607
|
+
return normalizeSha256Fingerprint(out);
|
|
608
|
+
} catch {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function isRpxRootCaInKeychain(caPath) {
|
|
613
|
+
const fp = readCertFingerprint(caPath);
|
|
614
|
+
if (!fp)
|
|
615
|
+
return !1;
|
|
616
|
+
const keychains = [
|
|
617
|
+
"/Library/Keychains/System.keychain",
|
|
618
|
+
LOGIN_KEYCHAIN
|
|
619
|
+
];
|
|
620
|
+
for (const keychain of keychains)
|
|
621
|
+
try {
|
|
622
|
+
const listing = execSync(`security find-certificate -a -Z "${keychain}" 2>/dev/null || true`, { encoding: "utf8" });
|
|
623
|
+
for (const line of listing.split(`
|
|
624
|
+
`))
|
|
625
|
+
if (line.toUpperCase().includes("SHA-256") && normalizeSha256Fingerprint(line) === fp)
|
|
626
|
+
return !0;
|
|
627
|
+
} catch {}
|
|
628
|
+
return !1;
|
|
629
|
+
}
|
|
630
|
+
function execSudoSh(command) {
|
|
631
|
+
const sudoPassword = process.env.SUDO_PASSWORD, escaped = command.replace(/'/g, "'\\''");
|
|
632
|
+
if (sudoPassword)
|
|
633
|
+
execSync(`echo '${sudoPassword}' | sudo -S sh -c '${escaped}' 2>/dev/null`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
634
|
+
else
|
|
635
|
+
execSync(`sudo -n sh -c '${escaped}'`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
636
|
+
}
|
|
637
|
+
const MACOS_CA_TRUST_FLAGS = "-d -r trustRoot -p ssl -p basic";
|
|
638
|
+
function listRpxRootCaHashesInKeychain(keychain) {
|
|
639
|
+
const listing = execSync(`security find-certificate -a -c "rpx.localhost" -Z "${keychain}" 2>/dev/null || true`, { encoding: "utf8" }), hashes = [];
|
|
640
|
+
for (const line of listing.split(`
|
|
641
|
+
`)) {
|
|
642
|
+
const match = line.match(/SHA-256 hash:\s*([A-F0-9]+)/i);
|
|
643
|
+
if (match)
|
|
644
|
+
hashes.push(match[1].toUpperCase());
|
|
645
|
+
}
|
|
646
|
+
return hashes;
|
|
647
|
+
}
|
|
648
|
+
function pruneStaleRpxRootCas(caPath) {
|
|
649
|
+
if (process.platform !== "darwin")
|
|
650
|
+
return;
|
|
651
|
+
const keep = readCertFingerprint(caPath);
|
|
652
|
+
if (!keep)
|
|
653
|
+
return;
|
|
654
|
+
for (const keychain of ["/Library/Keychains/System.keychain", LOGIN_KEYCHAIN])
|
|
655
|
+
for (const hash of listRpxRootCaHashesInKeychain(keychain)) {
|
|
656
|
+
if (hash === keep)
|
|
657
|
+
continue;
|
|
658
|
+
try {
|
|
659
|
+
if (keychain.startsWith("/Library"))
|
|
660
|
+
execSudoSh(`security delete-certificate -Z ${hash} "${keychain}"`);
|
|
661
|
+
else
|
|
662
|
+
execSync(`security delete-certificate -Z ${hash} "${keychain}"`, { stdio: "ignore" });
|
|
663
|
+
} catch {}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function isRpxRootCaTrustedForSsl(caPath, serverName) {
|
|
667
|
+
if (process.platform !== "darwin")
|
|
668
|
+
return isRpxRootCaInKeychain(caPath);
|
|
669
|
+
try {
|
|
670
|
+
return execSync(`security verify-cert -c "${caPath}" -s "${serverName}" -l -L -R ssl 2>&1`, { encoding: "utf8" }).includes("successful");
|
|
671
|
+
} catch {
|
|
672
|
+
return !1;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function trustRpxRootCaForBrowsers(caPath, serverName) {
|
|
676
|
+
if (process.platform !== "darwin")
|
|
677
|
+
return !1;
|
|
678
|
+
pruneStaleRpxRootCas(caPath);
|
|
679
|
+
try {
|
|
680
|
+
execSync(`security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k "${LOGIN_KEYCHAIN}" "${caPath}"`, { stdio: "ignore" });
|
|
681
|
+
} catch {}
|
|
682
|
+
try {
|
|
683
|
+
execSudoSh(`security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k /Library/Keychains/System.keychain "${caPath}"`);
|
|
684
|
+
} catch {
|
|
685
|
+
return !1;
|
|
686
|
+
}
|
|
687
|
+
return isRpxRootCaTrustedForSsl(caPath, serverName) || isRpxRootCaInKeychain(caPath);
|
|
688
|
+
}
|
|
689
|
+
function isLiveHttpsChainValid(domain) {
|
|
690
|
+
if (!existsSync(RPX_ROOT_CA_PATH))
|
|
691
|
+
return !1;
|
|
692
|
+
try {
|
|
693
|
+
return execSync(`echo | openssl s_client -connect ${domain}:443 -servername ${domain} -CAfile "${RPX_ROOT_CA_PATH}" 2>/dev/null | grep "Verify return code"`, { encoding: "utf8", timeout: 4000 }).includes(": 0 (ok)");
|
|
694
|
+
} catch {
|
|
695
|
+
return !1;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function buildDevelopmentTlsHostnames(domain, includeDashboard) {
|
|
699
|
+
return [
|
|
700
|
+
domain,
|
|
701
|
+
...includeDashboard ? [`dashboard.${domain}`] : [],
|
|
702
|
+
"rpx.localhost"
|
|
703
|
+
];
|
|
704
|
+
}
|
|
705
|
+
function readCertCommonName(certPath) {
|
|
706
|
+
try {
|
|
707
|
+
return execSync(`openssl x509 -in "${certPath}" -noout -subject -nameopt RFC2253`, { encoding: "utf8" }).match(/CN=([^,/]+)/)?.[1]?.trim() ?? null;
|
|
708
|
+
} catch {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async function buildDevelopmentTlsOptions(domain, includeDashboard, verbose) {
|
|
713
|
+
const hostnames = buildDevelopmentTlsHostnames(domain, includeDashboard);
|
|
714
|
+
try {
|
|
715
|
+
const { getRegistryDir, readAll } = await importDevelopmentRpx(), entries = await readAll(getRegistryDir(), !1);
|
|
716
|
+
for (const entry of entries) {
|
|
717
|
+
const host = entry.to?.trim();
|
|
718
|
+
if (host && !hostnames.includes(host))
|
|
719
|
+
hostnames.push(host);
|
|
720
|
+
}
|
|
721
|
+
} catch {}
|
|
722
|
+
return {
|
|
723
|
+
https: {
|
|
724
|
+
certPath: RPX_HOST_CERT_PATH,
|
|
725
|
+
keyPath: join(RPX_SSL_DIR, "rpx.localhost.key"),
|
|
726
|
+
caCertPath: join(RPX_SSL_DIR, "rpx.localhost.ca.crt"),
|
|
727
|
+
commonName: domain
|
|
728
|
+
},
|
|
729
|
+
verbose,
|
|
730
|
+
regenerateUntrustedCerts: !0,
|
|
731
|
+
proxies: hostnames.map((to) => ({ from: "localhost:1", to }))
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
async function ensureRpxDevelopmentHttps(domain, options, includeDashboard) {
|
|
735
|
+
const verbose = options.verbose ?? !1, {
|
|
736
|
+
checkExistingCertificates,
|
|
737
|
+
clearSslConfigCache,
|
|
738
|
+
forceTrustCertificate,
|
|
739
|
+
generateCertificate,
|
|
740
|
+
isDaemonRunning,
|
|
741
|
+
stopDaemon
|
|
742
|
+
} = await importDevelopmentRpx(), hostnames = buildDevelopmentTlsHostnames(domain, includeDashboard), tlsOptions = await buildDevelopmentTlsOptions(domain, includeDashboard, verbose), hostnameInCert = hostnames.every((host) => {
|
|
743
|
+
try {
|
|
744
|
+
return execSync(`openssl x509 -in "${RPX_HOST_CERT_PATH}" -noout -text`, { encoding: "utf8" }).includes(`DNS:${host}`);
|
|
745
|
+
} catch {
|
|
746
|
+
return !1;
|
|
747
|
+
}
|
|
748
|
+
}), cnMatchesApp = readCertCommonName(RPX_HOST_CERT_PATH) === domain;
|
|
749
|
+
let certRegenerated = !1;
|
|
750
|
+
if (!await checkExistingCertificates(tlsOptions) || !hostnameInCert || !cnMatchesApp) {
|
|
751
|
+
clearSslConfigCache();
|
|
752
|
+
await generateCertificate({ ...tlsOptions, forceRegenerate: !0 });
|
|
753
|
+
certRegenerated = !0;
|
|
754
|
+
}
|
|
755
|
+
let trusted = isRpxRootCaTrustedForSsl(RPX_ROOT_CA_PATH, domain);
|
|
756
|
+
if (!trusted) {
|
|
757
|
+
trusted = trustRpxRootCaForBrowsers(RPX_ROOT_CA_PATH, domain) || await forceTrustCertificate(RPX_ROOT_CA_PATH, { serverName: domain, verbose }) || isRpxRootCaTrustedForSsl(RPX_ROOT_CA_PATH, domain);
|
|
758
|
+
if (trusted)
|
|
759
|
+
console.log(` ${green("\u2713")} ${dim("HTTPS")}: ${dim("Local CA trusted \u2014 reload the browser if you still see a warning")}`);
|
|
760
|
+
else
|
|
761
|
+
console.log(` ${yellow("\u26A0")} ${yellow("HTTPS")}: ${yellow(`Local CA not trusted \u2014 run: sh ${join(RPX_SSL_DIR, "trust-rpx-cert.sh")}`)}`);
|
|
762
|
+
} else if (verbose)
|
|
763
|
+
console.log(` ${green("\u2713")} ${dim("HTTPS")}: ${dim("Local certificate trusted for SSL")}`);
|
|
764
|
+
const chainOk = isLiveHttpsChainValid(domain);
|
|
765
|
+
if ((certRegenerated || !hostnameInCert || !cnMatchesApp) && await isDaemonRunning()) {
|
|
766
|
+
await stopDaemon({ timeoutMs: 8000 }).catch(() => {});
|
|
767
|
+
if (verbose)
|
|
768
|
+
log.info("Restarting rpx daemon to load TLS certificates for this app");
|
|
769
|
+
} else if (!chainOk && verbose)
|
|
770
|
+
console.log(` ${dim(" ")}${dim("HTTPS chain not yet valid on :443 \u2014 daemon will (re)start after backends are ready")}`);
|
|
771
|
+
}
|
|
772
|
+
function resolveRpxDaemonSpawnCwd() {
|
|
773
|
+
const fromEnv = process.env.RPX_BIN || process.env.STACKS_RPX_BIN;
|
|
774
|
+
if (fromEnv && existsSync(fromEnv))
|
|
775
|
+
return dirname(fromEnv);
|
|
776
|
+
const bootstrap = join(dirname(fileURLToPath(import.meta.url)), "../../scripts/rpx-daemon-bootstrap.js");
|
|
777
|
+
if (existsSync(bootstrap))
|
|
778
|
+
return dirname(bootstrap);
|
|
779
|
+
return homedir();
|
|
780
|
+
}
|
|
781
|
+
async function resolveRpxDaemonSpawnCommand() {
|
|
782
|
+
const bootstrap = join(dirname(fileURLToPath(import.meta.url)), "../../scripts/rpx-daemon-bootstrap.js");
|
|
783
|
+
if (existsSync(bootstrap))
|
|
784
|
+
return [process.execPath, bootstrap];
|
|
785
|
+
const fromEnv = process.env.RPX_BIN || process.env.STACKS_RPX_BIN;
|
|
786
|
+
if (fromEnv && existsSync(fromEnv))
|
|
787
|
+
return [fromEnv, "daemon:start"];
|
|
788
|
+
const pkgUrl = import.meta.resolve("@stacksjs/rpx/package.json"), cli = join(dirname(fileURLToPath(pkgUrl)), "dist/bin/cli.js");
|
|
789
|
+
return [process.execPath, cli, "daemon:start"];
|
|
790
|
+
}
|
|
791
|
+
function buildRpxProxySpecs(input) {
|
|
792
|
+
const { domain, frontendPort, apiPort, docsPort, dashboardPort, includeDashboard } = input, dashboardDomain = `dashboard.${domain}`;
|
|
793
|
+
return [
|
|
794
|
+
{
|
|
795
|
+
id: `${domain}-frontend`,
|
|
796
|
+
from: `localhost:${frontendPort}`,
|
|
797
|
+
to: domain,
|
|
798
|
+
pathRewrites: [
|
|
799
|
+
{ from: "/api", to: `localhost:${apiPort}`, stripPrefix: !1 },
|
|
800
|
+
{ from: "/docs", to: `localhost:${docsPort}`, stripPrefix: !0 }
|
|
801
|
+
]
|
|
802
|
+
},
|
|
803
|
+
...includeDashboard ? [{ id: `${domain}-dashboard`, from: `localhost:${dashboardPort}`, to: dashboardDomain }] : []
|
|
804
|
+
];
|
|
805
|
+
}
|
|
806
|
+
async function unregisterRpxProxies(ids) {
|
|
807
|
+
if (ids.length === 0)
|
|
808
|
+
return;
|
|
809
|
+
try {
|
|
810
|
+
const { removeEntry } = await importDevelopmentRpx();
|
|
811
|
+
for (const id of ids)
|
|
812
|
+
await removeEntry(id).catch(() => {});
|
|
813
|
+
} catch {}
|
|
814
|
+
}
|
|
815
|
+
async function prepareRpxTlsForDev(input) {
|
|
816
|
+
const { domain, includeDashboard, options } = input, verbose = options.verbose ?? !1, hosts = [
|
|
817
|
+
domain,
|
|
818
|
+
...includeDashboard ? [`dashboard.${domain}`] : []
|
|
819
|
+
], hostsNeedingFile = hosts.filter((host) => {
|
|
820
|
+
const h = host.trim().toLowerCase();
|
|
821
|
+
return h !== "localhost" && !h.endsWith(".localhost") && !h.endsWith(".localhost.");
|
|
822
|
+
}), { addHosts, setupDevelopmentDns } = await importDevelopmentRpx(), dnsDomains = hostsNeedingFile.length > 0 ? hosts : [];
|
|
823
|
+
if (dnsDomains.length > 0) {
|
|
824
|
+
if (!await setupDevelopmentDns({ domains: dnsDomains, verbose }).catch(() => !1) && verbose)
|
|
825
|
+
log.warn(`Dev DNS not configured for ${dnsDomains.join(", ")} \u2014 falling back to /etc/hosts`);
|
|
826
|
+
}
|
|
827
|
+
if (hostsNeedingFile.length > 0)
|
|
828
|
+
await addHosts(hostsNeedingFile, verbose).catch((err) => {
|
|
829
|
+
log.warn(`Could not update /etc/hosts for ${hostsNeedingFile.join(", ")}: ${err.message}`);
|
|
830
|
+
log.warn("Add 127.0.0.1 entries manually or set SUDO_PASSWORD in .env");
|
|
831
|
+
});
|
|
832
|
+
await ensureRpxDevelopmentHttps(domain, options, includeDashboard);
|
|
833
|
+
}
|
|
834
|
+
async function startRpxDaemonIfNeeded(input) {
|
|
835
|
+
const { ensureDaemonRunning, isDaemonRunning: isRpxUp } = await importDevelopmentRpx();
|
|
836
|
+
if (await isRpxUp())
|
|
837
|
+
return;
|
|
838
|
+
const spawnOpts = {
|
|
839
|
+
spawnCommand: input.spawnCommand,
|
|
840
|
+
spawnCwd: resolveRpxDaemonSpawnCwd(),
|
|
841
|
+
startupTimeoutMs: 45000,
|
|
842
|
+
spawnEnv: input.spawnEnv,
|
|
843
|
+
verbose: input.verbose
|
|
844
|
+
};
|
|
845
|
+
try {
|
|
846
|
+
await ensureDaemonRunning(spawnOpts);
|
|
847
|
+
} catch (firstError) {
|
|
848
|
+
await input.stopRpx({ timeoutMs: 5000 }).catch(() => {});
|
|
849
|
+
await ensureDaemonRunning(spawnOpts).catch(() => {
|
|
850
|
+
throw firstError;
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
async function registerRpxProxiesForDomain(input) {
|
|
855
|
+
const { domain, frontendPort, apiPort, docsPort, dashboardPort, includeDashboard, options } = input, verbose = options.verbose ?? !1, proxies = buildRpxProxySpecs({ domain, frontendPort, apiPort, docsPort, dashboardPort, includeDashboard });
|
|
856
|
+
await unregisterRpxProxies([`${domain}-api`, `${domain}-docs`]);
|
|
857
|
+
const {
|
|
858
|
+
getRegistryDir,
|
|
859
|
+
readAll,
|
|
860
|
+
stopDaemon: stopRpx,
|
|
861
|
+
syncDevelopmentDnsFromRegistry,
|
|
862
|
+
writeEntry
|
|
863
|
+
} = await importDevelopmentRpx(), spawnCommand = await resolveRpxDaemonSpawnCommand(), spawnEnv = {};
|
|
864
|
+
if (process.env.SUDO_PASSWORD)
|
|
865
|
+
spawnEnv.SUDO_PASSWORD = process.env.SUDO_PASSWORD;
|
|
866
|
+
if (verbose)
|
|
867
|
+
spawnEnv.RPX_VERBOSE = "1";
|
|
868
|
+
const rpxEntry = resolveRpxEntryPath();
|
|
869
|
+
if (rpxEntry)
|
|
870
|
+
spawnEnv.RPX_MODULE = rpxEntry;
|
|
871
|
+
try {
|
|
872
|
+
await startRpxDaemonIfNeeded({ spawnCommand, spawnEnv, verbose, stopRpx });
|
|
873
|
+
} catch (error) {
|
|
874
|
+
const { isDaemonRunning } = await importDevelopmentRpx();
|
|
875
|
+
if (!await isDaemonRunning())
|
|
876
|
+
throw error;
|
|
877
|
+
if (verbose) {
|
|
878
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
879
|
+
console.log(` ${dim(" ")}${dim(`rpx daemon spawn skipped (${message}); reusing existing daemon`)}`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
const createdAt = new Date().toISOString();
|
|
883
|
+
for (const proxy of proxies) {
|
|
884
|
+
await writeEntry({
|
|
885
|
+
id: proxy.id,
|
|
886
|
+
from: proxy.from,
|
|
887
|
+
to: proxy.to,
|
|
888
|
+
cwd: process.cwd(),
|
|
889
|
+
createdAt,
|
|
890
|
+
pathRewrites: proxy.pathRewrites
|
|
891
|
+
}, void 0, verbose);
|
|
892
|
+
if (!activeRpxRegistryIds.includes(proxy.id))
|
|
893
|
+
activeRpxRegistryIds.push(proxy.id);
|
|
894
|
+
if (verbose)
|
|
895
|
+
console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`https://${proxy.to} \u2192 ${proxy.from}`)}`);
|
|
896
|
+
}
|
|
897
|
+
const registryDir = getRegistryDir(), entries = await readAll(registryDir, verbose).catch(() => []);
|
|
898
|
+
await syncDevelopmentDnsFromRegistry(entries, { verbose, ownerPid: process.pid }).catch((err) => {
|
|
899
|
+
if (verbose) {
|
|
900
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
901
|
+
console.log(` ${dim(" ")}${dim(`Dev DNS sync: ${message}`)}`);
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
function wantsInteractive(options) {
|
|
906
|
+
return options.interactive;
|
|
907
|
+
}
|