@bigcommerce/catalyst 1.4.1 → 1.4.2

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
@@ -25,7 +25,7 @@ import { randomBytes, randomUUID } from "crypto";
25
25
  // package.json
26
26
  var package_default = {
27
27
  name: "@bigcommerce/catalyst",
28
- version: "1.4.1",
28
+ version: "1.4.2",
29
29
  type: "module",
30
30
  repository: {
31
31
  type: "git",
@@ -619,6 +619,14 @@ function getProjectConfig(cwd = process.cwd()) {
619
619
  type: "object",
620
620
  additionalProperties: { type: "string" },
621
621
  default: {}
622
+ },
623
+ declinedSiteUrlChannels: {
624
+ type: "array",
625
+ items: { type: "number" }
626
+ },
627
+ declinedCheckoutUrlChannels: {
628
+ type: "array",
629
+ items: { type: "number" }
622
630
  }
623
631
  }
624
632
  });
@@ -1550,6 +1558,8 @@ async function buildCatalystProject(projectUuid, wranglerVersion = WRANGLER_VERS
1550
1558
  "pnpm",
1551
1559
  [
1552
1560
  "dlx",
1561
+ "--allow-build=esbuild",
1562
+ "--allow-build=workerd",
1553
1563
  `wrangler@${wranglerVersion}`,
1554
1564
  "deploy",
1555
1565
  "--config",
@@ -1617,12 +1627,17 @@ Examples:
1617
1627
  });
1618
1628
 
1619
1629
  // src/cli/commands/channels.ts
1620
- import { confirm as confirm3, select as select4 } from "@inquirer/prompts";
1630
+ import { confirm as confirm4, select as select4 } from "@inquirer/prompts";
1621
1631
  import { Command as Command3, InvalidArgumentError as InvalidArgumentError2, Option as Option4 } from "commander";
1622
1632
  import { colorize as colorize5 } from "consola/utils";
1623
1633
 
1634
+ // src/cli/lib/can-prompt.ts
1635
+ function canPrompt() {
1636
+ return Boolean(process.stdin.isTTY) && !process.env.CI;
1637
+ }
1638
+
1624
1639
  // src/cli/lib/channel-checkout-url-flow.ts
1625
- import { input as input3 } from "@inquirer/prompts";
1640
+ import { confirm as confirm3, input as input3 } from "@inquirer/prompts";
1626
1641
 
1627
1642
  // src/cli/lib/channel-site-flow.ts
1628
1643
  import { select as select2 } from "@inquirer/prompts";
@@ -1837,7 +1852,6 @@ async function deleteChannelCheckoutUrl(channelId, storeHash, accessToken, apiHo
1837
1852
  // src/cli/lib/checkout-url.ts
1838
1853
  import { getDomain } from "tldts";
1839
1854
  var normalizeHostname = (hostname) => hostname.toLowerCase().replace(/\.$/, "");
1840
- var parentDomain = (hostname) => hostname.split(".").slice(1).join(".");
1841
1855
  var isSubdomainOf = (hostname, parent) => hostname.endsWith(`.${parent}`);
1842
1856
  function hostnameOf(url) {
1843
1857
  try {
@@ -1850,11 +1864,45 @@ function sharesMainDomain(a, b) {
1850
1864
  const first = getDomain(normalizeHostname(a));
1851
1865
  return first !== null && first === getDomain(normalizeHostname(b));
1852
1866
  }
1853
- function suggestCheckoutUrl(storefrontUrl) {
1867
+ var MANAGED_ZONE_CHECKOUT_PREFIX = "c.";
1868
+ var MAX_HOSTNAME_LENGTH = 64;
1869
+ var CHECKOUT_HOSTNAME_READY_TIMEOUT_MS = 6 * 60 * 1e3;
1870
+ var CHECKOUT_HOSTNAME_POLL_INTERVAL_MS = 10 * 1e3;
1871
+ function managedCheckoutHostname(storefrontHostname) {
1872
+ const hostname = MANAGED_ZONE_CHECKOUT_PREFIX + normalizeHostname(storefrontHostname);
1873
+ return hostname.length <= MAX_HOSTNAME_LENGTH ? hostname : void 0;
1874
+ }
1875
+ async function checkoutHostnameIsServing(hostname) {
1876
+ try {
1877
+ await fetch(`https://${hostname}/`, {
1878
+ method: "HEAD",
1879
+ redirect: "manual",
1880
+ signal: AbortSignal.timeout(1e4)
1881
+ });
1882
+ return true;
1883
+ } catch {
1884
+ return false;
1885
+ }
1886
+ }
1887
+ async function waitForCheckoutHostname(hostname, { timeoutMs = CHECKOUT_HOSTNAME_READY_TIMEOUT_MS, onWait } = {}) {
1888
+ const deadline = Date.now() + timeoutMs;
1889
+ let waited = false;
1890
+ for (; ; ) {
1891
+ if (await checkoutHostnameIsServing(hostname)) return true;
1892
+ if (Date.now() + CHECKOUT_HOSTNAME_POLL_INTERVAL_MS >= deadline) return false;
1893
+ if (!waited) {
1894
+ waited = true;
1895
+ onWait?.();
1896
+ }
1897
+ await new Promise((resolve2) => setTimeout(resolve2, CHECKOUT_HOSTNAME_POLL_INTERVAL_MS));
1898
+ }
1899
+ }
1900
+ function suggestCheckoutUrl(storefrontUrl, { managedZone = false } = {}) {
1854
1901
  const host = hostnameOf(storefrontUrl);
1855
1902
  if (!host) return void 0;
1856
- const base = normalizeHostname(host).replace(/^www\./, "");
1857
- return `https://checkout.${base}`;
1903
+ const normalized = normalizeHostname(host);
1904
+ if (managedZone) return `https://${MANAGED_ZONE_CHECKOUT_PREFIX}${normalized}`;
1905
+ return `https://checkout.${normalized.replace(/^www\./, "")}`;
1858
1906
  }
1859
1907
  function normalizeCheckoutUrl(value) {
1860
1908
  const withScheme = /^[a-z][a-z\d+.-]*:\/\//i.test(value) ? value : `https://${value}`;
@@ -1873,18 +1921,24 @@ function normalizeCheckoutUrl(value) {
1873
1921
  }
1874
1922
  return parsed.origin;
1875
1923
  }
1876
- async function isManagedHostingHostname(hostname, context) {
1877
- try {
1878
- const projects2 = await fetchProjects(context.storeHash, context.accessToken, context.apiHost);
1879
- const zones = projects2.flatMap((project) => project.deployment_hostnames).map((deploymentHostname) => parentDomain(normalizeHostname(deploymentHostname))).filter((zone) => zone.includes("."));
1880
- if (zones.length === 0) return void 0;
1881
- const host = normalizeHostname(hostname);
1882
- return zones.some((zone) => host === zone || isSubdomainOf(host, zone));
1883
- } catch {
1884
- return void 0;
1885
- }
1924
+ var NATIVE_HOSTING_ZONES = [
1925
+ "catalyst-sandbox.store",
1926
+ "catalyst-sandbox-dev.store",
1927
+ "catalyst-sandbox-staging.store",
1928
+ "catalyst-sandbox-integration.store",
1929
+ "ignition-demo.store"
1930
+ ];
1931
+ function isManagedHostingHostname(hostname) {
1932
+ const host = normalizeHostname(hostname);
1933
+ return NATIVE_HOSTING_ZONES.some((zone) => isSubdomainOf(host, zone));
1934
+ }
1935
+ function isCrossDomainCheckout(site) {
1936
+ const storefrontHost = hostnameOf(findChannelSiteUrl(site, "primary") ?? site.url);
1937
+ const checkoutUrl = findChannelSiteUrl(site, "checkout");
1938
+ const checkoutHost = checkoutUrl ? hostnameOf(checkoutUrl) : void 0;
1939
+ return Boolean(storefrontHost && checkoutHost && !sharesMainDomain(storefrontHost, checkoutHost));
1886
1940
  }
1887
- async function warnOnCrossDomainCheckout(site, context) {
1941
+ function warnOnCrossDomainCheckout(site) {
1888
1942
  const storefrontUrl = findChannelSiteUrl(site, "primary") ?? site.url;
1889
1943
  const storefrontHost = hostnameOf(storefrontUrl);
1890
1944
  const checkoutUrl = findChannelSiteUrl(site, "checkout");
@@ -1906,22 +1960,23 @@ async function warnOnCrossDomainCheckout(site, context) {
1906
1960
  consola.info(
1907
1961
  "Shopper sessions and carts may not carry into checkout in browsers that restrict cross-domain cookies."
1908
1962
  );
1909
- const storefrontOnManagedZone = await isManagedHostingHostname(storefrontHost, context);
1910
- const report = { crossDomain: true, storefrontOnManagedZone, suggestion };
1911
- if (storefrontOnManagedZone === false) {
1963
+ const storefrontOnManagedZone = isManagedHostingHostname(storefrontHost);
1964
+ const report = {
1965
+ crossDomain: true,
1966
+ storefrontOnManagedZone,
1967
+ suggestion: storefrontOnManagedZone ? suggestCheckoutUrl(storefrontUrl, { managedZone: true }) : suggestion
1968
+ };
1969
+ if (storefrontOnManagedZone) {
1912
1970
  consola.info(
1913
- `To put checkout on this channel's own domain, point ${suggestion?.replace("https://", "") ?? "your checkout subdomain"} at BigCommerce and provision a certificate for it there.`
1971
+ `${storefrontHost} is an auto-generated deployment hostname, so its checkout hostname is ${MANAGED_ZONE_CHECKOUT_PREFIX}${storefrontHost}. BigCommerce provisions it when it's set as the checkout URL, and its certificate takes a few minutes to issue. Set it with:`
1914
1972
  );
1915
- return report;
1916
- }
1917
- if (storefrontOnManagedZone === true) {
1918
- consola.info(
1919
- `${storefrontHost} is an auto-generated deployment hostname, and a checkout subdomain of one cannot be issued a certificate \u2014 so no checkout URL can be set for this channel until its storefront is on a custom domain. Add one with \`catalyst domains add\`.`
1973
+ consola.log(
1974
+ ` catalyst channels update --channel-id ${site.channelId} --checkout-url ${report.suggestion ?? "<domain>"}`
1920
1975
  );
1921
1976
  return report;
1922
1977
  }
1923
1978
  consola.info(
1924
- "BigCommerce requires the checkout URL to share a main domain with the storefront. Set one with `catalyst channels update --checkout-url <domain>` once it points at BigCommerce."
1979
+ `To put checkout on this channel's own domain, point ${suggestion?.replace("https://", "") ?? "your checkout subdomain"} at BigCommerce and provision a certificate for it there.`
1925
1980
  );
1926
1981
  return report;
1927
1982
  }
@@ -1995,15 +2050,26 @@ async function runChannelSiteUrlFlow(options) {
1995
2050
  const channel = await resolveChannel(options);
1996
2051
  const hostname = await resolveHostname(project, options);
1997
2052
  const siteUrl = hostname.startsWith("https://") ? hostname : `https://${hostname}`;
1998
- await updateChannelSiteUrl(
2053
+ const channelLabel = channel.name ? `"${channel.name}" (${channel.id})` : String(channel.id);
2054
+ const current = await getChannelSite(
1999
2055
  channel.id,
2000
- siteUrl,
2001
2056
  options.storeHash,
2002
2057
  options.accessToken,
2003
2058
  options.apiHost
2004
- );
2005
- const channelLabel = channel.name ? `"${channel.name}" (${channel.id})` : String(channel.id);
2006
- consola.success(`Updated channel ${channelLabel} site URL to ${siteUrl}.`);
2059
+ ).catch(() => void 0);
2060
+ if (current && (findChannelSiteUrl(current, "primary") ?? current.url).replace(/\/$/, "") === siteUrl) {
2061
+ consola.info(`Channel ${channelLabel} site URL is already ${siteUrl}.`);
2062
+ } else {
2063
+ await updateChannelSiteUrl(
2064
+ channel.id,
2065
+ siteUrl,
2066
+ options.storeHash,
2067
+ options.accessToken,
2068
+ options.apiHost
2069
+ );
2070
+ consola.success(`Updated channel ${channelLabel} site URL to ${siteUrl}.`);
2071
+ }
2072
+ if (options.diagnoseCheckout === false) return { channelId: channel.id, hostname };
2007
2073
  try {
2008
2074
  const site = await getChannelSite(
2009
2075
  channel.id,
@@ -2011,11 +2077,7 @@ async function runChannelSiteUrlFlow(options) {
2011
2077
  options.accessToken,
2012
2078
  options.apiHost
2013
2079
  );
2014
- const report = await warnOnCrossDomainCheckout(site, {
2015
- storeHash: options.storeHash,
2016
- accessToken: options.accessToken,
2017
- apiHost: options.apiHost
2018
- });
2080
+ const report = warnOnCrossDomainCheckout(site);
2019
2081
  if (report.crossDomain && report.storefrontOnManagedZone !== true) {
2020
2082
  consola.info("Set a checkout URL for this channel with:");
2021
2083
  consola.log(
@@ -2024,13 +2086,22 @@ async function runChannelSiteUrlFlow(options) {
2024
2086
  }
2025
2087
  } catch {
2026
2088
  }
2027
- return { channelId: channel.id };
2089
+ return { channelId: channel.id, hostname };
2028
2090
  }
2029
2091
 
2030
2092
  // src/cli/lib/channel-checkout-url-flow.ts
2031
2093
  async function runChannelCheckoutUrlFlow(options) {
2032
2094
  const channel = options.channelId !== void 0 ? { id: options.channelId, name: options.channelName } : await resolveChannel(options);
2033
2095
  const label = channel.name ? `"${channel.name}" (${channel.id})` : String(channel.id);
2096
+ if (options.storefrontHostname !== void 0 && await setManagedCheckoutUrl({
2097
+ ...options,
2098
+ channelId: channel.id,
2099
+ label,
2100
+ storefrontHostname: options.storefrontHostname,
2101
+ timeoutMs: options.certificateTimeoutMs
2102
+ })) {
2103
+ return;
2104
+ }
2034
2105
  let storefrontUrl = options.storefrontUrl;
2035
2106
  if (!storefrontUrl) {
2036
2107
  const site = await getChannelSite(
@@ -2048,6 +2119,16 @@ async function runChannelCheckoutUrlFlow(options) {
2048
2119
  );
2049
2120
  }
2050
2121
  }
2122
+ const storefrontHostname = hostnameOf(storefrontUrl);
2123
+ if (options.url === void 0 && options.storefrontHostname === void 0 && storefrontHostname !== void 0 && await setManagedCheckoutUrl({
2124
+ ...options,
2125
+ channelId: channel.id,
2126
+ label,
2127
+ storefrontHostname,
2128
+ timeoutMs: options.certificateTimeoutMs
2129
+ })) {
2130
+ return;
2131
+ }
2051
2132
  const answer = options.url ?? await input3({
2052
2133
  message: "Checkout URL for this channel (must share a main domain with the storefront)",
2053
2134
  default: suggestCheckoutUrl(storefrontUrl)
@@ -2062,6 +2143,78 @@ async function runChannelCheckoutUrlFlow(options) {
2062
2143
  );
2063
2144
  consola.success(`Updated channel ${label} checkout URL to ${checkoutUrl}.`);
2064
2145
  }
2146
+ async function setManagedCheckoutUrl(options) {
2147
+ const { storeHash, accessToken, apiHost, channelId, label, storefrontHostname } = options;
2148
+ if (!isManagedHostingHostname(storefrontHostname)) return false;
2149
+ const hostname = managedCheckoutHostname(storefrontHostname);
2150
+ if (!hostname) {
2151
+ consola.warn(
2152
+ `${storefrontHostname} is too long to take a checkout prefix, so it has no checkout hostname. Checkout keeps its current URL.`
2153
+ );
2154
+ return true;
2155
+ }
2156
+ const checkoutUrl = `https://${hostname}`;
2157
+ const site = await getChannelSite(channelId, storeHash, accessToken, apiHost);
2158
+ if (site.isCheckoutUrlCustomized) {
2159
+ const alreadySet = site.urls.some(
2160
+ (entry) => entry.type === "checkout" && entry.url.replace(/\/$/, "") === checkoutUrl
2161
+ );
2162
+ if (!alreadySet) {
2163
+ consola.info(
2164
+ `Channel ${label} already has a custom checkout URL, so it was left alone. To replace it, run \`catalyst channels update --channel-id ${channelId} --checkout-url ${checkoutUrl}\`.`
2165
+ );
2166
+ return true;
2167
+ }
2168
+ } else {
2169
+ await updateChannelCheckoutUrl(channelId, checkoutUrl, storeHash, accessToken, apiHost);
2170
+ consola.success(`Updated channel ${label} checkout URL to ${checkoutUrl}.`);
2171
+ }
2172
+ const ready = await waitForCheckoutHostname(hostname, {
2173
+ timeoutMs: options.timeoutMs,
2174
+ onWait: () => consola.info(
2175
+ `Waiting for ${hostname} to be issued a certificate. This usually takes a minute or two...`
2176
+ )
2177
+ });
2178
+ if (ready) {
2179
+ consola.success(`${hostname} is serving checkout.`);
2180
+ return true;
2181
+ }
2182
+ await deleteChannelCheckoutUrl(channelId, storeHash, accessToken, apiHost);
2183
+ consola.warn(
2184
+ `${hostname} wasn't issued a certificate in time, so the checkout URL was removed and checkout falls back to the default channel's. Re-run with \`--update-checkout-url\` to try again.`
2185
+ );
2186
+ return true;
2187
+ }
2188
+ async function offerManagedCheckoutUrl(options) {
2189
+ const { storeHash, accessToken, apiHost, channelId, storefrontHostname, config: config2 } = options;
2190
+ const declined = config2.get("declinedCheckoutUrlChannels") ?? [];
2191
+ if (!isManagedHostingHostname(storefrontHostname)) return false;
2192
+ if (options.respectOptOut && declined.includes(channelId)) return false;
2193
+ const site = await getChannelSite(channelId, storeHash, accessToken, apiHost);
2194
+ if (site.isCheckoutUrlCustomized || !isCrossDomainCheckout(site)) return false;
2195
+ const shouldMove = await confirm3({
2196
+ message: `Checkout is on ${findChannelSiteUrl(site, "checkout") ?? "another domain"}. Move it to https://${MANAGED_ZONE_CHECKOUT_PREFIX}${storefrontHostname} so shoppers stay on this domain through payment?`,
2197
+ default: options.defaultAnswer
2198
+ });
2199
+ if (!shouldMove) {
2200
+ if (!declined.includes(channelId)) {
2201
+ config2.set("declinedCheckoutUrlChannels", [...declined, channelId]);
2202
+ }
2203
+ warnOnCrossDomainCheckout(site);
2204
+ consola.info(
2205
+ `Won't ask again after deploys for channel ${channelId}. To set it later, run \`catalyst channels update --channel-id ${channelId} --checkout-url https://${MANAGED_ZONE_CHECKOUT_PREFIX}${storefrontHostname}\`.`
2206
+ );
2207
+ return true;
2208
+ }
2209
+ await runChannelCheckoutUrlFlow({
2210
+ storeHash,
2211
+ accessToken,
2212
+ apiHost,
2213
+ channelId,
2214
+ storefrontHostname
2215
+ });
2216
+ return true;
2217
+ }
2065
2218
 
2066
2219
  // src/cli/lib/create-channel-flow.ts
2067
2220
  import { checkbox, input as input4, select as select3 } from "@inquirer/prompts";
@@ -2404,16 +2557,19 @@ Examples:
2404
2557
  await getTelemetry().identify(storeHash);
2405
2558
  const touchesCheckout = options.checkoutUrl !== void 0 || options.removeCheckoutUrl === true;
2406
2559
  const updatesSiteUrl = options.hostname !== void 0 || !touchesCheckout;
2560
+ const offersCheckout = updatesSiteUrl && !touchesCheckout && canPrompt();
2407
2561
  let channelId = options.channelId;
2562
+ let siteHostname;
2408
2563
  if (updatesSiteUrl) {
2409
2564
  try {
2410
- ({ channelId } = await runChannelSiteUrlFlow({
2565
+ ({ channelId, hostname: siteHostname } = await runChannelSiteUrlFlow({
2411
2566
  storeHash,
2412
2567
  accessToken,
2413
2568
  apiHost,
2414
2569
  projectUuid: options.projectUuid ?? config2.get("projectUuid"),
2415
2570
  channelId: options.channelId,
2416
- hostname: options.hostname
2571
+ hostname: options.hostname,
2572
+ diagnoseCheckout: !offersCheckout
2417
2573
  }));
2418
2574
  } catch (error) {
2419
2575
  if (error instanceof NoLinkedProjectError) {
@@ -2426,6 +2582,22 @@ Examples:
2426
2582
  throw error;
2427
2583
  }
2428
2584
  }
2585
+ if (offersCheckout && channelId !== void 0 && siteHostname !== void 0) {
2586
+ const offered = await offerManagedCheckoutUrl({
2587
+ storeHash,
2588
+ accessToken,
2589
+ apiHost,
2590
+ channelId,
2591
+ storefrontHostname: siteHostname,
2592
+ config: config2,
2593
+ // Explicit command, so an earlier decline doesn't silence it.
2594
+ respectOptOut: false,
2595
+ defaultAnswer: true
2596
+ });
2597
+ if (!offered) {
2598
+ warnOnCrossDomainCheckout(await getChannelSite(channelId, storeHash, accessToken, apiHost));
2599
+ }
2600
+ }
2429
2601
  if (touchesCheckout) {
2430
2602
  const channel = channelId === void 0 ? await resolveChannel({ storeHash, accessToken, apiHost }) : { id: channelId, name: void 0 };
2431
2603
  const label = channel.name ? `"${channel.name}" (${channel.id})` : String(channel.id);
@@ -2439,7 +2611,7 @@ Examples:
2439
2611
  );
2440
2612
  consola.success(`Updated channel ${label} checkout URL to ${options.checkoutUrl}.`);
2441
2613
  reportChannelSite(updated);
2442
- await warnOnCrossDomainCheckout(updated, { storeHash, accessToken, apiHost });
2614
+ warnOnCrossDomainCheckout(updated);
2443
2615
  } else {
2444
2616
  await deleteChannelCheckoutUrl(channel.id, storeHash, accessToken, apiHost);
2445
2617
  consola.success(
@@ -2447,7 +2619,7 @@ Examples:
2447
2619
  );
2448
2620
  const reverted = await getChannelSite(channel.id, storeHash, accessToken, apiHost);
2449
2621
  reportChannelSite(reverted);
2450
- await warnOnCrossDomainCheckout(reverted, { storeHash, accessToken, apiHost });
2622
+ warnOnCrossDomainCheckout(reverted);
2451
2623
  }
2452
2624
  }
2453
2625
  process.exit(0);
@@ -2666,7 +2838,7 @@ Examples:
2666
2838
  const site = await getChannelSite(channel.id, storeHash, accessToken, apiHost);
2667
2839
  consola.success(`Channel ${label}:`);
2668
2840
  reportChannelSite(site);
2669
- const report = await warnOnCrossDomainCheckout(site, { storeHash, accessToken, apiHost });
2841
+ const report = warnOnCrossDomainCheckout(site);
2670
2842
  if (report.crossDomain && report.storefrontOnManagedZone !== true) {
2671
2843
  const suggested = report.suggestion ?? "<domain>";
2672
2844
  if (!process.stdin.isTTY) {
@@ -2676,7 +2848,7 @@ Examples:
2676
2848
  process.exit(0);
2677
2849
  return;
2678
2850
  }
2679
- const shouldSet = await confirm3({
2851
+ const shouldSet = await confirm4({
2680
2852
  message: "Set a checkout URL for this channel now?",
2681
2853
  default: true
2682
2854
  });
@@ -3408,7 +3580,7 @@ Examples:
3408
3580
  });
3409
3581
 
3410
3582
  // src/cli/commands/deploy.ts
3411
- import { confirm as confirm4 } from "@inquirer/prompts";
3583
+ import { confirm as confirm6 } from "@inquirer/prompts";
3412
3584
  import AdmZip from "adm-zip";
3413
3585
  import { Command as Command6, Option as Option7 } from "commander";
3414
3586
  import { colorize as colorize7 } from "consola/utils";
@@ -3417,6 +3589,68 @@ import { join as join13 } from "path";
3417
3589
  import yoctoSpinner3 from "yocto-spinner";
3418
3590
  import { z as z13 } from "zod";
3419
3591
 
3592
+ // src/cli/lib/deploy-channel-urls.ts
3593
+ import { confirm as confirm5 } from "@inquirer/prompts";
3594
+ function deployedChannelId(secrets) {
3595
+ const raw = secrets.find((secret) => secret.key === "BIGCOMMERCE_CHANNEL_ID")?.value ?? process.env.BIGCOMMERCE_CHANNEL_ID;
3596
+ const id = Number(raw);
3597
+ return Number.isInteger(id) && id > 0 ? id : void 0;
3598
+ }
3599
+ async function offerChannelUrlUpdates(options) {
3600
+ const { storeHash, accessToken, apiHost, projectUuid, config: config2, channelId } = options;
3601
+ if (!canPrompt() || channelId === void 0) return;
3602
+ const [site, projects2] = await Promise.all([
3603
+ getChannelSite(channelId, storeHash, accessToken, apiHost),
3604
+ fetchProjects(storeHash, accessToken, apiHost)
3605
+ ]);
3606
+ const deploymentHostnames = projects2.find((project) => project.uuid === projectUuid)?.deployment_hostnames ?? [];
3607
+ const storefrontUrl = (findChannelSiteUrl(site, "primary") ?? site.url).replace(/\/$/, "");
3608
+ let storefrontHostname = deploymentHostnames.find(
3609
+ (deployed) => storefrontUrl === `https://${deployed}`
3610
+ );
3611
+ storefrontHostname ?? (storefrontHostname = await offerSiteUrl({ ...options, channelId }, storefrontUrl));
3612
+ if (storefrontHostname === void 0) return;
3613
+ await offerManagedCheckoutUrl({
3614
+ storeHash,
3615
+ accessToken,
3616
+ apiHost,
3617
+ channelId,
3618
+ storefrontHostname,
3619
+ config: config2,
3620
+ respectOptOut: true,
3621
+ defaultAnswer: false
3622
+ });
3623
+ }
3624
+ async function offerSiteUrl(options, storefrontUrl) {
3625
+ const { storeHash, accessToken, apiHost, projectUuid, config: config2, channelId } = options;
3626
+ const declined = config2.get("declinedSiteUrlChannels") ?? [];
3627
+ if (declined.includes(channelId)) return void 0;
3628
+ const shouldUpdate = await confirm5({
3629
+ message: `Channel ${channelId}'s site URL is ${storefrontUrl}. Point it at this deployment?`,
3630
+ // Unasked-for after a deploy, so Enter mustn't change a live channel.
3631
+ default: false
3632
+ });
3633
+ if (!shouldUpdate) {
3634
+ config2.set("declinedSiteUrlChannels", [...declined, channelId]);
3635
+ consola.info(
3636
+ `Won't ask again for channel ${channelId}. To change it later, run \`catalyst channels update --channel-id ${channelId}\`.`
3637
+ );
3638
+ return void 0;
3639
+ }
3640
+ const { hostname } = await runChannelSiteUrlFlow({
3641
+ storeHash,
3642
+ accessToken,
3643
+ apiHost,
3644
+ projectUuid,
3645
+ // The deploy knows both; the hostname picker only shows if it reported none.
3646
+ channelId,
3647
+ hostname: options.deploymentHostname,
3648
+ // On the managed zone the checkout offer follows; elsewhere, warn instead.
3649
+ diagnoseCheckout: !isManagedHostingHostname(options.deploymentHostname ?? "")
3650
+ });
3651
+ return hostname;
3652
+ }
3653
+
3420
3654
  // src/cli/lib/deployment-errors.ts
3421
3655
  var ERROR_MESSAGES = {
3422
3656
  10: "Something went wrong on our end. Please try again. If the issue persists, contact support.",
@@ -3543,6 +3777,11 @@ var uploadBundleZip = async (uploadUrl) => {
3543
3777
  consola.success("Bundle uploaded successfully.");
3544
3778
  return true;
3545
3779
  };
3780
+ var NATIVE_HOSTING_ENV = {
3781
+ type: "plain_text",
3782
+ key: "CATALYST_HOSTING",
3783
+ value: "native"
3784
+ };
3546
3785
  var parseEnvironmentVariables = (secretOption) => {
3547
3786
  return secretOption?.map((envVar) => {
3548
3787
  const { key, value } = parseEnvAssignment(envVar);
@@ -3671,6 +3910,10 @@ var deploy = new Command6("deploy").configureHelp({ showGlobalOptions: true }).d
3671
3910
  Environment variables saved with \`catalyst env add\` are sent automatically on every deploy.
3672
3911
  Use \`--secret\` to set or override a variable for a single run.
3673
3912
 
3913
+ Without \`--update-site-url\` or \`--update-checkout-url\`, an interactive deploy offers to
3914
+ point the channel's site URL at the deployment and to move its checkout onto the same domain.
3915
+ Each is checked on every deploy; declining either is saved in .bigcommerce/project.json.
3916
+
3674
3917
  Example:
3675
3918
  $ catalyst deploy --secret BIGCOMMERCE_STORE_HASH=<YOUR_STORE_HASH> --secret BIGCOMMERCE_STOREFRONT_TOKEN=<YOUR_STOREFRONT_TOKEN>`
3676
3919
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option(
@@ -3740,7 +3983,7 @@ Example:
3740
3983
  consola.success(`Linked project "${selected.name}".`);
3741
3984
  }
3742
3985
  if (!getProjectState().isTransformed) {
3743
- const shouldSetup = await confirm4({
3986
+ const shouldSetup = await confirm6({
3744
3987
  message: "Your project is not yet set up for Commerce Hosting deployments. Would you like to run the Commerce Hosting setup now?",
3745
3988
  default: true
3746
3989
  });
@@ -3796,7 +4039,7 @@ Example:
3796
4039
  const flagSecrets = parseEnvironmentVariables(options.secret) ?? [];
3797
4040
  const persistedSecrets = toDeploymentSecrets(getStoredEnv(config2));
3798
4041
  const mergedSecrets = mergeDeploymentSecrets(persistedSecrets, flagSecrets);
3799
- const environmentVariables = mergedSecrets.length > 0 ? mergedSecrets : void 0;
4042
+ const environmentVariables = [...mergedSecrets, NATIVE_HOSTING_ENV];
3800
4043
  const { deployment_uuid: deploymentUuid } = await createDeployment(
3801
4044
  projectUuid,
3802
4045
  uploadSignature.upload_uuid,
@@ -3820,9 +4063,10 @@ Example:
3820
4063
  );
3821
4064
  };
3822
4065
  let resolvedChannelId;
4066
+ let siteHostname;
3823
4067
  if (options.updateSiteUrl) {
3824
4068
  try {
3825
- ({ channelId: resolvedChannelId } = await runChannelSiteUrlFlow({
4069
+ ({ channelId: resolvedChannelId, hostname: siteHostname } = await runChannelSiteUrlFlow({
3826
4070
  storeHash,
3827
4071
  accessToken,
3828
4072
  apiHost,
@@ -3839,16 +4083,32 @@ Example:
3839
4083
  storeHash,
3840
4084
  accessToken,
3841
4085
  apiHost,
3842
- channelId: resolvedChannelId
4086
+ channelId: resolvedChannelId,
4087
+ storefrontHostname: siteHostname
3843
4088
  });
3844
4089
  } catch (error) {
3845
4090
  warnChannelFlowFailed("checkout URL", error);
3846
4091
  }
3847
4092
  }
4093
+ if (!options.updateSiteUrl && !options.updateCheckoutUrl) {
4094
+ try {
4095
+ await offerChannelUrlUpdates({
4096
+ storeHash,
4097
+ accessToken,
4098
+ apiHost,
4099
+ projectUuid,
4100
+ config: config2,
4101
+ deploymentHostname,
4102
+ channelId: deployedChannelId(mergedSecrets)
4103
+ });
4104
+ } catch (error) {
4105
+ warnChannelFlowFailed("URLs", error);
4106
+ }
4107
+ }
3848
4108
  });
3849
4109
 
3850
4110
  // src/cli/commands/domains.ts
3851
- import { confirm as confirm5, select as select6 } from "@inquirer/prompts";
4111
+ import { confirm as confirm7, select as select6 } from "@inquirer/prompts";
3852
4112
  import { Command as Command7, Option as Option8 } from "commander";
3853
4113
  import { colorize as colorize9 } from "consola/utils";
3854
4114
 
@@ -4630,7 +4890,7 @@ Examples:
4630
4890
  context.apiHost
4631
4891
  );
4632
4892
  if (current.verification_status === "verified" && !options.force) {
4633
- const confirmed = await confirm5({
4893
+ const confirmed = await confirm7({
4634
4894
  message: `Remove active domain ${current.domain}? Traffic may stop routing to this project.`,
4635
4895
  default: false
4636
4896
  });
@@ -5134,7 +5394,7 @@ Examples:
5134
5394
  var logs = new Command9("logs").alias("log").configureHelp({ showGlobalOptions: true }).description("View logs from your deployed application.").addCommand(tail, { isDefault: true }).addCommand(query);
5135
5395
 
5136
5396
  // src/cli/commands/projects.ts
5137
- import { confirm as confirm6, input as input6, select as select7 } from "@inquirer/prompts";
5397
+ import { confirm as confirm8, input as input6, select as select7 } from "@inquirer/prompts";
5138
5398
  import { Command as Command10 } from "commander";
5139
5399
  import { colorize as colorize11 } from "consola/utils";
5140
5400
  async function offerCommerceHostingSetup(projectUuid, credentials) {
@@ -5143,7 +5403,7 @@ async function offerCommerceHostingSetup(projectUuid, credentials) {
5143
5403
  await cleanupCloudflareIncompatibilities(projectDir);
5144
5404
  return;
5145
5405
  }
5146
- const shouldSetup = await confirm6({
5406
+ const shouldSetup = await confirm8({
5147
5407
  message: "Your project has been linked, but is not fully set up for Commerce Hosting deployments yet. Would you like to run the setup now?",
5148
5408
  default: true
5149
5409
  });
@@ -5339,7 +5599,7 @@ Examples:
5339
5599
  }
5340
5600
  if (!options.force) {
5341
5601
  const label = targetName ? `"${targetName}" (${targetUuid})` : targetUuid;
5342
- const confirmed = await confirm6({
5602
+ const confirmed = await confirm8({
5343
5603
  message: `Are you sure you want to delete project ${label}? This action is irreversible and will permanently destroy the project and all of its data.`,
5344
5604
  default: false
5345
5605
  });
@@ -5462,7 +5722,7 @@ Examples:
5462
5722
 
5463
5723
  // src/cli/commands/upgrade.ts
5464
5724
  import { Command as Command13, Option as Option11 } from "@commander-js/extra-typings";
5465
- import { confirm as confirm7 } from "@inquirer/prompts";
5725
+ import { confirm as confirm9 } from "@inquirer/prompts";
5466
5726
  import { execa as execa4 } from "execa";
5467
5727
  import {
5468
5728
  access as access3,
@@ -6042,7 +6302,7 @@ async function resolveBaseRef(project, fromOption, assumeYes) {
6042
6302
  const guess = `${family}@${project.pkg.version}`;
6043
6303
  consola.warn(`No \`catalyst.ref\` found in package.json. Inferred base: ${guess}`);
6044
6304
  if (assumeYes) return guess;
6045
- const ok = await confirm7({ message: `Upgrade from ${guess}?`, default: true }).catch(() => false);
6305
+ const ok = await confirm9({ message: `Upgrade from ${guess}?`, default: true }).catch(() => false);
6046
6306
  if (!ok) {
6047
6307
  consola.info("Re-run with `--from <ref>` to set the base version explicitly.");
6048
6308
  return null;
@@ -6344,7 +6604,7 @@ to raise the GitHub API rate limit.`
6344
6604
  printWorkspaceFindings(workspaceDeps);
6345
6605
  let migratedDeps = 0;
6346
6606
  if (workspaceDeps.length) {
6347
- const accepted = stampedText !== null && (options.yes === true || Boolean(process.stdin.isTTY) && await confirm7({
6607
+ const accepted = stampedText !== null && (options.yes === true || Boolean(process.stdin.isTTY) && await confirm9({
6348
6608
  message: "Replace them with the published versions?",
6349
6609
  default: true
6350
6610
  }).catch(() => false));