@stacksjs/buddy 0.70.232 → 0.70.234

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.
@@ -18,6 +18,7 @@ export function build(buddy) {
18
18
  buddy: "Build the Buddy binary",
19
19
  functions: "Build your function library",
20
20
  desktop: "Build the Desktop Application",
21
+ dmg: "Package the desktop build as a macOS .app inside a .dmg",
21
22
  pages: "Build your frontend",
22
23
  docs: "Build your documentation",
23
24
  framework: "Build Stacks framework",
@@ -153,6 +154,17 @@ export function build(buddy) {
153
154
  await outro("Exited", { startTime: perf, useSeconds: !0 });
154
155
  process.exit(ExitCode.Success);
155
156
  });
157
+ buddy.command("build:dmg", descriptions.dmg).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
158
+ log.debug("Running `buddy build:dmg` ...", options);
159
+ const perf = await intro("buddy build:dmg"), result = await runAction(Action.BuildDmg, options);
160
+ if (result.isErr) {
161
+ await outro("While running the build:dmg command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
162
+ process.exit(ExitCode.FatalError);
163
+ }
164
+ console.log("");
165
+ await outro("Exited", { startTime: perf, useSeconds: !0 });
166
+ process.exit(ExitCode.Success);
167
+ });
156
168
  buddy.command("build:stacks", "Build the Stacks framework.").option("-s, --stacks", descriptions.framework, { default: !0 }).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
157
169
  log.debug("Running `buddy build:stacks` ...", options);
158
170
  const startTime = await intro("buddy build:stacks"), result = await runAction(Action.BuildStacks, options);
@@ -831,7 +831,7 @@ async function runHetznerDeploy(args) {
831
831
  ".env.keys",
832
832
  ".env.production.bak",
833
833
  ".env.production.plain"
834
- ].flatMap((p) => [`--exclude='${p}'`, `--exclude='*/${p}'`]);
834
+ ];
835
835
  if (onlySite && !sites[onlySite]) {
836
836
  log.error(`--site '${onlySite}' is not a configured site. Available: ${Object.keys(sites).join(", ")}`);
837
837
  process.exit(ExitCode.FatalError);
@@ -851,9 +851,11 @@ async function runHetznerDeploy(args) {
851
851
  log.info(`Building static site '${siteName}': ${site.build}`);
852
852
  execSync(site.build, { stdio: verbose ? "inherit" : "pipe" });
853
853
  }
854
- const root = site.root || ".", tarballPath = join(tmpdir(), `${slug}-${siteName}-${sha}.tar.gz`);
854
+ const root = site.root || ".", tarballPath = join(tmpdir(), `${slug}-${siteName}-${sha}.tar.gz`), siteExcludes = Array.isArray(site.exclude) ? site.exclude.filter((entry) => typeof entry === "string" && entry.length > 0) : [], excludeArgs = [...tarExcludes, ...siteExcludes].flatMap((pattern) => [`--exclude='${pattern}'`, `--exclude='*/${pattern}'`]);
855
+ if (siteExcludes.length > 0)
856
+ log.info(`Excluding server-owned paths: ${siteExcludes.join(", ")}`);
855
857
  log.info(`Packaging ${root} \u2192 ${tarballPath}...`);
856
- execSync(`tar czf "${tarballPath}" ${tarExcludes.join(" ")} -C "${root}" .`, { stdio: verbose ? "inherit" : "pipe", env: { ...process.env, COPYFILE_DISABLE: "1" } });
858
+ execSync(`tar czf "${tarballPath}" ${excludeArgs.join(" ")} -C "${root}" .`, { stdio: verbose ? "inherit" : "pipe", env: { ...process.env, COPYFILE_DISABLE: "1" } });
857
859
  const sizeMb = Math.max(1, Math.round(statSync(tarballPath).size / 1048576));
858
860
  log.info(`Release tarball: ~${sizeMb} MB`);
859
861
  tarballs.set(siteName, tarballPath);
@@ -888,8 +890,32 @@ async function runHetznerDeploy(args) {
888
890
  success: (m) => log.success(m)
889
891
  }
890
892
  });
893
+ let publishedDns = [];
891
894
  if (ok)
892
- await reconcileHetznerDns(onlySite ? { [onlySite]: sites[onlySite] } : sites, ip, log, ipv6);
895
+ publishedDns = await reconcileHetznerDns(onlySite ? { [onlySite]: sites[onlySite] } : sites, ip, log, ipv6);
896
+ if (ok && publishedDns.length > 0) {
897
+ log.info(`Issuing TLS for ${publishedDns.length} newly published record(s)...`);
898
+ try {
899
+ const { renewRpxCertificates, reloadRpxGateway } = await import("@stacksjs/ts-cloud"), gatewayOptions = {
900
+ rpxConfig: { ...tsCloudConfig, sites: sitesWithResolvedEnv },
901
+ environment,
902
+ driver,
903
+ logger: {
904
+ info: (m) => log.info(m),
905
+ warn: (m) => log.warn(m),
906
+ error: (m) => log.error(m),
907
+ step: (m) => log.info(m),
908
+ success: (m) => log.success(m)
909
+ }
910
+ };
911
+ await renewRpxCertificates(gatewayOptions);
912
+ await reloadRpxGateway(gatewayOptions);
913
+ log.success("TLS issued and gateway reloaded for the new record(s)");
914
+ } catch (err) {
915
+ log.warn(`TLS issuance after DNS failed: ${err?.message || err}`);
916
+ log.warn(` Until it succeeds, ${publishedDns.join(", ")} serves a fallback certificate.`);
917
+ }
918
+ }
893
919
  if (ok)
894
920
  await reconcileConfigDns(onlySite ? { [onlySite]: sites[onlySite] } : sites, log);
895
921
  if (ok) {
@@ -1379,12 +1405,12 @@ async function reconcileConfigDns(sites, logger) {
1379
1405
  }
1380
1406
  }
1381
1407
  async function reconcileHetznerDns(sites, ip, logger, ipv6) {
1382
- const domains = new Set;
1408
+ const published = [], domains = new Set;
1383
1409
  for (const site of Object.values(sites))
1384
1410
  if (site?.domain && typeof site.domain === "string")
1385
1411
  domains.add(site.domain.replace(/^www\./, ""));
1386
1412
  if (domains.size === 0)
1387
- return;
1413
+ return published;
1388
1414
  const providerConfigs = [];
1389
1415
  if (process.env.PORKBUN_API_KEY && process.env.PORKBUN_SECRET_KEY)
1390
1416
  providerConfigs.push({ provider: "porkbun", apiKey: process.env.PORKBUN_API_KEY, secretKey: process.env.PORKBUN_SECRET_KEY });
@@ -1398,7 +1424,7 @@ async function reconcileHetznerDns(sites, ip, logger, ipv6) {
1398
1424
  logger.warn("DNS: no DNS provider credentials found (PORKBUN_API_KEY/\u2026); skipping DNS reconciliation.");
1399
1425
  for (const d of domains)
1400
1426
  logger.info(` Point manually: A ${d} \u2192 ${ip} and A www.${d} \u2192 ${ip}`);
1401
- return;
1427
+ return published;
1402
1428
  }
1403
1429
  const {
1404
1430
  createDnsProvider,
@@ -1423,7 +1449,10 @@ async function reconcileHetznerDns(sites, ip, logger, ipv6) {
1423
1449
  };
1424
1450
  for (const domain of domains)
1425
1451
  try {
1426
- let provider = await detectDnsProvider(domain, providerConfigs);
1452
+ let provider = await detectDnsProvider(domain, providerConfigs).catch((err) => {
1453
+ logger.warn(` DNS: ignoring a configured provider for ${domain} - its credentials were rejected (${err?.message || err})`);
1454
+ return;
1455
+ });
1427
1456
  if (!provider) {
1428
1457
  const providerName = dnsProviderNameFromNameservers(await resolveAuthoritativeNameservers(domain)), providerConfig = providerConfigs.find((config) => config.provider === providerName);
1429
1458
  if (providerConfig)
@@ -1443,14 +1472,17 @@ async function reconcileHetznerDns(sites, ip, logger, ipv6) {
1443
1472
  }
1444
1473
  for (const sub of ["", "www"]) {
1445
1474
  const fqdn = sub ? `${sub}.${domain}` : domain, report = await reconcileAddressRecords({ provider, zone: domain, fqdn, ipv4: ip, ipv6 });
1446
- for (const record of report.published)
1475
+ for (const record of report.published) {
1447
1476
  logger.success(` DNS: ${record.fqdn} \u2192 ${record.content} (${provider.name})`);
1477
+ published.push(record.fqdn);
1478
+ }
1448
1479
  for (const warning of report.warnings)
1449
1480
  logger.warn(` DNS: ${warning}`);
1450
1481
  }
1451
1482
  } catch (err) {
1452
1483
  logger.warn(` DNS: ${domain} reconciliation failed: ${err?.message || err}`);
1453
1484
  }
1485
+ return published;
1454
1486
  }
1455
1487
  async function buildContainerImageWithPantry(args) {
1456
1488
  const { slug, sites, verbose } = args, { execSync } = await import("node:child_process");
@@ -1,3 +1,4 @@
1
+ import { assertFrameworkRepo } from "./framework-repo";
1
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
3
  import { dirname, resolve } from "node:path";
3
4
  const root = resolve(import.meta.dir, "../../../../../../.."), outputPath = resolve(root, "docs/guide/buddy/commands.md"), cliPath = resolve(root, "storage/framework/core/buddy/src/cli.ts");
@@ -99,6 +100,7 @@ export function loadBuddyInventory() {
99
100
  return inventory;
100
101
  }
101
102
  export async function run() {
103
+ assertFrameworkRepo(root, "docs:buddy");
102
104
  const mode = process.argv.includes("--write") ? "write" : process.argv.includes("--check") ? "check" : null;
103
105
  if (!mode) {
104
106
  console.error("usage: bun storage/framework/core/buddy/src/commands/docs/buddy-commands.ts --write | --check");
@@ -0,0 +1,29 @@
1
+ /**
2
+ * These tools operate on the framework repository, not on an app.
3
+ *
4
+ * Each one locates its files by counting `../` up from its own source, so the
5
+ * root it edits is wherever the module happens to live. In a consumer app that
6
+ * resolves buddy from a linked framework checkout, that root is the framework
7
+ * checkout - so `buddy docs:artifacts` run from an application regenerated and
8
+ * overwrote the FRAMEWORK's `openapi.json`, from routes belonging to a
9
+ * different project entirely.
10
+ *
11
+ * The failure that surfaced first was milder and more confusing: the generator
12
+ * read the app's routes but compared them against the framework's artifact and
13
+ * reported "no routes were registered". Nobody would guess from that message
14
+ * that the command was editing another repository.
15
+ *
16
+ * So they refuse to run unless the working directory is inside the root they
17
+ * would write to.
18
+ */
19
+ /** Whether `cwd` sits inside `root`. */
20
+ export declare function isInsideRoot(root: string, cwd: string): boolean;
21
+ /** Whether `root` looks like the Stacks framework repository. */
22
+ export declare function looksLikeFrameworkRepo(root: string): boolean;
23
+ /**
24
+ * Stop unless this is the framework repository and we are standing in it.
25
+ *
26
+ * Throws rather than warns: the alternative is silently rewriting a file in a
27
+ * repository the caller is not looking at.
28
+ */
29
+ export declare function assertFrameworkRepo(root: string, command: string, cwd?: string): void;
@@ -0,0 +1,14 @@
1
+ import { existsSync } from "node:fs";
2
+ import { isAbsolute, relative, resolve } from "node:path";
3
+ export function isInsideRoot(root, cwd) {
4
+ const rel = relative(resolve(root), resolve(cwd));
5
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
6
+ }
7
+ export function looksLikeFrameworkRepo(root) {
8
+ return existsSync(resolve(root, "storage/framework/core/buddy"));
9
+ }
10
+ export function assertFrameworkRepo(root, command, cwd = process.cwd()) {
11
+ if (looksLikeFrameworkRepo(root) && isInsideRoot(root, cwd))
12
+ return;
13
+ throw Error(`${command} operates on the Stacks framework repository and would write to ${root}, which is not where you are (${cwd}). Run it from a framework checkout; an application has no generated framework artifacts to check.`);
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { assertFrameworkRepo, isInsideRoot, looksLikeFrameworkRepo } from "./framework-repo";
6
+ describe("isInsideRoot", () => {
7
+ it("accepts the root itself and anything under it", () => {
8
+ expect(isInsideRoot("/a/b", "/a/b")).toBe(!0);
9
+ expect(isInsideRoot("/a/b", "/a/b/c/d")).toBe(!0);
10
+ });
11
+ it("rejects a sibling and a parent", () => {
12
+ expect(isInsideRoot("/a/b", "/a/c")).toBe(!1);
13
+ expect(isInsideRoot("/a/b", "/a")).toBe(!1);
14
+ });
15
+ it("rejects another project entirely", () => {
16
+ expect(isInsideRoot("/Users/x/Code/stacks", "/Users/x/Code/Apps/reviewos.org")).toBe(!1);
17
+ });
18
+ });
19
+ describe("looksLikeFrameworkRepo", () => {
20
+ it("recognises a tree carrying the framework core", () => {
21
+ const dir = mkdtempSync(join(tmpdir(), "stacks-fw-"));
22
+ try {
23
+ mkdirSync(resolve(dir, "storage/framework/core/buddy"), { recursive: !0 });
24
+ expect(looksLikeFrameworkRepo(dir)).toBe(!0);
25
+ } finally {
26
+ rmSync(dir, { recursive: !0, force: !0 });
27
+ }
28
+ });
29
+ it("does not mistake an ordinary application for it", () => {
30
+ const dir = mkdtempSync(join(tmpdir(), "stacks-app-"));
31
+ try {
32
+ mkdirSync(resolve(dir, "app"), { recursive: !0 });
33
+ expect(looksLikeFrameworkRepo(dir)).toBe(!1);
34
+ } finally {
35
+ rmSync(dir, { recursive: !0, force: !0 });
36
+ }
37
+ });
38
+ });
39
+ describe("assertFrameworkRepo", () => {
40
+ it("passes inside the framework repo", () => {
41
+ const dir = mkdtempSync(join(tmpdir(), "stacks-fw-"));
42
+ try {
43
+ mkdirSync(resolve(dir, "storage/framework/core/buddy"), { recursive: !0 });
44
+ expect(() => assertFrameworkRepo(dir, "docs:artifacts", dir)).not.toThrow();
45
+ } finally {
46
+ rmSync(dir, { recursive: !0, force: !0 });
47
+ }
48
+ });
49
+ it("refuses when run from somewhere else, and says where it would have written", () => {
50
+ const framework = mkdtempSync(join(tmpdir(), "stacks-fw-")), app = mkdtempSync(join(tmpdir(), "stacks-app-"));
51
+ try {
52
+ mkdirSync(resolve(framework, "storage/framework/core/buddy"), { recursive: !0 });
53
+ expect(() => assertFrameworkRepo(framework, "docs:artifacts", app)).toThrow(/docs:artifacts/);
54
+ expect(() => assertFrameworkRepo(framework, "docs:artifacts", app)).toThrow(new RegExp(framework));
55
+ } finally {
56
+ rmSync(framework, { recursive: !0, force: !0 });
57
+ rmSync(app, { recursive: !0, force: !0 });
58
+ }
59
+ });
60
+ it("refuses when the root is not a framework tree at all", () => {
61
+ const dir = mkdtempSync(join(tmpdir(), "stacks-app-"));
62
+ try {
63
+ expect(() => assertFrameworkRepo(dir, "docs:links", dir)).toThrow();
64
+ } finally {
65
+ rmSync(dir, { recursive: !0, force: !0 });
66
+ }
67
+ });
68
+ });
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
+ import { assertFrameworkRepo } from "./framework-repo";
3
4
  import { generateOpenApi } from "../../../../api/src/generate-openapi";
4
5
  import { renderOpenApiTypes } from "../../../../api/src/generate-types";
5
6
  const root = resolve(import.meta.dir, "../../../../../../.."), openApiPath = resolve(root, "storage/framework/api/openapi.json"), apiTypesPath = resolve(root, "storage/framework/api/api-types.ts");
@@ -51,6 +52,7 @@ Run bun run docs:artifacts and review the generated diff.`);
51
52
  console.log(`Generated API artifacts are current (${Object.keys(JSON.parse(expected.openApi).paths).length} paths)`);
52
53
  }
53
54
  export async function run() {
55
+ assertFrameworkRepo(root, "docs:artifacts");
54
56
  try {
55
57
  if (process.argv.includes("--write"))
56
58
  await write();
@@ -1,3 +1,4 @@
1
+ import { assertFrameworkRepo } from "./framework-repo";
1
2
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
3
  import { dirname, join, relative, resolve } from "node:path";
3
4
  const root = resolve(import.meta.dir, "../../../../../../.."), docsDir = resolve(root, "docs"), INLINE_LINK = /\[[^\]]*\]\(([^)]+)\)/g;
@@ -68,6 +69,7 @@ export function checkDocsLinks(docsRoot = docsDir) {
68
69
  return broken;
69
70
  }
70
71
  export async function run() {
72
+ assertFrameworkRepo(root, "docs:links");
71
73
  const broken = checkDocsLinks();
72
74
  if (broken.length === 0)
73
75
  console.log("\u2713 All internal documentation links resolve.");
@@ -172,8 +172,13 @@ export function extension(buddy) {
172
172
  } else
173
173
  log.success(`Extension payload synced \u2192 ${resources}`);
174
174
  });
175
- buddy.command("extension:safari:publish", "Archive and validate or upload the Safari app to App Store Connect").option("--version <version>", "Override the marketing version (defaults to package.json)").option("--build-number <number>", "CFBundleVersion (defaults to GITHUB_RUN_NUMBER or Unix time)").option("--team-id <id>", "Apple Developer team (defaults to config safariTeamId)").option("--api-key-id <id>", "App Store Connect API key ID").option("--api-issuer-id <id>", "App Store Connect API issuer ID").option("--api-key-path <path>", "Path to the App Store Connect AuthKey_*.p8 file").option("--validate-only", "Create and validate the archive without uploading it").option("--platform <platform>", "Publish macos, ios, or all (defaults to config safariPlatforms)").action(async (options) => {
176
- const { publishSafariApp } = await import("@stacksjs/browser-extension"), { config, version } = await load(), result = await publishSafariApp(config, {
175
+ buddy.command("extension:safari:publish", "Archive and validate or upload the Safari app to App Store Connect").option("--version <version>", "Override the marketing version (defaults to package.json)").option("--build-number <number>", "CFBundleVersion (defaults to GITHUB_RUN_NUMBER or Unix time)").option("--team-id <id>", "Apple Developer team (defaults to config safariTeamId)").option("--api-key-id <id>", "App Store Connect API key ID").option("--api-issuer-id <id>", "App Store Connect API issuer ID").option("--api-key-path <path>", "Path to the App Store Connect AuthKey_*.p8 file").option("--validate-only", "Create and validate the archive without uploading it").option("--platform <platform>", "Publish macos, ios, or all (defaults to config safariPlatforms)").option("--skip-screenshots", "Do not regenerate the App Store screenshot set before publishing").action(async (options) => {
176
+ const { publishSafariApp } = await import("@stacksjs/browser-extension"), { config, version } = await load();
177
+ if (!options.skipScreenshots) {
178
+ const { generateProjectImages } = await import("@stacksjs/actions");
179
+ await generateProjectImages({ only: ["app-store"] });
180
+ }
181
+ const result = await publishSafariApp(config, {
177
182
  version: options.version ?? version,
178
183
  buildNumber: options.buildNumber,
179
184
  teamId: options.teamId,
@@ -6,6 +6,7 @@ import {
6
6
  generateLibEntries,
7
7
  generateOpenApiSpec,
8
8
  generatePantryConfig,
9
+ generateProjectImages,
9
10
  generateTypes,
10
11
  generateVsCodeCustomData,
11
12
  generateWebTypes,
@@ -26,11 +27,15 @@ export function generate(buddy) {
26
27
  coreSymlink: "Generate symlink of the core framework to the project root",
27
28
  pantry: "Generate the pantry configuration file",
28
29
  openApi: "Generate the OpenAPI specification",
30
+ images: "Generate every image declared in config/images.ts",
31
+ og: "Generate the social cards used by link previews",
32
+ appStore: "Generate the App Store screenshot set",
33
+ appIcons: "Generate the app icon and favicon sets",
29
34
  select: "What are you trying to generate?",
30
35
  project: "Target a specific project",
31
36
  verbose: "Enable verbose output"
32
37
  };
33
- buddy.command("generate", descriptions.command).option("-t, --types", descriptions.types).option("-e, --entries", descriptions.entries).option("-w, --web-types", descriptions.webTypes).option("-c, --custom-data", descriptions.customData).option("-i, --ide-helpers", descriptions.ideHelpers).option("-c, --component-meta", descriptions.componentMeta).option("-p, --pantry", descriptions.pantry).option("-o, --openapi", descriptions.openApi).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--core-symlink", descriptions.coreSymlink).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
38
+ buddy.command("generate", descriptions.command).option("-t, --types", descriptions.types).option("-e, --entries", descriptions.entries).option("-w, --web-types", descriptions.webTypes).option("-c, --custom-data", descriptions.customData).option("-i, --ide-helpers", descriptions.ideHelpers).option("-c, --component-meta", descriptions.componentMeta).option("-p, --pantry", descriptions.pantry).option("-o, --openapi", descriptions.openApi).option("--images", descriptions.images).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--core-symlink", descriptions.coreSymlink).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
34
39
  log.debug("Running `buddy generate` ...", options);
35
40
  await startGenerationProcess(options);
36
41
  process.exit(ExitCode.Success);
@@ -100,5 +105,35 @@ export function generate(buddy) {
100
105
  log.debug("Running `buddy core-symlink` ...", options);
101
106
  await generateCoreSymlink();
102
107
  });
108
+ buddy.command("generate:images", descriptions.images).alias("images:generate").option("--social", "Only build the social cards").option("--app-store", "Only build the App Store screenshots").option("--app-icons", "Only build the app icons and favicons").option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
109
+ log.debug("Running `buddy generate:images` ...", options);
110
+ const perf = await intro("buddy generate:images"), only = [];
111
+ if (options.social)
112
+ only.push("social");
113
+ if (options.appStore)
114
+ only.push("app-store");
115
+ if (options.appIcons)
116
+ only.push("app-icons");
117
+ await generateProjectImages({ only, verbose: options.verbose });
118
+ await outro("Generated images", { startTime: perf, useSeconds: !0 });
119
+ });
120
+ buddy.command("generate:og", descriptions.og).alias("generate:social").option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
121
+ log.debug("Running `buddy generate:og` ...", options);
122
+ const perf = await intro("buddy generate:og");
123
+ await generateProjectImages({ only: ["social"], verbose: options.verbose });
124
+ await outro("Generated social cards", { startTime: perf, useSeconds: !0 });
125
+ });
126
+ buddy.command("generate:app-store", descriptions.appStore).alias("generate:screenshots").option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
127
+ log.debug("Running `buddy generate:app-store` ...", options);
128
+ const perf = await intro("buddy generate:app-store");
129
+ await generateProjectImages({ only: ["app-store"], verbose: options.verbose });
130
+ await outro("Generated App Store screenshots", { startTime: perf, useSeconds: !0 });
131
+ });
132
+ buddy.command("generate:app-icons", descriptions.appIcons).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
133
+ log.debug("Running `buddy generate:app-icons` ...", options);
134
+ const perf = await intro("buddy generate:app-icons");
135
+ await generateProjectImages({ only: ["app-icons"], verbose: options.verbose });
136
+ await outro("Generated app icons", { startTime: perf, useSeconds: !0 });
137
+ });
103
138
  onUnknownSubcommand(buddy, "generate");
104
139
  }
@@ -18,4 +18,18 @@ export declare function resolveUserPartialsPath(cwd?: unknown, configuredDir?: s
18
18
  * it {@link resolveUserPartialsPath} falls back to the conventions.
19
19
  */
20
20
  export declare function loadStxPartialsDir(cwd?: unknown): Promise<string | undefined>;
21
+ /**` proxy target, or `null` when it cannot be
22
+ * known safely.
23
+ *
24
+ * `API_URL` and `PORT_API` are explicit operator intent and always win. The
25
+ * framework-wide `ports.api` default is only trusted *outside* deployed
26
+ * environments: locally one app owns the machine, so `127.0.0.1:3008` really is
27
+ * its own API. On a deployed box that assumption does not hold — ts-cloud runs
28
+ * many SSR sites side by side, each on its own port, and an unconfigured
29
+ * default resolves to whichever tenant happens to own it.
30
+ *
31
+ * Returning `null` makes the caller answer 502. That is the safe failure: the
32
+ * alternative is proxying authenticated requests into another app's process.
33
+ */
34
+ export declare function resolveApiBase(configuredPort?: number, env?: NodeJS.ProcessEnv): string | null;
21
35
  export declare function startProductionServer(options?: { port?: string | number, verbose?: boolean }): Promise<void>;
@@ -89,6 +89,16 @@ function resolveCsrfMiddlewarePath() {
89
89
  return vendored;
90
90
  }
91
91
  }
92
+ export function resolveApiBase(configuredPort, env = process.env) {
93
+ if (env.API_URL)
94
+ return env.API_URL;
95
+ const explicitPort = Number(env.PORT_API);
96
+ if (explicitPort)
97
+ return `http://127.0.0.1:${explicitPort}`;
98
+ if (["production", "staging", "development"].includes((env.APP_ENV || "").toLowerCase()))
99
+ return null;
100
+ return `http://127.0.0.1:${configuredPort || 3008}`;
101
+ }
92
102
  export async function startProductionServer(options) {
93
103
  if (options?.port)
94
104
  process.env.PORT = String(options.port);
@@ -111,7 +121,7 @@ export async function startProductionServer(options) {
111
121
  } catch {}
112
122
  if (!stxServe)
113
123
  ({ serve: stxServe } = await import("bun-plugin-stx/serve"));
114
- const stxModule = await resolveVendoredStxModule(), { site: siteConfig, i18n: i18nConfig } = await loadStxSiteConfig(), userViewsPath = "resources/views", defaultsResources = resolveDefaultsResources(), defaultViewsPath = join(defaultsResources, "views"), userLayoutsPath = existsSync("resources/views/layouts") ? "resources/views/layouts" : "resources/layouts", userPartialsPath = resolveUserPartialsPath(process.cwd(), await loadStxPartialsDir()), apiBase = process.env.API_URL || `http://127.0.0.1:${Number(process.env.PORT_API) || config.ports?.api || 3008}`;
124
+ const stxModule = await resolveVendoredStxModule(), { site: siteConfig, i18n: i18nConfig } = await loadStxSiteConfig(), userViewsPath = "resources/views", defaultsResources = resolveDefaultsResources(), defaultViewsPath = join(defaultsResources, "views"), userLayoutsPath = existsSync("resources/views/layouts") ? "resources/views/layouts" : "resources/layouts", userPartialsPath = resolveUserPartialsPath(process.cwd(), await loadStxPartialsDir()), apiBase = resolveApiBase(config.ports?.api);
115
125
  log.info(`Starting production server on port ${port}...`);
116
126
  await stxServe({
117
127
  patterns: [userViewsPath, defaultViewsPath],
@@ -132,13 +142,18 @@ export async function startProductionServer(options) {
132
142
  if (gated)
133
143
  return gated;
134
144
  const url = new URL(req.url);
135
- if (isApiBoundRequest(req, url.pathname))
145
+ if (isApiBoundRequest(req, url.pathname)) {
146
+ if (!apiBase) {
147
+ log.error(`No API target configured for ${url.pathname}. This app shares its host with other ` + "deployments, so there is no safe default port to guess \u2014 refusing to proxy. Set " + "PORT_API (or API_URL) for this site, and deploy an `api` site on its own port.");
148
+ return new Response("Bad Gateway", { status: 502 });
149
+ }
136
150
  try {
137
151
  return await proxyToBackend(req, apiBase);
138
152
  } catch (error) {
139
153
  log.error(`API proxy to ${apiBase} failed: ${error.message}`);
140
154
  return new Response("Bad Gateway", { status: 502 });
141
155
  }
156
+ }
142
157
  if (existsSync(join(process.cwd(), "resources/views/blog.stx"))) {
143
158
  const { renderBlogFeed } = await import("@stacksjs/actions/blog"), feed = await renderBlogFeed(req);
144
159
  if (feed)
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.232",
5
+ "version": "0.70.234",
6
6
  "description": "Meet Buddy. The Stacks runtime.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -41,20 +41,17 @@
41
41
  "exports": {
42
42
  ".": {
43
43
  "types": "./dist/index.d.ts",
44
- "development": "./src/index.ts",
45
44
  "bun": "./dist/index.js",
46
45
  "import": "./dist/index.js",
47
46
  "default": "./dist/index.js"
48
47
  },
49
48
  "./cli": {
50
49
  "types": "./dist/cli.d.ts",
51
- "development": "./src/cli.ts",
52
50
  "bun": "./dist/cli.js",
53
51
  "import": "./dist/cli.js",
54
52
  "default": "./dist/cli.js"
55
53
  },
56
54
  "./*": {
57
- "development": "./src/*",
58
55
  "bun": "./dist/*",
59
56
  "import": "./dist/*",
60
57
  "default": "./dist/*"
@@ -104,7 +101,7 @@
104
101
  "@stacksjs/collections": "^0.70.180",
105
102
  "@stacksjs/config": "^0.70.180",
106
103
  "@stacksjs/database": "^0.70.180",
107
- "@stacksjs/desktop": "^0.70.180",
104
+ "@stacksjs/desktop-build": "^0.70.180",
108
105
  "@stacksjs/dns": "^0.70.180",
109
106
  "@stacksjs/email": "^0.70.180",
110
107
  "@stacksjs/enums": "^0.70.180",
@@ -115,6 +112,7 @@
115
112
  "@stacksjs/health": "^0.70.180",
116
113
  "@stacksjs/dnsx": "^0.2.3",
117
114
  "@stacksjs/httx": "^0.1.10",
115
+ "@stacksjs/image": "^0.70.180",
118
116
  "@stacksjs/lint": "^0.70.180",
119
117
  "@stacksjs/logging": "^0.70.180",
120
118
  "@stacksjs/notifications": "^0.70.180",