@stacksjs/buddy 0.70.155 → 0.70.157

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.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import process from "node:process";
3
3
  import { cli, log } from "@stacksjs/cli";
4
4
  import { path as p } from "@stacksjs/path";
5
+ import { registerGlobalOptions } from "./global-options";
5
6
  try {
6
7
  const { isSupportedBunVersion, minimumBunVersion } = await import("@stacksjs/utils"), currentBunVersion = typeof Bun < "u" ? Bun.version : process.versions.bun;
7
8
  if (currentBunVersion && !isSupportedBunVersion(currentBunVersion)) {
@@ -9,7 +10,7 @@ try {
9
10
  process.exit(1);
10
11
  }
11
12
  } catch {}
12
- const args = process.argv.slice(2), requestedCommand = args[0] || "help", isHelpFlag = args.includes("--help") || args.includes("-h"), isVersionOnly = ["--version", "-v", "version"].includes(requestedCommand), isHelpMode = requestedCommand === "help" || isHelpFlag && args.length <= 2, skipAppKeyCheck = [
13
+ const args = process.argv.slice(2), requestedCommand = args[0] || "help", isHelpFlag = args.includes("--help") || args.includes("-h"), isVersionOnly = ["--version", "-V", "version"].includes(requestedCommand), isHelpMode = requestedCommand === "help" || isHelpFlag && args.length <= 2, skipAppKeyCheck = [
13
14
  "build",
14
15
  "lint",
15
16
  "lint:fix",
@@ -53,7 +54,9 @@ if (needsFullSetup) {
53
54
  process.on("unhandledRejection", (error) => reportFatal("unhandledRejection", error));
54
55
  }
55
56
  async function main() {
56
- const buddy = cli("buddy"), configPath = "./buddy.config.js";
57
+ const buddy = cli("buddy");
58
+ registerGlobalOptions(buddy);
59
+ const configPath = "./buddy.config.js";
57
60
  try {
58
61
  await Bun.file(configPath).text();
59
62
  const { applyBuddyConfig } = await import("./config.js");
@@ -27,7 +27,7 @@ export function create(buddy) {
27
27
  minimal: "Skip optional feature bundles (cms, commerce, dashboard, marketing, monitoring, realtime, queue) \u2014 bare-bones API/SPA starter that can re-add them later via `./buddy <feature>:install`.",
28
28
  verbose: "Enable verbose output"
29
29
  };
30
- buddy.command("new [name]", descriptions.command).alias("create [name]").option("-n, --name [name]", descriptions.name, { default: !1 }).option("-u, --ui", descriptions.ui, { default: !0 }).option("-c, --components", descriptions.components, { default: !0 }).option("-w, --web-components", descriptions.webComponents, { default: !0 }).option("-v, --vue", descriptions.vue, { default: !0 }).option("-p, --views", descriptions.views, { default: !0 }).option("-f, --functions", descriptions.functions, { default: !0 }).option("-a, --api", descriptions.api, { default: !0 }).option("-d, --database", descriptions.database, { default: !0 }).option("-ca, --cache", descriptions.cache, { default: !1 }).option("-e, --email", descriptions.email, { default: !1 }).option("-P, --project [project]", descriptions.project, { default: !1 }).option("-m, --minimal", descriptions.minimal, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (name, options) => {
30
+ buddy.command("new [name]", descriptions.command).alias("create [name]").option("-n, --name [name]", descriptions.name, { default: !1 }).option("-u, --ui", descriptions.ui, { default: !0 }).option("-c, --components", descriptions.components, { default: !0 }).option("-w, --web-components", descriptions.webComponents, { default: !0 }).option("--vue", descriptions.vue, { default: !0 }).option("-p, --views", descriptions.views, { default: !0 }).option("-f, --functions", descriptions.functions, { default: !0 }).option("-a, --api", descriptions.api, { default: !0 }).option("-d, --database", descriptions.database, { default: !0 }).option("-ca, --cache", descriptions.cache, { default: !1 }).option("-e, --email", descriptions.email, { default: !1 }).option("-P, --project [project]", descriptions.project, { default: !1 }).option("-m, --minimal", descriptions.minimal, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (name, options) => {
31
31
  log.debug("Running `buddy new <name>` ...", options);
32
32
  const startTime = await intro("buddy new");
33
33
  name = name ?? options.name;
@@ -1,7 +1,48 @@
1
1
  import type { CLI, DevOptions } from '@stacksjs/types';
2
+ export declare function dispatchInteractiveDevSelection(selection: string, runners: InteractiveDevRunners): Promise<boolean>;
3
+ export declare function resolvePrettyDevDomain(appUrl: string | undefined, nativeMode?: boolean): string | null;
4
+ export declare function shouldUsePrettyDevUrls(input: {
5
+ domain: string | null
6
+ localhostOnly: boolean
7
+ proxyManagedExternally: boolean
8
+ systemAuthorized: boolean
9
+ }): boolean;
2
10
  export declare function dev(buddy: CLI): void;
3
11
  export declare function startDevelopmentServer(_options: DevOptions, _startTime?: number): Promise<void>;
12
+ export declare function setupPrettyDevEnvironment(input: {
13
+ domain?: string
14
+ skipHosts?: boolean
15
+ skipTrust?: boolean
16
+ verbose?: boolean
17
+ }): Promise<boolean>;
18
+ export declare const interactiveDevChoices: readonly [{
19
+ value: 'all';
20
+ title: 'All'
21
+ }, {
22
+ value: 'frontend';
23
+ title: 'Frontend'
24
+ }, {
25
+ value: 'api';
26
+ title: 'Backend'
27
+ }, {
28
+ value: 'dashboard';
29
+ title: 'Dashboard'
30
+ }, {
31
+ value: 'desktop';
32
+ title: 'Desktop'
33
+ }, {
34
+ value: 'native';
35
+ title: 'Native App'
36
+ }, {
37
+ value: 'components';
38
+ title: 'Components'
39
+ }, {
40
+ value: 'docs';
41
+ title: 'Documentation'
42
+ }];
4
43
  declare type DevelopmentRpx = typeof import('@stacksjs/rpx');
44
+ declare type InteractiveDevSelection = typeof interactiveDevChoices[number]['value'];
45
+ declare type InteractiveDevRunners = Record<InteractiveDevSelection, () => Promise<unknown>>;
5
46
  type RpxProxySpec = {
6
47
  id: string
7
48
  from: string
@@ -41,6 +41,47 @@ async function actions() {
41
41
  _actions = await import("@stacksjs/actions");
42
42
  return _actions;
43
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
+ async function canStartPrettyDevProxy() {
76
+ if (await waitForHttpsProxy(443, 150))
77
+ return !0;
78
+ try {
79
+ const { authorizeSystemAccess } = await importDevelopmentRpx();
80
+ return authorizeSystemAccess({ interactive: !1 });
81
+ } catch {
82
+ return !1;
83
+ }
84
+ }
44
85
  export function dev(buddy) {
45
86
  const descriptions = {
46
87
  dev: "Start development server",
@@ -50,7 +91,6 @@ export function dev(buddy) {
50
91
  native: "Start the app in a native Craft window",
51
92
  dashboard: "Start the Dashboard development server",
52
93
  api: "Start the local API development server",
53
- email: "Start the Email development server",
54
94
  docs: "Start the Documentation development server",
55
95
  systemTray: "Start the System Tray development server",
56
96
  interactive: "Get asked which development server to start",
@@ -59,7 +99,7 @@ export function dev(buddy) {
59
99
  project: "Target a specific project",
60
100
  verbose: "Enable verbose output"
61
101
  };
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) => {
102
+ buddy.command("dev [server]", descriptions.dev).option("-f, --frontend", descriptions.frontend).option("-a, --api", descriptions.api).option("-c, --components", descriptions.components).option("-d, --dashboard", descriptions.dashboard).option("-k, --desktop", descriptions.desktop).option("-n, --native", descriptions.native).option("-o, --docs", descriptions.docs).option("-s, --system-tray", descriptions.systemTray).option("-i, --interactive", descriptions.interactive, { default: !1 }).option("-l, --with-localhost", descriptions.withLocalhost, { default: !1 }).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (server, options) => {
63
103
  const perf = Bun.nanoseconds(), target = server || (options.frontend ? "frontend" : void 0) || (options.api ? "api" : void 0) || (options.components ? "components" : void 0) || (options.dashboard ? "dashboard" : void 0) || (options.desktop ? "desktop" : void 0) || (options.native ? "native" : void 0) || (options.systemTray || options["system-tray"] ? "system-tray" : void 0) || (options.docs ? "docs" : void 0);
64
104
  if (target) {
65
105
  const serverOptions = { ...options }, a = await actions();
@@ -97,29 +137,18 @@ export function dev(buddy) {
97
137
  type: "select",
98
138
  name: "value",
99
139
  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 {
140
+ choices: interactiveDevChoices
141
+ })).value, a = await actions();
142
+ if (!await dispatchInteractiveDevSelection(selectedValue, {
143
+ all: () => startDevelopmentServer(options, perf),
144
+ frontend: () => a.runFrontendDevServer(options),
145
+ api: () => a.runApiDevServer(options),
146
+ dashboard: () => a.runDashboardDevServer(options),
147
+ desktop: () => a.runDesktopDevServer(options),
148
+ native: () => startDevelopmentServer({ ...options, native: !0 }, perf),
149
+ components: () => a.runComponentsDevServer(options),
150
+ docs: () => a.runDocsDevServer(options)
151
+ })) {
123
152
  log.error("Invalid option during interactive mode");
124
153
  process.exit(ExitCode.InvalidArgument);
125
154
  }
@@ -186,7 +215,12 @@ export function dev(buddy) {
186
215
  onUnknownSubcommand(buddy, "dev");
187
216
  }
188
217
  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, localhostOnly = process.env.STACKS_DEV_LOCALHOST === "1", hasCustomDomain = appLooksCustom && !proxyManagedExternally && !localhostOnly, 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 = [
218
+ const options = _options, startedAt = _startTime, appUrl = process.env.APP_URL ?? "stacks.localhost", 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", 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({
219
+ domain,
220
+ localhostOnly,
221
+ proxyManagedExternally,
222
+ systemAuthorized
223
+ }), prettySetupRequired = prettyUrlsRequested && !usePrettyUrls, hasCustomDomain = usePrettyUrls && !proxyManagedExternally, displayedDomain = usePrettyUrls ? domain : null, dashboardDomain = displayedDomain ? `dashboard.${displayedDomain}` : null, frontendUrl = displayedDomain ? `https://${displayedDomain}` : `http://localhost:${frontendPort}`, apiUrl = displayedDomain ? `https://${displayedDomain}/api` : `http://localhost:${apiPort}`, docsUrl = displayedDomain ? `https://${displayedDomain}/docs` : `http://localhost:${docsPort}`, dashboardUrl = dashboardDomain ? `https://${dashboardDomain}` : `http://localhost:${dashboardPort}`, managedPorts = [
190
224
  frontendPort,
191
225
  apiPort,
192
226
  docsPort,
@@ -195,6 +229,11 @@ export async function startDevelopmentServer(_options, _startTime) {
195
229
  process.env.STACKS_PROXY_MANAGED = "1";
196
230
  process.env.STACKS_DEV_QUIET = "1";
197
231
  console.log();
232
+ if (prettySetupRequired) {
233
+ console.log(` ${yellow("\u26A0")} ${yellow("Pretty URL setup required")} ${dim("- using localhost for this session")}`);
234
+ console.log(` ${dim(" ")}${dim("Run `./buddy setup:ssl` once, then restart `./buddy dev`.")}`);
235
+ console.log();
236
+ }
198
237
  console.log(` ${bold(cyan("stacks"))} ${dim(`v${version}`)} ${dim("starting\u2026")}`);
199
238
  console.log();
200
239
  if (process.env.STACKS_DEV_NO_KILL !== "1")
@@ -607,104 +646,7 @@ async function waitForHttpsProxy(port, timeoutMs) {
607
646
  }
608
647
  return !1;
609
648
  }
610
- 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");
611
- function normalizeSha256Fingerprint(raw) {
612
- return (raw.includes("=") ? raw.split("=").pop() : raw).replace(/SHA-256\s+hash:\s*/gi, "").replace(/:/g, "").trim().toUpperCase();
613
- }
614
- function readCertFingerprint(certPath) {
615
- try {
616
- const out = execSync(`openssl x509 -noout -fingerprint -sha256 -in "${certPath}"`, { encoding: "utf8" });
617
- return normalizeSha256Fingerprint(out);
618
- } catch {
619
- return null;
620
- }
621
- }
622
- function isRpxRootCaInKeychain(caPath) {
623
- const fp = readCertFingerprint(caPath);
624
- if (!fp)
625
- return !1;
626
- const keychains = [
627
- "/Library/Keychains/System.keychain",
628
- LOGIN_KEYCHAIN
629
- ];
630
- for (const keychain of keychains)
631
- try {
632
- const listing = execSync(`security find-certificate -a -Z "${keychain}" 2>/dev/null || true`, { encoding: "utf8" });
633
- for (const line of listing.split(`
634
- `))
635
- if (line.toUpperCase().includes("SHA-256") && normalizeSha256Fingerprint(line) === fp)
636
- return !0;
637
- } catch {}
638
- return !1;
639
- }
640
- function execSudoSh(command) {
641
- const sudoPassword = process.env.SUDO_PASSWORD, escaped = command.replace(/'/g, "'\\''");
642
- if (sudoPassword)
643
- execSync(`echo '${sudoPassword}' | sudo -S sh -c '${escaped}' 2>/dev/null`, { stdio: ["pipe", "pipe", "pipe"] });
644
- else
645
- execSync(`sudo -n sh -c '${escaped}'`, { stdio: ["pipe", "pipe", "pipe"] });
646
- }
647
- const MACOS_CA_TRUST_FLAGS = "-d -r trustRoot -p ssl -p basic";
648
- function listRpxRootCaHashesInKeychain(keychain) {
649
- const listing = execSync(`security find-certificate -a -c "rpx.localhost" -Z "${keychain}" 2>/dev/null || true`, { encoding: "utf8" }), hashes = [];
650
- for (const line of listing.split(`
651
- `)) {
652
- const match = line.match(/SHA-256 hash:\s*([A-F0-9]+)/i);
653
- if (match)
654
- hashes.push(match[1].toUpperCase());
655
- }
656
- return hashes;
657
- }
658
- function pruneStaleRpxRootCas(caPath) {
659
- if (process.platform !== "darwin")
660
- return;
661
- const keep = readCertFingerprint(caPath);
662
- if (!keep)
663
- return;
664
- for (const keychain of ["/Library/Keychains/System.keychain", LOGIN_KEYCHAIN])
665
- for (const hash of listRpxRootCaHashesInKeychain(keychain)) {
666
- if (hash === keep)
667
- continue;
668
- try {
669
- if (keychain.startsWith("/Library"))
670
- execSudoSh(`security delete-certificate -Z ${hash} "${keychain}"`);
671
- else
672
- execSync(`security delete-certificate -Z ${hash} "${keychain}"`, { stdio: "ignore" });
673
- } catch {}
674
- }
675
- }
676
- function isRpxRootCaTrustedForSsl(caPath, serverName) {
677
- if (process.platform !== "darwin")
678
- return isRpxRootCaInKeychain(caPath);
679
- try {
680
- return execSync(`security verify-cert -c "${caPath}" -s "${serverName}" -l -L -R ssl 2>&1`, { encoding: "utf8" }).includes("successful");
681
- } catch {
682
- return !1;
683
- }
684
- }
685
- function trustRpxRootCaForBrowsers(caPath, serverName) {
686
- if (process.platform !== "darwin")
687
- return !1;
688
- pruneStaleRpxRootCas(caPath);
689
- try {
690
- execSync(`security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k "${LOGIN_KEYCHAIN}" "${caPath}"`, { stdio: "ignore" });
691
- } catch {}
692
- try {
693
- execSudoSh(`security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k /Library/Keychains/System.keychain "${caPath}"`);
694
- } catch {
695
- return !1;
696
- }
697
- return isRpxRootCaTrustedForSsl(caPath, serverName) || isRpxRootCaInKeychain(caPath);
698
- }
699
- function isLiveHttpsChainValid(domain) {
700
- if (!existsSync(RPX_ROOT_CA_PATH))
701
- return !1;
702
- try {
703
- 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)");
704
- } catch {
705
- return !1;
706
- }
707
- }
649
+ 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");
708
650
  function buildDevelopmentTlsHostnames(domain, includeDashboard) {
709
651
  return [
710
652
  domain,
@@ -712,66 +654,45 @@ function buildDevelopmentTlsHostnames(domain, includeDashboard) {
712
654
  "rpx.localhost"
713
655
  ];
714
656
  }
715
- function readCertCommonName(certPath) {
716
- try {
717
- return execSync(`openssl x509 -in "${certPath}" -noout -subject -nameopt RFC2253`, { encoding: "utf8" }).match(/CN=([^,/]+)/)?.[1]?.trim() ?? null;
718
- } catch {
719
- return null;
720
- }
721
- }
722
- async function buildDevelopmentTlsOptions(domain, includeDashboard, verbose) {
723
- const hostnames = buildDevelopmentTlsHostnames(domain, includeDashboard);
724
- try {
725
- const { getRegistryDir, readAll } = await importDevelopmentRpx(), entries = await readAll(getRegistryDir(), !1);
726
- for (const entry of entries) {
727
- const host = entry.to?.trim();
728
- if (host && !hostnames.includes(host))
729
- hostnames.push(host);
730
- }
731
- } catch {}
732
- return {
733
- https: {
734
- certPath: RPX_HOST_CERT_PATH,
735
- keyPath: join(RPX_SSL_DIR, "rpx.localhost.key"),
736
- caCertPath: join(RPX_SSL_DIR, "rpx.localhost.ca.crt"),
737
- commonName: domain
738
- },
739
- verbose,
740
- regenerateUntrustedCerts: !0,
741
- proxies: hostnames.map((to) => ({ from: "localhost:1", to }))
742
- };
743
- }
744
- async function ensureRpxDevelopmentHttps(domain, options, includeDashboard) {
657
+ async function ensureRpxDevelopmentHttps(domain, options, includeDashboard, trustCertificate = !0) {
745
658
  const verbose = options.verbose ?? !1, {
659
+ buildRegistryTlsProxyOptions,
660
+ certIncludesSanHostnames,
746
661
  checkExistingCertificates,
747
662
  clearSslConfigCache,
748
663
  forceTrustCertificate,
749
664
  generateCertificate,
665
+ getRegistryDir,
750
666
  isDaemonRunning,
751
- stopDaemon
752
- } = await importDevelopmentRpx(), hostnames = buildDevelopmentTlsHostnames(domain, includeDashboard), tlsOptions = await buildDevelopmentTlsOptions(domain, includeDashboard, verbose), hostnameInCert = hostnames.every((host) => {
753
- try {
754
- return execSync(`openssl x509 -in "${RPX_HOST_CERT_PATH}" -noout -text`, { encoding: "utf8" }).includes(`DNS:${host}`);
755
- } catch {
756
- return !1;
757
- }
758
- }), cnMatchesApp = readCertCommonName(RPX_HOST_CERT_PATH) === domain;
667
+ isRootCaTrustedForSsl,
668
+ readAll,
669
+ readCertCommonName,
670
+ stopDaemon,
671
+ trustRootCaForBrowsers,
672
+ verifyHttpsChain
673
+ } = await importDevelopmentRpx(), hostnames = buildDevelopmentTlsHostnames(domain, includeDashboard), entries = await readAll(getRegistryDir(), !1).catch(() => []);
674
+ for (const entry of entries) {
675
+ const host = entry.to?.trim();
676
+ if (host && !hostnames.includes(host))
677
+ hostnames.push(host);
678
+ }
679
+ const tlsOptions = buildRegistryTlsProxyOptions(hostnames, domain, verbose), hostnameInCert = certIncludesSanHostnames(RPX_HOST_CERT_PATH, hostnames), cnMatchesApp = readCertCommonName(RPX_HOST_CERT_PATH) === domain;
759
680
  let certRegenerated = !1;
760
681
  if (!await checkExistingCertificates(tlsOptions) || !hostnameInCert || !cnMatchesApp) {
761
682
  clearSslConfigCache();
762
683
  await generateCertificate({ ...tlsOptions, forceRegenerate: !0 });
763
684
  certRegenerated = !0;
764
685
  }
765
- let trusted = isRpxRootCaTrustedForSsl(RPX_ROOT_CA_PATH, domain);
766
- if (!trusted) {
767
- trusted = trustRpxRootCaForBrowsers(RPX_ROOT_CA_PATH, domain) || await forceTrustCertificate(RPX_ROOT_CA_PATH, { serverName: domain, verbose }) || isRpxRootCaTrustedForSsl(RPX_ROOT_CA_PATH, domain);
686
+ let trusted = isRootCaTrustedForSsl(RPX_ROOT_CA_PATH, domain, { verbose });
687
+ if (!trusted && trustCertificate) {
688
+ 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 });
768
689
  if (trusted)
769
690
  console.log(` ${green("\u2713")} ${dim("HTTPS")}: ${dim("Local CA trusted \u2014 reload the browser if you still see a warning")}`);
770
691
  else
771
692
  console.log(` ${yellow("\u26A0")} ${yellow("HTTPS")}: ${yellow(`Local CA not trusted \u2014 run: sh ${join(RPX_SSL_DIR, "trust-rpx-cert.sh")}`)}`);
772
693
  } else if (verbose)
773
694
  console.log(` ${green("\u2713")} ${dim("HTTPS")}: ${dim("Local certificate trusted for SSL")}`);
774
- const chainOk = isLiveHttpsChainValid(domain);
695
+ const chainOk = existsSync(RPX_ROOT_CA_PATH) ? await verifyHttpsChain(domain, RPX_ROOT_CA_PATH) : !1;
775
696
  if ((certRegenerated || !hostnameInCert || !cnMatchesApp) && await isDaemonRunning()) {
776
697
  await stopDaemon({ timeoutMs: 8000 }).catch(() => {});
777
698
  if (verbose)
@@ -851,23 +772,62 @@ async function removeStalePublicDomainOverrides(domain, includeDashboard, verbos
851
772
  }
852
773
  }
853
774
  async function prepareRpxTlsForDev(input) {
854
- const { domain, includeDashboard, options } = input, verbose = options.verbose ?? !1, hosts = [
775
+ const { domain, includeDashboard, options, skipHosts = !1, trustCertificate = !0 } = input, verbose = options.verbose ?? !1, hosts = [
855
776
  domain,
856
777
  ...includeDashboard ? [`dashboard.${domain}`] : []
857
778
  ], hostsNeedingFile = hosts.filter((host) => {
858
779
  const h = host.trim().toLowerCase();
859
780
  return h !== "localhost" && !h.endsWith(".localhost") && !h.endsWith(".localhost.");
860
- }), { addHosts, setupDevelopmentDns } = await importDevelopmentRpx(), dnsDomains = hostsNeedingFile.length > 0 ? hosts : [];
781
+ }), { addHosts, setupDevelopmentDns } = await importDevelopmentRpx(), dnsDomains = !skipHosts && hostsNeedingFile.length > 0 ? hosts : [];
861
782
  if (dnsDomains.length > 0) {
862
783
  if (!await setupDevelopmentDns({ domains: dnsDomains, verbose }).catch(() => !1) && verbose)
863
784
  log.warn(`Dev DNS not configured for ${dnsDomains.join(", ")} \u2014 falling back to /etc/hosts`);
864
785
  }
865
- if (hostsNeedingFile.length > 0)
786
+ if (!skipHosts && hostsNeedingFile.length > 0)
866
787
  await addHosts(hostsNeedingFile, verbose).catch((err) => {
867
788
  log.warn(`Could not update /etc/hosts for ${hostsNeedingFile.join(", ")}: ${err.message}`);
868
789
  log.warn("Add 127.0.0.1 entries manually or set SUDO_PASSWORD in .env");
869
790
  });
870
- await ensureRpxDevelopmentHttps(domain, options, includeDashboard);
791
+ await ensureRpxDevelopmentHttps(domain, options, includeDashboard, trustCertificate);
792
+ }
793
+ export async function setupPrettyDevEnvironment(input) {
794
+ const domain = resolvePrettyDevDomain(input.domain ?? process.env.APP_URL ?? "stacks.localhost");
795
+ if (!domain) {
796
+ log.info("APP_URL already uses localhost; no pretty URL setup is needed");
797
+ return !0;
798
+ }
799
+ const rpx = await importDevelopmentRpx();
800
+ if (!rpx.authorizeSystemAccess({ interactive: process.stdin.isTTY === !0 && process.stdout.isTTY === !0 })) {
801
+ log.error("Pretty URL setup needs administrator authorization to bind ports 80 and 443 and trust the local CA");
802
+ return !1;
803
+ }
804
+ await prepareRpxTlsForDev({
805
+ domain,
806
+ includeDashboard: process.env.STACKS_DEV_DASHBOARD === "1",
807
+ options: { verbose: input.verbose },
808
+ skipHosts: input.skipHosts,
809
+ trustCertificate: !input.skipTrust
810
+ });
811
+ const spawnEnv = {};
812
+ if (process.env.SUDO_PASSWORD)
813
+ spawnEnv.SUDO_PASSWORD = process.env.SUDO_PASSWORD;
814
+ if (input.verbose)
815
+ spawnEnv.RPX_VERBOSE = "1";
816
+ const rpxEntry = resolveRpxEntryPath();
817
+ if (rpxEntry)
818
+ spawnEnv.RPX_MODULE = rpxEntry;
819
+ await startRpxDaemonIfNeeded({
820
+ spawnCommand: await resolveRpxDaemonSpawnCommand(),
821
+ spawnEnv,
822
+ verbose: input.verbose ?? !1,
823
+ stopRpx: rpx.stopDaemon
824
+ });
825
+ const ready = await waitForHttpsProxy(443, 5000);
826
+ if (ready)
827
+ log.success(`Pretty URLs are ready at https://${domain}`);
828
+ else
829
+ log.error("rpx did not become reachable on port 443");
830
+ return ready;
871
831
  }
872
832
  async function startRpxDaemonIfNeeded(input) {
873
833
  const { ensureDaemonRunning, isDaemonRunning: isRpxUp } = await importDevelopmentRpx();
@@ -1,13 +1,14 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import process from "node:process";
4
- import { runAction, setupSSL } from "@stacksjs/actions";
4
+ import { runAction } from "@stacksjs/actions";
5
5
  import { log, onUnknownSubcommand, runCommand } from "@stacksjs/cli";
6
6
  import { Action } from "@stacksjs/enums";
7
7
  import { handleError } from "@stacksjs/error-handling";
8
8
  import { path as p } from "@stacksjs/path";
9
9
  import { copyFile, storage } from "@stacksjs/storage";
10
10
  import { ExitCode } from "@stacksjs/types";
11
+ import { setupPrettyDevEnvironment } from "./dev";
11
12
  function getTimeoutMs(envVar, fallbackMs) {
12
13
  const value = Number(process.env[envVar]);
13
14
  if (Number.isFinite(value) && value > 0)
@@ -37,7 +38,7 @@ export function setup(buddy) {
37
38
  });
38
39
  buddy.command("setup:ssl", descriptions.ssl).alias("ssl:setup").option("-d, --domain [domain]", descriptions.domain).option("--skip-hosts", descriptions.skipHosts, { default: !1 }).option("--skip-trust", descriptions.skipTrust, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
39
40
  log.debug("Running `buddy setup:ssl` ...", options);
40
- if (!await setupSSL({
41
+ if (!await setupPrettyDevEnvironment({
41
42
  domain: options.domain,
42
43
  skipHosts: options.skipHosts,
43
44
  skipTrust: options.skipTrust,
@@ -21,7 +21,7 @@ export function upgrade(buddy) {
21
21
  project: "Target a specific project",
22
22
  all: "Upgrade framework, dependencies, Bun, and binary"
23
23
  };
24
- buddy.command("upgrade", descriptions.upgrade).option("-v, --version <version>", descriptions.version).option("--canary", descriptions.canary, { default: !1 }).option("--stable", descriptions.stable, { default: !1 }).option("--dry-run", descriptions.dryRun, { default: !1 }).option("-f, --force", descriptions.force, { default: !1 }).option("--from <path>", descriptions.from).option("--no-postinstall", descriptions.noPostinstall).option("--verbose", descriptions.verbose, { default: !1 }).alias("update").example("buddy upgrade").example("buddy update").example("buddy upgrade --from ~/Code/stacks").example("buddy upgrade --version 0.70.23").example("buddy upgrade --dry-run").example("buddy upgrade --canary").example("buddy upgrade --stable").example("buddy upgrade --force").action(async (options) => {
24
+ buddy.command("upgrade", descriptions.upgrade).option("-V, --version <version>", descriptions.version).option("--canary", descriptions.canary, { default: !1 }).option("--stable", descriptions.stable, { default: !1 }).option("--dry-run", descriptions.dryRun, { default: !1 }).option("-f, --force", descriptions.force, { default: !1 }).option("--from <path>", descriptions.from).option("--no-postinstall", descriptions.noPostinstall).option("--verbose", descriptions.verbose, { default: !1 }).alias("update").example("buddy upgrade").example("buddy update").example("buddy upgrade --from ~/Code/stacks").example("buddy upgrade --version 0.70.23").example("buddy upgrade --dry-run").example("buddy upgrade --canary").example("buddy upgrade --stable").example("buddy upgrade --force").action(async (options) => {
25
25
  log.debug("Running `buddy upgrade` ...", options);
26
26
  const opts = { ...options };
27
27
  if (opts.postinstall === !1) {
@@ -1,6 +1,5 @@
1
- import process from "node:process";
2
1
  import { bold, dim, green, intro, log, onUnknownSubcommand } from "@stacksjs/cli";
3
- import { storage } from "@stacksjs/storage";
2
+ import { versionLine } from "../version-info";
4
3
  export function version(buddy) {
5
4
  const descriptions = {
6
5
  version: "Retrieving Stacks build version"
@@ -8,9 +7,8 @@ export function version(buddy) {
8
7
  buddy.command("version", descriptions.version).action(async () => {
9
8
  log.debug("Running `buddy version` ...");
10
9
  await intro("buddy version");
11
- const pkg = await storage.readPackageJson("./package.json"), bunVersion = "wip", stacksVersion = pkg.version;
12
- log.info(green(bold("@stacksjs/ ")) + dim(` ${stacksVersion}`));
13
- log.info(green(bold("Bun: ")) + dim(` ${bunVersion}`));
10
+ log.info(green(bold("Stacks: ")) + dim(` ${versionLine}`));
11
+ log.info(green(bold("Bun: ")) + dim(` ${Bun.version}`));
14
12
  });
15
13
  onUnknownSubcommand(buddy, "version");
16
14
  }
@@ -0,0 +1,3 @@
1
+ import type { CLI } from '@stacksjs/cli';
2
+ /** Register Buddy's process-wide controls before any command modules load. */
3
+ export declare function registerGlobalOptions(buddy: CLI): void;
@@ -0,0 +1,4 @@
1
+ import { versionDescriptor } from "./version-info";
2
+ export function registerGlobalOptions(buddy) {
3
+ buddy.version(versionDescriptor, "-V, --version").option("-v, --verbose", "Enable verbose output").option("-q, --quiet", "Suppress non-essential output").option("--debug", "Enable debug output and stack traces").option("--no-interaction", "Do not ask interactive questions").option("--env <environment>", "Target an environment").option("--dry-run", "Preview actions without making changes").option("--force", "Skip confirmation prompts").option("--no-emoji", "Disable emoji in output").option("--no-cache", "Disable command metadata caching");
4
+ }
@@ -0,0 +1,9 @@
1
+ import { version as buddyVersion } from '../package.json';
2
+ // Every published Stacks framework package shares the release version. Buddy's
3
+ // own package metadata is therefore the portable source of truth for both
4
+ // labels. Reading the monorepo parent's package.json works from src/ but escapes
5
+ // the package after compilation to dist/ and breaks clean npm installs.
6
+ declare const stacksVersion: unknown;
7
+ export declare const versionDescriptor: string;
8
+ export declare const versionLine: string;
9
+ export { buddyVersion, stacksVersion };
@@ -0,0 +1,5 @@
1
+ import { version as buddyVersion } from "../package.json";
2
+ const stacksVersion = buddyVersion;
3
+
4
+ export { buddyVersion, stacksVersion };
5
+ export const versionDescriptor = `${buddyVersion} stacks/${stacksVersion}`, versionLine = `buddy/${versionDescriptor}`;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/buddy",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.155",
5
+ "version": "0.70.157",
6
6
  "description": "Meet Buddy. The Stacks runtime.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -92,51 +92,51 @@
92
92
  "prepublishOnly": "bun run build"
93
93
  },
94
94
  "dependencies": {
95
- "@stacksjs/actions": "^0.70.155",
96
- "@stacksjs/ai": "^0.70.155",
97
- "@stacksjs/alias": "^0.70.155",
98
- "@stacksjs/arrays": "^0.70.155",
99
- "@stacksjs/auth": "^0.70.155",
100
- "@stacksjs/build": "^0.70.155",
101
- "@stacksjs/cache": "^0.70.155",
102
- "@stacksjs/cli": "^0.70.155",
103
- "@stacksjs/clapp": "^0.2.10",
104
- "@stacksjs/cloud": "^0.70.155",
105
- "@stacksjs/collections": "^0.70.155",
106
- "@stacksjs/config": "^0.70.155",
107
- "@stacksjs/database": "^0.70.155",
108
- "@stacksjs/desktop": "^0.70.155",
109
- "@stacksjs/dns": "^0.70.155",
110
- "@stacksjs/email": "^0.70.155",
111
- "@stacksjs/enums": "^0.70.155",
112
- "@stacksjs/error-handling": "^0.70.155",
113
- "@stacksjs/events": "^0.70.155",
114
- "@stacksjs/git": "^0.70.155",
95
+ "@stacksjs/actions": "^0.70.157",
96
+ "@stacksjs/ai": "^0.70.157",
97
+ "@stacksjs/alias": "^0.70.157",
98
+ "@stacksjs/arrays": "^0.70.157",
99
+ "@stacksjs/auth": "^0.70.157",
100
+ "@stacksjs/build": "^0.70.157",
101
+ "@stacksjs/cache": "^0.70.157",
102
+ "@stacksjs/cli": "^0.70.157",
103
+ "@stacksjs/clapp": "^0.2.12",
104
+ "@stacksjs/cloud": "^0.70.157",
105
+ "@stacksjs/collections": "^0.70.157",
106
+ "@stacksjs/config": "^0.70.157",
107
+ "@stacksjs/database": "^0.70.157",
108
+ "@stacksjs/desktop": "^0.70.157",
109
+ "@stacksjs/dns": "^0.70.157",
110
+ "@stacksjs/email": "^0.70.157",
111
+ "@stacksjs/enums": "^0.70.157",
112
+ "@stacksjs/error-handling": "^0.70.157",
113
+ "@stacksjs/events": "^0.70.157",
114
+ "@stacksjs/git": "^0.70.157",
115
115
  "@stacksjs/gitit": "^0.2.5",
116
- "@stacksjs/health": "^0.70.155",
116
+ "@stacksjs/health": "^0.70.157",
117
117
  "@stacksjs/dnsx": "^0.2.3",
118
118
  "@stacksjs/httx": "^0.1.10",
119
- "@stacksjs/lint": "^0.70.155",
120
- "@stacksjs/logging": "^0.70.155",
121
- "@stacksjs/notifications": "^0.70.155",
122
- "@stacksjs/objects": "^0.70.155",
123
- "@stacksjs/orm": "^0.70.155",
124
- "@stacksjs/path": "^0.70.155",
125
- "@stacksjs/payments": "^0.70.155",
126
- "@stacksjs/realtime": "^0.70.155",
127
- "@stacksjs/router": "^0.70.155",
128
- "@stacksjs/rpx": "^0.11.29",
129
- "@stacksjs/search-engine": "^0.70.155",
130
- "@stacksjs/security": "^0.70.155",
131
- "@stacksjs/server": "^0.70.155",
132
- "@stacksjs/storage": "^0.70.155",
133
- "@stacksjs/strings": "^0.70.155",
134
- "@stacksjs/testing": "^0.70.155",
135
- "@stacksjs/tunnel": "^0.70.155",
136
- "@stacksjs/types": "^0.70.155",
137
- "@stacksjs/ui": "^0.70.155",
138
- "@stacksjs/utils": "^0.70.155",
139
- "@stacksjs/validation": "^0.70.155",
119
+ "@stacksjs/lint": "^0.70.157",
120
+ "@stacksjs/logging": "^0.70.157",
121
+ "@stacksjs/notifications": "^0.70.157",
122
+ "@stacksjs/objects": "^0.70.157",
123
+ "@stacksjs/orm": "^0.70.157",
124
+ "@stacksjs/path": "^0.70.157",
125
+ "@stacksjs/payments": "^0.70.157",
126
+ "@stacksjs/realtime": "^0.70.157",
127
+ "@stacksjs/router": "^0.70.157",
128
+ "@stacksjs/rpx": "^0.11.31",
129
+ "@stacksjs/search-engine": "^0.70.157",
130
+ "@stacksjs/security": "^0.70.157",
131
+ "@stacksjs/server": "^0.70.157",
132
+ "@stacksjs/storage": "^0.70.157",
133
+ "@stacksjs/strings": "^0.70.157",
134
+ "@stacksjs/testing": "^0.70.157",
135
+ "@stacksjs/tunnel": "^0.70.157",
136
+ "@stacksjs/types": "^0.70.157",
137
+ "@stacksjs/ui": "^0.70.157",
138
+ "@stacksjs/utils": "^0.70.157",
139
+ "@stacksjs/validation": "^0.70.157",
140
140
  "@stacksjs/ts-cloud": "^0.7.47"
141
141
  },
142
142
  "devDependencies": {