@bigcommerce/catalyst 1.4.0 → 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.0",
28
+ version: "1.4.2",
29
29
  type: "module",
30
30
  repository: {
31
31
  type: "git",
@@ -215,7 +215,7 @@ import { Command as Command15 } from "commander";
215
215
  import { colorize as colorize13 } from "consola/utils";
216
216
 
217
217
  // src/cli/commands/auth.ts
218
- import { Command } from "commander";
218
+ import { Command, Option as Option2 } from "commander";
219
219
  import { z as z5 } from "zod";
220
220
 
221
221
  // src/cli/lib/auth-errors.ts
@@ -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
  });
@@ -685,6 +693,19 @@ async function fetchStoreProfile2(storeHash, accessToken, apiHost) {
685
693
  }
686
694
  return result.data.data;
687
695
  }
696
+ async function checkCredentials(storeHash, accessToken, apiHost) {
697
+ try {
698
+ const { store_name: storeName } = await fetchStoreProfile2(storeHash, accessToken, apiHost);
699
+ return { valid: true, storeName };
700
+ } catch (error) {
701
+ if (error instanceof UnauthorizedError) return { valid: false, reason: "unauthorized" };
702
+ return {
703
+ valid: false,
704
+ reason: "unverifiable",
705
+ message: error instanceof Error ? error.message : String(error)
706
+ };
707
+ }
708
+ }
688
709
  var whoami = new Command("whoami").configureHelp({ showGlobalOptions: true }).description("Verify stored credentials and display store/project info.").addHelpText(
689
710
  "after",
690
711
  `
@@ -739,7 +760,7 @@ Example:
739
760
  }
740
761
  });
741
762
  var login2 = new Command("login").configureHelp({ showGlobalOptions: true }).description(
742
- "Authenticate via browser using the OAuth device code flow. Falls back to an interactive store hash + access token prompt if the browser flow is unavailable. If already logged in, displays current credentials and suggests running `catalyst auth logout` to re-authenticate."
763
+ "Authenticate via browser using the OAuth device code flow. Falls back to an interactive store hash + access token prompt if the browser flow is unavailable. Passing --store-hash and --access-token verifies and stores those credentials instead. If already logged in with credentials that still work, displays them and suggests `catalyst auth logout`; if the stored token has expired or been revoked, re-authenticates in place."
743
764
  ).addHelpText(
744
765
  "after",
745
766
  `
@@ -748,19 +769,67 @@ Examples:
748
769
  $ catalyst auth login
749
770
 
750
771
  # Login with existing credentials (skips interactive flow)
751
- $ catalyst auth login --store-hash <STORE_HASH> --access-token <ACCESS_TOKEN>`
752
- ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(loginUrlOption()).action(async (options) => {
772
+ $ catalyst auth login --store-hash <STORE_HASH> --access-token <ACCESS_TOKEN>
773
+
774
+ # Re-authenticate without checking the stored credentials first
775
+ $ catalyst auth login --force`
776
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(loginUrlOption()).addOption(
777
+ new Option2(
778
+ "--force",
779
+ "Skip the check on any credentials already stored and re-authenticate regardless."
780
+ )
781
+ ).action(async (options, command) => {
753
782
  try {
754
783
  const config2 = getProjectConfig();
755
784
  const apiHost = resolveApiHost(options, config2);
756
785
  const storeHash = options.storeHash ?? config2.get("storeHash");
757
786
  const accessToken = options.accessToken ?? config2.get("accessToken");
758
- if (storeHash && accessToken) {
759
- consola.info(`Already logged in to store ${storeHash}.`);
760
- consola.info("Run `catalyst auth logout` first to re-authenticate.");
787
+ const suppliedOnCli = command.getOptionValueSource("storeHash") === "cli" && command.getOptionValueSource("accessToken") === "cli";
788
+ if (suppliedOnCli && storeHash && accessToken) {
789
+ if (!options.force) {
790
+ const status = await checkCredentials(storeHash, accessToken, apiHost);
791
+ if (!status.valid && status.reason === "unauthorized") {
792
+ consola.error(`The credentials provided for store ${storeHash} were rejected.`);
793
+ consola.info(
794
+ "Check the store hash and access token, or run `catalyst auth login` without them to authenticate in the browser."
795
+ );
796
+ process.exit(1);
797
+ return;
798
+ }
799
+ if (!status.valid) {
800
+ consola.warn(`Couldn't verify the credentials provided: ${status.message}`);
801
+ }
802
+ }
803
+ config2.set("storeHash", storeHash);
804
+ config2.set("accessToken", accessToken);
805
+ consola.success(`Logged in to store ${storeHash}.`);
761
806
  process.exit(0);
762
807
  return;
763
808
  }
809
+ if (storeHash && accessToken && !options.force) {
810
+ const status = await checkCredentials(storeHash, accessToken, apiHost);
811
+ if (status.valid) {
812
+ consola.info(`Already logged in to ${status.storeName} (${storeHash}).`);
813
+ consola.info(
814
+ "Run `catalyst auth logout` first to re-authenticate, or `catalyst auth login --force` to skip this check."
815
+ );
816
+ process.exit(0);
817
+ return;
818
+ }
819
+ if (status.reason === "unverifiable") {
820
+ consola.warn(
821
+ `Found credentials for store ${storeHash} but couldn't verify them: ${status.message}`
822
+ );
823
+ consola.info(
824
+ "Keeping them. Re-run once the API is reachable, or `catalyst auth login --force` to re-authenticate regardless."
825
+ );
826
+ process.exit(0);
827
+ return;
828
+ }
829
+ consola.warn(
830
+ `Stored credentials for store ${storeHash} are no longer valid \u2014 re-authenticating.`
831
+ );
832
+ }
764
833
  const credentials = await login(options.loginUrl, apiHost);
765
834
  config2.set("storeHash", credentials.storeHash);
766
835
  config2.set("accessToken", credentials.accessToken);
@@ -807,7 +876,7 @@ Example:
807
876
  var auth = new Command("auth").configureHelp({ showGlobalOptions: true }).description("Manage authentication for the BigCommerce CLI.").addCommand(whoami).addCommand(login2).addCommand(logout);
808
877
 
809
878
  // src/cli/commands/build.ts
810
- import { Command as Command2, InvalidArgumentError, Option as Option2 } from "commander";
879
+ import { Command as Command2, InvalidArgumentError, Option as Option3 } from "commander";
811
880
  import { execa as execa2 } from "execa";
812
881
  import { copyFile, cp, rm, writeFile } from "fs/promises";
813
882
  import { join as join6 } from "path";
@@ -918,7 +987,7 @@ var cleanupCloudflareIncompatibilities = async (projectDir) => {
918
987
  return;
919
988
  }
920
989
  const shouldRemove = await confirm2({
921
- message: 'Catalyst found instrumentation.ts, which is incompatible with the Cloudflare Workers bundle when it uses @vercel/otel (causes "Failed to prepare server" at cold start). Remove it and drop @vercel/otel from package.json?',
990
+ message: 'Catalyst found instrumentation.ts, which is incompatible with the Cloudflare Workers bundle when it uses @vercel/otel (causes "Failed to prepare server" at cold start).\nRemove it and drop @vercel/otel from package.json? (required for native hosting)',
922
991
  default: true
923
992
  });
924
993
  if (!shouldRemove) {
@@ -1489,6 +1558,8 @@ async function buildCatalystProject(projectUuid, wranglerVersion = WRANGLER_VERS
1489
1558
  "pnpm",
1490
1559
  [
1491
1560
  "dlx",
1561
+ "--allow-build=esbuild",
1562
+ "--allow-build=workerd",
1492
1563
  `wrangler@${wranglerVersion}`,
1493
1564
  "deploy",
1494
1565
  "--config",
@@ -1523,12 +1594,12 @@ Examples:
1523
1594
  # Build with a specific Wrangler version
1524
1595
  $ catalyst build --wrangler-version 4.24.3`
1525
1596
  ).addOption(
1526
- new Option2(
1597
+ new Option3(
1527
1598
  "--project-uuid <uuid>",
1528
1599
  "Project UUID to be included in the deployment configuration."
1529
1600
  ).env("CATALYST_PROJECT_UUID")
1530
1601
  ).addOption(
1531
- new Option2(
1602
+ new Option3(
1532
1603
  "--wrangler-version <version>",
1533
1604
  `Wrangler version or dist-tag to build with. Defaults to ${WRANGLER_VERSION}.`
1534
1605
  ).argParser(parseWranglerVersion)
@@ -1556,12 +1627,17 @@ Examples:
1556
1627
  });
1557
1628
 
1558
1629
  // src/cli/commands/channels.ts
1559
- import { confirm as confirm3, select as select4 } from "@inquirer/prompts";
1560
- import { Command as Command3, InvalidArgumentError as InvalidArgumentError2, Option as Option3 } from "commander";
1630
+ import { confirm as confirm4, select as select4 } from "@inquirer/prompts";
1631
+ import { Command as Command3, InvalidArgumentError as InvalidArgumentError2, Option as Option4 } from "commander";
1561
1632
  import { colorize as colorize5 } from "consola/utils";
1562
1633
 
1634
+ // src/cli/lib/can-prompt.ts
1635
+ function canPrompt() {
1636
+ return Boolean(process.stdin.isTTY) && !process.env.CI;
1637
+ }
1638
+
1563
1639
  // src/cli/lib/channel-checkout-url-flow.ts
1564
- import { input as input3 } from "@inquirer/prompts";
1640
+ import { confirm as confirm3, input as input3 } from "@inquirer/prompts";
1565
1641
 
1566
1642
  // src/cli/lib/channel-site-flow.ts
1567
1643
  import { select as select2 } from "@inquirer/prompts";
@@ -1776,7 +1852,6 @@ async function deleteChannelCheckoutUrl(channelId, storeHash, accessToken, apiHo
1776
1852
  // src/cli/lib/checkout-url.ts
1777
1853
  import { getDomain } from "tldts";
1778
1854
  var normalizeHostname = (hostname) => hostname.toLowerCase().replace(/\.$/, "");
1779
- var parentDomain = (hostname) => hostname.split(".").slice(1).join(".");
1780
1855
  var isSubdomainOf = (hostname, parent) => hostname.endsWith(`.${parent}`);
1781
1856
  function hostnameOf(url) {
1782
1857
  try {
@@ -1789,11 +1864,45 @@ function sharesMainDomain(a, b) {
1789
1864
  const first = getDomain(normalizeHostname(a));
1790
1865
  return first !== null && first === getDomain(normalizeHostname(b));
1791
1866
  }
1792
- 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 } = {}) {
1793
1901
  const host = hostnameOf(storefrontUrl);
1794
1902
  if (!host) return void 0;
1795
- const base = normalizeHostname(host).replace(/^www\./, "");
1796
- 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\./, "")}`;
1797
1906
  }
1798
1907
  function normalizeCheckoutUrl(value) {
1799
1908
  const withScheme = /^[a-z][a-z\d+.-]*:\/\//i.test(value) ? value : `https://${value}`;
@@ -1812,18 +1921,24 @@ function normalizeCheckoutUrl(value) {
1812
1921
  }
1813
1922
  return parsed.origin;
1814
1923
  }
1815
- async function isManagedHostingHostname(hostname, context) {
1816
- try {
1817
- const projects2 = await fetchProjects(context.storeHash, context.accessToken, context.apiHost);
1818
- const zones = projects2.flatMap((project) => project.deployment_hostnames).map((deploymentHostname) => parentDomain(normalizeHostname(deploymentHostname))).filter((zone) => zone.includes("."));
1819
- if (zones.length === 0) return void 0;
1820
- const host = normalizeHostname(hostname);
1821
- return zones.some((zone) => host === zone || isSubdomainOf(host, zone));
1822
- } catch {
1823
- return void 0;
1824
- }
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));
1825
1940
  }
1826
- async function warnOnCrossDomainCheckout(site, context) {
1941
+ function warnOnCrossDomainCheckout(site) {
1827
1942
  const storefrontUrl = findChannelSiteUrl(site, "primary") ?? site.url;
1828
1943
  const storefrontHost = hostnameOf(storefrontUrl);
1829
1944
  const checkoutUrl = findChannelSiteUrl(site, "checkout");
@@ -1845,22 +1960,23 @@ async function warnOnCrossDomainCheckout(site, context) {
1845
1960
  consola.info(
1846
1961
  "Shopper sessions and carts may not carry into checkout in browsers that restrict cross-domain cookies."
1847
1962
  );
1848
- const storefrontOnManagedZone = await isManagedHostingHostname(storefrontHost, context);
1849
- const report = { crossDomain: true, storefrontOnManagedZone, suggestion };
1850
- 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) {
1851
1970
  consola.info(
1852
- `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:`
1853
1972
  );
1854
- return report;
1855
- }
1856
- if (storefrontOnManagedZone === true) {
1857
- consola.info(
1858
- `${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>"}`
1859
1975
  );
1860
1976
  return report;
1861
1977
  }
1862
1978
  consola.info(
1863
- "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.`
1864
1980
  );
1865
1981
  return report;
1866
1982
  }
@@ -1900,7 +2016,7 @@ async function resolveChannel(options) {
1900
2016
  );
1901
2017
  }
1902
2018
  const id = await select2({
1903
- message: "Select the channel to update.",
2019
+ message: options.message ?? "Select the channel to update.",
1904
2020
  choices: catalystChannels.map((c) => ({
1905
2021
  name: c.name,
1906
2022
  value: c.id,
@@ -1934,15 +2050,26 @@ async function runChannelSiteUrlFlow(options) {
1934
2050
  const channel = await resolveChannel(options);
1935
2051
  const hostname = await resolveHostname(project, options);
1936
2052
  const siteUrl = hostname.startsWith("https://") ? hostname : `https://${hostname}`;
1937
- await updateChannelSiteUrl(
2053
+ const channelLabel = channel.name ? `"${channel.name}" (${channel.id})` : String(channel.id);
2054
+ const current = await getChannelSite(
1938
2055
  channel.id,
1939
- siteUrl,
1940
2056
  options.storeHash,
1941
2057
  options.accessToken,
1942
2058
  options.apiHost
1943
- );
1944
- const channelLabel = channel.name ? `"${channel.name}" (${channel.id})` : String(channel.id);
1945
- 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 };
1946
2073
  try {
1947
2074
  const site = await getChannelSite(
1948
2075
  channel.id,
@@ -1950,11 +2077,7 @@ async function runChannelSiteUrlFlow(options) {
1950
2077
  options.accessToken,
1951
2078
  options.apiHost
1952
2079
  );
1953
- const report = await warnOnCrossDomainCheckout(site, {
1954
- storeHash: options.storeHash,
1955
- accessToken: options.accessToken,
1956
- apiHost: options.apiHost
1957
- });
2080
+ const report = warnOnCrossDomainCheckout(site);
1958
2081
  if (report.crossDomain && report.storefrontOnManagedZone !== true) {
1959
2082
  consola.info("Set a checkout URL for this channel with:");
1960
2083
  consola.log(
@@ -1963,13 +2086,22 @@ async function runChannelSiteUrlFlow(options) {
1963
2086
  }
1964
2087
  } catch {
1965
2088
  }
1966
- return { channelId: channel.id };
2089
+ return { channelId: channel.id, hostname };
1967
2090
  }
1968
2091
 
1969
2092
  // src/cli/lib/channel-checkout-url-flow.ts
1970
2093
  async function runChannelCheckoutUrlFlow(options) {
1971
2094
  const channel = options.channelId !== void 0 ? { id: options.channelId, name: options.channelName } : await resolveChannel(options);
1972
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
+ }
1973
2105
  let storefrontUrl = options.storefrontUrl;
1974
2106
  if (!storefrontUrl) {
1975
2107
  const site = await getChannelSite(
@@ -1987,6 +2119,16 @@ async function runChannelCheckoutUrlFlow(options) {
1987
2119
  );
1988
2120
  }
1989
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
+ }
1990
2132
  const answer = options.url ?? await input3({
1991
2133
  message: "Checkout URL for this channel (must share a main domain with the storefront)",
1992
2134
  default: suggestCheckoutUrl(storefrontUrl)
@@ -2001,6 +2143,78 @@ async function runChannelCheckoutUrlFlow(options) {
2001
2143
  );
2002
2144
  consola.success(`Updated channel ${label} checkout URL to ${checkoutUrl}.`);
2003
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
+ }
2004
2218
 
2005
2219
  // src/cli/lib/create-channel-flow.ts
2006
2220
  import { checkbox, input as input4, select as select3 } from "@inquirer/prompts";
@@ -2317,22 +2531,22 @@ Examples:
2317
2531
  # Fall back to the shared checkout domain
2318
2532
  $ catalyst channels update --channel-id 123 --remove-checkout-url`
2319
2533
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).addOption(
2320
- new Option3(
2534
+ new Option4(
2321
2535
  "--channel-id <id>",
2322
2536
  "Skip the channel prompt and target this channel directly."
2323
2537
  ).argParser((value) => Number(value))
2324
2538
  ).addOption(
2325
- new Option3(
2539
+ new Option4(
2326
2540
  "--hostname <hostname>",
2327
2541
  "Skip the hostname prompt and use this hostname directly. Must be one of the project's deployment_hostnames."
2328
2542
  )
2329
2543
  ).addOption(
2330
- new Option3(
2544
+ new Option4(
2331
2545
  "--checkout-url <url>",
2332
2546
  "Set the channel's checkout URL. Must share a main domain with the storefront URL."
2333
2547
  ).argParser(parseCheckoutUrl)
2334
2548
  ).addOption(
2335
- new Option3(
2549
+ new Option4(
2336
2550
  "--remove-checkout-url",
2337
2551
  "Remove the channel's custom checkout URL so checkout falls back to the shared checkout domain."
2338
2552
  ).conflicts("checkoutUrl")
@@ -2343,16 +2557,19 @@ Examples:
2343
2557
  await getTelemetry().identify(storeHash);
2344
2558
  const touchesCheckout = options.checkoutUrl !== void 0 || options.removeCheckoutUrl === true;
2345
2559
  const updatesSiteUrl = options.hostname !== void 0 || !touchesCheckout;
2560
+ const offersCheckout = updatesSiteUrl && !touchesCheckout && canPrompt();
2346
2561
  let channelId = options.channelId;
2562
+ let siteHostname;
2347
2563
  if (updatesSiteUrl) {
2348
2564
  try {
2349
- ({ channelId } = await runChannelSiteUrlFlow({
2565
+ ({ channelId, hostname: siteHostname } = await runChannelSiteUrlFlow({
2350
2566
  storeHash,
2351
2567
  accessToken,
2352
2568
  apiHost,
2353
2569
  projectUuid: options.projectUuid ?? config2.get("projectUuid"),
2354
2570
  channelId: options.channelId,
2355
- hostname: options.hostname
2571
+ hostname: options.hostname,
2572
+ diagnoseCheckout: !offersCheckout
2356
2573
  }));
2357
2574
  } catch (error) {
2358
2575
  if (error instanceof NoLinkedProjectError) {
@@ -2365,6 +2582,22 @@ Examples:
2365
2582
  throw error;
2366
2583
  }
2367
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
+ }
2368
2601
  if (touchesCheckout) {
2369
2602
  const channel = channelId === void 0 ? await resolveChannel({ storeHash, accessToken, apiHost }) : { id: channelId, name: void 0 };
2370
2603
  const label = channel.name ? `"${channel.name}" (${channel.id})` : String(channel.id);
@@ -2378,7 +2611,7 @@ Examples:
2378
2611
  );
2379
2612
  consola.success(`Updated channel ${label} checkout URL to ${options.checkoutUrl}.`);
2380
2613
  reportChannelSite(updated);
2381
- await warnOnCrossDomainCheckout(updated, { storeHash, accessToken, apiHost });
2614
+ warnOnCrossDomainCheckout(updated);
2382
2615
  } else {
2383
2616
  await deleteChannelCheckoutUrl(channel.id, storeHash, accessToken, apiHost);
2384
2617
  consola.success(
@@ -2386,7 +2619,7 @@ Examples:
2386
2619
  );
2387
2620
  const reverted = await getChannelSite(channel.id, storeHash, accessToken, apiHost);
2388
2621
  reportChannelSite(reverted);
2389
- await warnOnCrossDomainCheckout(reverted, { storeHash, accessToken, apiHost });
2622
+ warnOnCrossDomainCheckout(reverted);
2390
2623
  }
2391
2624
  }
2392
2625
  process.exit(0);
@@ -2406,14 +2639,14 @@ Examples:
2406
2639
  # Append extra environment variables to .env.local
2407
2640
  $ catalyst channels link --channel-id 123 --env MY_FLAG=1`
2408
2641
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(loginUrlOption()).addOption(
2409
- new Option3("--channel-id <id>", "Link this channel directly, skipping the picker.").argParser(
2642
+ new Option4("--channel-id <id>", "Link this channel directly, skipping the picker.").argParser(
2410
2643
  parseChannelId
2411
2644
  )
2412
2645
  ).option(
2413
2646
  "--env <vars...>",
2414
2647
  "Arbitrary environment variables to set in .env.local. Format: KEY=VALUE (repeatable)."
2415
2648
  ).addOption(
2416
- new Option3("--cli-api-origin <origin>", "Catalyst CLI API origin").default("https://cxm-prd.bigcommerceapp.com").hideHelp()
2649
+ new Option4("--cli-api-origin <origin>", "Catalyst CLI API origin").default("https://cxm-prd.bigcommerceapp.com").hideHelp()
2417
2650
  ).action(async (options) => {
2418
2651
  const config2 = getProjectConfig();
2419
2652
  const apiHost = resolveApiHost(options, config2);
@@ -2484,11 +2717,11 @@ Examples:
2484
2717
  ).option(
2485
2718
  "--additional-locales <locales...>",
2486
2719
  "Additional storefront locales, max 4 (e.g. --additional-locales es fr). Skips the additional-languages prompt."
2487
- ).addOption(new Option3("--sample-data", "Install sample data on the new channel.")).addOption(new Option3("--no-sample-data", "Create the channel without sample data.")).option(
2720
+ ).addOption(new Option4("--sample-data", "Install sample data on the new channel.")).addOption(new Option4("--no-sample-data", "Create the channel without sample data.")).option(
2488
2721
  "--link",
2489
2722
  "Link the new channel to this project \u2014 write its credentials to .env.local without prompting."
2490
2723
  ).addOption(
2491
- new Option3("--cli-api-origin <origin>", "Catalyst CLI API origin").default("https://cxm-prd.bigcommerceapp.com").hideHelp()
2724
+ new Option4("--cli-api-origin <origin>", "Catalyst CLI API origin").default("https://cxm-prd.bigcommerceapp.com").hideHelp()
2492
2725
  ).action(async (options) => {
2493
2726
  if (options.additionalLocales && options.additionalLocales.length > 4) {
2494
2727
  consola.error("You can only set up to 4 additional locales.");
@@ -2570,7 +2803,10 @@ async function resolveChannelTarget(options) {
2570
2803
  storeHash,
2571
2804
  accessToken,
2572
2805
  apiHost,
2573
- channelId: options.channelId
2806
+ channelId: options.channelId,
2807
+ // Read-only: these commands report on a channel, so the picker must not
2808
+ // say "to update" — the shared default is written for `channels update`.
2809
+ message: "Select a channel."
2574
2810
  });
2575
2811
  return {
2576
2812
  config: config2,
@@ -2592,7 +2828,7 @@ Examples:
2592
2828
  # Target a channel directly
2593
2829
  $ catalyst channels info --channel-id 123`
2594
2830
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(
2595
- new Option3(
2831
+ new Option4(
2596
2832
  "--channel-id <id>",
2597
2833
  "Skip the channel prompt and target this channel directly."
2598
2834
  ).argParser(parseChannelId)
@@ -2602,7 +2838,7 @@ Examples:
2602
2838
  const site = await getChannelSite(channel.id, storeHash, accessToken, apiHost);
2603
2839
  consola.success(`Channel ${label}:`);
2604
2840
  reportChannelSite(site);
2605
- const report = await warnOnCrossDomainCheckout(site, { storeHash, accessToken, apiHost });
2841
+ const report = warnOnCrossDomainCheckout(site);
2606
2842
  if (report.crossDomain && report.storefrontOnManagedZone !== true) {
2607
2843
  const suggested = report.suggestion ?? "<domain>";
2608
2844
  if (!process.stdin.isTTY) {
@@ -2612,7 +2848,7 @@ Examples:
2612
2848
  process.exit(0);
2613
2849
  return;
2614
2850
  }
2615
- const shouldSet = await confirm3({
2851
+ const shouldSet = await confirm4({
2616
2852
  message: "Set a checkout URL for this channel now?",
2617
2853
  default: true
2618
2854
  });
@@ -2638,7 +2874,7 @@ Examples:
2638
2874
  var channels = new Command3("channels").alias("channel").configureHelp({ showGlobalOptions: true }).description("Manage BigCommerce channels.").addCommand(info, { isDefault: true }).addCommand(create).addCommand(link).addCommand(update);
2639
2875
 
2640
2876
  // src/cli/commands/create.ts
2641
- import { Command as Command4, InvalidArgumentError as InvalidArgumentError3, Option as Option4 } from "@commander-js/extra-typings";
2877
+ import { Command as Command4, InvalidArgumentError as InvalidArgumentError3, Option as Option5 } from "@commander-js/extra-typings";
2642
2878
  import { input as input5, select as select5 } from "@inquirer/prompts";
2643
2879
  import { execSync as execSync2 } from "child_process";
2644
2880
  import { colorize as colorize6 } from "consola/utils";
@@ -2915,7 +3151,7 @@ Examples:
2915
3151
  "Arbitrary environment variables to set in .env.local. Format: KEY=VALUE (repeatable).",
2916
3152
  parseEnvFlag
2917
3153
  ).addOption(
2918
- new Option4(
3154
+ new Option5(
2919
3155
  "--hosting <mode>",
2920
3156
  'Hosting mode: "self-hosted" (default) or "commerce" to set up Commerce Hosting at create time. When omitted, scaffolding is hosting-agnostic; run `catalyst deploy` later to opt in.'
2921
3157
  ).choices(["self-hosted", "commerce"])
@@ -2923,11 +3159,11 @@ Examples:
2923
3159
  "--use-existing",
2924
3160
  "Only used with --hosting commerce and --project-name. When the named project already exists on the store, reuse it instead of prompting. Has no effect without --hosting commerce."
2925
3161
  ).addOption(
2926
- new Option4("--bigcommerce-hostname <hostname>", "BigCommerce hostname").default("bigcommerce.com").hideHelp()
3162
+ new Option5("--bigcommerce-hostname <hostname>", "BigCommerce hostname").default("bigcommerce.com").hideHelp()
2927
3163
  ).addOption(
2928
- new Option4("--login-url <url>", "BigCommerce login URL.").env("BIGCOMMERCE_LOGIN_URL").default(DEFAULT_LOGIN_URL).hideHelp()
3164
+ new Option5("--login-url <url>", "BigCommerce login URL.").env("BIGCOMMERCE_LOGIN_URL").default(DEFAULT_LOGIN_URL).hideHelp()
2929
3165
  ).addOption(
2930
- new Option4("--cli-api-origin <origin>", "Catalyst CLI API origin").default("https://cxm-prd.bigcommerceapp.com").hideHelp()
3166
+ new Option5("--cli-api-origin <origin>", "Catalyst CLI API origin").default("https://cxm-prd.bigcommerceapp.com").hideHelp()
2931
3167
  ).action(async (options) => {
2932
3168
  const { ghRef, repository } = options;
2933
3169
  if (options.useExisting && options.hosting !== "commerce") {
@@ -3083,7 +3319,7 @@ ${steps.map((step) => ` ${colorize6("yellow", step)}`).join("\n")}`
3083
3319
  });
3084
3320
 
3085
3321
  // src/cli/commands/debug.ts
3086
- import { Command as Command5, Option as Option5 } from "commander";
3322
+ import { Command as Command5, Option as Option6 } from "commander";
3087
3323
 
3088
3324
  // src/cli/lib/collect-diagnostics.ts
3089
3325
  import { parse } from "dotenv";
@@ -3334,7 +3570,7 @@ Examples:
3334
3570
 
3335
3571
  # Print machine-readable JSON (useful for copy/paste or piping)
3336
3572
  $ catalyst debug --json`
3337
- ).addOption(new Option5("--json", "Output the report as JSON.")).action((options) => {
3573
+ ).addOption(new Option6("--json", "Output the report as JSON.")).action((options) => {
3338
3574
  const diagnostics = collectDiagnostics();
3339
3575
  if (options.json) {
3340
3576
  consola.log(JSON.stringify(diagnostics, null, 2));
@@ -3344,15 +3580,77 @@ Examples:
3344
3580
  });
3345
3581
 
3346
3582
  // src/cli/commands/deploy.ts
3347
- import { confirm as confirm4 } from "@inquirer/prompts";
3583
+ import { confirm as confirm6 } from "@inquirer/prompts";
3348
3584
  import AdmZip from "adm-zip";
3349
- import { Command as Command6, Option as Option6 } from "commander";
3585
+ import { Command as Command6, Option as Option7 } from "commander";
3350
3586
  import { colorize as colorize7 } from "consola/utils";
3351
3587
  import { access as access2, readdir, readFile } from "fs/promises";
3352
3588
  import { join as join13 } from "path";
3353
3589
  import yoctoSpinner3 from "yocto-spinner";
3354
3590
  import { z as z13 } from "zod";
3355
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
+
3356
3654
  // src/cli/lib/deployment-errors.ts
3357
3655
  var ERROR_MESSAGES = {
3358
3656
  10: "Something went wrong on our end. Please try again. If the issue persists, contact support.",
@@ -3479,6 +3777,11 @@ var uploadBundleZip = async (uploadUrl) => {
3479
3777
  consola.success("Bundle uploaded successfully.");
3480
3778
  return true;
3481
3779
  };
3780
+ var NATIVE_HOSTING_ENV = {
3781
+ type: "plain_text",
3782
+ key: "CATALYST_HOSTING",
3783
+ value: "native"
3784
+ };
3482
3785
  var parseEnvironmentVariables = (secretOption) => {
3483
3786
  return secretOption?.map((envVar) => {
3484
3787
  const { key, value } = parseEnvAssignment(envVar);
@@ -3607,6 +3910,10 @@ var deploy = new Command6("deploy").configureHelp({ showGlobalOptions: true }).d
3607
3910
  Environment variables saved with \`catalyst env add\` are sent automatically on every deploy.
3608
3911
  Use \`--secret\` to set or override a variable for a single run.
3609
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
+
3610
3917
  Example:
3611
3918
  $ catalyst deploy --secret BIGCOMMERCE_STORE_HASH=<YOUR_STORE_HASH> --secret BIGCOMMERCE_STOREFRONT_TOKEN=<YOUR_STOREFRONT_TOKEN>`
3612
3919
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option(
@@ -3616,7 +3923,7 @@ Example:
3616
3923
  "--update-checkout-url",
3617
3924
  "After a successful deploy, prompt to update a channel's checkout URL."
3618
3925
  ).addOption(
3619
- new Option6(
3926
+ new Option7(
3620
3927
  "--secret <value>",
3621
3928
  "Secret to set for this deployment (repeatable). Overrides a stored value for the same key. Format: --secret KEY=VALUE"
3622
3929
  ).argParser((value, previous = []) => {
@@ -3626,7 +3933,7 @@ Example:
3626
3933
  "--prebuilt",
3627
3934
  "Skip the build step. Requires .bigcommerce/dist/ to already contain build output."
3628
3935
  ).addOption(
3629
- new Option6(
3936
+ new Option7(
3630
3937
  "--wrangler-version <version>",
3631
3938
  `Wrangler version or dist-tag to build with. Ignored with --prebuilt. Defaults to ${WRANGLER_VERSION}.`
3632
3939
  ).argParser(parseWranglerVersion)
@@ -3676,7 +3983,7 @@ Example:
3676
3983
  consola.success(`Linked project "${selected.name}".`);
3677
3984
  }
3678
3985
  if (!getProjectState().isTransformed) {
3679
- const shouldSetup = await confirm4({
3986
+ const shouldSetup = await confirm6({
3680
3987
  message: "Your project is not yet set up for Commerce Hosting deployments. Would you like to run the Commerce Hosting setup now?",
3681
3988
  default: true
3682
3989
  });
@@ -3732,7 +4039,7 @@ Example:
3732
4039
  const flagSecrets = parseEnvironmentVariables(options.secret) ?? [];
3733
4040
  const persistedSecrets = toDeploymentSecrets(getStoredEnv(config2));
3734
4041
  const mergedSecrets = mergeDeploymentSecrets(persistedSecrets, flagSecrets);
3735
- const environmentVariables = mergedSecrets.length > 0 ? mergedSecrets : void 0;
4042
+ const environmentVariables = [...mergedSecrets, NATIVE_HOSTING_ENV];
3736
4043
  const { deployment_uuid: deploymentUuid } = await createDeployment(
3737
4044
  projectUuid,
3738
4045
  uploadSignature.upload_uuid,
@@ -3756,9 +4063,10 @@ Example:
3756
4063
  );
3757
4064
  };
3758
4065
  let resolvedChannelId;
4066
+ let siteHostname;
3759
4067
  if (options.updateSiteUrl) {
3760
4068
  try {
3761
- ({ channelId: resolvedChannelId } = await runChannelSiteUrlFlow({
4069
+ ({ channelId: resolvedChannelId, hostname: siteHostname } = await runChannelSiteUrlFlow({
3762
4070
  storeHash,
3763
4071
  accessToken,
3764
4072
  apiHost,
@@ -3775,17 +4083,33 @@ Example:
3775
4083
  storeHash,
3776
4084
  accessToken,
3777
4085
  apiHost,
3778
- channelId: resolvedChannelId
4086
+ channelId: resolvedChannelId,
4087
+ storefrontHostname: siteHostname
3779
4088
  });
3780
4089
  } catch (error) {
3781
4090
  warnChannelFlowFailed("checkout URL", error);
3782
4091
  }
3783
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
+ }
3784
4108
  });
3785
4109
 
3786
4110
  // src/cli/commands/domains.ts
3787
- import { confirm as confirm5, select as select6 } from "@inquirer/prompts";
3788
- import { Command as Command7, Option as Option7 } from "commander";
4111
+ import { confirm as confirm7, select as select6 } from "@inquirer/prompts";
4112
+ import { Command as Command7, Option as Option8 } from "commander";
3789
4113
  import { colorize as colorize9 } from "consola/utils";
3790
4114
 
3791
4115
  // src/cli/lib/domains.ts
@@ -4357,7 +4681,7 @@ Examples:
4357
4681
  # Show pending domains only
4358
4682
  $ catalyst domains list --status pending`
4359
4683
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option("--domain <domain>", "Only show a specific domain.").addOption(
4360
- new Option7("--status <status>", "Filter by verification status.").choices(
4684
+ new Option8("--status <status>", "Filter by verification status.").choices(
4361
4685
  DOMAIN_STATUS_FILTERS
4362
4686
  )
4363
4687
  ).action(async (options) => {
@@ -4566,7 +4890,7 @@ Examples:
4566
4890
  context.apiHost
4567
4891
  );
4568
4892
  if (current.verification_status === "verified" && !options.force) {
4569
- const confirmed = await confirm5({
4893
+ const confirmed = await confirm7({
4570
4894
  message: `Remove active domain ${current.domain}? Traffic may stop routing to this project.`,
4571
4895
  default: false
4572
4896
  });
@@ -4662,7 +4986,7 @@ var env = new Command8("env").configureHelp({ showGlobalOptions: true }).descrip
4662
4986
  ).addCommand(add2).addCommand(remove2).addCommand(list2);
4663
4987
 
4664
4988
  // src/cli/commands/logs.ts
4665
- import { Command as Command9, InvalidArgumentError as InvalidArgumentError4, Option as Option8 } from "commander";
4989
+ import { Command as Command9, InvalidArgumentError as InvalidArgumentError4, Option as Option9 } from "commander";
4666
4990
  import { colorize as colorize10 } from "consola/utils";
4667
4991
  import { z as z16 } from "zod";
4668
4992
  var DEFAULT_CONNECTION_TTL_MS = 1 * 60 * 1e3;
@@ -4879,7 +5203,7 @@ Examples:
4879
5203
  # Tail logs as raw JSON (useful for piping to other tools)
4880
5204
  $ catalyst logs tail --format json`
4881
5205
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).addOption(
4882
- new Option8("--format <format>", "Output format for log events.").choices(["json", "pretty", "default", "short", "request"]).default("default")
5206
+ new Option9("--format <format>", "Output format for log events.").choices(["json", "pretty", "default", "short", "request"]).default("default")
4883
5207
  ).action(async (options) => {
4884
5208
  try {
4885
5209
  const config2 = getProjectConfig();
@@ -4980,7 +5304,7 @@ Examples:
4980
5304
  # Raw JSON (NDJSON) for piping to other tools
4981
5305
  $ catalyst logs query --since 2d --format json`
4982
5306
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).addOption(
4983
- new Option8(
5307
+ new Option9(
4984
5308
  "--since <duration>",
4985
5309
  "Relative window ending at --end (default: now), e.g. 30m, 6h, 2d (units: s, m, h, d)."
4986
5310
  ).conflicts("start")
@@ -4988,28 +5312,28 @@ Examples:
4988
5312
  "--end <time>",
4989
5313
  "Window end: ISO-8601 timestamp or Unix epoch (seconds). Defaults to now. The window must not exceed 7 days."
4990
5314
  ).option("--method <method>", "Filter by request HTTP method (case-insensitive).").addOption(
4991
- new Option8("--status-code <code>", "Filter by response status code (100-599).").argParser(
5315
+ new Option9("--status-code <code>", "Filter by response status code (100-599).").argParser(
4992
5316
  parseIntInRange("--status-code", 100, 599)
4993
5317
  )
4994
5318
  ).option("--url-like <substring>", "Filter by URL substring (case-sensitive).").addOption(
4995
- new Option8("--level-min <level>", "Minimum log level.").choices(Array.from(LOG_LEVELS))
5319
+ new Option9("--level-min <level>", "Minimum log level.").choices(Array.from(LOG_LEVELS))
4996
5320
  ).addOption(
4997
- new Option8(
5321
+ new Option9(
4998
5322
  "--limit <count>",
4999
5323
  "Maximum entries per page (1-500). Defaults to the API default (100)."
5000
5324
  ).argParser(parseIntInRange("--limit", 1, 500))
5001
5325
  ).addOption(
5002
- new Option8(
5326
+ new Option9(
5003
5327
  "--after <cursor>",
5004
5328
  "Return the page after (older than) this cursor, from a previous page's end_cursor."
5005
5329
  ).conflicts("before")
5006
5330
  ).addOption(
5007
- new Option8(
5331
+ new Option9(
5008
5332
  "--before <cursor>",
5009
5333
  "Return the page before (newer than) this cursor, from a previous page's start_cursor."
5010
5334
  )
5011
5335
  ).addOption(
5012
- new Option8("--format <format>", "Output format for log entries.").choices(["json", "pretty", "default", "short", "request"]).default("default")
5336
+ new Option9("--format <format>", "Output format for log entries.").choices(["json", "pretty", "default", "short", "request"]).default("default")
5013
5337
  ).action(async (options) => {
5014
5338
  try {
5015
5339
  const config2 = getProjectConfig();
@@ -5070,7 +5394,7 @@ Examples:
5070
5394
  var logs = new Command9("logs").alias("log").configureHelp({ showGlobalOptions: true }).description("View logs from your deployed application.").addCommand(tail, { isDefault: true }).addCommand(query);
5071
5395
 
5072
5396
  // src/cli/commands/projects.ts
5073
- 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";
5074
5398
  import { Command as Command10 } from "commander";
5075
5399
  import { colorize as colorize11 } from "consola/utils";
5076
5400
  async function offerCommerceHostingSetup(projectUuid, credentials) {
@@ -5079,7 +5403,7 @@ async function offerCommerceHostingSetup(projectUuid, credentials) {
5079
5403
  await cleanupCloudflareIncompatibilities(projectDir);
5080
5404
  return;
5081
5405
  }
5082
- const shouldSetup = await confirm6({
5406
+ const shouldSetup = await confirm8({
5083
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?",
5084
5408
  default: true
5085
5409
  });
@@ -5275,7 +5599,7 @@ Examples:
5275
5599
  }
5276
5600
  if (!options.force) {
5277
5601
  const label = targetName ? `"${targetName}" (${targetUuid})` : targetUuid;
5278
- const confirmed = await confirm6({
5602
+ const confirmed = await confirm8({
5279
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.`,
5280
5604
  default: false
5281
5605
  });
@@ -5352,7 +5676,7 @@ Example:
5352
5676
  });
5353
5677
 
5354
5678
  // src/cli/commands/telemetry.ts
5355
- import { Argument, Command as Command12, Option as Option9 } from "commander";
5679
+ import { Argument, Command as Command12, Option as Option10 } from "commander";
5356
5680
  import { colorize as colorize12 } from "consola/utils";
5357
5681
  var telemetryService = getTelemetry();
5358
5682
  var isEnabled = telemetryService.isEnabled();
@@ -5370,7 +5694,7 @@ Examples:
5370
5694
 
5371
5695
  # Disable telemetry collection
5372
5696
  $ catalyst telemetry --disable`
5373
- ).addArgument(new Argument("[arg]").choices(["disable", "enable", "status"])).addOption(new Option9("--enable", `Enables CLI telemetry collection.`).conflicts("disable")).option("--disable", `Disables CLI telemetry collection.`).action((arg, options) => {
5697
+ ).addArgument(new Argument("[arg]").choices(["disable", "enable", "status"])).addOption(new Option10("--enable", `Enables CLI telemetry collection.`).conflicts("disable")).option("--disable", `Disables CLI telemetry collection.`).action((arg, options) => {
5374
5698
  if (options.enable || arg === "enable") {
5375
5699
  telemetryService.setEnabled(true);
5376
5700
  isEnabled = true;
@@ -5397,8 +5721,8 @@ Examples:
5397
5721
  });
5398
5722
 
5399
5723
  // src/cli/commands/upgrade.ts
5400
- import { Command as Command13, Option as Option10 } from "@commander-js/extra-typings";
5401
- import { confirm as confirm7 } from "@inquirer/prompts";
5724
+ import { Command as Command13, Option as Option11 } from "@commander-js/extra-typings";
5725
+ import { confirm as confirm9 } from "@inquirer/prompts";
5402
5726
  import { execa as execa4 } from "execa";
5403
5727
  import {
5404
5728
  access as access3,
@@ -5559,6 +5883,72 @@ async function findStaleCli(pkgRaw) {
5559
5883
  const current = minVersion2(range)?.version;
5560
5884
  return current !== void 0 && semverLt2(current, latest) ? { current, latest } : null;
5561
5885
  }
5886
+ var CLI_MANAGED_SCRIPTS = ["build", "start", "deploy"];
5887
+ var ScriptsPackageJson = z17.looseObject({
5888
+ scripts: z17.record(z17.string(), z17.string()).optional()
5889
+ });
5890
+ var readScripts = (raw) => {
5891
+ try {
5892
+ return ScriptsPackageJson.parse(JSON.parse(raw)).scripts ?? {};
5893
+ } catch {
5894
+ return {};
5895
+ }
5896
+ };
5897
+ var SCRIPTS_BLOCK = /"scripts"\s*:\s*\{(?:[^{}"]|"(?:[^"\\]|\\.)*")*\}/;
5898
+ var rewriteScriptCommand = (raw, name, command) => {
5899
+ const block = SCRIPTS_BLOCK.exec(raw);
5900
+ if (!block) return raw;
5901
+ const splice = (replacement) => raw.slice(0, block.index) + replacement + raw.slice(block.index + block[0].length);
5902
+ const updated = block[0].replace(
5903
+ new RegExp(`("${escapeRegExp(name)}"\\s*:\\s*)"(?:[^"\\\\]|\\\\.)*"`),
5904
+ (_match, key) => `${key}${JSON.stringify(command)}`
5905
+ );
5906
+ if (updated !== block[0]) return splice(updated);
5907
+ const closeIdx = block[0].length - 1;
5908
+ const head = block[0].slice(0, closeIdx).replace(/\s+$/, "");
5909
+ const closeIndent = /\n([ \t]*)$/.exec(block[0].slice(0, closeIdx))?.[1] ?? "";
5910
+ const entryIndent = /\n([ \t]*)"/.exec(block[0])?.[1] ?? `${closeIndent} `;
5911
+ const separator = head.endsWith("{") ? "" : ",";
5912
+ return splice(
5913
+ `${head}${separator}
5914
+ ${entryIndent}${JSON.stringify(name)}: ${JSON.stringify(command)}
5915
+ ${closeIndent}}`
5916
+ );
5917
+ };
5918
+ async function normalizeManagedScripts(ourPkgRaw, baseDir, theirsDir) {
5919
+ const basePath = join15(baseDir, "package.json");
5920
+ const theirsPath = join15(theirsDir, "package.json");
5921
+ const [originalBase, originalTheirs] = await Promise.all([
5922
+ readFile2(basePath, "utf-8").catch(() => null),
5923
+ readFile2(theirsPath, "utf-8").catch(() => null)
5924
+ ]);
5925
+ if (originalBase === null || originalTheirs === null) return { pinned: [], upstreamChanged: [] };
5926
+ const ourScripts = readScripts(ourPkgRaw);
5927
+ const baseScripts = readScripts(originalBase);
5928
+ const theirsScripts = readScripts(originalTheirs);
5929
+ const decisions = CLI_MANAGED_SCRIPTS.flatMap((name) => {
5930
+ const ours = ourScripts[name];
5931
+ return ours !== void 0 && ours !== baseScripts[name] ? [{ name, ours }] : [];
5932
+ });
5933
+ const upstreamChanged = decisions.flatMap(({ name }) => {
5934
+ const base = baseScripts[name] ?? null;
5935
+ const theirs = theirsScripts[name] ?? null;
5936
+ return base !== theirs ? [{ name, base, theirs }] : [];
5937
+ });
5938
+ const baseRaw = decisions.reduce(
5939
+ (acc, { name, ours }) => rewriteScriptCommand(acc, name, ours),
5940
+ originalBase
5941
+ );
5942
+ const theirsRaw = decisions.reduce(
5943
+ (acc, { name, ours }) => rewriteScriptCommand(acc, name, ours),
5944
+ originalTheirs
5945
+ );
5946
+ await Promise.all([
5947
+ baseRaw === originalBase ? null : writeFile2(basePath, baseRaw),
5948
+ theirsRaw === originalTheirs ? null : writeFile2(theirsPath, theirsRaw)
5949
+ ]);
5950
+ return { pinned: decisions.map(({ name }) => name), upstreamChanged };
5951
+ }
5562
5952
  async function mergeFile(oursPath, basePath, theirsPath) {
5563
5953
  const merged = await execa4(
5564
5954
  "git",
@@ -5912,7 +6302,7 @@ async function resolveBaseRef(project, fromOption, assumeYes) {
5912
6302
  const guess = `${family}@${project.pkg.version}`;
5913
6303
  consola.warn(`No \`catalyst.ref\` found in package.json. Inferred base: ${guess}`);
5914
6304
  if (assumeYes) return guess;
5915
- 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);
5916
6306
  if (!ok) {
5917
6307
  consola.info("Re-run with `--from <ref>` to set the base version explicitly.");
5918
6308
  return null;
@@ -5930,6 +6320,20 @@ ${rows}
5930
6320
  They resolve to your local packages/ copies, which \`catalyst upgrade\` does not update \u2014 so they never receive upgrades.`
5931
6321
  );
5932
6322
  }
6323
+ function printManagedScriptFindings(findings) {
6324
+ if (findings.length === 0) return;
6325
+ const pad = Math.max(...findings.map((finding) => finding.name.length));
6326
+ const show = (command) => command ?? "(absent)";
6327
+ const rows = findings.map(
6328
+ (finding) => ` ${finding.name.padEnd(pad)} ${show(finding.base)} \u2192 ${show(finding.theirs)}`
6329
+ ).join("\n");
6330
+ consola.warn(
6331
+ `Core's own version of ${findings.length} npm script${findings.length === 1 ? "" : "s"} the CLI manages for you changed:
6332
+ ${rows}
6333
+
6334
+ Your project keeps its \`catalyst\`-backed command, which is almost always what you want \u2014 \`catalyst build\`/\`start\`/\`deploy\` wrap the equivalent \`next\` command. Apply the change by hand only if you have opted out of the CLI scripts.`
6335
+ );
6336
+ }
5933
6337
  function printSummary({
5934
6338
  result,
5935
6339
  relDir,
@@ -5973,23 +6377,42 @@ Resolve them, then: git add ${relDir} && git commit${install}`
5973
6377
  );
5974
6378
  }
5975
6379
  }
6380
+ async function reconcileNativeHosting(catalystRoot, result) {
6381
+ const state = getProjectState(catalystRoot);
6382
+ if (!state.isLinked && !state.isTransformed) return;
6383
+ const instrumentationRel = "instrumentation.ts";
6384
+ const instrumentationPath = join15(catalystRoot, instrumentationRel);
6385
+ const hadInstrumentation = await pathExists(instrumentationPath);
6386
+ try {
6387
+ await cleanupCloudflareIncompatibilities(catalystRoot);
6388
+ } catch (err) {
6389
+ consola.warn(
6390
+ `Couldn't fully reconcile native-hosting files (${err instanceof Error ? err.message : String(err)}). Remove @vercel/otel from package.json by hand if the merge brought it back.`
6391
+ );
6392
+ }
6393
+ if (hadInstrumentation && !await pathExists(instrumentationPath)) {
6394
+ result.conflicted = result.conflicted.filter((rel) => rel !== instrumentationRel);
6395
+ result.applied = result.applied.filter((rel) => rel !== instrumentationRel);
6396
+ result.added = result.added.filter((rel) => rel !== instrumentationRel);
6397
+ }
6398
+ }
5976
6399
  var upgrade = new Command13("upgrade").configureHelp({ showGlobalOptions: true }).aliases([
5977
6400
  "up",
5978
6401
  // These are hidden aliases
5979
6402
  "upgrayedd",
5980
6403
  "ugrad"
5981
6404
  ]).description("Upgrade your Catalyst project to a newer version by applying a 3-way merge.").argument("[version]", "Target version to upgrade to (default: latest)").addOption(
5982
- new Option10(
6405
+ new Option11(
5983
6406
  "--ref <ref>",
5984
6407
  "Full git tag to upgrade to, e.g. an integration family like @bigcommerce/catalyst-makeswift@1.7.0. Overrides [version]."
5985
6408
  )
5986
- ).addOption(new Option10("--from <ref>", "Base ref to upgrade from (when catalyst.ref is missing)")).option("--dry-run", "Generate and display the diff without applying it").option("--yes", "Skip confirmation prompts (e.g. inferred base ref)").addOption(
5987
- new Option10(
6409
+ ).addOption(new Option11("--from <ref>", "Base ref to upgrade from (when catalyst.ref is missing)")).option("--dry-run", "Generate and display the diff without applying it").option("--yes", "Skip confirmation prompts (e.g. inferred base ref)").addOption(
6410
+ new Option11(
5988
6411
  "--strategy <strategy>",
5989
6412
  "Merge engine: tree (git merge-tree, full fidelity), per-file (git merge-file, no-history fallback), or auto (tree when git >= 2.38, else per-file)"
5990
6413
  ).choices(["auto", "tree", "per-file"]).default("auto").hideHelp()
5991
6414
  ).addOption(
5992
- new Option10("--repository <owner/repo>", "GitHub repository to pull versions from").default(
6415
+ new Option11("--repository <owner/repo>", "GitHub repository to pull versions from").default(
5993
6416
  DEFAULT_REPOSITORY
5994
6417
  )
5995
6418
  ).addHelpText(
@@ -6104,6 +6527,11 @@ to raise the GitHub API rate limit.`
6104
6527
  baseVersions,
6105
6528
  targetVersions
6106
6529
  );
6530
+ const { upstreamChanged: managedScripts } = await normalizeManagedScripts(
6531
+ project.rawContent,
6532
+ baseDir,
6533
+ theirsDir
6534
+ );
6107
6535
  if (options.dryRun) {
6108
6536
  const diffSpinner = yoctoSpinner4().start("Generating diff...");
6109
6537
  const diff = await execa4(
@@ -6124,6 +6552,7 @@ to raise the GitHub API rate limit.`
6124
6552
  consola.log("\nDiff preview (--dry-run, not applied):\n");
6125
6553
  consola.log(diff.stdout);
6126
6554
  printWorkspaceFindings(workspaceDeps);
6555
+ printManagedScriptFindings(managedScripts);
6127
6556
  return;
6128
6557
  }
6129
6558
  const strategy = await resolveStrategy(options.strategy);
@@ -6171,10 +6600,11 @@ to raise the GitHub API rate limit.`
6171
6600
  );
6172
6601
  }
6173
6602
  }
6603
+ printManagedScriptFindings(managedScripts);
6174
6604
  printWorkspaceFindings(workspaceDeps);
6175
6605
  let migratedDeps = 0;
6176
6606
  if (workspaceDeps.length) {
6177
- 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({
6178
6608
  message: "Replace them with the published versions?",
6179
6609
  default: true
6180
6610
  }).catch(() => false));
@@ -6190,6 +6620,7 @@ to raise the GitHub API rate limit.`
6190
6620
  );
6191
6621
  }
6192
6622
  }
6623
+ await reconcileNativeHosting(catalystRoot, result);
6193
6624
  try {
6194
6625
  await applyIndexState(gitRoot, relDir, baseDir, theirsDir, result, stampedText !== null);
6195
6626
  } catch (err) {