@bigcommerce/catalyst 1.2.0 → 1.4.0

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.2.0",
28
+ version: "1.4.0",
29
29
  type: "module",
30
30
  repository: {
31
31
  type: "git",
@@ -49,7 +49,7 @@ var package_default = {
49
49
  build: "tsup"
50
50
  },
51
51
  engines: {
52
- node: "^20.0.0 || ^22.0.0 || ^24.0.0"
52
+ node: "^22.0.0 || ^24.0.0"
53
53
  },
54
54
  dependencies: {
55
55
  "@commander-js/extra-typings": "^14.0.0",
@@ -69,6 +69,7 @@ var package_default = {
69
69
  semver: "^7.7.4",
70
70
  "std-env": "^3.9.0",
71
71
  tar: "^7.5.7",
72
+ tldts: "^7.4.12",
72
73
  "yocto-spinner": "^1.0.0",
73
74
  zod: "^4.0.5"
74
75
  },
@@ -91,7 +92,12 @@ var package_default = {
91
92
  vitest: "^3.2.4"
92
93
  },
93
94
  peerDependencies: {
94
- "@opennextjs/cloudflare": "1.17.3"
95
+ "@opennextjs/cloudflare": "^1.17.3"
96
+ },
97
+ peerDependenciesMeta: {
98
+ "@opennextjs/cloudflare": {
99
+ optional: true
100
+ }
95
101
  }
96
102
  };
97
103
 
@@ -594,9 +600,9 @@ async function deleteProject(projectUuid, storeHash, accessToken, apiHost) {
594
600
  // src/cli/lib/project-config.ts
595
601
  import Conf2 from "conf";
596
602
  import { join } from "path";
597
- function getProjectConfig() {
603
+ function getProjectConfig(cwd = process.cwd()) {
598
604
  return new Conf2({
599
- cwd: join(process.cwd(), ".bigcommerce"),
605
+ cwd: join(cwd, ".bigcommerce"),
600
606
  projectSuffix: "",
601
607
  configName: "project",
602
608
  schema: {
@@ -647,7 +653,7 @@ var resolveProjectUuid = (options) => {
647
653
  const projectUuid = options.projectUuid ?? config2.get("projectUuid");
648
654
  if (!projectUuid) {
649
655
  throw new Error(
650
- "Project UUID is required. Please run either `catalyst project link` or `catalyst project create` or this command again with --project-uuid <uuid>."
656
+ "Project UUID is required. Please run either `catalyst projects link` or `catalyst projects create` or this command again with --project-uuid <uuid>."
651
657
  );
652
658
  }
653
659
  return projectUuid;
@@ -703,8 +709,8 @@ Example:
703
709
  const store = await fetchStoreProfile2(storeHash, accessToken, apiHost);
704
710
  const projectUuid = config2.get("projectUuid");
705
711
  if (projectUuid) {
706
- const projects = await fetchProjects(storeHash, accessToken, apiHost);
707
- const linkedProject = projects.find((p) => p.uuid === projectUuid);
712
+ const projects2 = await fetchProjects(storeHash, accessToken, apiHost);
713
+ const linkedProject = projects2.find((p) => p.uuid === projectUuid);
708
714
  if (linkedProject) {
709
715
  consola.info(
710
716
  `Logged in to ${store.store_name} (${storeHash}), connected to project ${linkedProject.name} (${projectUuid})`
@@ -804,7 +810,7 @@ var auth = new Command("auth").configureHelp({ showGlobalOptions: true }).descri
804
810
  import { Command as Command2, InvalidArgumentError, Option as Option2 } from "commander";
805
811
  import { execa as execa2 } from "execa";
806
812
  import { copyFile, cp, rm, writeFile } from "fs/promises";
807
- import { join as join4 } from "path";
813
+ import { join as join6 } from "path";
808
814
  import { valid as validSemver } from "semver";
809
815
 
810
816
  // src/cli/lib/build-env.ts
@@ -839,223 +845,679 @@ function loadBuildEnv({ envPath, cwd = process.cwd() }) {
839
845
  }
840
846
  }
841
847
 
842
- // src/cli/lib/get-module-cli-path.ts
848
+ // src/cli/lib/commerce-hosting.ts
849
+ import { confirm as confirm2, input as input2, select, Separator } from "@inquirer/prompts";
850
+ import { colorize as colorize3 } from "consola/utils";
851
+ import { existsSync as existsSync2, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
843
852
  import { dirname, join as join2 } from "path";
844
- import { fileURLToPath } from "url";
845
- function getModuleCliPath() {
846
- const __filename = fileURLToPath(import.meta.url);
847
- const __dirname = dirname(__filename);
848
- return join2(__dirname, "..");
853
+ import { minVersion, satisfies, lt as semverLt, validRange } from "semver";
854
+ import { z as z6 } from "zod";
855
+
856
+ // src/cli/lib/sort-package-json.ts
857
+ var FIELD_ORDER = [
858
+ "name",
859
+ "description",
860
+ "version",
861
+ "private",
862
+ "engines",
863
+ "scripts",
864
+ "dependencies",
865
+ "devDependencies"
866
+ ];
867
+ function sortPackageJsonFields(pkg) {
868
+ const ordered = {};
869
+ FIELD_ORDER.forEach((field) => {
870
+ if (field in pkg) {
871
+ ordered[field] = pkg[field];
872
+ }
873
+ });
874
+ Object.keys(pkg).forEach((key) => {
875
+ if (!(key in ordered)) {
876
+ ordered[key] = pkg[key];
877
+ }
878
+ });
879
+ return ordered;
849
880
  }
850
881
 
851
- // src/cli/lib/project-state.ts
852
- import { existsSync as existsSync2, readFileSync } from "fs";
853
- import { join as join3 } from "path";
854
- import { z as z6 } from "zod";
855
- var projectJsonSchema = z6.looseObject({
856
- projectUuid: z6.string().optional()
857
- });
858
- var packageJsonSchema = z6.looseObject({
882
+ // src/cli/lib/commerce-hosting.ts
883
+ var OPENNEXT_CLOUDFLARE_VERSION = "1.20.6";
884
+ var OPENNEXT_REQUIRED_NEXT_RANGE = ">=15.5.24 <16 || >=16.3.3";
885
+ var corePackageJsonSchema = z6.looseObject({
859
886
  dependencies: z6.record(z6.string(), z6.string()).optional()
860
887
  });
861
- var safeReadJson = (path) => {
888
+ var projectJsonSchema = z6.looseObject({});
889
+ var readProjectJson = (path) => {
890
+ if (!existsSync2(path)) return {};
862
891
  try {
863
- return JSON.parse(readFileSync(path, "utf-8"));
892
+ const parsed = projectJsonSchema.safeParse(JSON.parse(readFileSync(path, "utf-8")));
893
+ return parsed.success ? parsed.data : {};
864
894
  } catch {
865
- return null;
895
+ return {};
866
896
  }
867
897
  };
868
- function getProjectState(cwd = process.cwd()) {
869
- const projectJson = projectJsonSchema.safeParse(
870
- safeReadJson(join3(cwd, ".bigcommerce", "project.json"))
871
- );
872
- const projectUuid = projectJson.success ? projectJson.data.projectUuid : void 0;
873
- const hasMiddleware = existsSync2(join3(cwd, "middleware.ts"));
874
- const hasProxy = existsSync2(join3(cwd, "proxy.ts"));
875
- const pkgJson = packageJsonSchema.safeParse(safeReadJson(join3(cwd, "package.json")));
876
- const hasOpenNextDep = pkgJson.success ? Boolean(pkgJson.data.dependencies?.["@opennextjs/cloudflare"]) : false;
877
- const isLinked = Boolean(projectUuid);
878
- const isTransformed = hasMiddleware && !hasProxy && hasOpenNextDep;
879
- const isFullySetUp = isLinked && isTransformed;
880
- return {
881
- projectUuid,
882
- hasMiddleware,
883
- hasProxy,
884
- hasOpenNextDep,
885
- isLinked,
886
- isTransformed,
887
- isFullySetUp
888
- };
889
- }
890
-
891
- // src/cli/lib/required-build-env.ts
892
- var REQUIRED_BUILD_ENV_VARS = [
893
- "BIGCOMMERCE_STORE_HASH",
894
- "BIGCOMMERCE_STOREFRONT_TOKEN",
895
- "BIGCOMMERCE_CHANNEL_ID",
896
- "AUTH_SECRET"
897
- ];
898
- var findMissingBuildEnv = (env2 = process.env) => {
899
- return REQUIRED_BUILD_ENV_VARS.filter((name) => {
900
- const value = env2[name];
901
- return value === void 0 || value.trim() === "";
902
- });
898
+ var writeJson = (path, value) => {
899
+ mkdirSync(dirname(path), { recursive: true });
900
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
901
+ `);
903
902
  };
904
- var assertRequiredBuildEnv = (env2 = process.env) => {
905
- const missing = findMissingBuildEnv(env2);
906
- if (missing.length === 0) {
903
+ var convertProxyToMiddleware = (projectDir) => {
904
+ const proxyPath = join2(projectDir, "proxy.ts");
905
+ const middlewarePath = join2(projectDir, "middleware.ts");
906
+ if (!existsSync2(proxyPath)) return;
907
+ const contents = readFileSync(proxyPath, "utf-8").replace("export const proxy", "export const middleware").replace("export const config = {", "export const config = {\n runtime: 'experimental-edge',");
908
+ writeFileSync(middlewarePath, contents);
909
+ unlinkSync(proxyPath);
910
+ };
911
+ var cleanupCloudflareIncompatibilities = async (projectDir) => {
912
+ const instrumentationPath = join2(projectDir, "instrumentation.ts");
913
+ if (!existsSync2(instrumentationPath)) return;
914
+ if (!process.stdin.isTTY) {
915
+ consola.warn(
916
+ "instrumentation.ts is present and may be incompatible with the Cloudflare Workers bundle (the default @vercel/otel scaffolding throws at cold start under workerd). Skipping automatic cleanup in non-interactive mode \u2014 re-run interactively to remove it, or delete/gate it manually."
917
+ );
907
918
  return;
908
919
  }
909
- const missingList = missing.map((name) => ` - ${name}`).join("\n");
910
- throw new UserActionableError(
911
- `Missing required environment variable${missing.length === 1 ? "" : "s"} for the build:
912
- ${missingList}
913
-
914
- These are read by the build itself, so they must be set before it runs. The build automatically loads \`.env.local\` and \`.env\` from the current directory (${process.cwd()}) \u2014 add the variables above to one of those files (\`.env.local\` takes precedence).
915
- To load an env file from a different location, pass \`--env-path <path>\` (for example \`--env-path ../.env.local\` when running from a subdirectory).`
916
- );
917
- };
918
-
919
- // src/cli/lib/wrangler-config.ts
920
- function getCompatibilityDate(now = /* @__PURE__ */ new Date()) {
921
- const date = new Date(now);
922
- date.setUTCMonth(date.getUTCMonth() - 1);
923
- return date.toISOString().slice(0, 10);
924
- }
925
- function getWranglerConfig(projectUuid) {
926
- return {
927
- $schema: "node_modules/wrangler/config-schema.json",
928
- main: "../.open-next/worker.js",
929
- name: `project-${projectUuid}`,
930
- compatibility_date: getCompatibilityDate(),
931
- compatibility_flags: ["nodejs_compat", "global_fetch_strictly_public"],
932
- observability: {
933
- enabled: true,
934
- head_sampling_rate: 0.05,
935
- logs: {
936
- enabled: true,
937
- head_sampling_rate: 1,
938
- invocation_logs: false
939
- }
940
- },
941
- assets: {
942
- directory: "../.open-next/assets",
943
- binding: "ASSETS"
944
- },
945
- services: [
946
- {
947
- binding: "WORKER_SELF_REFERENCE",
948
- service: `project-${projectUuid}`
949
- }
950
- ],
951
- r2_buckets: [
952
- {
953
- binding: "NEXT_INC_CACHE_R2_BUCKET",
954
- bucket_name: `project-${projectUuid}`
955
- }
956
- ],
957
- durable_objects: {
958
- bindings: [
959
- {
960
- name: "NEXT_CACHE_DO_QUEUE",
961
- class_name: "DOQueueHandler"
962
- },
963
- {
964
- name: "NEXT_TAG_CACHE_DO_SHARDED",
965
- class_name: "DOShardedTagCache"
966
- },
967
- {
968
- name: "NEXT_CACHE_DO_PURGE",
969
- class_name: "BucketCachePurge"
970
- }
971
- ]
972
- },
973
- migrations: [
974
- {
975
- tag: "v1",
976
- new_sqlite_classes: ["DOQueueHandler", "DOShardedTagCache", "BucketCachePurge"]
977
- }
978
- ]
979
- };
980
- }
981
-
982
- // src/cli/commands/build.ts
983
- var WRANGLER_VERSION = "4.90.0";
984
- var DIST_TAG_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
985
- var parseWranglerVersion = (value) => {
986
- if (!validSemver(value) && !DIST_TAG_PATTERN.test(value)) {
987
- throw new InvalidArgumentError(
988
- `"${value}" is not a valid Wrangler version or dist-tag (e.g. 4.90.0 or latest).`
920
+ 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?',
922
+ default: true
923
+ });
924
+ if (!shouldRemove) {
925
+ consola.info(
926
+ 'Leaving instrumentation.ts in place. The Cloudflare worker will continue to log "Failed to prepare server" at cold start until this is resolved manually.'
989
927
  );
928
+ return;
990
929
  }
991
- return value;
992
- };
993
- async function buildCatalystProject(projectUuid, wranglerVersion = WRANGLER_VERSION) {
994
- assertRequiredBuildEnv();
995
- const coreDir = process.cwd();
996
- const openNextOutDir = join4(coreDir, ".open-next");
997
- const bigcommerceDistDir = join4(coreDir, ".bigcommerce", "dist");
998
- const wranglerConfig = getWranglerConfig(projectUuid);
999
- await rm(bigcommerceDistDir, { recursive: true, force: true });
1000
- consola.start("Copying templates...");
1001
- await copyFile(
1002
- join4(getModuleCliPath(), "templates", "open-next.config.ts"),
1003
- join4(coreDir, ".bigcommerce", "open-next.config.ts")
1004
- );
1005
- await writeFile(
1006
- join4(coreDir, ".bigcommerce", "wrangler.jsonc"),
1007
- JSON.stringify(wranglerConfig, null, 2)
1008
- );
1009
- consola.success("Templates copied");
1010
- consola.start("Building project...");
1011
- await execa2(
1012
- "pnpm",
1013
- [
1014
- "exec",
1015
- "opennextjs-cloudflare",
1016
- "build",
1017
- "--skipWranglerConfigCheck",
1018
- "--openNextConfigPath",
1019
- join4(coreDir, ".bigcommerce", "open-next.config.ts")
1020
- ],
1021
- {
1022
- stdout: ["pipe", "inherit"],
1023
- cwd: coreDir
1024
- }
1025
- );
1026
- await execa2(
1027
- "pnpm",
1028
- [
1029
- "dlx",
1030
- `wrangler@${wranglerVersion}`,
1031
- "deploy",
1032
- "--config",
1033
- join4(coreDir, ".bigcommerce", "wrangler.jsonc"),
1034
- "--keep-vars",
1035
- "--outdir",
1036
- bigcommerceDistDir,
1037
- "--dry-run"
1038
- ],
1039
- {
1040
- stdout: ["pipe", "inherit"],
1041
- cwd: coreDir
930
+ unlinkSync(instrumentationPath);
931
+ consola.info("Removed instrumentation.ts (incompatible with Cloudflare Workers).");
932
+ const corePackageJsonPath = join2(projectDir, "package.json");
933
+ if (existsSync2(corePackageJsonPath)) {
934
+ const pkg = corePackageJsonSchema.parse(JSON.parse(readFileSync(corePackageJsonPath, "utf-8")));
935
+ if (pkg.dependencies && "@vercel/otel" in pkg.dependencies) {
936
+ const { "@vercel/otel": _removedVercelOtel, ...preservedDeps } = pkg.dependencies;
937
+ pkg.dependencies = preservedDeps;
938
+ writeJson(corePackageJsonPath, sortPackageJsonFields(pkg));
939
+ consola.info("Dropped @vercel/otel from package.json.");
1042
940
  }
1043
- );
1044
- consola.success("Project built");
1045
- await cp(join4(openNextOutDir, "assets"), join4(bigcommerceDistDir, "assets"), {
1046
- recursive: true,
1047
- force: true
941
+ }
942
+ };
943
+ var reconcileOpenNextVersion = async (projectDir, packageManager, { canUpgrade }) => {
944
+ const corePackageJsonPath = join2(projectDir, "package.json");
945
+ if (!existsSync2(corePackageJsonPath)) return false;
946
+ const pkg = corePackageJsonSchema.parse(JSON.parse(readFileSync(corePackageJsonPath, "utf-8")));
947
+ const range = pkg.dependencies?.["@opennextjs/cloudflare"];
948
+ if (range === void 0) return false;
949
+ if (!validRange(range)) return false;
950
+ if (satisfies(OPENNEXT_CLOUDFLARE_VERSION, range)) return false;
951
+ const current = minVersion(range)?.version;
952
+ if (current === void 0 || !semverLt(current, OPENNEXT_CLOUDFLARE_VERSION)) return false;
953
+ const manualStep = `\`${packageManager} add @opennextjs/cloudflare@${OPENNEXT_CLOUDFLARE_VERSION}\``;
954
+ const behind = `This project pins @opennextjs/cloudflare ${current}, but this CLI targets ${OPENNEXT_CLOUDFLARE_VERSION}.`;
955
+ const nextRange = pkg.dependencies?.next;
956
+ const nextVersion = nextRange !== void 0 && validRange(nextRange) ? minVersion(nextRange) : null;
957
+ if (nextVersion !== null && !satisfies(nextVersion.version, OPENNEXT_REQUIRED_NEXT_RANGE)) {
958
+ consola.warn(
959
+ `${behind} That release needs Next.js ${OPENNEXT_REQUIRED_NEXT_RANGE}, but this project is on ${nextVersion.version}. Run \`catalyst upgrade\` to move core and Next first, then re-run the deploy to pick up the adapter.`
960
+ );
961
+ return false;
962
+ }
963
+ if (!canUpgrade) {
964
+ consola.warn(
965
+ `${behind} The deploy will use the existing build output. Re-run without \`--prebuilt\`, or update it with ${manualStep}, to pick up adapter fixes released since then.`
966
+ );
967
+ return false;
968
+ }
969
+ if (!process.stdin.isTTY) {
970
+ consola.warn(
971
+ `${behind} Skipping the upgrade prompt in a non-interactive environment. Update it with ${manualStep} and commit the lockfile to pick up adapter fixes released since then.`
972
+ );
973
+ return false;
974
+ }
975
+ const shouldUpgrade = await confirm2({
976
+ message: `${behind} Adapter fixes released since then are not applied. Update it to ${OPENNEXT_CLOUDFLARE_VERSION} and reinstall now?`,
977
+ default: true
1048
978
  });
1049
- }
1050
- var build = new Command2("build").configureHelp({ showGlobalOptions: true }).description(
1051
- "Build your Catalyst project using the OpenNext/Cloudflare build pipeline. Also runs a Wrangler dry-run to generate deployment artifacts."
1052
- ).addHelpText(
1053
- "after",
1054
- `
1055
- Examples:
1056
- $ catalyst build
1057
-
1058
- # Include project UUID
979
+ if (!shouldUpgrade) {
980
+ consola.info(`Keeping @opennextjs/cloudflare ${current}. Update it later with ${manualStep}.`);
981
+ return false;
982
+ }
983
+ pkg.dependencies = {
984
+ ...pkg.dependencies,
985
+ "@opennextjs/cloudflare": OPENNEXT_CLOUDFLARE_VERSION
986
+ };
987
+ writeJson(corePackageJsonPath, sortPackageJsonFields(pkg));
988
+ consola.info(`Updated @opennextjs/cloudflare to ${OPENNEXT_CLOUDFLARE_VERSION}.`);
989
+ return true;
990
+ };
991
+ var setupCommerceHosting = async ({
992
+ projectDir,
993
+ projectUuid,
994
+ storeHash,
995
+ accessToken
996
+ }) => {
997
+ await cleanupCloudflareIncompatibilities(projectDir);
998
+ const corePackageJsonPath = join2(projectDir, "package.json");
999
+ const pkg = corePackageJsonSchema.parse(JSON.parse(readFileSync(corePackageJsonPath, "utf-8")));
1000
+ pkg.dependencies = {
1001
+ ...pkg.dependencies,
1002
+ "@opennextjs/cloudflare": OPENNEXT_CLOUDFLARE_VERSION
1003
+ };
1004
+ writeJson(corePackageJsonPath, sortPackageJsonFields(pkg));
1005
+ const projectJsonPath = join2(projectDir, ".bigcommerce", "project.json");
1006
+ const projectJson = {
1007
+ ...readProjectJson(projectJsonPath),
1008
+ projectUuid,
1009
+ framework: "catalyst"
1010
+ };
1011
+ if (storeHash) projectJson.storeHash = storeHash;
1012
+ if (accessToken) projectJson.accessToken = accessToken;
1013
+ writeJson(projectJsonPath, projectJson);
1014
+ convertProxyToMiddleware(projectDir);
1015
+ };
1016
+ var NoLinkedProjectError = class extends Error {
1017
+ constructor() {
1018
+ super("No infrastructure project linked: user declined to create one.");
1019
+ this.name = "NoLinkedProjectError";
1020
+ }
1021
+ };
1022
+ async function promptForNewProjectName(api) {
1023
+ const newProjectName = await input2({ message: "Enter a name for the new project:" });
1024
+ const data = await createProject(newProjectName, api.storeHash, api.accessToken, api.apiHost);
1025
+ consola.success(`Project "${data.name}" created successfully.`);
1026
+ return { uuid: data.uuid, name: data.name, deployment_hostnames: [] };
1027
+ }
1028
+ async function selectOrCreateInfrastructureProject(api, linkedProjectUuid) {
1029
+ consola.start("Fetching projects...");
1030
+ const existingProjects = await fetchProjects(api.storeHash, api.accessToken, api.apiHost);
1031
+ consola.success("Projects fetched.");
1032
+ if (existingProjects.length === 0) {
1033
+ const shouldCreate = await confirm2({
1034
+ message: "There are not any hosting projects that you can link to yet. Would you like to create one?",
1035
+ default: true
1036
+ });
1037
+ if (!shouldCreate) {
1038
+ throw new NoLinkedProjectError();
1039
+ }
1040
+ return promptForNewProjectName(api);
1041
+ }
1042
+ const choices = [
1043
+ ...existingProjects.map((p) => ({
1044
+ name: p.uuid === linkedProjectUuid ? `${p.name} ${colorize3("green", "[linked]")}` : p.name,
1045
+ value: p.uuid,
1046
+ description: p.uuid
1047
+ })),
1048
+ {
1049
+ name: "Create a new project",
1050
+ value: "create",
1051
+ description: "Create a new hosting project for this Catalyst storefront."
1052
+ }
1053
+ ];
1054
+ const selected = await select({
1055
+ message: "Select a project or create a new project (Press <enter> to select).",
1056
+ choices
1057
+ });
1058
+ if (selected === "create") {
1059
+ return promptForNewProjectName(api);
1060
+ }
1061
+ const matched = existingProjects.find((p) => p.uuid === selected);
1062
+ if (!matched) {
1063
+ throw new Error(`Selected project ${String(selected)} not found in fetched list.`);
1064
+ }
1065
+ return matched;
1066
+ }
1067
+ async function promptForCommerceHostingProject(api, defaultName, autoUseDefaultName, useExistingOnCollision) {
1068
+ const existingProjects = await fetchProjects(api.storeHash, api.accessToken, api.apiHost);
1069
+ const takenNames = existingProjects.map((project) => project.name);
1070
+ if (autoUseDefaultName) {
1071
+ return autoCreateCommerceHostingProject(
1072
+ api,
1073
+ defaultName,
1074
+ existingProjects,
1075
+ useExistingOnCollision
1076
+ );
1077
+ }
1078
+ const conflict = existingProjects.find(
1079
+ (project) => project.name.toLowerCase() === defaultName.toLowerCase()
1080
+ );
1081
+ if (!conflict) {
1082
+ return autoCreateCommerceHostingProject(
1083
+ api,
1084
+ defaultName,
1085
+ existingProjects,
1086
+ useExistingOnCollision
1087
+ );
1088
+ }
1089
+ const hasOtherProjects = existingProjects.length > 1;
1090
+ const choices = [
1091
+ { name: `Use "${conflict.name}"`, value: "use-named" }
1092
+ ];
1093
+ if (hasOtherProjects) {
1094
+ choices.push({ name: "Select from my projects", value: "select-from-list" });
1095
+ }
1096
+ choices.push({ name: "Create a new project", value: "create" });
1097
+ const action = await select({
1098
+ message: hasOtherProjects ? `It looks like you already have an existing Commerce Hosting project named "${conflict.name}". Would you like to use it, select from your projects, or create a new one?` : `It looks like you already have an existing Commerce Hosting project named "${conflict.name}". Would you like to use it, or create a new one?`,
1099
+ choices
1100
+ });
1101
+ if (action === "use-named") {
1102
+ consola.success(`Using existing Commerce Hosting project "${conflict.name}"`);
1103
+ return conflict;
1104
+ }
1105
+ if (action === "create") {
1106
+ return promptAndCreateCommerceHostingProject(api, takenNames, defaultName);
1107
+ }
1108
+ const selected = await select({
1109
+ message: "Which Commerce Hosting project would you like to use?",
1110
+ choices: [
1111
+ ...existingProjects.map((project) => ({
1112
+ name: project.name,
1113
+ value: project,
1114
+ description: project.uuid
1115
+ })),
1116
+ new Separator(),
1117
+ { name: "Create a new project", value: "create-new" }
1118
+ ]
1119
+ });
1120
+ if (selected === "create-new") {
1121
+ return promptAndCreateCommerceHostingProject(api, takenNames, defaultName);
1122
+ }
1123
+ consola.success(`Using existing Commerce Hosting project "${selected.name}"`);
1124
+ return selected;
1125
+ }
1126
+ async function promptAndCreateCommerceHostingProject(api, takenNames, defaultName) {
1127
+ const projectName = await input2({
1128
+ message: "What would you like to name your Commerce Hosting project?",
1129
+ default: defaultName,
1130
+ validate: (value) => {
1131
+ const trimmed = value.trim();
1132
+ if (!trimmed) return "Project name is required";
1133
+ const conflict = takenNames.find((taken) => taken.toLowerCase() === trimmed.toLowerCase());
1134
+ if (conflict) {
1135
+ return `A Commerce Hosting project named "${conflict}" already exists`;
1136
+ }
1137
+ return true;
1138
+ },
1139
+ theme: {
1140
+ style: {
1141
+ help: () => colorize3(
1142
+ "dim",
1143
+ "(The project that hosts your storefront on Commerce \u2014 often matches your folder name.)"
1144
+ )
1145
+ }
1146
+ }
1147
+ });
1148
+ try {
1149
+ const created = await createProject(
1150
+ projectName.trim(),
1151
+ api.storeHash,
1152
+ api.accessToken,
1153
+ api.apiHost
1154
+ );
1155
+ consola.success(`Commerce Hosting project "${created.name}" created successfully`);
1156
+ return { uuid: created.uuid, name: created.name, deployment_hostnames: [] };
1157
+ } catch (error) {
1158
+ if (error instanceof InfrastructureProjectValidationError) {
1159
+ consola.error(error.message);
1160
+ return promptAndCreateCommerceHostingProject(api, takenNames, defaultName);
1161
+ }
1162
+ throw error;
1163
+ }
1164
+ }
1165
+ async function resolveCollisionChoice(existingName, useExistingOnCollision) {
1166
+ if (useExistingOnCollision === true) return true;
1167
+ if (!process.stdin.isTTY) return false;
1168
+ return select({
1169
+ message: `A Commerce Hosting project named "${existingName}" already exists. Use the existing project?`,
1170
+ choices: [
1171
+ { name: "Yes", value: true },
1172
+ { name: "No", value: false }
1173
+ ]
1174
+ });
1175
+ }
1176
+ async function autoCreateCommerceHostingProject(api, name, existingProjects, useExistingOnCollision) {
1177
+ const existing = existingProjects.find(
1178
+ (project) => project.name.toLowerCase() === name.toLowerCase()
1179
+ );
1180
+ if (existing) {
1181
+ const shouldUseExisting = await resolveCollisionChoice(existing.name, useExistingOnCollision);
1182
+ if (shouldUseExisting) {
1183
+ consola.success(`Using existing Commerce Hosting project "${existing.name}"`);
1184
+ return existing;
1185
+ }
1186
+ consola.error(
1187
+ "Not reusing the existing project. Re-run with a different --project-name, or pass --use-existing to reuse it."
1188
+ );
1189
+ process.exit(1);
1190
+ }
1191
+ try {
1192
+ const created = await createProject(name, api.storeHash, api.accessToken, api.apiHost);
1193
+ consola.success(`Commerce Hosting project "${created.name}" created successfully`);
1194
+ return { uuid: created.uuid, name: created.name, deployment_hostnames: [] };
1195
+ } catch (error) {
1196
+ if (error instanceof InfrastructureProjectValidationError) {
1197
+ consola.error(
1198
+ `Failed to create Commerce Hosting project "${name}": ${error.message}
1199
+ Re-run with a different --project-name.`
1200
+ );
1201
+ process.exit(1);
1202
+ }
1203
+ throw error;
1204
+ }
1205
+ }
1206
+
1207
+ // src/cli/lib/detect-package-manager.ts
1208
+ import { access } from "fs/promises";
1209
+ import { join as join3 } from "path";
1210
+ import { detectPackageManager as detectFromDir } from "nypm";
1211
+ var PROJECT_LOCKFILES = [
1212
+ ["pnpm-lock.yaml", "pnpm"],
1213
+ ["yarn.lock", "yarn"],
1214
+ ["bun.lock", "bun"],
1215
+ ["bun.lockb", "bun"],
1216
+ ["package-lock.json", "npm"]
1217
+ ];
1218
+ var detectLockfileManager = async (dir) => {
1219
+ const found = await Promise.all(
1220
+ PROJECT_LOCKFILES.map(
1221
+ async ([file, manager]) => access(join3(dir, file)).then(() => manager).catch(() => null)
1222
+ )
1223
+ );
1224
+ return found.find((manager) => manager !== null) ?? null;
1225
+ };
1226
+ var detectPackageManager = () => {
1227
+ const userAgent = process.env.npm_config_user_agent ?? "";
1228
+ const name = userAgent.split(" ")[0]?.split("/")[0];
1229
+ if (name === "pnpm" || name === "yarn" || name === "bun") {
1230
+ return name;
1231
+ }
1232
+ if (process.versions.bun) {
1233
+ return "bun";
1234
+ }
1235
+ return "npm";
1236
+ };
1237
+ var detectProjectPackageManager = async (projectDir) => {
1238
+ const detected = await detectFromDir(projectDir);
1239
+ const name = detected?.name;
1240
+ if (name === "pnpm" || name === "yarn" || name === "bun" || name === "npm") {
1241
+ return name;
1242
+ }
1243
+ return "npm";
1244
+ };
1245
+
1246
+ // src/cli/lib/get-module-cli-path.ts
1247
+ import { dirname as dirname2, join as join4 } from "path";
1248
+ import { fileURLToPath } from "url";
1249
+ function getModuleCliPath() {
1250
+ const __filename = fileURLToPath(import.meta.url);
1251
+ const __dirname = dirname2(__filename);
1252
+ return join4(__dirname, "..");
1253
+ }
1254
+
1255
+ // src/cli/lib/install-dependencies.ts
1256
+ import { installDependencies as installDeps } from "nypm";
1257
+ import yoctoSpinner2 from "yocto-spinner";
1258
+ var restoreEnv = (key, previous) => {
1259
+ if (previous === void 0) {
1260
+ Reflect.deleteProperty(process.env, key);
1261
+ } else {
1262
+ process.env[key] = previous;
1263
+ }
1264
+ };
1265
+ var withQuietInstallEnv = async (run2) => {
1266
+ var _a, _b;
1267
+ const prevNodeNoWarnings = process.env.NODE_NO_WARNINGS;
1268
+ const prevDownloadPrompt = process.env.COREPACK_ENABLE_DOWNLOAD_PROMPT;
1269
+ (_a = process.env).NODE_NO_WARNINGS ?? (_a.NODE_NO_WARNINGS = "1");
1270
+ (_b = process.env).COREPACK_ENABLE_DOWNLOAD_PROMPT ?? (_b.COREPACK_ENABLE_DOWNLOAD_PROMPT = "0");
1271
+ try {
1272
+ return await run2();
1273
+ } finally {
1274
+ restoreEnv("NODE_NO_WARNINGS", prevNodeNoWarnings);
1275
+ restoreEnv("COREPACK_ENABLE_DOWNLOAD_PROMPT", prevDownloadPrompt);
1276
+ }
1277
+ };
1278
+ var installDependencies = async (projectDir, packageManager = "pnpm") => {
1279
+ const spinner = yoctoSpinner2().start(
1280
+ `Installing dependencies with ${packageManager}. This could take a minute...`
1281
+ );
1282
+ try {
1283
+ await withQuietInstallEnv(() => installDeps({ cwd: projectDir, silent: true, packageManager }));
1284
+ spinner.success("Dependencies installed successfully.");
1285
+ } catch (error) {
1286
+ const message = error instanceof Error ? error.message : "unknown error";
1287
+ spinner.error(`Failed to install dependencies: ${message}`);
1288
+ throw error;
1289
+ }
1290
+ };
1291
+
1292
+ // src/cli/lib/project-state.ts
1293
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1294
+ import { join as join5 } from "path";
1295
+ import { z as z7 } from "zod";
1296
+ var projectJsonSchema2 = z7.looseObject({
1297
+ projectUuid: z7.string().optional()
1298
+ });
1299
+ var packageJsonSchema = z7.looseObject({
1300
+ dependencies: z7.record(z7.string(), z7.string()).optional()
1301
+ });
1302
+ var safeReadJson = (path) => {
1303
+ try {
1304
+ return JSON.parse(readFileSync2(path, "utf-8"));
1305
+ } catch {
1306
+ return null;
1307
+ }
1308
+ };
1309
+ function getProjectState(cwd = process.cwd()) {
1310
+ const projectJson = projectJsonSchema2.safeParse(
1311
+ safeReadJson(join5(cwd, ".bigcommerce", "project.json"))
1312
+ );
1313
+ const projectUuid = projectJson.success ? projectJson.data.projectUuid : void 0;
1314
+ const hasMiddleware = existsSync3(join5(cwd, "middleware.ts"));
1315
+ const hasProxy = existsSync3(join5(cwd, "proxy.ts"));
1316
+ const pkgJson = packageJsonSchema.safeParse(safeReadJson(join5(cwd, "package.json")));
1317
+ const hasOpenNextDep = pkgJson.success ? Boolean(pkgJson.data.dependencies?.["@opennextjs/cloudflare"]) : false;
1318
+ const isLinked = Boolean(projectUuid);
1319
+ const isTransformed = hasMiddleware && !hasProxy && hasOpenNextDep;
1320
+ const isFullySetUp = isLinked && isTransformed;
1321
+ return {
1322
+ projectUuid,
1323
+ hasMiddleware,
1324
+ hasProxy,
1325
+ hasOpenNextDep,
1326
+ isLinked,
1327
+ isTransformed,
1328
+ isFullySetUp
1329
+ };
1330
+ }
1331
+
1332
+ // src/cli/lib/required-build-env.ts
1333
+ var REQUIRED_BUILD_ENV_VARS = [
1334
+ "BIGCOMMERCE_STORE_HASH",
1335
+ "BIGCOMMERCE_STOREFRONT_TOKEN",
1336
+ "BIGCOMMERCE_CHANNEL_ID",
1337
+ "AUTH_SECRET"
1338
+ ];
1339
+ var findMissingBuildEnv = (env2 = process.env) => {
1340
+ return REQUIRED_BUILD_ENV_VARS.filter((name) => {
1341
+ const value = env2[name];
1342
+ return value === void 0 || value.trim() === "";
1343
+ });
1344
+ };
1345
+ var assertRequiredBuildEnv = (env2 = process.env) => {
1346
+ const missing = findMissingBuildEnv(env2);
1347
+ if (missing.length === 0) {
1348
+ return;
1349
+ }
1350
+ const missingList = missing.map((name) => ` - ${name}`).join("\n");
1351
+ throw new UserActionableError(
1352
+ `Missing required environment variable${missing.length === 1 ? "" : "s"} for the build:
1353
+ ${missingList}
1354
+
1355
+ These are read by the build itself, so they must be set before it runs. The build automatically loads \`.env.local\` and \`.env\` from the current directory (${process.cwd()}) \u2014 add the variables above to one of those files (\`.env.local\` takes precedence).
1356
+ To load an env file from a different location, pass \`--env-path <path>\` (for example \`--env-path ../.env.local\` when running from a subdirectory).`
1357
+ );
1358
+ };
1359
+
1360
+ // src/cli/lib/wrangler-config.ts
1361
+ function getCompatibilityDate(now = /* @__PURE__ */ new Date()) {
1362
+ const date = new Date(now);
1363
+ date.setUTCMonth(date.getUTCMonth() - 1);
1364
+ return date.toISOString().slice(0, 10);
1365
+ }
1366
+ var LOCAL_ROUTES_KV_NAMESPACE_ID = "catalyst-routes-kv-local";
1367
+ function getWranglerConfig(projectUuid) {
1368
+ return {
1369
+ $schema: "node_modules/wrangler/config-schema.json",
1370
+ main: "../.open-next/worker.js",
1371
+ name: `project-${projectUuid}`,
1372
+ compatibility_date: getCompatibilityDate(),
1373
+ compatibility_flags: ["nodejs_compat", "global_fetch_strictly_public"],
1374
+ observability: {
1375
+ enabled: true,
1376
+ head_sampling_rate: 0.05,
1377
+ logs: {
1378
+ enabled: true,
1379
+ head_sampling_rate: 1,
1380
+ invocation_logs: false
1381
+ }
1382
+ },
1383
+ assets: {
1384
+ directory: "../.open-next/assets",
1385
+ binding: "ASSETS"
1386
+ },
1387
+ services: [
1388
+ {
1389
+ binding: "WORKER_SELF_REFERENCE",
1390
+ service: `project-${projectUuid}`
1391
+ }
1392
+ ],
1393
+ r2_buckets: [
1394
+ {
1395
+ binding: "NEXT_INC_CACHE_R2_BUCKET",
1396
+ bucket_name: `project-${projectUuid}`
1397
+ }
1398
+ ],
1399
+ kv_namespaces: [
1400
+ {
1401
+ binding: "CATALYST_ROUTES_KV",
1402
+ id: LOCAL_ROUTES_KV_NAMESPACE_ID
1403
+ }
1404
+ ],
1405
+ durable_objects: {
1406
+ bindings: [
1407
+ {
1408
+ name: "NEXT_CACHE_DO_QUEUE",
1409
+ class_name: "DOQueueHandler"
1410
+ },
1411
+ {
1412
+ name: "NEXT_TAG_CACHE_DO_SHARDED",
1413
+ class_name: "DOShardedTagCache"
1414
+ },
1415
+ {
1416
+ name: "NEXT_CACHE_DO_PURGE",
1417
+ class_name: "BucketCachePurge"
1418
+ }
1419
+ ]
1420
+ },
1421
+ migrations: [
1422
+ {
1423
+ tag: "v1",
1424
+ new_sqlite_classes: ["DOQueueHandler", "DOShardedTagCache", "BucketCachePurge"]
1425
+ }
1426
+ ]
1427
+ };
1428
+ }
1429
+
1430
+ // src/cli/commands/build.ts
1431
+ var WRANGLER_VERSION = "4.128.0";
1432
+ var DIST_TAG_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
1433
+ var parseWranglerVersion = (value) => {
1434
+ if (!validSemver(value) && !DIST_TAG_PATTERN.test(value)) {
1435
+ throw new InvalidArgumentError(
1436
+ `"${value}" is not a valid Wrangler version or dist-tag (e.g. 4.90.0 or latest).`
1437
+ );
1438
+ }
1439
+ return value;
1440
+ };
1441
+ async function buildCatalystProject(projectUuid, wranglerVersion = WRANGLER_VERSION) {
1442
+ assertRequiredBuildEnv();
1443
+ const coreDir = process.cwd();
1444
+ const projectPackageManager = await detectProjectPackageManager(coreDir);
1445
+ if (await reconcileOpenNextVersion(coreDir, projectPackageManager, { canUpgrade: true })) {
1446
+ await installDependencies(coreDir, projectPackageManager);
1447
+ }
1448
+ const openNextOutDir = join6(coreDir, ".open-next");
1449
+ const bigcommerceDistDir = join6(coreDir, ".bigcommerce", "dist");
1450
+ consola.start("Generating GraphQL types...");
1451
+ await execa2("node", [join6(coreDir, "scripts", "generate.cjs")], {
1452
+ stdio: "inherit",
1453
+ cwd: coreDir
1454
+ });
1455
+ consola.success("GraphQL types generated");
1456
+ const wranglerConfig = getWranglerConfig(projectUuid);
1457
+ await rm(bigcommerceDistDir, { recursive: true, force: true });
1458
+ consola.start("Copying templates...");
1459
+ await copyFile(
1460
+ join6(getModuleCliPath(), "templates", "open-next.config.ts"),
1461
+ join6(coreDir, ".bigcommerce", "open-next.config.ts")
1462
+ );
1463
+ await writeFile(
1464
+ join6(coreDir, ".bigcommerce", "wrangler.jsonc"),
1465
+ JSON.stringify(wranglerConfig, null, 2)
1466
+ );
1467
+ consola.success("Templates copied");
1468
+ consola.start("Building project...");
1469
+ await execa2(
1470
+ "pnpm",
1471
+ [
1472
+ "exec",
1473
+ "opennextjs-cloudflare",
1474
+ "build",
1475
+ "--skipWranglerConfigCheck",
1476
+ "--openNextConfigPath",
1477
+ join6(coreDir, ".bigcommerce", "open-next.config.ts")
1478
+ ],
1479
+ {
1480
+ stdout: ["pipe", "inherit"],
1481
+ cwd: coreDir
1482
+ }
1483
+ );
1484
+ await copyFile(
1485
+ join6(getModuleCliPath(), "templates", "public_headers"),
1486
+ join6(openNextOutDir, "assets", "_headers")
1487
+ );
1488
+ await execa2(
1489
+ "pnpm",
1490
+ [
1491
+ "dlx",
1492
+ `wrangler@${wranglerVersion}`,
1493
+ "deploy",
1494
+ "--config",
1495
+ join6(coreDir, ".bigcommerce", "wrangler.jsonc"),
1496
+ "--keep-vars",
1497
+ "--outdir",
1498
+ bigcommerceDistDir,
1499
+ "--dry-run"
1500
+ ],
1501
+ {
1502
+ stdout: ["pipe", "inherit"],
1503
+ cwd: coreDir
1504
+ }
1505
+ );
1506
+ consola.success("Project built");
1507
+ await cp(join6(openNextOutDir, "assets"), join6(bigcommerceDistDir, "assets"), {
1508
+ recursive: true,
1509
+ force: true
1510
+ });
1511
+ }
1512
+ var build = new Command2("build").configureHelp({ showGlobalOptions: true }).description(
1513
+ "Build your Catalyst project using the OpenNext/Cloudflare build pipeline. Also runs a Wrangler dry-run to generate deployment artifacts."
1514
+ ).addHelpText(
1515
+ "after",
1516
+ `
1517
+ Examples:
1518
+ $ catalyst build
1519
+
1520
+ # Include project UUID
1059
1521
  $ catalyst build --project-uuid <UUID>
1060
1522
 
1061
1523
  # Build with a specific Wrangler version
@@ -1087,22 +1549,25 @@ Examples:
1087
1549
  const projectUuid = options.projectUuid ?? config2.get("projectUuid");
1088
1550
  if (!projectUuid) {
1089
1551
  throw new Error(
1090
- "Project UUID is required. Please run `catalyst project create` or `catalyst project link` or this command again with --project-uuid <uuid>."
1552
+ "Project UUID is required. Please run `catalyst projects create` or `catalyst projects link` or this command again with --project-uuid <uuid>."
1091
1553
  );
1092
1554
  }
1093
1555
  await buildCatalystProject(projectUuid, options.wranglerVersion);
1094
1556
  });
1095
1557
 
1096
- // src/cli/commands/channel.ts
1097
- import { select as select4 } from "@inquirer/prompts";
1558
+ // src/cli/commands/channels.ts
1559
+ import { confirm as confirm3, select as select4 } from "@inquirer/prompts";
1098
1560
  import { Command as Command3, InvalidArgumentError as InvalidArgumentError2, Option as Option3 } from "commander";
1099
1561
  import { colorize as colorize5 } from "consola/utils";
1100
1562
 
1563
+ // src/cli/lib/channel-checkout-url-flow.ts
1564
+ import { input as input3 } from "@inquirer/prompts";
1565
+
1101
1566
  // src/cli/lib/channel-site-flow.ts
1102
1567
  import { select as select2 } from "@inquirer/prompts";
1103
1568
 
1104
1569
  // src/cli/lib/channels.ts
1105
- import { z as z7 } from "zod";
1570
+ import { z as z8 } from "zod";
1106
1571
  var cliApiUrl = (origin, storeHash, path) => `${origin}/stores/${storeHash}/cli-api/v3${path}`;
1107
1572
  var channelsUrl = (storeHash, apiHost, query2 = {}) => {
1108
1573
  const params = new URLSearchParams(query2).toString();
@@ -1113,18 +1578,18 @@ var authHeaders2 = (accessToken) => ({
1113
1578
  "X-Correlation-Id": getTelemetry().correlationId,
1114
1579
  Accept: "application/json"
1115
1580
  });
1116
- var envVarsSchema = z7.record(z7.string(), z7.coerce.string());
1117
- var channelSchema = z7.object({
1118
- id: z7.number(),
1119
- name: z7.string(),
1120
- platform: z7.string()
1581
+ var envVarsSchema = z8.record(z8.string(), z8.coerce.string());
1582
+ var channelSchema = z8.object({
1583
+ id: z8.number(),
1584
+ name: z8.string(),
1585
+ platform: z8.string()
1121
1586
  });
1122
- var channelsResponseSchema = z7.object({
1123
- data: z7.array(channelSchema)
1587
+ var channelsResponseSchema = z8.object({
1588
+ data: z8.array(channelSchema)
1124
1589
  });
1125
1590
  var CHANNEL_PLATFORM_ORDER = ["catalyst", "next", "bigcommerce"];
1126
- function sortChannelsByPlatform(channels) {
1127
- return [...channels].sort((a, b) => {
1591
+ function sortChannelsByPlatform(channels2) {
1592
+ return [...channels2].sort((a, b) => {
1128
1593
  const aIndex = CHANNEL_PLATFORM_ORDER.indexOf(a.platform);
1129
1594
  const bIndex = CHANNEL_PLATFORM_ORDER.indexOf(b.platform);
1130
1595
  if (aIndex === -1 && bIndex === -1) return 0;
@@ -1136,32 +1601,46 @@ function sortChannelsByPlatform(channels) {
1136
1601
  function channelPlatformLabel(platform) {
1137
1602
  return platform === "bigcommerce" ? "Stencil" : platform.charAt(0).toUpperCase() + platform.slice(1);
1138
1603
  }
1139
- var initResponseSchema = z7.object({
1140
- data: z7.object({
1141
- storefront_api_token: z7.string(),
1604
+ var initResponseSchema = z8.object({
1605
+ data: z8.object({
1606
+ storefront_api_token: z8.string(),
1142
1607
  envVars: envVarsSchema
1143
1608
  })
1144
1609
  });
1145
- var createChannelResponseSchema = z7.object({
1146
- data: z7.object({
1147
- id: z7.number(),
1148
- storefront_api_token: z7.string(),
1610
+ var createChannelResponseSchema = z8.object({
1611
+ data: z8.object({
1612
+ id: z8.number(),
1613
+ storefront_api_token: z8.string(),
1149
1614
  envVars: envVarsSchema
1150
1615
  })
1151
1616
  });
1152
- var eligibilityResponseSchema = z7.object({
1153
- data: z7.object({
1154
- eligible: z7.boolean(),
1155
- message: z7.string()
1617
+ var eligibilityResponseSchema = z8.object({
1618
+ data: z8.object({
1619
+ eligible: z8.boolean(),
1620
+ message: z8.string()
1156
1621
  })
1157
1622
  });
1158
- var channelSiteSchema = z7.object({
1159
- data: z7.object({
1160
- id: z7.number(),
1161
- url: z7.string(),
1162
- channel_id: z7.number()
1623
+ var siteUrlSchema = z8.object({
1624
+ url: z8.string(),
1625
+ type: z8.string()
1626
+ });
1627
+ var channelSiteSchema = z8.object({
1628
+ data: z8.object({
1629
+ id: z8.number(),
1630
+ url: z8.string(),
1631
+ channel_id: z8.number(),
1632
+ ssl_status: z8.string().nullable().optional(),
1633
+ is_checkout_url_customized: z8.boolean().optional(),
1634
+ urls: z8.array(siteUrlSchema).optional()
1163
1635
  })
1164
1636
  });
1637
+ function assertChannelSettingsAuthorized(response, action) {
1638
+ if (response.status === 401 || response.status === 403) {
1639
+ throw new UserActionableError(
1640
+ `${action} (${response.status}). Re-run \`catalyst auth login\` to refresh your access token with the store_channel_settings and store_sites scopes.`
1641
+ );
1642
+ }
1643
+ }
1165
1644
  async function getChannelInit(channelId, storeHash, accessToken, origin) {
1166
1645
  const response = await fetch(cliApiUrl(origin, storeHash, `/channels/${channelId}/init`), {
1167
1646
  method: "GET",
@@ -1194,358 +1673,196 @@ async function createChannel(name, storefrontLocale, additionalLocales, installS
1194
1673
  devOrigin: "http://localhost:3000",
1195
1674
  storefrontLanguage: storefrontLocale,
1196
1675
  additionalLocales
1197
- })
1198
- });
1199
- if (!response.ok) {
1200
- throw await httpError(response, "Failed to create channel");
1201
- }
1202
- const { data } = createChannelResponseSchema.parse(await response.json());
1203
- return {
1204
- channelId: data.id,
1205
- storefrontToken: data.storefront_api_token,
1206
- envVars: data.envVars
1207
- };
1208
- }
1209
- async function fetchAvailableChannels(storeHash, accessToken, apiHost) {
1210
- const response = await fetch(
1211
- channelsUrl(storeHash, apiHost, { available: "true", type: "storefront" }),
1212
- {
1213
- method: "GET",
1214
- headers: {
1215
- "X-Auth-Token": accessToken,
1216
- "X-Correlation-Id": getTelemetry().correlationId,
1217
- Accept: "application/json"
1218
- }
1219
- }
1220
- );
1221
- assertAuthorized(response);
1222
- if (!response.ok) {
1223
- throw await httpError(response, "Failed to fetch channels");
1224
- }
1225
- return channelsResponseSchema.parse(await response.json()).data;
1226
- }
1227
- async function updateChannelSiteUrl(channelId, siteUrl, storeHash, accessToken, apiHost) {
1228
- const response = await fetch(
1229
- `https://${apiHost}/stores/${storeHash}/v3/channels/${channelId}/site`,
1230
- {
1231
- method: "PUT",
1232
- headers: {
1233
- "X-Auth-Token": accessToken,
1234
- "Content-Type": "application/json",
1235
- Accept: "application/json",
1236
- "X-Correlation-Id": getTelemetry().correlationId
1237
- },
1238
- body: JSON.stringify({ url: siteUrl })
1239
- }
1240
- );
1241
- if (response.status === 401 || response.status === 403) {
1242
- throw new UserActionableError(
1243
- `Failed to update channel site (${response.status}). Re-run \`catalyst auth login\` to refresh your access token with the store_channel_settings scope.`
1244
- );
1245
- }
1246
- if (!response.ok) {
1247
- throw await httpError(response, "Failed to update channel site");
1248
- }
1249
- const res = await response.json();
1250
- const { data } = channelSiteSchema.parse(res);
1251
- return { id: data.id, url: data.url, channelId: data.channel_id };
1252
- }
1253
-
1254
- // src/cli/lib/commerce-hosting.ts
1255
- import { confirm as confirm2, input as input2, select, Separator } from "@inquirer/prompts";
1256
- import { colorize as colorize3 } from "consola/utils";
1257
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
1258
- import { dirname as dirname2, join as join5 } from "path";
1259
- import { z as z8 } from "zod";
1260
-
1261
- // src/cli/lib/sort-package-json.ts
1262
- var FIELD_ORDER = [
1263
- "name",
1264
- "description",
1265
- "version",
1266
- "private",
1267
- "engines",
1268
- "scripts",
1269
- "dependencies",
1270
- "devDependencies"
1271
- ];
1272
- function sortPackageJsonFields(pkg) {
1273
- const ordered = {};
1274
- FIELD_ORDER.forEach((field) => {
1275
- if (field in pkg) {
1276
- ordered[field] = pkg[field];
1277
- }
1278
- });
1279
- Object.keys(pkg).forEach((key) => {
1280
- if (!(key in ordered)) {
1281
- ordered[key] = pkg[key];
1282
- }
1283
- });
1284
- return ordered;
1285
- }
1286
-
1287
- // src/cli/lib/commerce-hosting.ts
1288
- var OPENNEXT_CLOUDFLARE_VERSION = "1.17.3";
1289
- var corePackageJsonSchema = z8.looseObject({
1290
- dependencies: z8.record(z8.string(), z8.string()).optional()
1291
- });
1292
- var writeJson = (path, value) => {
1293
- mkdirSync(dirname2(path), { recursive: true });
1294
- writeFileSync(path, `${JSON.stringify(value, null, 2)}
1295
- `);
1296
- };
1297
- var convertProxyToMiddleware = (projectDir) => {
1298
- const proxyPath = join5(projectDir, "proxy.ts");
1299
- const middlewarePath = join5(projectDir, "middleware.ts");
1300
- if (!existsSync3(proxyPath)) return;
1301
- const contents = readFileSync2(proxyPath, "utf-8").replace("export const proxy", "export const middleware").replace("export const config = {", "export const config = {\n runtime: 'experimental-edge',");
1302
- writeFileSync(middlewarePath, contents);
1303
- unlinkSync(proxyPath);
1304
- };
1305
- var cleanupCloudflareIncompatibilities = async (projectDir) => {
1306
- const instrumentationPath = join5(projectDir, "instrumentation.ts");
1307
- if (!existsSync3(instrumentationPath)) return;
1308
- if (!process.stdin.isTTY) {
1309
- consola.warn(
1310
- "instrumentation.ts is present and may be incompatible with the Cloudflare Workers bundle (the default @vercel/otel scaffolding throws at cold start under workerd). Skipping automatic cleanup in non-interactive mode \u2014 re-run interactively to remove it, or delete/gate it manually."
1311
- );
1312
- return;
1313
- }
1314
- const shouldRemove = await confirm2({
1315
- 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?',
1316
- default: true
1317
- });
1318
- if (!shouldRemove) {
1319
- consola.info(
1320
- 'Leaving instrumentation.ts in place. The Cloudflare worker will continue to log "Failed to prepare server" at cold start until this is resolved manually.'
1321
- );
1322
- return;
1323
- }
1324
- unlinkSync(instrumentationPath);
1325
- consola.info("Removed instrumentation.ts (incompatible with Cloudflare Workers).");
1326
- const corePackageJsonPath = join5(projectDir, "package.json");
1327
- if (existsSync3(corePackageJsonPath)) {
1328
- const pkg = corePackageJsonSchema.parse(JSON.parse(readFileSync2(corePackageJsonPath, "utf-8")));
1329
- if (pkg.dependencies && "@vercel/otel" in pkg.dependencies) {
1330
- const { "@vercel/otel": _removedVercelOtel, ...preservedDeps } = pkg.dependencies;
1331
- pkg.dependencies = preservedDeps;
1332
- writeJson(corePackageJsonPath, sortPackageJsonFields(pkg));
1333
- consola.info("Dropped @vercel/otel from package.json.");
1334
- }
1335
- }
1336
- };
1337
- var setupCommerceHosting = async ({
1338
- projectDir,
1339
- projectUuid,
1340
- storeHash,
1341
- accessToken
1342
- }) => {
1343
- await cleanupCloudflareIncompatibilities(projectDir);
1344
- const corePackageJsonPath = join5(projectDir, "package.json");
1345
- const pkg = corePackageJsonSchema.parse(JSON.parse(readFileSync2(corePackageJsonPath, "utf-8")));
1346
- pkg.dependencies = {
1347
- ...pkg.dependencies,
1348
- "@opennextjs/cloudflare": OPENNEXT_CLOUDFLARE_VERSION
1349
- };
1350
- writeJson(corePackageJsonPath, sortPackageJsonFields(pkg));
1351
- const projectJson = {
1352
- projectUuid,
1353
- framework: "catalyst"
1354
- };
1355
- if (storeHash) projectJson.storeHash = storeHash;
1356
- if (accessToken) projectJson.accessToken = accessToken;
1357
- writeJson(join5(projectDir, ".bigcommerce", "project.json"), projectJson);
1358
- convertProxyToMiddleware(projectDir);
1359
- };
1360
- var NoLinkedProjectError = class extends Error {
1361
- constructor() {
1362
- super("No infrastructure project linked: user declined to create one.");
1363
- this.name = "NoLinkedProjectError";
1364
- }
1365
- };
1366
- async function promptForNewProjectName(api) {
1367
- const newProjectName = await input2({ message: "Enter a name for the new project:" });
1368
- const data = await createProject(newProjectName, api.storeHash, api.accessToken, api.apiHost);
1369
- consola.success(`Project "${data.name}" created successfully.`);
1370
- return { uuid: data.uuid, name: data.name, deployment_hostnames: [] };
1676
+ })
1677
+ });
1678
+ if (!response.ok) {
1679
+ throw await httpError(response, "Failed to create channel");
1680
+ }
1681
+ const { data } = createChannelResponseSchema.parse(await response.json());
1682
+ return {
1683
+ channelId: data.id,
1684
+ storefrontToken: data.storefront_api_token,
1685
+ envVars: data.envVars
1686
+ };
1371
1687
  }
1372
- async function selectOrCreateInfrastructureProject(api, linkedProjectUuid) {
1373
- consola.start("Fetching projects...");
1374
- const existingProjects = await fetchProjects(api.storeHash, api.accessToken, api.apiHost);
1375
- consola.success("Projects fetched.");
1376
- if (existingProjects.length === 0) {
1377
- const shouldCreate = await confirm2({
1378
- message: "There are not any hosting projects that you can link to yet. Would you like to create one?",
1379
- default: true
1380
- });
1381
- if (!shouldCreate) {
1382
- throw new NoLinkedProjectError();
1688
+ async function fetchAvailableChannels(storeHash, accessToken, apiHost) {
1689
+ const response = await fetch(
1690
+ channelsUrl(storeHash, apiHost, { available: "true", type: "storefront" }),
1691
+ {
1692
+ method: "GET",
1693
+ headers: {
1694
+ "X-Auth-Token": accessToken,
1695
+ "X-Correlation-Id": getTelemetry().correlationId,
1696
+ Accept: "application/json"
1697
+ }
1383
1698
  }
1384
- return promptForNewProjectName(api);
1699
+ );
1700
+ assertAuthorized(response);
1701
+ if (!response.ok) {
1702
+ throw await httpError(response, "Failed to fetch channels");
1385
1703
  }
1386
- const choices = [
1387
- ...existingProjects.map((p) => ({
1388
- name: p.uuid === linkedProjectUuid ? `${p.name} ${colorize3("green", "[linked]")}` : p.name,
1389
- value: p.uuid,
1390
- description: p.uuid
1391
- })),
1704
+ return channelsResponseSchema.parse(await response.json()).data;
1705
+ }
1706
+ async function updateChannelSiteUrl(channelId, siteUrl, storeHash, accessToken, apiHost) {
1707
+ const response = await fetch(
1708
+ `https://${apiHost}/stores/${storeHash}/v3/channels/${channelId}/site`,
1392
1709
  {
1393
- name: "Create a new project",
1394
- value: "create",
1395
- description: "Create a new hosting project for this Catalyst storefront."
1710
+ method: "PUT",
1711
+ headers: {
1712
+ "X-Auth-Token": accessToken,
1713
+ "Content-Type": "application/json",
1714
+ Accept: "application/json",
1715
+ "X-Correlation-Id": getTelemetry().correlationId
1716
+ },
1717
+ body: JSON.stringify({ url: siteUrl })
1396
1718
  }
1397
- ];
1398
- const selected = await select({
1399
- message: "Select a project or create a new project (Press <enter> to select).",
1400
- choices
1401
- });
1402
- if (selected === "create") {
1403
- return promptForNewProjectName(api);
1404
- }
1405
- const matched = existingProjects.find((p) => p.uuid === selected);
1406
- if (!matched) {
1407
- throw new Error(`Selected project ${String(selected)} not found in fetched list.`);
1719
+ );
1720
+ assertChannelSettingsAuthorized(response, "Failed to update channel site");
1721
+ if (!response.ok) {
1722
+ throw await httpError(response, "Failed to update channel site");
1408
1723
  }
1409
- return matched;
1724
+ const res = await response.json();
1725
+ const { data } = channelSiteSchema.parse(res);
1726
+ return { id: data.id, url: data.url, channelId: data.channel_id };
1410
1727
  }
1411
- async function promptForCommerceHostingProject(api, defaultName, autoUseDefaultName, useExistingOnCollision) {
1412
- const existingProjects = await fetchProjects(api.storeHash, api.accessToken, api.apiHost);
1413
- const takenNames = existingProjects.map((project2) => project2.name);
1414
- if (autoUseDefaultName) {
1415
- return autoCreateCommerceHostingProject(
1416
- api,
1417
- defaultName,
1418
- existingProjects,
1419
- useExistingOnCollision
1420
- );
1421
- }
1422
- const conflict = existingProjects.find(
1423
- (project2) => project2.name.toLowerCase() === defaultName.toLowerCase()
1728
+ var toChannelSiteDetails = (data) => ({
1729
+ id: data.id,
1730
+ url: data.url,
1731
+ channelId: data.channel_id,
1732
+ sslStatus: data.ssl_status ?? null,
1733
+ isCheckoutUrlCustomized: data.is_checkout_url_customized ?? false,
1734
+ urls: data.urls ?? []
1735
+ });
1736
+ function findChannelSiteUrl(site, type) {
1737
+ return site.urls.find((entry) => entry.type === type)?.url;
1738
+ }
1739
+ async function getChannelSite(channelId, storeHash, accessToken, apiHost) {
1740
+ const response = await fetch(
1741
+ `https://${apiHost}/stores/${storeHash}/v3/channels/${channelId}/site`,
1742
+ { method: "GET", headers: authHeaders2(accessToken) }
1424
1743
  );
1425
- if (!conflict) {
1426
- return autoCreateCommerceHostingProject(
1427
- api,
1428
- defaultName,
1429
- existingProjects,
1430
- useExistingOnCollision
1431
- );
1432
- }
1433
- const hasOtherProjects = existingProjects.length > 1;
1434
- const choices = [
1435
- { name: `Use "${conflict.name}"`, value: "use-named" }
1436
- ];
1437
- if (hasOtherProjects) {
1438
- choices.push({ name: "Select from my projects", value: "select-from-list" });
1744
+ assertChannelSettingsAuthorized(response, "Failed to fetch channel site");
1745
+ if (!response.ok) {
1746
+ throw await httpError(response, "Failed to fetch channel site");
1439
1747
  }
1440
- choices.push({ name: "Create a new project", value: "create" });
1441
- const action = await select({
1442
- message: hasOtherProjects ? `It looks like you already have an existing Commerce Hosting project named "${conflict.name}". Would you like to use it, select from your projects, or create a new one?` : `It looks like you already have an existing Commerce Hosting project named "${conflict.name}". Would you like to use it, or create a new one?`,
1443
- choices
1748
+ const res = await response.json();
1749
+ return toChannelSiteDetails(channelSiteSchema.parse(res).data);
1750
+ }
1751
+ var checkoutUrlPath = (storeHash, apiHost, channelId) => `https://${apiHost}/stores/${storeHash}/v3/channels/${channelId}/site/checkout-url`;
1752
+ async function updateChannelCheckoutUrl(channelId, checkoutUrl, storeHash, accessToken, apiHost) {
1753
+ const response = await fetch(checkoutUrlPath(storeHash, apiHost, channelId), {
1754
+ method: "PUT",
1755
+ headers: { ...authHeaders2(accessToken), "Content-Type": "application/json" },
1756
+ body: JSON.stringify({ url: checkoutUrl })
1444
1757
  });
1445
- if (action === "use-named") {
1446
- consola.success(`Using existing Commerce Hosting project "${conflict.name}"`);
1447
- return conflict;
1448
- }
1449
- if (action === "create") {
1450
- return promptAndCreateCommerceHostingProject(api, takenNames, defaultName);
1758
+ assertChannelSettingsAuthorized(response, "Failed to update channel checkout URL");
1759
+ if (!response.ok) {
1760
+ throw await httpError(response, "Failed to update channel checkout URL");
1451
1761
  }
1452
- const selected = await select({
1453
- message: "Which Commerce Hosting project would you like to use?",
1454
- choices: [
1455
- ...existingProjects.map((project2) => ({
1456
- name: project2.name,
1457
- value: project2,
1458
- description: project2.uuid
1459
- })),
1460
- new Separator(),
1461
- { name: "Create a new project", value: "create-new" }
1462
- ]
1762
+ const res = await response.json();
1763
+ return toChannelSiteDetails(channelSiteSchema.parse(res).data);
1764
+ }
1765
+ async function deleteChannelCheckoutUrl(channelId, storeHash, accessToken, apiHost) {
1766
+ const response = await fetch(checkoutUrlPath(storeHash, apiHost, channelId), {
1767
+ method: "DELETE",
1768
+ headers: authHeaders2(accessToken)
1463
1769
  });
1464
- if (selected === "create-new") {
1465
- return promptAndCreateCommerceHostingProject(api, takenNames, defaultName);
1770
+ assertChannelSettingsAuthorized(response, "Failed to remove channel checkout URL");
1771
+ if (!response.ok) {
1772
+ throw await httpError(response, "Failed to remove channel checkout URL");
1466
1773
  }
1467
- consola.success(`Using existing Commerce Hosting project "${selected.name}"`);
1468
- return selected;
1469
1774
  }
1470
- async function promptAndCreateCommerceHostingProject(api, takenNames, defaultName) {
1471
- const projectName = await input2({
1472
- message: "What would you like to name your Commerce Hosting project?",
1473
- default: defaultName,
1474
- validate: (value) => {
1475
- const trimmed = value.trim();
1476
- if (!trimmed) return "Project name is required";
1477
- const conflict = takenNames.find((taken) => taken.toLowerCase() === trimmed.toLowerCase());
1478
- if (conflict) {
1479
- return `A Commerce Hosting project named "${conflict}" already exists`;
1480
- }
1481
- return true;
1482
- },
1483
- theme: {
1484
- style: {
1485
- help: () => colorize3(
1486
- "dim",
1487
- "(The project that hosts your storefront on Commerce \u2014 often matches your folder name.)"
1488
- )
1489
- }
1490
- }
1491
- });
1775
+
1776
+ // src/cli/lib/checkout-url.ts
1777
+ import { getDomain } from "tldts";
1778
+ var normalizeHostname = (hostname) => hostname.toLowerCase().replace(/\.$/, "");
1779
+ var parentDomain = (hostname) => hostname.split(".").slice(1).join(".");
1780
+ var isSubdomainOf = (hostname, parent) => hostname.endsWith(`.${parent}`);
1781
+ function hostnameOf(url) {
1492
1782
  try {
1493
- const created = await createProject(
1494
- projectName.trim(),
1495
- api.storeHash,
1496
- api.accessToken,
1497
- api.apiHost
1783
+ return new URL(url).hostname;
1784
+ } catch {
1785
+ return void 0;
1786
+ }
1787
+ }
1788
+ function sharesMainDomain(a, b) {
1789
+ const first = getDomain(normalizeHostname(a));
1790
+ return first !== null && first === getDomain(normalizeHostname(b));
1791
+ }
1792
+ function suggestCheckoutUrl(storefrontUrl) {
1793
+ const host = hostnameOf(storefrontUrl);
1794
+ if (!host) return void 0;
1795
+ const base = normalizeHostname(host).replace(/^www\./, "");
1796
+ return `https://checkout.${base}`;
1797
+ }
1798
+ function normalizeCheckoutUrl(value) {
1799
+ const withScheme = /^[a-z][a-z\d+.-]*:\/\//i.test(value) ? value : `https://${value}`;
1800
+ let parsed;
1801
+ try {
1802
+ parsed = new URL(withScheme);
1803
+ } catch {
1804
+ throw new UserActionableError(
1805
+ `"${value}" is not a valid URL. Pass a hostname or an https URL, e.g. https://checkout.example.com.`
1806
+ );
1807
+ }
1808
+ if (parsed.protocol !== "https:") {
1809
+ throw new UserActionableError(
1810
+ `The checkout URL must use https, but "${value}" uses ${parsed.protocol.replace(":", "")}.`
1498
1811
  );
1499
- consola.success(`Commerce Hosting project "${created.name}" created successfully`);
1500
- return { uuid: created.uuid, name: created.name, deployment_hostnames: [] };
1501
- } catch (error) {
1502
- if (error instanceof InfrastructureProjectValidationError) {
1503
- consola.error(error.message);
1504
- return promptAndCreateCommerceHostingProject(api, takenNames, defaultName);
1505
- }
1506
- throw error;
1507
1812
  }
1813
+ return parsed.origin;
1508
1814
  }
1509
- async function resolveCollisionChoice(existingName, useExistingOnCollision) {
1510
- if (useExistingOnCollision === true) return true;
1511
- if (!process.stdin.isTTY) return false;
1512
- return select({
1513
- message: `A Commerce Hosting project named "${existingName}" already exists. Use the existing project?`,
1514
- choices: [
1515
- { name: "Yes", value: true },
1516
- { name: "No", value: false }
1517
- ]
1518
- });
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
+ }
1519
1825
  }
1520
- async function autoCreateCommerceHostingProject(api, name, existingProjects, useExistingOnCollision) {
1521
- const existing = existingProjects.find(
1522
- (project2) => project2.name.toLowerCase() === name.toLowerCase()
1826
+ async function warnOnCrossDomainCheckout(site, context) {
1827
+ const storefrontUrl = findChannelSiteUrl(site, "primary") ?? site.url;
1828
+ const storefrontHost = hostnameOf(storefrontUrl);
1829
+ const checkoutUrl = findChannelSiteUrl(site, "checkout");
1830
+ if (!storefrontHost) return { crossDomain: false };
1831
+ const suggestion = suggestCheckoutUrl(storefrontUrl);
1832
+ const checkoutHost = checkoutUrl ? hostnameOf(checkoutUrl) : void 0;
1833
+ if (!checkoutHost || sharesMainDomain(storefrontHost, checkoutHost)) {
1834
+ return { crossDomain: false, suggestion };
1835
+ }
1836
+ if (site.isCheckoutUrlCustomized) {
1837
+ consola.warn(
1838
+ `This channel's checkout URL (${checkoutHost}) is on a different domain than its storefront (${storefrontHost}) \u2014 most likely a storefront URL change left it behind.`
1839
+ );
1840
+ } else {
1841
+ consola.warn(
1842
+ `Checkout is on the default channel's domain (${checkoutHost}), not this channel's storefront (${storefrontHost}).`
1843
+ );
1844
+ }
1845
+ consola.info(
1846
+ "Shopper sessions and carts may not carry into checkout in browsers that restrict cross-domain cookies."
1523
1847
  );
1524
- if (existing) {
1525
- const shouldUseExisting = await resolveCollisionChoice(existing.name, useExistingOnCollision);
1526
- if (shouldUseExisting) {
1527
- consola.success(`Using existing Commerce Hosting project "${existing.name}"`);
1528
- return existing;
1529
- }
1530
- consola.error(
1531
- "Not reusing the existing project. Re-run with a different --project-name, or pass --use-existing to reuse it."
1848
+ const storefrontOnManagedZone = await isManagedHostingHostname(storefrontHost, context);
1849
+ const report = { crossDomain: true, storefrontOnManagedZone, suggestion };
1850
+ if (storefrontOnManagedZone === false) {
1851
+ 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.`
1532
1853
  );
1533
- process.exit(1);
1854
+ return report;
1534
1855
  }
1535
- try {
1536
- const created = await createProject(name, api.storeHash, api.accessToken, api.apiHost);
1537
- consola.success(`Commerce Hosting project "${created.name}" created successfully`);
1538
- return { uuid: created.uuid, name: created.name, deployment_hostnames: [] };
1539
- } catch (error) {
1540
- if (error instanceof InfrastructureProjectValidationError) {
1541
- consola.error(
1542
- `Failed to create Commerce Hosting project "${name}": ${error.message}
1543
- Re-run with a different --project-name.`
1544
- );
1545
- process.exit(1);
1546
- }
1547
- throw error;
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\`.`
1859
+ );
1860
+ return report;
1548
1861
  }
1862
+ 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."
1864
+ );
1865
+ return report;
1549
1866
  }
1550
1867
 
1551
1868
  // src/cli/lib/channel-site-flow.ts
@@ -1556,8 +1873,8 @@ async function resolveProject(options) {
1556
1873
  apiHost: options.apiHost
1557
1874
  };
1558
1875
  if (options.projectUuid) {
1559
- const projects = await fetchProjects(api.storeHash, api.accessToken, api.apiHost);
1560
- const matched = projects.find((p) => p.uuid === options.projectUuid);
1876
+ const projects2 = await fetchProjects(api.storeHash, api.accessToken, api.apiHost);
1877
+ const matched = projects2.find((p) => p.uuid === options.projectUuid);
1561
1878
  if (matched) return matched;
1562
1879
  consola.warn(
1563
1880
  `Project ${options.projectUuid} not found on this store. Pick another to continue.`
@@ -1570,13 +1887,13 @@ async function resolveChannel(options) {
1570
1887
  return { id: options.channelId };
1571
1888
  }
1572
1889
  consola.start("Fetching channels...");
1573
- const channels = await fetchAvailableChannels(
1890
+ const channels2 = await fetchAvailableChannels(
1574
1891
  options.storeHash,
1575
1892
  options.accessToken,
1576
1893
  options.apiHost
1577
1894
  );
1578
1895
  consola.success("Channels fetched.");
1579
- const catalystChannels = channels.filter((c) => c.platform === "catalyst");
1896
+ const catalystChannels = channels2.filter((c) => c.platform === "catalyst");
1580
1897
  if (catalystChannels.length === 0) {
1581
1898
  throw new Error(
1582
1899
  "No Catalyst channels found on this store. Create one with `catalyst create` and try again."
@@ -1593,19 +1910,19 @@ async function resolveChannel(options) {
1593
1910
  const matched = catalystChannels.find((c) => c.id === id);
1594
1911
  return { id, name: matched?.name };
1595
1912
  }
1596
- async function resolveHostname(project2, options) {
1913
+ async function resolveHostname(project, options) {
1597
1914
  if (options.hostname) {
1598
1915
  return options.hostname;
1599
1916
  }
1600
- if (project2.deployment_hostnames.length === 0) {
1917
+ if (project.deployment_hostnames.length === 0) {
1601
1918
  throw new Error(
1602
- `Project "${project2.name}" has no deployment hostnames yet. Run \`catalyst deploy\` first to create one.`
1919
+ `Project "${project.name}" has no deployment hostnames yet. Run \`catalyst deploy\` first to create one.`
1603
1920
  );
1604
1921
  }
1605
1922
  const ordered = options.preferHostname ? [
1606
- ...project2.deployment_hostnames.filter((h) => h === options.preferHostname),
1607
- ...project2.deployment_hostnames.filter((h) => h !== options.preferHostname)
1608
- ] : project2.deployment_hostnames;
1923
+ ...project.deployment_hostnames.filter((h) => h === options.preferHostname),
1924
+ ...project.deployment_hostnames.filter((h) => h !== options.preferHostname)
1925
+ ] : project.deployment_hostnames;
1609
1926
  const selected = await select2({
1610
1927
  message: "Select the hostname to point the channel at.",
1611
1928
  choices: ordered.map((h) => ({ name: h, value: h }))
@@ -1613,23 +1930,80 @@ async function resolveHostname(project2, options) {
1613
1930
  return selected;
1614
1931
  }
1615
1932
  async function runChannelSiteUrlFlow(options) {
1616
- const project2 = await resolveProject(options);
1617
- const channel2 = await resolveChannel(options);
1618
- const hostname = await resolveHostname(project2, options);
1933
+ const project = await resolveProject(options);
1934
+ const channel = await resolveChannel(options);
1935
+ const hostname = await resolveHostname(project, options);
1619
1936
  const siteUrl = hostname.startsWith("https://") ? hostname : `https://${hostname}`;
1620
1937
  await updateChannelSiteUrl(
1621
- channel2.id,
1938
+ channel.id,
1622
1939
  siteUrl,
1623
1940
  options.storeHash,
1624
1941
  options.accessToken,
1625
1942
  options.apiHost
1626
1943
  );
1627
- const channelLabel = channel2.name ? `"${channel2.name}" (${channel2.id})` : String(channel2.id);
1944
+ const channelLabel = channel.name ? `"${channel.name}" (${channel.id})` : String(channel.id);
1628
1945
  consola.success(`Updated channel ${channelLabel} site URL to ${siteUrl}.`);
1946
+ try {
1947
+ const site = await getChannelSite(
1948
+ channel.id,
1949
+ options.storeHash,
1950
+ options.accessToken,
1951
+ options.apiHost
1952
+ );
1953
+ const report = await warnOnCrossDomainCheckout(site, {
1954
+ storeHash: options.storeHash,
1955
+ accessToken: options.accessToken,
1956
+ apiHost: options.apiHost
1957
+ });
1958
+ if (report.crossDomain && report.storefrontOnManagedZone !== true) {
1959
+ consola.info("Set a checkout URL for this channel with:");
1960
+ consola.log(
1961
+ ` catalyst channels update --channel-id ${channel.id} --checkout-url ${report.suggestion ?? "<domain>"}`
1962
+ );
1963
+ }
1964
+ } catch {
1965
+ }
1966
+ return { channelId: channel.id };
1967
+ }
1968
+
1969
+ // src/cli/lib/channel-checkout-url-flow.ts
1970
+ async function runChannelCheckoutUrlFlow(options) {
1971
+ const channel = options.channelId !== void 0 ? { id: options.channelId, name: options.channelName } : await resolveChannel(options);
1972
+ const label = channel.name ? `"${channel.name}" (${channel.id})` : String(channel.id);
1973
+ let storefrontUrl = options.storefrontUrl;
1974
+ if (!storefrontUrl) {
1975
+ const site = await getChannelSite(
1976
+ channel.id,
1977
+ options.storeHash,
1978
+ options.accessToken,
1979
+ options.apiHost
1980
+ );
1981
+ storefrontUrl = findChannelSiteUrl(site, "primary") ?? site.url;
1982
+ const currentCheckoutUrl = findChannelSiteUrl(site, "checkout");
1983
+ consola.info(`Channel ${label} storefront is ${storefrontUrl}.`);
1984
+ if (currentCheckoutUrl) {
1985
+ consola.info(
1986
+ `Checkout is currently ${currentCheckoutUrl}${site.isCheckoutUrlCustomized ? "" : ", inherited from the default channel"}.`
1987
+ );
1988
+ }
1989
+ }
1990
+ const answer = options.url ?? await input3({
1991
+ message: "Checkout URL for this channel (must share a main domain with the storefront)",
1992
+ default: suggestCheckoutUrl(storefrontUrl)
1993
+ });
1994
+ const checkoutUrl = normalizeCheckoutUrl(answer);
1995
+ await updateChannelCheckoutUrl(
1996
+ channel.id,
1997
+ checkoutUrl,
1998
+ options.storeHash,
1999
+ options.accessToken,
2000
+ options.apiHost
2001
+ );
2002
+ consola.success(`Updated channel ${label} checkout URL to ${checkoutUrl}.`);
1629
2003
  }
1630
2004
 
1631
2005
  // src/cli/lib/create-channel-flow.ts
1632
- import { checkbox, input as input3, select as select3 } from "@inquirer/prompts";
2006
+ import { checkbox, input as input4, select as select3 } from "@inquirer/prompts";
1633
2007
  import { colorize as colorize4 } from "consola/utils";
1634
2008
 
1635
2009
  // src/cli/lib/localization.ts
@@ -1704,7 +2078,7 @@ async function runCreateChannelFlow(options) {
1704
2078
  throw new UserActionableError(nameError);
1705
2079
  }
1706
2080
  }
1707
- const name = options.name ?? await input3({
2081
+ const name = options.name ?? await input4({
1708
2082
  message: "What would you like to name your new channel?",
1709
2083
  validate: (value) => getChannelNameError(value) ?? true
1710
2084
  });
@@ -1749,7 +2123,8 @@ async function runCreateChannelFlow(options) {
1749
2123
  { name: "No", value: false }
1750
2124
  ]
1751
2125
  });
1752
- return createChannel(
2126
+ consola.start("Creating channel...");
2127
+ const channel = await createChannel(
1753
2128
  name,
1754
2129
  storefrontLocale,
1755
2130
  additionalLocales,
@@ -1758,19 +2133,21 @@ async function runCreateChannelFlow(options) {
1758
2133
  accessToken,
1759
2134
  cliApiOrigin
1760
2135
  );
2136
+ consola.success("Channel created.");
2137
+ return channel;
1761
2138
  }
1762
2139
 
1763
2140
  // src/cli/lib/env-config.ts
1764
2141
  var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
1765
- var parseEnvAssignment = (input6) => {
1766
- const separatorIndex = input6.indexOf("=");
2142
+ var parseEnvAssignment = (input7) => {
2143
+ const separatorIndex = input7.indexOf("=");
1767
2144
  if (separatorIndex === -1) {
1768
- throw new Error(`Invalid env var format: ${input6}. Expected format: KEY=VALUE`);
2145
+ throw new Error(`Invalid env var format: ${input7}. Expected format: KEY=VALUE`);
1769
2146
  }
1770
- const key = input6.slice(0, separatorIndex).trim();
1771
- const value = input6.slice(separatorIndex + 1).trim();
2147
+ const key = input7.slice(0, separatorIndex).trim();
2148
+ const value = input7.slice(separatorIndex + 1).trim();
1772
2149
  if (!key || !value) {
1773
- throw new Error(`Invalid env var format: ${input6}. Expected format: KEY=VALUE`);
2150
+ throw new Error(`Invalid env var format: ${input7}. Expected format: KEY=VALUE`);
1774
2151
  }
1775
2152
  if (!ENV_KEY_PATTERN.test(key)) {
1776
2153
  throw new Error(
@@ -1810,7 +2187,7 @@ function resolveCredentials(options, config2) {
1810
2187
  // src/cli/lib/write-env.ts
1811
2188
  import { outputFileSync } from "fs-extra/esm";
1812
2189
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1813
- import { join as join6 } from "path";
2190
+ import { join as join7 } from "path";
1814
2191
  var ACTIVE_KEY_LINE = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
1815
2192
  var keyFromLine = (line) => {
1816
2193
  const match = ACTIVE_KEY_LINE.exec(line);
@@ -1827,8 +2204,8 @@ var parseEnvValues = (contents) => {
1827
2204
  return values;
1828
2205
  };
1829
2206
  var writeEnv = (projectDir, envVars) => {
1830
- const examplePath = join6(projectDir, ".env.example");
1831
- const localPath = join6(projectDir, ".env.local");
2207
+ const examplePath = join7(projectDir, ".env.example");
2208
+ const localPath = join7(projectDir, ".env.local");
1832
2209
  const existingLocal = existsSync4(localPath) ? parseEnvValues(readFileSync3(localPath, "utf-8")) : /* @__PURE__ */ new Map();
1833
2210
  const resolve2 = (key) => {
1834
2211
  if (key in envVars && envVars[key] !== "") {
@@ -1874,7 +2251,7 @@ var writeEnv = (projectDir, envVars) => {
1874
2251
  `);
1875
2252
  };
1876
2253
 
1877
- // src/cli/commands/channel.ts
2254
+ // src/cli/commands/channels.ts
1878
2255
  var parseChannelId = (value) => {
1879
2256
  const parsed = Number.parseInt(value, 10);
1880
2257
  if (Number.isNaN(parsed)) {
@@ -1882,6 +2259,22 @@ var parseChannelId = (value) => {
1882
2259
  }
1883
2260
  return parsed;
1884
2261
  };
2262
+ var parseCheckoutUrl = (value) => {
2263
+ try {
2264
+ return normalizeCheckoutUrl(value);
2265
+ } catch (error) {
2266
+ throw new InvalidArgumentError2(error instanceof Error ? error.message : String(error));
2267
+ }
2268
+ };
2269
+ var CHECKOUT_URL_NOTES = `
2270
+ Checkout is hosted by BigCommerce, so a checkout URL must be a domain you have
2271
+ pointed at BigCommerce with a certificate provisioned there \u2014 not one added with
2272
+ \`catalyst domains add\`, which only routes traffic to this project.
2273
+
2274
+ BigCommerce also requires the checkout URL to share a main domain with the
2275
+ channel's storefront URL, so that sessions carry between the two. This means a
2276
+ custom checkout URL is only possible on a custom storefront domain; channels on
2277
+ an auto-generated deployment hostname use the shared checkout domain.`;
1885
2278
  async function resolveCredentialsWithLogin(options, config2, apiHost) {
1886
2279
  const storeHash = options.storeHash ?? config2.get("storeHash");
1887
2280
  const accessToken = options.accessToken ?? config2.get("accessToken");
@@ -1900,17 +2293,29 @@ async function resolveCredentialsWithLogin(options, config2, apiHost) {
1900
2293
  throw error;
1901
2294
  }
1902
2295
  }
1903
- var update = new Command3("update").configureHelp({ showGlobalOptions: true }).description(
1904
- "Update a BigCommerce channel's site URL to point at one of your project's deployment hostnames."
1905
- ).addHelpText(
2296
+ var update = new Command3("update").configureHelp({ showGlobalOptions: true }).description("Update a BigCommerce channel's storefront and checkout URLs.").addHelpText(
1906
2297
  "after",
1907
- `
2298
+ `${CHECKOUT_URL_NOTES}
2299
+
2300
+ With no flags, this updates the storefront URL and prompts for the hostname.
2301
+ Pass a checkout flag on its own to change only the checkout URL, leaving the
2302
+ storefront URL alone.
2303
+
1908
2304
  Examples:
1909
2305
  # Pick a channel and hostname interactively
1910
- $ catalyst channel update
2306
+ $ catalyst channels update
2307
+
2308
+ # Point the storefront at a deployment hostname
2309
+ $ catalyst channels update --channel-id 123 --hostname my-storefront.example.com
2310
+
2311
+ # Set both at once
2312
+ $ catalyst channels update --channel-id 123 --hostname my-storefront.example.com --checkout-url checkout.example.com
2313
+
2314
+ # Change only the checkout URL
2315
+ $ catalyst channels update --channel-id 123 --checkout-url checkout.example.com
1911
2316
 
1912
- # Skip both prompts
1913
- $ catalyst channel update --channel-id 123 --hostname my-storefront.example.com`
2317
+ # Fall back to the shared checkout domain
2318
+ $ catalyst channels update --channel-id 123 --remove-checkout-url`
1914
2319
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).addOption(
1915
2320
  new Option3(
1916
2321
  "--channel-id <id>",
@@ -1921,29 +2326,68 @@ Examples:
1921
2326
  "--hostname <hostname>",
1922
2327
  "Skip the hostname prompt and use this hostname directly. Must be one of the project's deployment_hostnames."
1923
2328
  )
2329
+ ).addOption(
2330
+ new Option3(
2331
+ "--checkout-url <url>",
2332
+ "Set the channel's checkout URL. Must share a main domain with the storefront URL."
2333
+ ).argParser(parseCheckoutUrl)
2334
+ ).addOption(
2335
+ new Option3(
2336
+ "--remove-checkout-url",
2337
+ "Remove the channel's custom checkout URL so checkout falls back to the shared checkout domain."
2338
+ ).conflicts("checkoutUrl")
1924
2339
  ).action(async (options) => {
1925
2340
  const config2 = getProjectConfig();
1926
2341
  const apiHost = resolveApiHost(options, config2);
1927
2342
  const { storeHash, accessToken } = resolveCredentials(options, config2);
1928
2343
  await getTelemetry().identify(storeHash);
1929
- try {
1930
- await runChannelSiteUrlFlow({
1931
- storeHash,
1932
- accessToken,
1933
- apiHost,
1934
- projectUuid: options.projectUuid ?? config2.get("projectUuid"),
1935
- channelId: options.channelId,
1936
- hostname: options.hostname
1937
- });
1938
- } catch (error) {
1939
- if (error instanceof NoLinkedProjectError) {
1940
- consola.info(
1941
- "When you're ready to create a project, run `catalyst project create` or re-run `catalyst channel update`."
2344
+ const touchesCheckout = options.checkoutUrl !== void 0 || options.removeCheckoutUrl === true;
2345
+ const updatesSiteUrl = options.hostname !== void 0 || !touchesCheckout;
2346
+ let channelId = options.channelId;
2347
+ if (updatesSiteUrl) {
2348
+ try {
2349
+ ({ channelId } = await runChannelSiteUrlFlow({
2350
+ storeHash,
2351
+ accessToken,
2352
+ apiHost,
2353
+ projectUuid: options.projectUuid ?? config2.get("projectUuid"),
2354
+ channelId: options.channelId,
2355
+ hostname: options.hostname
2356
+ }));
2357
+ } catch (error) {
2358
+ if (error instanceof NoLinkedProjectError) {
2359
+ consola.info(
2360
+ "When you're ready to create a project, run `catalyst projects create` or re-run `catalyst channels update`."
2361
+ );
2362
+ process.exit(0);
2363
+ return;
2364
+ }
2365
+ throw error;
2366
+ }
2367
+ }
2368
+ if (touchesCheckout) {
2369
+ const channel = channelId === void 0 ? await resolveChannel({ storeHash, accessToken, apiHost }) : { id: channelId, name: void 0 };
2370
+ const label = channel.name ? `"${channel.name}" (${channel.id})` : String(channel.id);
2371
+ if (options.checkoutUrl !== void 0) {
2372
+ const updated = await updateChannelCheckoutUrl(
2373
+ channel.id,
2374
+ options.checkoutUrl,
2375
+ storeHash,
2376
+ accessToken,
2377
+ apiHost
1942
2378
  );
1943
- process.exit(0);
1944
- return;
2379
+ consola.success(`Updated channel ${label} checkout URL to ${options.checkoutUrl}.`);
2380
+ reportChannelSite(updated);
2381
+ await warnOnCrossDomainCheckout(updated, { storeHash, accessToken, apiHost });
2382
+ } else {
2383
+ await deleteChannelCheckoutUrl(channel.id, storeHash, accessToken, apiHost);
2384
+ consola.success(
2385
+ `Removed the custom checkout URL from channel ${label}. Checkout now uses the shared checkout domain.`
2386
+ );
2387
+ const reverted = await getChannelSite(channel.id, storeHash, accessToken, apiHost);
2388
+ reportChannelSite(reverted);
2389
+ await warnOnCrossDomainCheckout(reverted, { storeHash, accessToken, apiHost });
1945
2390
  }
1946
- throw error;
1947
2391
  }
1948
2392
  process.exit(0);
1949
2393
  });
@@ -1954,13 +2398,13 @@ var link = new Command3("link").configureHelp({ showGlobalOptions: true }).descr
1954
2398
  `
1955
2399
  Examples:
1956
2400
  # Pick a channel interactively (logs you in if needed)
1957
- $ catalyst channel link
2401
+ $ catalyst channels link
1958
2402
 
1959
2403
  # Non-interactive
1960
- $ catalyst channel link --store-hash <hash> --access-token <token> --channel-id 123
2404
+ $ catalyst channels link --store-hash <hash> --access-token <token> --channel-id 123
1961
2405
 
1962
2406
  # Append extra environment variables to .env.local
1963
- $ catalyst channel link --channel-id 123 --env MY_FLAG=1`
2407
+ $ catalyst channels link --channel-id 123 --env MY_FLAG=1`
1964
2408
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(loginUrlOption()).addOption(
1965
2409
  new Option3("--channel-id <id>", "Link this channel directly, skipping the picker.").argParser(
1966
2410
  parseChannelId
@@ -1976,7 +2420,7 @@ Examples:
1976
2420
  const credentials = await resolveCredentialsWithLogin(options, config2, apiHost);
1977
2421
  if (!credentials) {
1978
2422
  consola.info(
1979
- "Login aborted. Re-run `catalyst channel link` when you have your credentials ready."
2423
+ "Login aborted. Re-run `catalyst channels link` when you have your credentials ready."
1980
2424
  );
1981
2425
  process.exit(0);
1982
2426
  return;
@@ -1986,8 +2430,8 @@ Examples:
1986
2430
  let channelId = options.channelId;
1987
2431
  let channelName;
1988
2432
  if (channelId === void 0) {
1989
- const channels = await fetchAvailableChannels(storeHash, accessToken, apiHost);
1990
- if (channels.length === 0) {
2433
+ const availableChannels = await fetchAvailableChannels(storeHash, accessToken, apiHost);
2434
+ if (availableChannels.length === 0) {
1991
2435
  consola.info(
1992
2436
  "No storefront channels found on this store. Create one with `catalyst create` and try again."
1993
2437
  );
@@ -1996,13 +2440,13 @@ Examples:
1996
2440
  }
1997
2441
  channelId = await select4({
1998
2442
  message: "Which channel would you like to link?",
1999
- choices: sortChannelsByPlatform(channels).map((c) => ({
2443
+ choices: sortChannelsByPlatform(availableChannels).map((c) => ({
2000
2444
  name: c.name,
2001
2445
  value: c.id,
2002
2446
  description: channelPlatformLabel(c.platform)
2003
2447
  }))
2004
2448
  });
2005
- channelName = channels.find((c) => c.id === channelId)?.name;
2449
+ channelName = availableChannels.find((c) => c.id === channelId)?.name;
2006
2450
  }
2007
2451
  const initData = await getChannelInit(channelId, storeHash, accessToken, options.cliApiOrigin);
2008
2452
  const envVars = { ...initData.envVars };
@@ -2027,13 +2471,13 @@ var create = new Command3("create").configureHelp({ showGlobalOptions: true }).d
2027
2471
  `
2028
2472
  Examples:
2029
2473
  # Create a channel interactively (logs you in if needed)
2030
- $ catalyst channel create
2474
+ $ catalyst channels create
2031
2475
 
2032
2476
  # Non-interactive, and link it to this project afterwards
2033
- $ catalyst channel create --name "My Store" --locale en --no-sample-data --link
2477
+ $ catalyst channels create --name "My Store" --locale en --no-sample-data --link
2034
2478
 
2035
2479
  # Add additional storefront languages (max 4)
2036
- $ catalyst channel create --name "My Store" --additional-locales es fr`
2480
+ $ catalyst channels create --name "My Store" --additional-locales es fr`
2037
2481
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(loginUrlOption()).option("--name <name>", "Name for the new channel. Skips the name prompt.").option(
2038
2482
  "--locale <locale>",
2039
2483
  'Default storefront locale (e.g. "en"). Skips the default-language prompt.'
@@ -2056,7 +2500,7 @@ Examples:
2056
2500
  const credentials = await resolveCredentialsWithLogin(options, config2, apiHost);
2057
2501
  if (!credentials) {
2058
2502
  consola.info(
2059
- "Login aborted. Re-run `catalyst channel create` when you have your credentials ready."
2503
+ "Login aborted. Re-run `catalyst channels create` when you have your credentials ready."
2060
2504
  );
2061
2505
  process.exit(0);
2062
2506
  return;
@@ -2101,38 +2545,106 @@ Examples:
2101
2545
  }
2102
2546
  process.exit(0);
2103
2547
  });
2104
- var channel = new Command3("channel").configureHelp({ showGlobalOptions: true }).description("Manage BigCommerce channels.").addCommand(create).addCommand(link).addCommand(update);
2548
+ var CHECKOUT_LABEL_WIDTH = "Storefront".length;
2549
+ var row = (label, value) => ` ${label.padEnd(CHECKOUT_LABEL_WIDTH)} ${value}`;
2550
+ function reportChannelSite(site) {
2551
+ const canonical = findChannelSiteUrl(site, "canonical");
2552
+ const checkout = findChannelSiteUrl(site, "checkout");
2553
+ consola.log(row("Storefront", findChannelSiteUrl(site, "primary") ?? site.url));
2554
+ if (canonical) {
2555
+ consola.log(row("Canonical", canonical));
2556
+ }
2557
+ consola.log(row("Checkout", checkout ?? "(none)"));
2558
+ if (!site.isCheckoutUrlCustomized) {
2559
+ consola.info(
2560
+ "This channel has no checkout URL of its own, so checkout uses the default channel's primary URL."
2561
+ );
2562
+ }
2563
+ }
2564
+ async function resolveChannelTarget(options) {
2565
+ const config2 = getProjectConfig();
2566
+ const apiHost = resolveApiHost(options, config2);
2567
+ const { storeHash, accessToken } = resolveCredentials(options, config2);
2568
+ await getTelemetry().identify(storeHash);
2569
+ const channel = await resolveChannel({
2570
+ storeHash,
2571
+ accessToken,
2572
+ apiHost,
2573
+ channelId: options.channelId
2574
+ });
2575
+ return {
2576
+ config: config2,
2577
+ apiHost,
2578
+ storeHash,
2579
+ accessToken,
2580
+ channel,
2581
+ label: channel.name ? `"${channel.name}" (${channel.id})` : String(channel.id)
2582
+ };
2583
+ }
2584
+ var info = new Command3("info").configureHelp({ showGlobalOptions: true }).description("Show a channel's site URLs \u2014 storefront, canonical and checkout.").addHelpText(
2585
+ "after",
2586
+ `${CHECKOUT_URL_NOTES}
2587
+
2588
+ Examples:
2589
+ # Pick a channel interactively; the same as bare \`catalyst channels\`
2590
+ $ catalyst channels info
2591
+
2592
+ # Target a channel directly
2593
+ $ catalyst channels info --channel-id 123`
2594
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(
2595
+ new Option3(
2596
+ "--channel-id <id>",
2597
+ "Skip the channel prompt and target this channel directly."
2598
+ ).argParser(parseChannelId)
2599
+ ).action(async (options) => {
2600
+ const { apiHost, storeHash, accessToken, channel, label } = await resolveChannelTarget(options);
2601
+ consola.start("Fetching channel site...");
2602
+ const site = await getChannelSite(channel.id, storeHash, accessToken, apiHost);
2603
+ consola.success(`Channel ${label}:`);
2604
+ reportChannelSite(site);
2605
+ const report = await warnOnCrossDomainCheckout(site, { storeHash, accessToken, apiHost });
2606
+ if (report.crossDomain && report.storefrontOnManagedZone !== true) {
2607
+ const suggested = report.suggestion ?? "<domain>";
2608
+ if (!process.stdin.isTTY) {
2609
+ consola.info(
2610
+ `Set one with \`catalyst channels update --channel-id ${channel.id} --checkout-url ${suggested}\`.`
2611
+ );
2612
+ process.exit(0);
2613
+ return;
2614
+ }
2615
+ const shouldSet = await confirm3({
2616
+ message: "Set a checkout URL for this channel now?",
2617
+ default: true
2618
+ });
2619
+ if (shouldSet) {
2620
+ await runChannelCheckoutUrlFlow({
2621
+ storeHash,
2622
+ accessToken,
2623
+ apiHost,
2624
+ // Already chosen above; don't ask again.
2625
+ channelId: channel.id,
2626
+ channelName: channel.name,
2627
+ storefrontUrl: findChannelSiteUrl(site, "primary") ?? site.url
2628
+ });
2629
+ process.exit(0);
2630
+ return;
2631
+ }
2632
+ consola.info(
2633
+ `When you're ready, run \`catalyst channels update --channel-id ${channel.id} --checkout-url <domain>\`.`
2634
+ );
2635
+ }
2636
+ process.exit(0);
2637
+ });
2638
+ var channels = new Command3("channels").alias("channel").configureHelp({ showGlobalOptions: true }).description("Manage BigCommerce channels.").addCommand(info, { isDefault: true }).addCommand(create).addCommand(link).addCommand(update);
2105
2639
 
2106
2640
  // src/cli/commands/create.ts
2107
2641
  import { Command as Command4, InvalidArgumentError as InvalidArgumentError3, Option as Option4 } from "@commander-js/extra-typings";
2108
- import { input as input4, select as select5 } from "@inquirer/prompts";
2642
+ import { input as input5, select as select5 } from "@inquirer/prompts";
2109
2643
  import { execSync as execSync2 } from "child_process";
2110
2644
  import { colorize as colorize6 } from "consola/utils";
2111
2645
  import { pathExistsSync } from "fs-extra/esm";
2112
2646
  import kebabCase from "lodash.kebabcase";
2113
- import { join as join10 } from "path";
2114
-
2115
- // src/cli/lib/detect-package-manager.ts
2116
- import { detectPackageManager as detectFromDir } from "nypm";
2117
- var detectPackageManager = () => {
2118
- const userAgent = process.env.npm_config_user_agent ?? "";
2119
- const name = userAgent.split(" ")[0]?.split("/")[0];
2120
- if (name === "pnpm" || name === "yarn" || name === "bun") {
2121
- return name;
2122
- }
2123
- if (process.versions.bun) {
2124
- return "bun";
2125
- }
2126
- return "npm";
2127
- };
2128
- var detectProjectPackageManager = async (projectDir) => {
2129
- const detected = await detectFromDir(projectDir);
2130
- const name = detected?.name;
2131
- if (name === "pnpm" || name === "yarn" || name === "bun" || name === "npm") {
2132
- return name;
2133
- }
2134
- return "npm";
2135
- };
2647
+ import { join as join11 } from "path";
2136
2648
 
2137
2649
  // src/cli/lib/extract-catalyst.ts
2138
2650
  import { mkdirSync as mkdirSync2 } from "fs";
@@ -2189,7 +2701,7 @@ var extractCatalyst = async ({
2189
2701
  // src/cli/lib/init-git-repo.ts
2190
2702
  import { execSync } from "child_process";
2191
2703
  import { rmSync } from "fs";
2192
- import { join as join7 } from "path";
2704
+ import { join as join8 } from "path";
2193
2705
  var run = (command, cwd) => execSync(command, { cwd, stdio: "ignore" });
2194
2706
  var initGitRepo = (projectDir) => {
2195
2707
  try {
@@ -2198,51 +2710,14 @@ var initGitRepo = (projectDir) => {
2198
2710
  run('git commit -m "Initial commit from Catalyst"', projectDir);
2199
2711
  consola.success("Initialized a git repository.");
2200
2712
  } catch {
2201
- rmSync(join7(projectDir, ".git"), { recursive: true, force: true });
2713
+ rmSync(join8(projectDir, ".git"), { recursive: true, force: true });
2202
2714
  consola.warn("Could not initialize a git repository. Skipping.");
2203
2715
  }
2204
2716
  };
2205
2717
 
2206
- // src/cli/lib/install-dependencies.ts
2207
- import { installDependencies as installDeps } from "nypm";
2208
- import yoctoSpinner2 from "yocto-spinner";
2209
- var restoreEnv = (key, previous) => {
2210
- if (previous === void 0) {
2211
- Reflect.deleteProperty(process.env, key);
2212
- } else {
2213
- process.env[key] = previous;
2214
- }
2215
- };
2216
- var withQuietInstallEnv = async (run2) => {
2217
- var _a, _b;
2218
- const prevNodeNoWarnings = process.env.NODE_NO_WARNINGS;
2219
- const prevDownloadPrompt = process.env.COREPACK_ENABLE_DOWNLOAD_PROMPT;
2220
- (_a = process.env).NODE_NO_WARNINGS ?? (_a.NODE_NO_WARNINGS = "1");
2221
- (_b = process.env).COREPACK_ENABLE_DOWNLOAD_PROMPT ?? (_b.COREPACK_ENABLE_DOWNLOAD_PROMPT = "0");
2222
- try {
2223
- return await run2();
2224
- } finally {
2225
- restoreEnv("NODE_NO_WARNINGS", prevNodeNoWarnings);
2226
- restoreEnv("COREPACK_ENABLE_DOWNLOAD_PROMPT", prevDownloadPrompt);
2227
- }
2228
- };
2229
- var installDependencies = async (projectDir, packageManager = "pnpm") => {
2230
- const spinner = yoctoSpinner2().start(
2231
- `Installing dependencies with ${packageManager}. This could take a minute...`
2232
- );
2233
- try {
2234
- await withQuietInstallEnv(() => installDeps({ cwd: projectDir, silent: true, packageManager }));
2235
- spinner.success("Dependencies installed successfully.");
2236
- } catch (error) {
2237
- const message = error instanceof Error ? error.message : "unknown error";
2238
- spinner.error(`Failed to install dependencies: ${message}`);
2239
- throw error;
2240
- }
2241
- };
2242
-
2243
2718
  // src/cli/lib/rewrite-core-package.ts
2244
2719
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
2245
- import { join as join8 } from "path";
2720
+ import { join as join9 } from "path";
2246
2721
  import { z as z10 } from "zod";
2247
2722
  var NATIVE_BUILD_ALLOWLIST = [
2248
2723
  "@parcel/watcher",
@@ -2284,7 +2759,7 @@ var resolvePublishedVersion = async (pkgName) => {
2284
2759
  return `^${version2}`;
2285
2760
  };
2286
2761
  var rewriteCorePackage = async (projectDir, ref) => {
2287
- const packageJsonPath = join8(projectDir, "package.json");
2762
+ const packageJsonPath = join9(projectDir, "package.json");
2288
2763
  const pkg = corePackageJsonSchema2.parse(JSON.parse(readFileSync4(packageJsonPath, "utf-8")));
2289
2764
  const workspaceDeps = DEP_FIELDS.flatMap((field) => {
2290
2765
  const deps = pkg[field];
@@ -2309,12 +2784,12 @@ var rewriteCorePackage = async (projectDir, ref) => {
2309
2784
  pkg.catalyst = { ...pkg.catalyst, version: pkg.version, ref };
2310
2785
  writeFileSync2(packageJsonPath, `${JSON.stringify(sortPackageJsonFields(pkg), null, 2)}
2311
2786
  `);
2312
- writeFileSync2(join8(projectDir, ".npmrc"), "legacy-peer-deps=true\n");
2787
+ writeFileSync2(join9(projectDir, ".npmrc"), "legacy-peer-deps=true\n");
2313
2788
  };
2314
2789
 
2315
2790
  // src/cli/lib/setup-core-project.ts
2316
2791
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
2317
- import { dirname as dirname3, join as join9 } from "path";
2792
+ import { dirname as dirname3, join as join10 } from "path";
2318
2793
  import { z as z11 } from "zod";
2319
2794
  var corePackageJsonSchema3 = z11.looseObject({
2320
2795
  scripts: z11.record(z11.string(), z11.string()).optional(),
@@ -2326,7 +2801,7 @@ var writeJson2 = (path, value) => {
2326
2801
  `);
2327
2802
  };
2328
2803
  var setupCoreProject = (projectDir) => {
2329
- const corePackageJsonPath = join9(projectDir, "package.json");
2804
+ const corePackageJsonPath = join10(projectDir, "package.json");
2330
2805
  const pkg = corePackageJsonSchema3.parse(JSON.parse(readFileSync5(corePackageJsonPath, "utf-8")));
2331
2806
  pkg.scripts = {
2332
2807
  ...pkg.scripts,
@@ -2366,10 +2841,10 @@ function parseEnvFlag(value, previous = {}) {
2366
2841
  return { ...previous, [key]: val };
2367
2842
  }
2368
2843
  async function handleChannelSelection(storeHash, accessToken, apiHost) {
2369
- const channels = await fetchAvailableChannels(storeHash, accessToken, apiHost);
2844
+ const channels2 = await fetchAvailableChannels(storeHash, accessToken, apiHost);
2370
2845
  const existingChannel = await select5({
2371
2846
  message: "Which channel would you like to use?",
2372
- choices: sortChannelsByPlatform(channels).map((ch) => ({
2847
+ choices: sortChannelsByPlatform(channels2).map((ch) => ({
2373
2848
  name: ch.name,
2374
2849
  value: ch,
2375
2850
  description: `Channel Platform: ${channelPlatformLabel(ch.platform)}`
@@ -2385,7 +2860,7 @@ async function setupProject(options) {
2385
2860
  }
2386
2861
  if (projectName) {
2387
2862
  projectName = kebabCase(projectName);
2388
- projectDir = join10(options.projectDir, projectName);
2863
+ projectDir = join11(options.projectDir, projectName);
2389
2864
  if (pathExistsSync(projectDir)) {
2390
2865
  consola.error(`${projectDir} already exists`);
2391
2866
  process.exit(1);
@@ -2395,13 +2870,13 @@ async function setupProject(options) {
2395
2870
  const validateProjectName = (i) => {
2396
2871
  const formatted = kebabCase(i);
2397
2872
  if (!formatted) return "Project name is required";
2398
- const targetDir = join10(options.projectDir, formatted);
2873
+ const targetDir = join11(options.projectDir, formatted);
2399
2874
  if (pathExistsSync(targetDir)) return `Destination '${targetDir}' already exists`;
2400
2875
  projectName = formatted;
2401
2876
  projectDir = targetDir;
2402
2877
  return true;
2403
2878
  };
2404
- await input4({
2879
+ await input5({
2405
2880
  message: "What do you want to name your project directory?",
2406
2881
  default: "my-catalyst-app",
2407
2882
  validate: validateProjectName
@@ -2546,7 +3021,6 @@ Examples:
2546
3021
  if (useCommerceHosting && accessToken) {
2547
3022
  envVars.BIGCOMMERCE_ACCESS_TOKEN = accessToken;
2548
3023
  }
2549
- if (accessToken) envVars.CATALYST_ACCESS_TOKEN = accessToken;
2550
3024
  let commerceHostingProject;
2551
3025
  if (useCommerceHosting && storeHash && accessToken) {
2552
3026
  const apiHost = `api.${options.bigcommerceHostname}`;
@@ -2578,6 +3052,11 @@ Examples:
2578
3052
  accessToken
2579
3053
  });
2580
3054
  }
3055
+ if (storeHash && accessToken) {
3056
+ const projectConfig = getProjectConfig(projectDir);
3057
+ projectConfig.set("storeHash", storeHash);
3058
+ projectConfig.set("accessToken", accessToken);
3059
+ }
2581
3060
  writeEnv(projectDir, envVars);
2582
3061
  await installDependencies(projectDir, packageManager);
2583
3062
  initGitRepo(projectDir);
@@ -2610,7 +3089,7 @@ import { Command as Command5, Option as Option5 } from "commander";
2610
3089
  import { parse } from "dotenv";
2611
3090
  import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
2612
3091
  import { release } from "os";
2613
- import { dirname as dirname4, join as join11 } from "path";
3092
+ import { dirname as dirname4, join as join12 } from "path";
2614
3093
  import { z as z12 } from "zod";
2615
3094
  var CLI_ENV_VARS = [
2616
3095
  "CATALYST_STORE_HASH",
@@ -2627,7 +3106,7 @@ var BUILD_ENV_VARS = [
2627
3106
  "AUTH_SECRET"
2628
3107
  ];
2629
3108
  var ENV_FILES = [".env.local", ".env"];
2630
- var PROJECT_LOCKFILES = [
3109
+ var PROJECT_LOCKFILES2 = [
2631
3110
  ["pnpm-lock.yaml", "pnpm"],
2632
3111
  ["yarn.lock", "yarn"],
2633
3112
  ["bun.lock", "bun"],
@@ -2638,7 +3117,7 @@ var hasValue = (value) => value !== void 0 && value.trim() !== "";
2638
3117
  var buildEnvLayers = (cwd, env2) => {
2639
3118
  const layers = [{ source: "process.env", values: env2 }];
2640
3119
  ENV_FILES.forEach((file) => {
2641
- const path = join11(cwd, file);
3120
+ const path = join12(cwd, file);
2642
3121
  if (!existsSync5(path)) {
2643
3122
  return;
2644
3123
  }
@@ -2661,7 +3140,7 @@ var resolveValue = (envSource, configValue) => {
2661
3140
  return { present: false, source: "unset" };
2662
3141
  };
2663
3142
  var asString = (value) => typeof value === "string" ? value : void 0;
2664
- var lockfileManager = (dir) => PROJECT_LOCKFILES.find(([file]) => existsSync5(join11(dir, file)))?.[1];
3143
+ var lockfileManager = (dir) => PROJECT_LOCKFILES2.find(([file]) => existsSync5(join12(dir, file)))?.[1];
2665
3144
  var detectProjectPackageManager2 = (cwd) => {
2666
3145
  let dir = cwd;
2667
3146
  for (; ; ) {
@@ -2676,7 +3155,7 @@ var detectProjectPackageManager2 = (cwd) => {
2676
3155
  dir = parent;
2677
3156
  }
2678
3157
  };
2679
- var projectJsonSchema2 = z12.looseObject({});
3158
+ var projectJsonSchema3 = z12.looseObject({});
2680
3159
  var safeReadJson2 = (path) => {
2681
3160
  try {
2682
3161
  return JSON.parse(readFileSync6(path, "utf-8"));
@@ -2684,9 +3163,9 @@ var safeReadJson2 = (path) => {
2684
3163
  return null;
2685
3164
  }
2686
3165
  };
2687
- var readProjectJson = (cwd) => {
2688
- const result = projectJsonSchema2.safeParse(
2689
- safeReadJson2(join11(cwd, ".bigcommerce", "project.json"))
3166
+ var readProjectJson2 = (cwd) => {
3167
+ const result = projectJsonSchema3.safeParse(
3168
+ safeReadJson2(join12(cwd, ".bigcommerce", "project.json"))
2690
3169
  );
2691
3170
  return result.success ? result.data : null;
2692
3171
  };
@@ -2696,7 +3175,7 @@ var storefrontPackageSchema = z12.looseObject({
2696
3175
  catalyst: z12.looseObject({ version: z12.string().optional() }).optional()
2697
3176
  });
2698
3177
  var readStorefrontInfo = (cwd) => {
2699
- const result = storefrontPackageSchema.safeParse(safeReadJson2(join11(cwd, "package.json")));
3178
+ const result = storefrontPackageSchema.safeParse(safeReadJson2(join12(cwd, "package.json")));
2700
3179
  if (!result.success) {
2701
3180
  return { name: null, version: null };
2702
3181
  }
@@ -2718,7 +3197,7 @@ function collectDiagnostics({
2718
3197
  } = {}) {
2719
3198
  const state = getProjectState(cwd);
2720
3199
  const telemetry2 = getTelemetry();
2721
- const projectJson = readProjectJson(cwd);
3200
+ const projectJson = readProjectJson2(cwd);
2722
3201
  const storefront = readStorefrontInfo(cwd);
2723
3202
  const cliEnv = [{ source: "process.env", values: env2 }];
2724
3203
  const buildEnv = buildEnvLayers(cwd, env2);
@@ -2773,12 +3252,12 @@ function collectDiagnostics({
2773
3252
  correlationId: telemetry2.correlationId
2774
3253
  },
2775
3254
  files: {
2776
- ".env.local": existsSync5(join11(cwd, ".env.local")),
2777
- ".env": existsSync5(join11(cwd, ".env")),
2778
- ".bigcommerce/project.json": existsSync5(join11(cwd, ".bigcommerce", "project.json")),
2779
- ".bigcommerce/wrangler.jsonc": existsSync5(join11(cwd, ".bigcommerce", "wrangler.jsonc")),
2780
- ".open-next/": existsSync5(join11(cwd, ".open-next")),
2781
- "package.json": existsSync5(join11(cwd, "package.json"))
3255
+ ".env.local": existsSync5(join12(cwd, ".env.local")),
3256
+ ".env": existsSync5(join12(cwd, ".env")),
3257
+ ".bigcommerce/project.json": existsSync5(join12(cwd, ".bigcommerce", "project.json")),
3258
+ ".bigcommerce/wrangler.jsonc": existsSync5(join12(cwd, ".bigcommerce", "wrangler.jsonc")),
3259
+ ".open-next/": existsSync5(join12(cwd, ".open-next")),
3260
+ "package.json": existsSync5(join12(cwd, "package.json"))
2782
3261
  }
2783
3262
  };
2784
3263
  }
@@ -2865,12 +3344,12 @@ Examples:
2865
3344
  });
2866
3345
 
2867
3346
  // src/cli/commands/deploy.ts
2868
- import { confirm as confirm3 } from "@inquirer/prompts";
3347
+ import { confirm as confirm4 } from "@inquirer/prompts";
2869
3348
  import AdmZip from "adm-zip";
2870
3349
  import { Command as Command6, Option as Option6 } from "commander";
2871
3350
  import { colorize as colorize7 } from "consola/utils";
2872
- import { access, readdir, readFile } from "fs/promises";
2873
- import { join as join12 } from "path";
3351
+ import { access as access2, readdir, readFile } from "fs/promises";
3352
+ import { join as join13 } from "path";
2874
3353
  import yoctoSpinner3 from "yocto-spinner";
2875
3354
  import { z as z13 } from "zod";
2876
3355
 
@@ -2942,10 +3421,10 @@ var DeploymentStatusSchema = z13.object({
2942
3421
  });
2943
3422
  var generateBundleZip = async () => {
2944
3423
  consola.info("Generating bundle...");
2945
- const bigcommerceDir = join12(process.cwd(), ".bigcommerce");
2946
- const distDir = join12(process.cwd(), ".bigcommerce", "dist");
3424
+ const bigcommerceDir = join13(process.cwd(), ".bigcommerce");
3425
+ const distDir = join13(process.cwd(), ".bigcommerce", "dist");
2947
3426
  try {
2948
- await access(distDir);
3427
+ await access2(distDir);
2949
3428
  } catch {
2950
3429
  throw new Error(`Dist directory not found: ${distDir}`);
2951
3430
  }
@@ -2953,7 +3432,7 @@ var generateBundleZip = async () => {
2953
3432
  if (buildDirContents.length === 0) {
2954
3433
  throw new Error(`Dist directory is empty: ${distDir}`);
2955
3434
  }
2956
- const outputZip = join12(bigcommerceDir, "bundle.zip");
3435
+ const outputZip = join13(bigcommerceDir, "bundle.zip");
2957
3436
  const zip = new AdmZip();
2958
3437
  zip.addLocalFolder(distDir, "output");
2959
3438
  zip.writeZip(outputZip);
@@ -2985,7 +3464,7 @@ var generateUploadSignature = async (storeHash, accessToken, apiHost) => {
2985
3464
  };
2986
3465
  var uploadBundleZip = async (uploadUrl) => {
2987
3466
  consola.info("Uploading bundle...");
2988
- const zipPath = join12(process.cwd(), ".bigcommerce", "bundle.zip");
3467
+ const zipPath = join13(process.cwd(), ".bigcommerce", "bundle.zip");
2989
3468
  const fileBuffer = await readFile(zipPath);
2990
3469
  const response = await fetch(uploadUrl, {
2991
3470
  method: "PUT",
@@ -3120,7 +3599,7 @@ var fetchProject = async (projectUuid, storeHash, accessToken, apiHost) => {
3120
3599
  }
3121
3600
  const res = await response.json();
3122
3601
  const { data } = ProjectSchema.parse(res);
3123
- return data.find((project2) => project2.uuid === projectUuid);
3602
+ return data.find((project) => project.uuid === projectUuid);
3124
3603
  };
3125
3604
  var deploy = new Command6("deploy").configureHelp({ showGlobalOptions: true }).description("Deploy your application to Cloudflare.").addHelpText(
3126
3605
  "after",
@@ -3133,6 +3612,9 @@ Example:
3133
3612
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option(
3134
3613
  "--update-site-url",
3135
3614
  "After a successful deploy, prompt to update a channel's site URL to the new hostname."
3615
+ ).option(
3616
+ "--update-checkout-url",
3617
+ "After a successful deploy, prompt to update a channel's checkout URL."
3136
3618
  ).addOption(
3137
3619
  new Option6(
3138
3620
  "--secret <value>",
@@ -3166,7 +3648,7 @@ Example:
3166
3648
  } catch (error) {
3167
3649
  if (error instanceof NoLinkedProjectError) {
3168
3650
  consola.info(
3169
- "When you're ready to create a project, run `catalyst project create` or re-run `catalyst deploy`."
3651
+ "When you're ready to create a project, run `catalyst projects create` or re-run `catalyst deploy`."
3170
3652
  );
3171
3653
  process.exit(0);
3172
3654
  }
@@ -3194,7 +3676,7 @@ Example:
3194
3676
  consola.success(`Linked project "${selected.name}".`);
3195
3677
  }
3196
3678
  if (!getProjectState().isTransformed) {
3197
- const shouldSetup = await confirm3({
3679
+ const shouldSetup = await confirm4({
3198
3680
  message: "Your project is not yet set up for Commerce Hosting deployments. Would you like to run the Commerce Hosting setup now?",
3199
3681
  default: true
3200
3682
  });
@@ -3209,11 +3691,17 @@ Example:
3209
3691
  await installDependencies(projectDir, packageManager);
3210
3692
  } else {
3211
3693
  await cleanupCloudflareIncompatibilities(process.cwd());
3694
+ if (options.prebuilt) {
3695
+ const projectDir = process.cwd();
3696
+ await reconcileOpenNextVersion(projectDir, await detectProjectPackageManager(projectDir), {
3697
+ canUpgrade: false
3698
+ });
3699
+ }
3212
3700
  }
3213
3701
  if (options.prebuilt) {
3214
- const distDir = join12(process.cwd(), ".bigcommerce", "dist");
3702
+ const distDir = join13(process.cwd(), ".bigcommerce", "dist");
3215
3703
  try {
3216
- await access(distDir);
3704
+ await access2(distDir);
3217
3705
  } catch {
3218
3706
  throw new Error(
3219
3707
  "No build output found at .bigcommerce/dist/. Run `catalyst build` first or remove `--prebuilt` to build automatically."
@@ -3259,29 +3747,44 @@ Example:
3259
3747
  accessToken,
3260
3748
  apiHost
3261
3749
  );
3262
- if (!options.updateSiteUrl) {
3263
- return;
3264
- }
3265
- try {
3266
- await runChannelSiteUrlFlow({
3267
- storeHash,
3268
- accessToken,
3269
- apiHost,
3270
- projectUuid,
3271
- preferHostname: deploymentHostname
3272
- });
3273
- } catch (error) {
3750
+ const warnChannelFlowFailed = (what, error) => {
3274
3751
  consola.warn(
3275
- `Failed to update channel site URL: ${error instanceof Error ? error.message : String(error)}`
3752
+ `Failed to update channel ${what}: ${error instanceof Error ? error.message : String(error)}`
3276
3753
  );
3277
3754
  consola.info(
3278
3755
  "Update it manually in the control panel, or re-run `catalyst auth login` if the token is missing the store_channel_settings scope."
3279
3756
  );
3757
+ };
3758
+ let resolvedChannelId;
3759
+ if (options.updateSiteUrl) {
3760
+ try {
3761
+ ({ channelId: resolvedChannelId } = await runChannelSiteUrlFlow({
3762
+ storeHash,
3763
+ accessToken,
3764
+ apiHost,
3765
+ projectUuid,
3766
+ preferHostname: deploymentHostname
3767
+ }));
3768
+ } catch (error) {
3769
+ warnChannelFlowFailed("site URL", error);
3770
+ }
3771
+ }
3772
+ if (options.updateCheckoutUrl) {
3773
+ try {
3774
+ await runChannelCheckoutUrlFlow({
3775
+ storeHash,
3776
+ accessToken,
3777
+ apiHost,
3778
+ channelId: resolvedChannelId
3779
+ });
3780
+ } catch (error) {
3781
+ warnChannelFlowFailed("checkout URL", error);
3782
+ }
3280
3783
  }
3281
3784
  });
3282
3785
 
3283
3786
  // src/cli/commands/domains.ts
3284
- import { confirm as confirm4, select as select6 } from "@inquirer/prompts";
3787
+ import { confirm as confirm5, select as select6 } from "@inquirer/prompts";
3285
3788
  import { Command as Command7, Option as Option7 } from "commander";
3286
3789
  import { colorize as colorize9 } from "consola/utils";
3287
3790
 
@@ -3295,6 +3798,7 @@ var LOG_LEVELS = ["debug", "info", "warn", "error"];
3295
3798
  var SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
3296
3799
  var logEntrySchema = z14.object({
3297
3800
  timestamp: z14.string().nullable().optional(),
3801
+ entry_type: z14.string().optional(),
3298
3802
  level: z14.string().optional(),
3299
3803
  messages: z14.array(z14.unknown()).optional(),
3300
3804
  is_exception: z14.boolean().optional(),
@@ -3411,8 +3915,16 @@ function isRedundantExceptionName(name, message) {
3411
3915
  return parts.length > 0 && parts.every((part) => message.includes(part));
3412
3916
  }
3413
3917
  var stringifyMessage = (message) => typeof message === "string" ? message : JSON.stringify(message);
3918
+ var joinMessages = (messages) => (messages ?? []).map(stringifyMessage).join(" ").replace(/\s+/g, " ").trim();
3919
+ function isRequestLogEntry(entry) {
3920
+ if (entry.entry_type != null) return entry.entry_type === "request";
3921
+ const method = entry.request?.method;
3922
+ const url = entry.request?.url;
3923
+ if (!method || !url) return false;
3924
+ return joinMessages(entry.messages) === `${method} ${url}`;
3925
+ }
3414
3926
  function formatLogEntry(entry, format = "default") {
3415
- const message = (entry.messages ?? []).map(stringifyMessage).join(" ").replace(/\s+/g, " ").trim();
3927
+ const message = joinMessages(entry.messages);
3416
3928
  if (format === "short") return message;
3417
3929
  const level = entry.level ?? "unknown";
3418
3930
  const coloredLevel = colorize8(LEVEL_COLORS[level.toLowerCase()] ?? "white", level.toUpperCase());
@@ -3425,12 +3937,16 @@ function formatLogEntry(entry, format = "default") {
3425
3937
  const status = entry.request.status_code ? ` (${entry.request.status_code})` : "";
3426
3938
  if (parts.length > 0) requestStr = ` ${parts.join(" ")}${status}`;
3427
3939
  }
3428
- return `[${entry.timestamp ?? UNKNOWN_TIME}] [${coloredLevel}]${requestStr}${exceptionStr} ${message}`;
3940
+ if (format === "request" && isRequestLogEntry(entry)) {
3941
+ return `[${entry.timestamp ?? UNKNOWN_TIME}]${requestStr}`;
3942
+ }
3943
+ return `[${entry.timestamp ?? UNKNOWN_TIME}]${requestStr} [${coloredLevel}]${exceptionStr} ${message}`;
3429
3944
  }
3430
3945
  async function queryLogs(projectUuid, storeHash, accessToken, apiHost, params) {
3431
3946
  const search = new URLSearchParams();
3432
3947
  search.set("start", params.start);
3433
3948
  search.set("end", params.end);
3949
+ if (params.entryType) search.set("entry_type", params.entryType);
3434
3950
  if (params.method) search.set("method", params.method);
3435
3951
  if (params.statusCode != null) search.set("status_code", String(params.statusCode));
3436
3952
  if (params.urlLike) search.set("url:like", params.urlLike);
@@ -3473,11 +3989,18 @@ async function queryLogs(projectUuid, storeHash, accessToken, apiHost, params) {
3473
3989
 
3474
3990
  // src/cli/lib/domains.ts
3475
3991
  var domainStatusSchema = z15.enum(["pending", "verified", "failed", "unknown"]);
3992
+ var pointingRecordsSchema = z15.object({
3993
+ a_record_value: z15.string().nullish(),
3994
+ cname_record_value: z15.string().nullish()
3995
+ });
3476
3996
  var domainSchema = z15.object({
3477
3997
  domain: z15.string(),
3478
3998
  project_uuid: z15.string(),
3479
3999
  verification_status: domainStatusSchema
3480
4000
  });
4001
+ var createdDomainSchema = domainSchema.extend({
4002
+ pointing_records: pointingRecordsSchema.nullish()
4003
+ });
3481
4004
  var ownershipVerificationSchema = z15.object({
3482
4005
  type: z15.string(),
3483
4006
  name: z15.string(),
@@ -3518,6 +4041,9 @@ function parseBoundProjectUuid(body) {
3518
4041
  var domainResponseSchema = z15.object({
3519
4042
  data: domainSchema
3520
4043
  });
4044
+ var createdDomainResponseSchema = z15.object({
4045
+ data: createdDomainSchema
4046
+ });
3521
4047
  var domainListResponseSchema = z15.object({
3522
4048
  data: z15.array(domainSchema)
3523
4049
  });
@@ -3586,9 +4112,9 @@ async function createDomain(domain, projectUuid, storeHash, accessToken, apiHost
3586
4112
  });
3587
4113
  await assertDomainResponse(response, "Failed to add domain");
3588
4114
  const result = await response.json();
3589
- return domainResponseSchema.parse(result).data;
4115
+ return createdDomainResponseSchema.parse(result).data;
3590
4116
  }
3591
- async function getDomain(domain, projectUuid, storeHash, accessToken, apiHost) {
4117
+ async function getDomain2(domain, projectUuid, storeHash, accessToken, apiHost) {
3592
4118
  const response = await fetch(domainUrl(storeHash, projectUuid, domain, apiHost), {
3593
4119
  method: "GET",
3594
4120
  headers: authHeaders3(accessToken)
@@ -3657,6 +4183,7 @@ async function transferDomain(domain, projectUuid, newProjectUuid, storeHash, ac
3657
4183
  var WAIT_INTERVAL_MS = 5e3;
3658
4184
  var WAIT_TIMEOUT_MS = 5 * 60 * 1e3;
3659
4185
  var DOMAIN_STATUS_FILTERS = ["pending", "verified", "failed"];
4186
+ var RECORD_TYPE_WIDTH = "CNAME".length;
3660
4187
  var STATUS_COLORS = {
3661
4188
  pending: "yellow",
3662
4189
  verified: "green",
@@ -3690,20 +4217,20 @@ async function resolveDestinationProject(domain, context, toProjectUuid) {
3690
4217
  return toProjectUuid;
3691
4218
  }
3692
4219
  consola.start("Fetching projects...");
3693
- const projects = await fetchProjects(context.storeHash, context.accessToken, context.apiHost);
4220
+ const projects2 = await fetchProjects(context.storeHash, context.accessToken, context.apiHost);
3694
4221
  consola.success("Projects fetched.");
3695
- const destinations = projects.filter((project2) => project2.uuid !== context.projectUuid);
4222
+ const destinations = projects2.filter((project) => project.uuid !== context.projectUuid);
3696
4223
  if (destinations.length === 0) {
3697
4224
  throw new UserActionableError(
3698
- "No other projects to transfer to. Create another project with `catalyst project create` first."
4225
+ "No other projects to transfer to. Create another project with `catalyst projects create` first."
3699
4226
  );
3700
4227
  }
3701
4228
  return select6({
3702
4229
  message: `Select the project to transfer ${domain} to (Press <enter> to select).`,
3703
- choices: destinations.map((project2) => ({
3704
- name: project2.name,
3705
- value: project2.uuid,
3706
- description: project2.uuid
4230
+ choices: destinations.map((project) => ({
4231
+ name: project.name,
4232
+ value: project.uuid,
4233
+ description: project.uuid
3707
4234
  }))
3708
4235
  });
3709
4236
  }
@@ -3713,6 +4240,19 @@ function formatDomainStatus(status) {
3713
4240
  function formatDomain(domain) {
3714
4241
  return `${domain.domain} ${formatDomainStatus(domain.verification_status)}`;
3715
4242
  }
4243
+ function formatPointingRecords(records) {
4244
+ const rows = [];
4245
+ if (records?.a_record_value) {
4246
+ rows.push(`${"A".padEnd(RECORD_TYPE_WIDTH)} ${records.a_record_value}`);
4247
+ }
4248
+ if (records?.cname_record_value) {
4249
+ rows.push(`${"CNAME".padEnd(RECORD_TYPE_WIDTH)} ${records.cname_record_value}`);
4250
+ }
4251
+ if (rows.length === 0) {
4252
+ return null;
4253
+ }
4254
+ return rows.map((row2) => ` ${row2}`).join("\n");
4255
+ }
3716
4256
  function formatOwnershipVerification(record) {
3717
4257
  const lines = [
3718
4258
  "Add this DNS record to verify ownership:",
@@ -3734,16 +4274,20 @@ async function waitForDomainVerification({
3734
4274
  timeoutMs = WAIT_TIMEOUT_MS
3735
4275
  }) {
3736
4276
  const startedAt = Date.now();
3737
- let current = await getDomain(domain, projectUuid, storeHash, accessToken, apiHost);
4277
+ let current = await getDomain2(domain, projectUuid, storeHash, accessToken, apiHost);
3738
4278
  while (current.verification_status === "pending" && Date.now() - startedAt < timeoutMs) {
3739
4279
  await sleep(intervalMs);
3740
- current = await getDomain(domain, projectUuid, storeHash, accessToken, apiHost);
4280
+ current = await getDomain2(domain, projectUuid, storeHash, accessToken, apiHost);
3741
4281
  }
3742
4282
  return current;
3743
4283
  }
3744
4284
  var add = new Command7("add").configureHelp({ showGlobalOptions: true }).description("Add a custom domain to the current Native Hosting project.").argument("<domain>", "Custom domain to add.").addHelpText(
3745
4285
  "after",
3746
4286
  `
4287
+ The DNS records that point the domain at this project are printed once the domain
4288
+ is added. They are returned only by this command, so save them: publish the CNAME
4289
+ for a subdomain, or the A record for an apex domain.
4290
+
3747
4291
  Examples:
3748
4292
  $ catalyst domains add www.example.com
3749
4293
 
@@ -3753,9 +4297,9 @@ Examples:
3753
4297
  const context = resolveDomainCommandContext(options);
3754
4298
  await getTelemetry().identify(context.storeHash);
3755
4299
  consola.start(`Adding domain ${domain}...`);
3756
- let result;
4300
+ let created;
3757
4301
  try {
3758
- result = await createDomain(
4302
+ created = await createDomain(
3759
4303
  domain,
3760
4304
  context.projectUuid,
3761
4305
  context.storeHash,
@@ -3782,12 +4326,26 @@ Examples:
3782
4326
  }
3783
4327
  throw error;
3784
4328
  }
3785
- consola.success(`Domain ${result.domain} added.`);
4329
+ consola.success(`Domain ${created.domain} added.`);
4330
+ const records = formatPointingRecords(created.pointing_records);
4331
+ let result = created;
3786
4332
  if (options.wait && result.verification_status === "pending") {
3787
4333
  consola.start(`Waiting for ${result.domain} to verify...`);
3788
4334
  result = await waitForDomainVerification({ domain: result.domain, ...context });
3789
4335
  }
3790
4336
  consola.log(formatDomain(result));
4337
+ if (records) {
4338
+ consola.info(`Point ${created.domain} at this project with one of these DNS records:`);
4339
+ consola.log(records);
4340
+ consola.info(
4341
+ "Use the CNAME for a subdomain, or the A record for an apex domain. Save them now \u2014 only `domains add` returns them."
4342
+ );
4343
+ }
4344
+ if (result.verification_status === "pending") {
4345
+ consola.info(
4346
+ `Run \`catalyst domains status ${result.domain}\` to check verification progress.`
4347
+ );
4348
+ }
3791
4349
  process.exit(0);
3792
4350
  });
3793
4351
  var list = new Command7("list").configureHelp({ showGlobalOptions: true }).description("List custom domains for the current Native Hosting project.").addHelpText(
@@ -3837,7 +4395,7 @@ Examples:
3837
4395
  const context = resolveDomainCommandContext(options);
3838
4396
  await getTelemetry().identify(context.storeHash);
3839
4397
  consola.start(`Fetching status for ${domain}...`);
3840
- let result = await getDomain(
4398
+ let result = await getDomain2(
3841
4399
  domain,
3842
4400
  context.projectUuid,
3843
4401
  context.storeHash,
@@ -3888,7 +4446,7 @@ Examples:
3888
4446
  throw error;
3889
4447
  }
3890
4448
  consola.success(`Domain ${domain} claimed.`);
3891
- let result = await getDomain(
4449
+ let result = await getDomain2(
3892
4450
  domain,
3893
4451
  context.projectUuid,
3894
4452
  context.storeHash,
@@ -3927,14 +4485,14 @@ Examples:
3927
4485
  );
3928
4486
  if (!owned) {
3929
4487
  consola.start(`Locating the project that owns ${domain}...`);
3930
- const projects = await fetchProjects(context.storeHash, context.accessToken, context.apiHost);
3931
- const candidates = projects.filter((project2) => project2.uuid !== context.projectUuid);
4488
+ const projects2 = await fetchProjects(context.storeHash, context.accessToken, context.apiHost);
4489
+ const candidates = projects2.filter((project) => project.uuid !== context.projectUuid);
3932
4490
  const matches = await Promise.all(
3933
- candidates.map(async (project2) => ({
3934
- project: project2,
4491
+ candidates.map(async (project) => ({
4492
+ project,
3935
4493
  found: await findDomain(
3936
4494
  domain,
3937
- project2.uuid,
4495
+ project.uuid,
3938
4496
  context.storeHash,
3939
4497
  context.accessToken,
3940
4498
  context.apiHost
@@ -3968,7 +4526,7 @@ Examples:
3968
4526
  context.apiHost
3969
4527
  );
3970
4528
  consola.success(`Domain ${domain} transferred.`);
3971
- let result = await getDomain(
4529
+ let result = await getDomain2(
3972
4530
  domain,
3973
4531
  newProjectUuid,
3974
4532
  context.storeHash,
@@ -4000,7 +4558,7 @@ Examples:
4000
4558
  const context = resolveDomainCommandContext(options);
4001
4559
  await getTelemetry().identify(context.storeHash);
4002
4560
  consola.start(`Fetching status for ${domain}...`);
4003
- const current = await getDomain(
4561
+ const current = await getDomain2(
4004
4562
  domain,
4005
4563
  context.projectUuid,
4006
4564
  context.storeHash,
@@ -4008,7 +4566,7 @@ Examples:
4008
4566
  context.apiHost
4009
4567
  );
4010
4568
  if (current.verification_status === "verified" && !options.force) {
4011
- const confirmed = await confirm4({
4569
+ const confirmed = await confirm5({
4012
4570
  message: `Remove active domain ${current.domain}? Traffic may stop routing to this project.`,
4013
4571
  default: false
4014
4572
  });
@@ -4029,7 +4587,7 @@ Examples:
4029
4587
  consola.success(`Domain ${current.domain} removed.`);
4030
4588
  process.exit(0);
4031
4589
  });
4032
- var domains = new Command7("domains").configureHelp({ showGlobalOptions: true }).description("Manage custom domains for the current Native Hosting project.").addCommand(add).addCommand(list).addCommand(showStatus).addCommand(claim).addCommand(transfer).addCommand(remove);
4590
+ var domains = new Command7("domains").alias("domain").configureHelp({ showGlobalOptions: true }).description("Manage custom domains for the current Native Hosting project.").addCommand(add).addCommand(list).addCommand(showStatus).addCommand(claim).addCommand(transfer).addCommand(remove);
4033
4591
 
4034
4592
  // src/cli/commands/env.ts
4035
4593
  import { Command as Command8 } from "commander";
@@ -4141,8 +4699,19 @@ var StreamError = class extends Error {
4141
4699
  }
4142
4700
  };
4143
4701
  var formatMessages = (messages) => messages.map((m) => typeof m === "string" ? m : JSON.stringify(m)).join(" ");
4702
+ var formatRequestLine = (event, entry) => {
4703
+ const { request } = event;
4704
+ const msg = entry ? formatMessages(entry.messages) : "";
4705
+ const msgPart = msg ? ` ${msg}` : "";
4706
+ const level = msg ? entry?.level.toUpperCase() : void 0;
4707
+ const levelPart = level ? ` [${colorize10(LEVEL_COLORS2[level] ?? "white", level)}]` : "";
4708
+ return `[${entry?.timestamp ?? event.timestamp}] ${request.method} ${request.url} (${request.status_code})${levelPart}${msgPart}`;
4709
+ };
4144
4710
  var formatLogEvent = (event, format) => {
4145
- const { request, logs: logEntries, exceptions } = event;
4711
+ const { logs: logEntries, exceptions } = event;
4712
+ if (format === "request" && logEntries.length === 0) {
4713
+ consola.log(formatRequestLine(event));
4714
+ }
4146
4715
  logEntries.forEach((entry) => {
4147
4716
  const msg = formatMessages(entry.messages);
4148
4717
  const level = entry.level.toUpperCase();
@@ -4152,9 +4721,7 @@ var formatLogEvent = (event, format) => {
4152
4721
  consola.log(msg);
4153
4722
  break;
4154
4723
  case "request":
4155
- consola.log(
4156
- `[${entry.timestamp}] [${coloredLevel}] ${request.method} ${request.url} (${request.status_code}) ${msg}`
4157
- );
4724
+ consola.log(formatRequestLine(event, entry));
4158
4725
  break;
4159
4726
  default:
4160
4727
  consola.log(`[${entry.timestamp}] [${coloredLevel}] ${msg}`);
@@ -4290,6 +4857,19 @@ var tailLogs = async (projectUuid, storeHash, accessToken, apiHost, format, conn
4290
4857
  var tail = new Command9("tail").configureHelp({ showGlobalOptions: true }).description("Tail live logs from your deployed application.").addHelpText(
4291
4858
  "after",
4292
4859
  `
4860
+ Formats:
4861
+ default One line per log message: timestamp, level, and message.
4862
+ short Message text only.
4863
+ request One line per log message: timestamp, request method, URL, status
4864
+ code, level, and message. Requests that logged no messages are
4865
+ printed too, without the level and message.
4866
+ pretty Indented JSON of the whole event.
4867
+ json Raw JSON, one event per line (useful for piping to other tools).
4868
+
4869
+ The \`default\` and \`short\` formats only print requests that produced a log
4870
+ message. Use \`--format request\` (or \`json\`/\`pretty\`) to see every request,
4871
+ including ones with no message body.
4872
+
4293
4873
  Examples:
4294
4874
  $ catalyst logs tail
4295
4875
 
@@ -4349,6 +4929,8 @@ function printPaginationHints(pagination, start2, end, options) {
4349
4929
  ...options.urlLike ? [`--url-like ${quoteArg(options.urlLike)}`] : [],
4350
4930
  ...options.levelMin ? [`--level-min ${options.levelMin}`] : [],
4351
4931
  ...options.limit != null ? [`--limit ${options.limit}`] : [],
4932
+ // The entry_type filter derives from --format (default/short request only
4933
+ // application logs), so echoing --format is enough to reproduce it.
4352
4934
  ...options.format !== "default" ? [`--format ${options.format}`] : []
4353
4935
  ];
4354
4936
  const command = `${invocationPrefix()} logs query ${baseFlags.join(" ")}`;
@@ -4375,6 +4957,10 @@ entries with \`--before\`. Cursors are only valid with the same window and
4375
4957
  filters they came from. With \`--format json\`, the pagination cursors are
4376
4958
  emitted as a final \`{"meta": ...}\` line after the entries.
4377
4959
 
4960
+ The \`default\` and \`short\` formats show only application log output,
4961
+ excluding Cloudflare's auto-generated per-request rows. Use
4962
+ \`--format request\` (or \`json\`/\`pretty\`) to include them.
4963
+
4378
4964
  Examples:
4379
4965
  # Last hour of logs
4380
4966
  $ catalyst logs query --since 1h
@@ -4432,9 +5018,11 @@ Examples:
4432
5018
  await getTelemetry().identify(storeHash);
4433
5019
  const projectUuid = resolveProjectUuid(options);
4434
5020
  const { start: start2, end } = resolveTimeWindow(options);
5021
+ const hideRequestRows = options.format === "default" || options.format === "short";
4435
5022
  const result = await queryLogs(projectUuid, storeHash, accessToken, apiHost, {
4436
5023
  start: start2,
4437
5024
  end,
5025
+ entryType: hideRequestRows ? "log" : void 0,
4438
5026
  method: options.method,
4439
5027
  statusCode: options.statusCode,
4440
5028
  urlLike: options.urlLike,
@@ -4466,9 +5054,12 @@ Examples:
4466
5054
  return;
4467
5055
  }
4468
5056
  const lineFormat = options.format;
4469
- ordered.forEach((entry) => consola.log(formatLogEntry(entry, lineFormat)));
5057
+ const visible = hideRequestRows ? ordered.filter((entry) => !isRequestLogEntry(entry)) : ordered;
5058
+ const hiddenCount = ordered.length - visible.length;
5059
+ visible.forEach((entry) => consola.log(formatLogEntry(entry, lineFormat)));
5060
+ const shownPart = `${visible.length} ${visible.length === 1 ? "entry" : "entries"} shown`;
4470
5061
  consola.info(
4471
- `${entries.length} ${entries.length === 1 ? "entry" : "entries"} shown (oldest first, times in UTC).`
5062
+ hiddenCount > 0 ? `${shownPart}, ${hiddenCount} request ${hiddenCount === 1 ? "row" : "rows"} hidden (oldest first, times in UTC). Use --format request to see request rows.` : `${shownPart} (oldest first, times in UTC).`
4472
5063
  );
4473
5064
  printPaginationHints(result.meta?.cursor_pagination, start2, end, options);
4474
5065
  } catch (error) {
@@ -4476,10 +5067,10 @@ Examples:
4476
5067
  process.exit(1);
4477
5068
  }
4478
5069
  });
4479
- var logs = new Command9("logs").configureHelp({ showGlobalOptions: true }).description("View logs from your deployed application.").addCommand(tail, { isDefault: true }).addCommand(query);
5070
+ var logs = new Command9("logs").alias("log").configureHelp({ showGlobalOptions: true }).description("View logs from your deployed application.").addCommand(tail, { isDefault: true }).addCommand(query);
4480
5071
 
4481
- // src/cli/commands/project.ts
4482
- import { confirm as confirm5, input as input5, select as select7 } from "@inquirer/prompts";
5072
+ // src/cli/commands/projects.ts
5073
+ import { confirm as confirm6, input as input6, select as select7 } from "@inquirer/prompts";
4483
5074
  import { Command as Command10 } from "commander";
4484
5075
  import { colorize as colorize11 } from "consola/utils";
4485
5076
  async function offerCommerceHostingSetup(projectUuid, credentials) {
@@ -4488,7 +5079,7 @@ async function offerCommerceHostingSetup(projectUuid, credentials) {
4488
5079
  await cleanupCloudflareIncompatibilities(projectDir);
4489
5080
  return;
4490
5081
  }
4491
- const shouldSetup = await confirm5({
5082
+ const shouldSetup = await confirm6({
4492
5083
  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?",
4493
5084
  default: true
4494
5085
  });
@@ -4507,22 +5098,22 @@ var list3 = new Command10("list").configureHelp({ showGlobalOptions: true }).des
4507
5098
  "after",
4508
5099
  `
4509
5100
  Example:
4510
- $ catalyst project list`
5101
+ $ catalyst projects list`
4511
5102
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).action(async (options) => {
4512
5103
  const config2 = getProjectConfig();
4513
5104
  const apiHost = resolveApiHost(options, config2);
4514
5105
  const { storeHash, accessToken } = resolveCredentials(options, config2);
4515
5106
  await getTelemetry().identify(storeHash);
4516
5107
  consola.start("Fetching projects...");
4517
- const projects = await fetchProjects(storeHash, accessToken, apiHost);
5108
+ const storeProjects = await fetchProjects(storeHash, accessToken, apiHost);
4518
5109
  consola.success("Projects fetched.");
4519
- if (projects.length === 0) {
5110
+ if (storeProjects.length === 0) {
4520
5111
  consola.info("No projects found.");
4521
5112
  process.exit(0);
4522
5113
  return;
4523
5114
  }
4524
5115
  const linkedProjectUuid = config2.get("projectUuid");
4525
- const projectList = projects.map((p) => {
5116
+ const projectList = storeProjects.map((p) => {
4526
5117
  const marker = p.uuid === linkedProjectUuid ? ` ${colorize11("green", "[linked]")}` : "";
4527
5118
  const hostnames = p.deployment_hostnames.length === 0 ? [" (not deployed)"] : p.deployment_hostnames.map(
4528
5119
  (hostname) => ` ${colorize11("blue", `https://${hostname}`)}`
@@ -4538,7 +5129,7 @@ var create3 = new Command10("create").configureHelp({ showGlobalOptions: true })
4538
5129
  "after",
4539
5130
  `
4540
5131
  Example:
4541
- $ catalyst project create`
5132
+ $ catalyst projects create`
4542
5133
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(loginUrlOption()).action(async (options) => {
4543
5134
  const config2 = getProjectConfig();
4544
5135
  const apiHost = resolveApiHost(options, config2);
@@ -4558,7 +5149,7 @@ Example:
4558
5149
  } catch (error) {
4559
5150
  if (error instanceof LoginAbortedError) {
4560
5151
  consola.info(
4561
- "Login aborted. Re-run `catalyst project create` when you have your credentials ready."
5152
+ "Login aborted. Re-run `catalyst projects create` when you have your credentials ready."
4562
5153
  );
4563
5154
  process.exit(0);
4564
5155
  return;
@@ -4567,7 +5158,7 @@ Example:
4567
5158
  }
4568
5159
  }
4569
5160
  await getTelemetry().identify(storeHash);
4570
- const newProjectName = await input5({ message: "Enter a name for the new project:" });
5161
+ const newProjectName = await input6({ message: "Enter a name for the new project:" });
4571
5162
  const data = await createProject(newProjectName, storeHash, accessToken, apiHost);
4572
5163
  consola.success(`Project "${data.name}" created successfully.`);
4573
5164
  consola.start("Writing project UUID to .bigcommerce/project.json...");
@@ -4584,10 +5175,10 @@ var link2 = new Command10("link").configureHelp({ showGlobalOptions: true }).des
4584
5175
  `
4585
5176
  Examples:
4586
5177
  # Link interactively (prompts to select or create)
4587
- $ catalyst project link
5178
+ $ catalyst projects link
4588
5179
 
4589
5180
  # Link using a project UUID directly
4590
- $ catalyst project link --project-uuid <UUID>`
5181
+ $ catalyst projects link --project-uuid <UUID>`
4591
5182
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).action(async (options) => {
4592
5183
  const config2 = getProjectConfig();
4593
5184
  const apiHost = resolveApiHost(options, config2);
@@ -4617,7 +5208,7 @@ Examples:
4617
5208
  } catch (error) {
4618
5209
  if (error instanceof NoLinkedProjectError) {
4619
5210
  consola.info(
4620
- "When you're ready to create a project, run `catalyst project create` or re-run `catalyst project link`."
5211
+ "When you're ready to create a project, run `catalyst projects create` or re-run `catalyst projects link`."
4621
5212
  );
4622
5213
  process.exit(0);
4623
5214
  throw error;
@@ -4635,13 +5226,13 @@ var del = new Command10("delete").configureHelp({ showGlobalOptions: true }).des
4635
5226
  `
4636
5227
  Examples:
4637
5228
  # Select a project to delete interactively
4638
- $ catalyst project delete
5229
+ $ catalyst projects delete
4639
5230
 
4640
5231
  # Delete a specific project (still prompts for confirmation)
4641
- $ catalyst project delete --project-uuid <UUID>
5232
+ $ catalyst projects delete --project-uuid <UUID>
4642
5233
 
4643
5234
  # Skip the confirmation prompt
4644
- $ catalyst project delete --project-uuid <UUID> --force`
5235
+ $ catalyst projects delete --project-uuid <UUID> --force`
4645
5236
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option("--force", "Skip the confirmation prompt before deleting.").action(async (options) => {
4646
5237
  const config2 = getProjectConfig();
4647
5238
  const apiHost = resolveApiHost(options, config2);
@@ -4651,9 +5242,9 @@ Examples:
4651
5242
  let targetName;
4652
5243
  if (!targetUuid) {
4653
5244
  consola.start("Fetching projects...");
4654
- const projects = await fetchProjects(storeHash, accessToken, apiHost);
5245
+ const storeProjects = await fetchProjects(storeHash, accessToken, apiHost);
4655
5246
  consola.success("Projects fetched.");
4656
- if (projects.length === 0) {
5247
+ if (storeProjects.length === 0) {
4657
5248
  consola.info("No projects found.");
4658
5249
  process.exit(0);
4659
5250
  return;
@@ -4662,7 +5253,7 @@ Examples:
4662
5253
  const selected = await select7({
4663
5254
  message: "Select a project to delete (Press <enter> to select).",
4664
5255
  choices: [
4665
- ...projects.map((p) => ({
5256
+ ...storeProjects.map((p) => ({
4666
5257
  name: p.uuid === linkedProjectUuid ? `${p.name} ${colorize11("green", "[linked]")}` : p.name,
4667
5258
  value: p.uuid,
4668
5259
  description: p.uuid
@@ -4675,7 +5266,7 @@ Examples:
4675
5266
  process.exit(0);
4676
5267
  return;
4677
5268
  }
4678
- const matched = projects.find((p) => p.uuid === selected);
5269
+ const matched = storeProjects.find((p) => p.uuid === selected);
4679
5270
  if (!matched) {
4680
5271
  throw new Error(`Selected project ${String(selected)} not found in fetched list.`);
4681
5272
  }
@@ -4684,7 +5275,7 @@ Examples:
4684
5275
  }
4685
5276
  if (!options.force) {
4686
5277
  const label = targetName ? `"${targetName}" (${targetUuid})` : targetUuid;
4687
- const confirmed = await confirm5({
5278
+ const confirmed = await confirm6({
4688
5279
  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.`,
4689
5280
  default: false
4690
5281
  });
@@ -4703,13 +5294,13 @@ Examples:
4703
5294
  }
4704
5295
  process.exit(0);
4705
5296
  });
4706
- var project = new Command10("project").configureHelp({ showGlobalOptions: true }).description("Manage your BigCommerce infrastructure project.").addCommand(create3).addCommand(list3).addCommand(link2).addCommand(del);
5297
+ var projects = new Command10("projects").alias("project").configureHelp({ showGlobalOptions: true }).description("Manage your BigCommerce infrastructure projects.").addCommand(create3).addCommand(list3).addCommand(link2).addCommand(del);
4707
5298
 
4708
5299
  // src/cli/commands/start.ts
4709
5300
  import { Command as Command11 } from "commander";
4710
5301
  import { execa as execa3 } from "execa";
4711
5302
  import { existsSync as existsSync6, lstatSync, symlinkSync } from "fs";
4712
- import { join as join13, relative } from "path";
5303
+ import { join as join14, relative } from "path";
4713
5304
  var start = new Command11("start").configureHelp({ showGlobalOptions: true }).description(
4714
5305
  "Start a local preview of your Catalyst storefront using the OpenNext Cloudflare adapter."
4715
5306
  ).addHelpText(
@@ -4727,11 +5318,11 @@ Example:
4727
5318
  });
4728
5319
  return;
4729
5320
  }
4730
- const envLocal = join13(process.cwd(), ".env.local");
4731
- const devVars = join13(process.cwd(), ".bigcommerce", ".dev.vars");
5321
+ const envLocal = join14(process.cwd(), ".env.local");
5322
+ const devVars = join14(process.cwd(), ".bigcommerce", ".dev.vars");
4732
5323
  if (existsSync6(envLocal)) {
4733
5324
  if (!existsSync6(devVars)) {
4734
- const target = relative(join13(process.cwd(), ".bigcommerce"), envLocal);
5325
+ const target = relative(join14(process.cwd(), ".bigcommerce"), envLocal);
4735
5326
  symlinkSync(target, devVars);
4736
5327
  consola.success("Symlinked .bigcommerce/.dev.vars \u2192 .env.local");
4737
5328
  } else if (!lstatSync(devVars).isSymbolicLink()) {
@@ -4751,7 +5342,7 @@ Example:
4751
5342
  "opennextjs-cloudflare",
4752
5343
  "preview",
4753
5344
  "--config",
4754
- join13(".bigcommerce", "wrangler.jsonc")
5345
+ join14(".bigcommerce", "wrangler.jsonc")
4755
5346
  ],
4756
5347
  {
4757
5348
  stdio: "inherit",
@@ -4807,10 +5398,10 @@ Examples:
4807
5398
 
4808
5399
  // src/cli/commands/upgrade.ts
4809
5400
  import { Command as Command13, Option as Option10 } from "@commander-js/extra-typings";
4810
- import { confirm as confirm6 } from "@inquirer/prompts";
5401
+ import { confirm as confirm7 } from "@inquirer/prompts";
4811
5402
  import { execa as execa4 } from "execa";
4812
5403
  import {
4813
- access as access2,
5404
+ access as access3,
4814
5405
  copyFile as copyFile2,
4815
5406
  cp as cp2,
4816
5407
  mkdir,
@@ -4822,7 +5413,8 @@ import {
4822
5413
  writeFile as writeFile2
4823
5414
  } from "fs/promises";
4824
5415
  import { homedir, tmpdir } from "os";
4825
- import { dirname as dirname5, join as join14, relative as relative2 } from "path";
5416
+ import { dirname as dirname5, join as join15, relative as relative2 } from "path";
5417
+ import { minVersion as minVersion2, satisfies as satisfies2, lt as semverLt2, validRange as validRange2, valid as validSemver2 } from "semver";
4826
5418
  import yoctoSpinner4 from "yocto-spinner";
4827
5419
  import { z as z17 } from "zod";
4828
5420
  var CorePackageJson = z17.object({
@@ -4830,6 +5422,20 @@ var CorePackageJson = z17.object({
4830
5422
  version: z17.string(),
4831
5423
  catalyst: z17.object({ version: z17.string(), ref: z17.string() }).optional()
4832
5424
  });
5425
+ var DEP_FIELDS2 = [
5426
+ "dependencies",
5427
+ "devDependencies",
5428
+ "optionalDependencies",
5429
+ "peerDependencies"
5430
+ ];
5431
+ var DepsPackageJson = z17.looseObject({
5432
+ dependencies: z17.record(z17.string(), z17.string()).optional(),
5433
+ devDependencies: z17.record(z17.string(), z17.string()).optional(),
5434
+ optionalDependencies: z17.record(z17.string(), z17.string()).optional(),
5435
+ peerDependencies: z17.record(z17.string(), z17.string()).optional()
5436
+ });
5437
+ var WorkspacePackageJson = z17.object({ name: z17.string(), version: z17.string() });
5438
+ var CATALYST_CLI_PACKAGE = "@bigcommerce/catalyst";
4833
5439
  var DEFAULT_REPOSITORY = "bigcommerce/catalyst";
4834
5440
  var MOVING_TAGS = /* @__PURE__ */ new Set(["latest", "canary", "alpha"]);
4835
5441
  var KNOWN_FAMILIES = /* @__PURE__ */ new Set([
@@ -4838,14 +5444,14 @@ var KNOWN_FAMILIES = /* @__PURE__ */ new Set([
4838
5444
  "@bigcommerce/catalyst-b2b-makeswift",
4839
5445
  "@bigcommerce/catalyst-b2b"
4840
5446
  ]);
4841
- var CACHE_DIR = join14(homedir(), ".cache", "catalyst-cli", "cores");
4842
- var pathExists = (p) => access2(p).then(() => true).catch(() => false);
5447
+ var CACHE_DIR = join15(homedir(), ".cache", "catalyst-cli", "cores");
5448
+ var pathExists = (p) => access3(p).then(() => true).catch(() => false);
4843
5449
  async function listFiles(dir, prefix = "") {
4844
5450
  const entries = await readdir2(dir, { withFileTypes: true }).catch(() => []);
4845
5451
  const nested = await Promise.all(
4846
5452
  entries.map((entry) => {
4847
5453
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
4848
- return entry.isDirectory() ? listFiles(join14(dir, entry.name), rel) : Promise.resolve([rel]);
5454
+ return entry.isDirectory() ? listFiles(join15(dir, entry.name), rel) : Promise.resolve([rel]);
4849
5455
  })
4850
5456
  );
4851
5457
  return nested.flat();
@@ -4861,7 +5467,7 @@ async function computeBaseSimilarity(baseDir, destDir) {
4861
5467
  const baseFiles = await listFiles(baseDir);
4862
5468
  if (baseFiles.length === 0) return 0;
4863
5469
  const matches = await Promise.all(
4864
- baseFiles.map((rel) => filesEqual(join14(baseDir, rel), join14(destDir, rel)))
5470
+ baseFiles.map((rel) => filesEqual(join15(baseDir, rel), join15(destDir, rel)))
4865
5471
  );
4866
5472
  return matches.filter(Boolean).length / baseFiles.length;
4867
5473
  }
@@ -4880,10 +5486,79 @@ function parseRef(ref) {
4880
5486
  return { packageName: ref.slice(0, lastAt), version: ref.slice(lastAt + 1) };
4881
5487
  }
4882
5488
  async function readResolvedVersion(coreDir) {
4883
- const raw = await readFile2(join14(coreDir, "package.json"), "utf-8");
5489
+ const raw = await readFile2(join15(coreDir, "package.json"), "utf-8");
4884
5490
  const pkg = CorePackageJson.parse(JSON.parse(raw));
4885
5491
  return pkg.catalyst?.version ?? pkg.version;
4886
5492
  }
5493
+ var readDepSpecifiers = (raw) => {
5494
+ let parsed;
5495
+ try {
5496
+ parsed = DepsPackageJson.parse(JSON.parse(raw));
5497
+ } catch {
5498
+ return {};
5499
+ }
5500
+ return Object.fromEntries(DEP_FIELDS2.flatMap((field) => Object.entries(parsed[field] ?? {})));
5501
+ };
5502
+ var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5503
+ var rewriteWorkspaceSpecifier = (raw, name, version2) => raw.replace(
5504
+ new RegExp(`("${escapeRegExp(name)}"\\s*:\\s*)"workspace:[^"]*"`, "g"),
5505
+ (_match, key) => `${key}"^${version2}"`
5506
+ );
5507
+ async function normalizeWorkspaceDeps(ourPkgRaw, baseDir, theirsDir, baseVersions, targetVersions) {
5508
+ const basePath = join15(baseDir, "package.json");
5509
+ const theirsPath = join15(theirsDir, "package.json");
5510
+ const [originalBase, originalTheirs] = await Promise.all([
5511
+ readFile2(basePath, "utf-8").catch(() => null),
5512
+ readFile2(theirsPath, "utf-8").catch(() => null)
5513
+ ]);
5514
+ if (originalBase === null || originalTheirs === null) return { workspace: [], bumped: [] };
5515
+ const ourSpecifiers = readDepSpecifiers(ourPkgRaw);
5516
+ const names = new Set(
5517
+ [
5518
+ ...Object.entries(readDepSpecifiers(originalBase)),
5519
+ ...Object.entries(readDepSpecifiers(originalTheirs))
5520
+ ].filter(([, range]) => range?.startsWith("workspace:")).map(([name]) => name)
5521
+ );
5522
+ const decisions = [...names].map((name) => ({
5523
+ name,
5524
+ ours: ourSpecifiers[name],
5525
+ baseVersion: baseVersions[name],
5526
+ targetVersion: targetVersions[name]
5527
+ }));
5528
+ const workspace = decisions.flatMap(
5529
+ ({ name, ours, targetVersion }) => ours?.startsWith("workspace:") && targetVersion !== void 0 ? [{ name, ours, version: targetVersion }] : []
5530
+ );
5531
+ const normalizable = decisions.filter(({ ours }) => !ours?.startsWith("workspace:"));
5532
+ const baseRaw = normalizable.reduce(
5533
+ (acc, { name, baseVersion }) => baseVersion === void 0 ? acc : rewriteWorkspaceSpecifier(acc, name, baseVersion),
5534
+ originalBase
5535
+ );
5536
+ const theirsRaw = normalizable.reduce(
5537
+ (acc, { name, targetVersion }) => targetVersion === void 0 ? acc : rewriteWorkspaceSpecifier(acc, name, targetVersion),
5538
+ originalTheirs
5539
+ );
5540
+ const bumped = normalizable.filter(
5541
+ ({ baseVersion, targetVersion }) => baseVersion !== void 0 && targetVersion !== void 0 && baseVersion !== targetVersion
5542
+ ).map(({ name }) => name);
5543
+ await Promise.all([
5544
+ baseRaw === originalBase ? null : writeFile2(basePath, baseRaw),
5545
+ theirsRaw === originalTheirs ? null : writeFile2(theirsPath, theirsRaw)
5546
+ ]);
5547
+ return { workspace, bumped };
5548
+ }
5549
+ var migrateWorkspaceDeps = (raw, findings) => findings.reduce((acc, { name, version: version2 }) => rewriteWorkspaceSpecifier(acc, name, version2), raw);
5550
+ async function findStaleCli(pkgRaw) {
5551
+ const range = readDepSpecifiers(pkgRaw)[CATALYST_CLI_PACKAGE];
5552
+ if (range === void 0 || !validRange2(range)) return null;
5553
+ const body = await fetch(`https://registry.npmjs.org/${CATALYST_CLI_PACKAGE}/latest`, {
5554
+ // A hanging registry would otherwise stall the CLI indefinitely.
5555
+ signal: AbortSignal.timeout(5e3)
5556
+ }).then((res) => res.ok ? res.json() : null).catch(() => null);
5557
+ const latest = z17.object({ version: z17.string() }).safeParse(body).data?.version;
5558
+ if (latest === void 0 || !validSemver2(latest) || satisfies2(latest, range)) return null;
5559
+ const current = minVersion2(range)?.version;
5560
+ return current !== void 0 && semverLt2(current, latest) ? { current, latest } : null;
5561
+ }
4887
5562
  async function mergeFile(oursPath, basePath, theirsPath) {
4888
5563
  const merged = await execa4(
4889
5564
  "git",
@@ -4907,9 +5582,9 @@ async function mergeFile(oursPath, basePath, theirsPath) {
4907
5582
  return (merged.exitCode ?? 0) > 0;
4908
5583
  }
4909
5584
  async function mergeModified(rel, baseDir, theirsDir, catalystRoot) {
4910
- const basePath = join14(baseDir, rel);
4911
- const theirsPath = join14(theirsDir, rel);
4912
- const oursPath = join14(catalystRoot, rel);
5585
+ const basePath = join15(baseDir, rel);
5586
+ const theirsPath = join15(theirsDir, rel);
5587
+ const oursPath = join15(catalystRoot, rel);
4913
5588
  if (await filesEqual(basePath, theirsPath)) return null;
4914
5589
  if (!await pathExists(oursPath)) {
4915
5590
  await copyInto(theirsPath, oursPath);
@@ -4925,8 +5600,8 @@ async function mergeModified(rel, baseDir, theirsDir, catalystRoot) {
4925
5600
  return await mergeFile(oursPath, basePath, theirsPath) ? "conflicted" : "applied";
4926
5601
  }
4927
5602
  async function mergeAdded(rel, theirsDir, catalystRoot, emptyFile) {
4928
- const theirsPath = join14(theirsDir, rel);
4929
- const oursPath = join14(catalystRoot, rel);
5603
+ const theirsPath = join15(theirsDir, rel);
5604
+ const oursPath = join15(catalystRoot, rel);
4930
5605
  if (!await pathExists(oursPath)) {
4931
5606
  await copyInto(theirsPath, oursPath);
4932
5607
  return "added";
@@ -4937,8 +5612,8 @@ async function mergeAdded(rel, theirsDir, catalystRoot, emptyFile) {
4937
5612
  return hadConflict ? "conflicted" : "applied";
4938
5613
  }
4939
5614
  async function mergeDeleted(rel, baseDir, catalystRoot) {
4940
- const basePath = join14(baseDir, rel);
4941
- const oursPath = join14(catalystRoot, rel);
5615
+ const basePath = join15(baseDir, rel);
5616
+ const oursPath = join15(catalystRoot, rel);
4942
5617
  if (!await pathExists(oursPath)) return null;
4943
5618
  if (await filesEqual(oursPath, basePath)) {
4944
5619
  await rm2(oursPath, { force: true });
@@ -4984,15 +5659,15 @@ async function resolveStrategy(strategy) {
4984
5659
  return "per-file";
4985
5660
  }
4986
5661
  async function mergeCoreTree(baseDir, theirsDir, catalystRoot) {
4987
- const scratch = await mkdtemp(join14(tmpdir(), "catalyst-merge-"));
5662
+ const scratch = await mkdtemp(join15(tmpdir(), "catalyst-merge-"));
4988
5663
  try {
4989
5664
  await execa4("git", ["init", "-q", scratch]);
4990
5665
  await execa4("git", ["config", "core.autocrlf", "false"], {
4991
- env: { ...process.env, GIT_DIR: join14(scratch, ".git") }
5666
+ env: { ...process.env, GIT_DIR: join15(scratch, ".git") }
4992
5667
  });
4993
5668
  const env2 = {
4994
5669
  ...process.env,
4995
- GIT_DIR: join14(scratch, ".git"),
5670
+ GIT_DIR: join15(scratch, ".git"),
4996
5671
  GIT_AUTHOR_NAME: "catalyst",
4997
5672
  GIT_AUTHOR_EMAIL: "catalyst@bigcommerce.com",
4998
5673
  GIT_COMMITTER_NAME: "catalyst",
@@ -5002,7 +5677,7 @@ async function mergeCoreTree(baseDir, theirsDir, catalystRoot) {
5002
5677
  const sideEnv = {
5003
5678
  ...env2,
5004
5679
  GIT_WORK_TREE: workTree,
5005
- GIT_INDEX_FILE: join14(scratch, `index.${tag}`)
5680
+ GIT_INDEX_FILE: join15(scratch, `index.${tag}`)
5006
5681
  };
5007
5682
  await execa4("git", ["add", "-A"], { env: sideEnv });
5008
5683
  return (await execa4("git", ["write-tree"], { env: sideEnv })).stdout.trim();
@@ -5046,7 +5721,7 @@ async function mergeCoreTree(baseDir, theirsDir, catalystRoot) {
5046
5721
  const outEnv = {
5047
5722
  ...env2,
5048
5723
  GIT_WORK_TREE: catalystRoot,
5049
- GIT_INDEX_FILE: join14(scratch, "index.out")
5724
+ GIT_INDEX_FILE: join15(scratch, "index.out")
5050
5725
  };
5051
5726
  await execa4("git", ["read-tree", mergedTree], { env: outEnv });
5052
5727
  await execa4("git", ["checkout-index", "-a", "-f"], { env: outEnv });
@@ -5065,7 +5740,7 @@ async function mergeCoreTree(baseDir, theirsDir, catalystRoot) {
5065
5740
  }
5066
5741
  }
5067
5742
  const deleted = changes.filter((change) => change.status === "D").map((change) => change.path);
5068
- await Promise.all(deleted.map((path) => rm2(join14(catalystRoot, path), { force: true })));
5743
+ await Promise.all(deleted.map((path) => rm2(join15(catalystRoot, path), { force: true })));
5069
5744
  return {
5070
5745
  applied: changes.filter((change) => change.status !== "A" && change.status !== "D").map((change) => change.path),
5071
5746
  added: changes.filter((change) => change.status === "A").map((change) => change.path),
@@ -5108,9 +5783,9 @@ async function applyIndexState(gitRoot, relDir, baseDir, theirsDir, result, stam
5108
5783
  stillConflicted.map(async (rel) => {
5109
5784
  const repoPath = toRepoPath(rel);
5110
5785
  const [base, ours, theirs] = await Promise.all([
5111
- hashBlob(join14(baseDir, rel)),
5786
+ hashBlob(join15(baseDir, rel)),
5112
5787
  headEntry(repoPath),
5113
- hashBlob(join14(theirsDir, rel))
5788
+ hashBlob(join15(theirsDir, rel))
5114
5789
  ]);
5115
5790
  return [
5116
5791
  `0 ${NULL_OID} ${repoPath}`,
@@ -5133,10 +5808,10 @@ async function resolveProject2(cwd) {
5133
5808
  } catch {
5134
5809
  return null;
5135
5810
  }
5136
- const candidateDirs = [.../* @__PURE__ */ new Set([join14(gitRoot, "core"), gitRoot, join14(cwd, "core"), cwd])];
5811
+ const candidateDirs = [.../* @__PURE__ */ new Set([join15(gitRoot, "core"), gitRoot, join15(cwd, "core"), cwd])];
5137
5812
  const parsed = (await Promise.all(
5138
5813
  candidateDirs.map(async (dir) => {
5139
- const pkgPath = join14(dir, "package.json");
5814
+ const pkgPath = join15(dir, "package.json");
5140
5815
  const raw = await readFile2(pkgPath, "utf-8").catch(() => null);
5141
5816
  if (!raw) return null;
5142
5817
  let pkgJson;
@@ -5160,13 +5835,44 @@ async function resolveProject2(cwd) {
5160
5835
  pkg: chosen.pkg
5161
5836
  };
5162
5837
  }
5838
+ var readWorkspaceVersions = async (repoDir) => {
5839
+ const packagesDir = join15(repoDir, "packages");
5840
+ const entries = await readdir2(packagesDir, { withFileTypes: true }).catch(() => []);
5841
+ const manifests = await Promise.all(
5842
+ entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
5843
+ const raw = await readFile2(join15(packagesDir, entry.name, "package.json"), "utf-8").catch(
5844
+ () => null
5845
+ );
5846
+ if (raw === null) return null;
5847
+ try {
5848
+ return WorkspacePackageJson.parse(JSON.parse(raw));
5849
+ } catch {
5850
+ return null;
5851
+ }
5852
+ })
5853
+ );
5854
+ return Object.fromEntries(
5855
+ manifests.filter((pkg) => pkg !== null).map(({ name, version: version2 }) => [name, version2])
5856
+ );
5857
+ };
5858
+ var readCachedVersions = async (path) => {
5859
+ const raw = await readFile2(path, "utf-8").catch(() => null);
5860
+ if (raw === null) return null;
5861
+ try {
5862
+ return z17.record(z17.string(), z17.string()).parse(JSON.parse(raw));
5863
+ } catch {
5864
+ return null;
5865
+ }
5866
+ };
5163
5867
  async function downloadCore(repository, ref, destDir, token) {
5164
5868
  const cacheable = !MOVING_TAGS.has(parseRef(ref).version);
5165
5869
  const cacheKey = `${repository.replace(/[^a-zA-Z0-9._-]/g, "_")}__${ref.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
5166
- const cachePath = join14(CACHE_DIR, cacheKey);
5167
- if (cacheable && await pathExists(cachePath) && await pathExists(join14(cachePath, "package.json"))) {
5870
+ const cachePath = join15(CACHE_DIR, cacheKey);
5871
+ const versionsPath = `${cachePath}.versions.json`;
5872
+ const cachedVersions = cacheable && await pathExists(join15(cachePath, "package.json")) ? await readCachedVersions(versionsPath) : null;
5873
+ if (cachedVersions) {
5168
5874
  await cp2(cachePath, destDir, { recursive: true });
5169
- return;
5875
+ return cachedVersions;
5170
5876
  }
5171
5877
  const url = `https://api.github.com/repos/${repository}/tarball/${encodeURIComponent(ref)}`;
5172
5878
  const headers = {
@@ -5186,32 +5892,52 @@ async function downloadCore(repository, ref, destDir, token) {
5186
5892
  await writeFile2(tarballPath, Buffer.from(await res.arrayBuffer()));
5187
5893
  await mkdir(rawDir, { recursive: true });
5188
5894
  await execa4("tar", ["-xzf", tarballPath, "-C", rawDir, "--strip-components=1"]);
5189
- const coreDir = join14(rawDir, "core");
5895
+ const coreDir = join15(rawDir, "core");
5190
5896
  const sourceDir = await pathExists(coreDir) ? coreDir : rawDir;
5897
+ const workspaceVersions = await readWorkspaceVersions(rawDir);
5191
5898
  await rename(sourceDir, destDir);
5192
5899
  await rm2(tarballPath, { force: true });
5193
5900
  await rm2(rawDir, { recursive: true, force: true });
5194
5901
  if (cacheable) {
5195
5902
  await mkdir(CACHE_DIR, { recursive: true });
5196
- await cp2(destDir, cachePath, { recursive: true }).catch(() => {
5903
+ await cp2(destDir, cachePath, { recursive: true }).then(() => writeFile2(versionsPath, JSON.stringify(workspaceVersions))).catch(() => {
5197
5904
  });
5198
5905
  }
5906
+ return workspaceVersions;
5199
5907
  }
5200
- async function resolveBaseRef(project2, fromOption, assumeYes) {
5201
- if (project2.pkg.catalyst?.ref) return project2.pkg.catalyst.ref;
5908
+ async function resolveBaseRef(project, fromOption, assumeYes) {
5909
+ if (project.pkg.catalyst?.ref) return project.pkg.catalyst.ref;
5202
5910
  if (fromOption) return fromOption;
5203
- const family = project2.pkg.name && KNOWN_FAMILIES.has(project2.pkg.name) ? project2.pkg.name : "@bigcommerce/catalyst-core";
5204
- const guess = `${family}@${project2.pkg.version}`;
5911
+ const family = project.pkg.name && KNOWN_FAMILIES.has(project.pkg.name) ? project.pkg.name : "@bigcommerce/catalyst-core";
5912
+ const guess = `${family}@${project.pkg.version}`;
5205
5913
  consola.warn(`No \`catalyst.ref\` found in package.json. Inferred base: ${guess}`);
5206
5914
  if (assumeYes) return guess;
5207
- const ok = await confirm6({ message: `Upgrade from ${guess}?`, default: true }).catch(() => false);
5915
+ const ok = await confirm7({ message: `Upgrade from ${guess}?`, default: true }).catch(() => false);
5208
5916
  if (!ok) {
5209
5917
  consola.info("Re-run with `--from <ref>` to set the base version explicitly.");
5210
5918
  return null;
5211
5919
  }
5212
5920
  return guess;
5213
5921
  }
5214
- function printSummary(result, relDir, stampedPkg) {
5922
+ function printWorkspaceFindings(findings) {
5923
+ if (findings.length === 0) return;
5924
+ const pad = Math.max(...findings.map((finding) => finding.name.length));
5925
+ const rows = findings.map((finding) => ` ${finding.name.padEnd(pad)} ${finding.ours} \u2192 ^${finding.version}`).join("\n");
5926
+ consola.warn(
5927
+ `Catalyst dependencies still using the workspace protocol (${findings.length}):
5928
+ ${rows}
5929
+
5930
+ They resolve to your local packages/ copies, which \`catalyst upgrade\` does not update \u2014 so they never receive upgrades.`
5931
+ );
5932
+ }
5933
+ function printSummary({
5934
+ result,
5935
+ relDir,
5936
+ stampedPkg,
5937
+ migratedDeps,
5938
+ packageManager,
5939
+ staleCli
5940
+ }) {
5215
5941
  const unresolved = result.conflicted.filter((f) => !(stampedPkg && f === "package.json"));
5216
5942
  const parts = [
5217
5943
  `${result.applied.length} updated`,
@@ -5221,18 +5947,31 @@ function printSummary(result, relDir, stampedPkg) {
5221
5947
  if (unresolved.length) parts.push(`${unresolved.length} with conflicts`);
5222
5948
  consola.log(`
5223
5949
  ${parts.join(", ")}`);
5950
+ const pkgConflicted = unresolved.includes("package.json");
5951
+ const depsChanged = migratedDeps > 0 || [...result.applied, ...result.added, ...result.conflicted].includes("package.json");
5952
+ if (depsChanged && !pkgConflicted) {
5953
+ consola.info(
5954
+ `Dependencies changed \u2014 run \`${packageManager} install\` to update your lockfile.`
5955
+ );
5956
+ }
5224
5957
  if (unresolved.length) {
5225
5958
  const files = unresolved.map((f) => ` ${f}`).join("\n");
5959
+ const install = depsChanged && pkgConflicted ? ` && ${packageManager} install` : "";
5226
5960
  consola.warn(
5227
5961
  `
5228
5962
  Staged the clean changes. ${unresolved.length} file(s) need conflict resolution (the <<<ours/===/theirs>>> markers):
5229
5963
  ${files}
5230
5964
 
5231
- Resolve them, then: git add ${relDir} && git commit`
5965
+ Resolve them, then: git add ${relDir} && git commit${install}`
5232
5966
  );
5233
5967
  } else {
5234
5968
  consola.success("Staged all changes \u2014 review with `git diff --cached`, then commit.");
5235
5969
  }
5970
+ if (staleCli) {
5971
+ consola.info(
5972
+ `Your Catalyst CLI is behind (${staleCli.current} \u2192 ${staleCli.latest}). Update it with \`${packageManager} add -D ${CATALYST_CLI_PACKAGE}@${staleCli.latest}\`.`
5973
+ );
5974
+ }
5236
5975
  }
5237
5976
  var upgrade = new Command13("upgrade").configureHelp({ showGlobalOptions: true }).aliases([
5238
5977
  "up",
@@ -5273,15 +6012,15 @@ Conflicts are written as standard <<<ours/===/theirs>>> markers \u2014 the upgra
5273
6012
  never aborts. Versions are git tags on the repository (not npm). Set GITHUB_TOKEN
5274
6013
  to raise the GitHub API rate limit.`
5275
6014
  ).action(async (version2, options) => {
5276
- const project2 = await resolveProject2(process.cwd());
5277
- if (!project2) {
6015
+ const project = await resolveProject2(process.cwd());
6016
+ if (!project) {
5278
6017
  consola.error(
5279
6018
  "Run `catalyst upgrade` from inside a Catalyst git repository (flat core/ repo or a project containing core/)."
5280
6019
  );
5281
6020
  process.exit(1);
5282
6021
  }
5283
- const { gitRoot, catalystRoot, relDir, pkgPath } = project2;
5284
- const baseRef = await resolveBaseRef(project2, options.from, options.yes ?? false);
6022
+ const { gitRoot, catalystRoot, relDir, pkgPath } = project;
6023
+ const baseRef = await resolveBaseRef(project, options.from, options.yes ?? false);
5285
6024
  if (!baseRef) process.exit(1);
5286
6025
  let basePackage;
5287
6026
  let upstreamRef;
@@ -5316,7 +6055,7 @@ to raise the GitHub API rate limit.`
5316
6055
  process.exit(1);
5317
6056
  }
5318
6057
  }
5319
- if (!project2.pkg.catalyst?.ref && !options.from && !options.dryRun) {
6058
+ if (!project.pkg.catalyst?.ref && !options.from && !options.dryRun) {
5320
6059
  const { version: baseVersion } = parseRef(baseRef);
5321
6060
  const rawPkg = await readFile2(pkgPath, "utf-8");
5322
6061
  const parsedPkg = z17.record(z17.string(), z17.unknown()).parse(JSON.parse(rawPkg));
@@ -5327,15 +6066,18 @@ to raise the GitHub API rate limit.`
5327
6066
  consola.success(`Added catalyst.ref \u2192 ${baseRef}`);
5328
6067
  }
5329
6068
  const token = process.env.GITHUB_TOKEN;
5330
- const tmpDir = await mkdtemp(join14(tmpdir(), "catalyst-upgrade-"));
6069
+ const tmpDir = await mkdtemp(join15(tmpdir(), "catalyst-upgrade-"));
5331
6070
  try {
5332
- const baseDir = join14(tmpDir, "base");
5333
- const theirsDir = join14(tmpDir, "theirs");
5334
- const emptyFile = join14(tmpDir, ".empty");
6071
+ const baseDir = join15(tmpDir, "base");
6072
+ const theirsDir = join15(tmpDir, "theirs");
6073
+ const emptyFile = join15(tmpDir, ".empty");
5335
6074
  await writeFile2(emptyFile, "");
6075
+ const staleCliCheck = findStaleCli(project.rawContent).catch(() => null);
5336
6076
  const downloadSpinner = yoctoSpinner4().start(`Downloading ${baseRef} and ${upstreamRef}...`);
6077
+ let baseVersions;
6078
+ let targetVersions;
5337
6079
  try {
5338
- await Promise.all([
6080
+ [baseVersions, targetVersions] = await Promise.all([
5339
6081
  downloadCore(options.repository, baseRef, baseDir, token),
5340
6082
  downloadCore(options.repository, upstreamRef, theirsDir, token)
5341
6083
  ]);
@@ -5344,7 +6086,7 @@ to raise the GitHub API rate limit.`
5344
6086
  downloadSpinner.error("Download failed");
5345
6087
  throw err;
5346
6088
  }
5347
- if (!project2.pkg.catalyst?.ref && !options.from) {
6089
+ if (!project.pkg.catalyst?.ref && !options.from) {
5348
6090
  const similarity = await computeBaseSimilarity(baseDir, catalystRoot);
5349
6091
  const pct = Math.round(similarity * 100);
5350
6092
  if (similarity < 0.5) {
@@ -5355,6 +6097,13 @@ to raise the GitHub API rate limit.`
5355
6097
  }
5356
6098
  const resolvedVersion = MOVING_TAGS.has(upstreamTagVersion) ? await readResolvedVersion(theirsDir) : upstreamTagVersion;
5357
6099
  const newRef = MOVING_TAGS.has(upstreamTagVersion) ? `${basePackage}@${resolvedVersion}` : upstreamRef;
6100
+ const { workspace: workspaceDeps, bumped } = await normalizeWorkspaceDeps(
6101
+ project.rawContent,
6102
+ baseDir,
6103
+ theirsDir,
6104
+ baseVersions,
6105
+ targetVersions
6106
+ );
5358
6107
  if (options.dryRun) {
5359
6108
  const diffSpinner = yoctoSpinner4().start("Generating diff...");
5360
6109
  const diff = await execa4(
@@ -5374,6 +6123,7 @@ to raise the GitHub API rate limit.`
5374
6123
  diffSpinner.success(`Diff ready \u2014 ${fileCount} file(s) affected.`);
5375
6124
  consola.log("\nDiff preview (--dry-run, not applied):\n");
5376
6125
  consola.log(diff.stdout);
6126
+ printWorkspaceFindings(workspaceDeps);
5377
6127
  return;
5378
6128
  }
5379
6129
  const strategy = await resolveStrategy(options.strategy);
@@ -5390,13 +6140,13 @@ to raise the GitHub API rate limit.`
5390
6140
  mergeSpinner.success("Merged cleanly.");
5391
6141
  }
5392
6142
  const patchedRaw = await readFile2(pkgPath, "utf-8");
5393
- let stampedPkg = false;
6143
+ let stampedText = null;
5394
6144
  try {
5395
6145
  const patchedPkg = z17.record(z17.string(), z17.unknown()).parse(JSON.parse(patchedRaw));
5396
6146
  patchedPkg.catalyst = { version: resolvedVersion, ref: newRef };
5397
- await writeFile2(pkgPath, `${JSON.stringify(patchedPkg, null, 2)}
5398
- `);
5399
- stampedPkg = true;
6147
+ stampedText = `${JSON.stringify(patchedPkg, null, 2)}
6148
+ `;
6149
+ await writeFile2(pkgPath, stampedText);
5400
6150
  consola.success(`catalyst.ref updated \u2192 ${newRef}`);
5401
6151
  } catch {
5402
6152
  const catalystReplacement = JSON.stringify(
@@ -5421,14 +6171,35 @@ to raise the GitHub API rate limit.`
5421
6171
  );
5422
6172
  }
5423
6173
  }
6174
+ printWorkspaceFindings(workspaceDeps);
6175
+ let migratedDeps = 0;
6176
+ if (workspaceDeps.length) {
6177
+ const accepted = stampedText !== null && (options.yes === true || Boolean(process.stdin.isTTY) && await confirm7({
6178
+ message: "Replace them with the published versions?",
6179
+ default: true
6180
+ }).catch(() => false));
6181
+ if (accepted && stampedText !== null) {
6182
+ await writeFile2(pkgPath, migrateWorkspaceDeps(stampedText, workspaceDeps));
6183
+ migratedDeps = workspaceDeps.length;
6184
+ consola.success(
6185
+ `Migrated ${migratedDeps} dependenc${migratedDeps === 1 ? "y" : "ies"} to published versions.`
6186
+ );
6187
+ } else {
6188
+ consola.info(
6189
+ "Keeping the workspace references. Switching abandons any local packages/ copy you've customized; otherwise apply the versions above in package.json and reinstall."
6190
+ );
6191
+ }
6192
+ }
5424
6193
  try {
5425
- await applyIndexState(gitRoot, relDir, baseDir, theirsDir, result, stampedPkg);
6194
+ await applyIndexState(gitRoot, relDir, baseDir, theirsDir, result, stampedText !== null);
5426
6195
  } catch (err) {
5427
6196
  consola.warn(
5428
6197
  `Couldn't auto-stage the changes (${err instanceof Error ? err.message : String(err)}). Your files are merged on disk \u2014 run \`git add ${relDir}\` yourself.`
5429
6198
  );
5430
6199
  }
5431
6200
  const gitVersion = await execa4("git", ["--version"]).then((r) => r.stdout.trim()).catch(() => "unknown");
6201
+ const packageManager = await detectLockfileManager(gitRoot) ?? "pnpm";
6202
+ const staleCli = await staleCliCheck;
5432
6203
  await getTelemetry().track("upgrade", {
5433
6204
  strategy,
5434
6205
  gitVersion,
@@ -5437,9 +6208,20 @@ to raise the GitHub API rate limit.`
5437
6208
  added: result.added.length,
5438
6209
  deleted: result.deleted.length,
5439
6210
  conflicts: result.conflicted.length,
5440
- hasConflicts: result.conflicted.length > 0
6211
+ hasConflicts: result.conflicted.length > 0,
6212
+ workspaceDepsFound: workspaceDeps.length,
6213
+ workspaceDepsMigrated: migratedDeps,
6214
+ catalystDepsBumped: bumped.length,
6215
+ cliOutdated: staleCli !== null
6216
+ });
6217
+ printSummary({
6218
+ result,
6219
+ relDir,
6220
+ stampedPkg: stampedText !== null,
6221
+ migratedDeps,
6222
+ packageManager,
6223
+ staleCli
5441
6224
  });
5442
- printSummary(result, relDir, stampedPkg);
5443
6225
  } finally {
5444
6226
  await rm2(tmpDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 500 });
5445
6227
  }
@@ -5516,7 +6298,7 @@ consola.log(colorize13("cyanBright", `\u25E2 ${package_default.name} v${package_
5516
6298
  `));
5517
6299
  program.name(package_default.name).version(package_default.version).summary("CLI tool for Catalyst development").description(
5518
6300
  "CLI tool for Catalyst development.\n\nConfiguration priority: flags > process.env > .bigcommerce/project.json. `build` and `deploy` additionally load env files (--env-path, or an auto-loaded .env.local/.env) for the build.\n\nCATALYST_STORE_HASH falls back to BIGCOMMERCE_STORE_HASH if unset.\n\nRun `catalyst <command> --help` for details on a specific command."
5519
- ).configureHelp({ showGlobalOptions: true }).addCommand(version).addCommand(create2).addCommand(start).addCommand(build).addCommand(deploy).addCommand(domains).addCommand(logs).addCommand(project).addCommand(env).addCommand(channel).addCommand(auth).addCommand(upgrade).addCommand(telemetry).addCommand(debug).hook("preAction", telemetryPreHook).hook("postAction", telemetryPostHook);
6301
+ ).configureHelp({ showGlobalOptions: true }).addCommand(version).addCommand(create2).addCommand(start).addCommand(build).addCommand(deploy).addCommand(domains).addCommand(logs).addCommand(projects).addCommand(env).addCommand(channels).addCommand(auth).addCommand(upgrade).addCommand(telemetry).addCommand(debug).hook("preAction", telemetryPreHook).hook("postAction", telemetryPostHook);
5520
6302
 
5521
6303
  // src/cli/index.ts
5522
6304
  var handleFatalError = async (error) => {