@bigcommerce/catalyst 1.0.0-alpha.5 → 1.0.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
@@ -28,8 +28,13 @@ import { randomBytes, randomUUID } from "crypto";
28
28
  // package.json
29
29
  var package_default = {
30
30
  name: "@bigcommerce/catalyst",
31
- version: "1.0.0-alpha.5",
31
+ version: "1.0.0",
32
32
  type: "module",
33
+ repository: {
34
+ type: "git",
35
+ url: "https://github.com/bigcommerce/catalyst",
36
+ directory: "packages/catalyst"
37
+ },
33
38
  bin: {
34
39
  catalyst: "dist/cli.js"
35
40
  },
@@ -65,6 +70,7 @@ var package_default = {
65
70
  nypm: "^0.5.4",
66
71
  open: "^10.1.0",
67
72
  "std-env": "^3.9.0",
73
+ tar: "^7.5.7",
68
74
  "yocto-spinner": "^1.0.0",
69
75
  zod: "^4.0.5"
70
76
  },
@@ -128,7 +134,7 @@ var Telemetry = class {
128
134
  this.correlationId = randomUUID();
129
135
  this.startTime = Date.now();
130
136
  this.analytics = new Analytics({
131
- writeKey: "not-a-valid-segment-write-key"
137
+ writeKey: "OCkxLj3KTc1N3ebEbnFBqbhxfE3tVb3Z"
132
138
  });
133
139
  }
134
140
  durationMs() {
@@ -195,11 +201,11 @@ function getTelemetry() {
195
201
  }
196
202
 
197
203
  // src/cli/program.ts
198
- import { Option as Option9 } from "@commander-js/extra-typings";
199
- import { Command as Command12 } from "commander";
200
- import { colorize as colorize8 } from "consola/utils";
204
+ import { Option as Option11 } from "@commander-js/extra-typings";
205
+ import { Command as Command14 } from "commander";
206
+ import { colorize as colorize11 } from "consola/utils";
201
207
  import { config } from "dotenv";
202
- import { existsSync as existsSync5 } from "fs";
208
+ import { existsSync as existsSync4 } from "fs";
203
209
  import { resolve } from "path";
204
210
 
205
211
  // src/cli/commands/auth.ts
@@ -207,7 +213,7 @@ import { Command, Option as Option2 } from "commander";
207
213
  import { z as z4 } from "zod";
208
214
 
209
215
  // src/cli/lib/login.ts
210
- import { password } from "@inquirer/prompts";
216
+ import { confirm, input, password } from "@inquirer/prompts";
211
217
  import { colorize } from "consola/utils";
212
218
  import open from "open";
213
219
  import yoctoSpinner from "yocto-spinner";
@@ -337,8 +343,8 @@ async function manualLogin(apiHost) {
337
343
  Settings \u2192 API \u2192 Store-level API accounts \u2192 Create API account
338
344
  Grant these OAuth scopes: ${DEVICE_OAUTH_SCOPES}`
339
345
  );
340
- const storeHashInput = await consola.prompt("Store hash:", { type: "text" });
341
- const storeHash = String(storeHashInput).trim();
346
+ const storeHashInput = await input({ message: "Store hash:" });
347
+ const storeHash = storeHashInput.trim();
342
348
  if (!storeHash) {
343
349
  throw new Error("Store hash is required.");
344
350
  }
@@ -366,10 +372,10 @@ async function login(loginUrl, apiHost) {
366
372
  } catch (error) {
367
373
  const message = error instanceof Error ? error.message : String(error);
368
374
  consola.warn(`Browser login didn't work (${message}).`);
369
- const shouldFallback = await consola.prompt(
370
- "Try logging in manually with a store hash and access token instead?",
371
- { type: "confirm", initial: true }
372
- );
375
+ const shouldFallback = await confirm({
376
+ message: "Try logging in manually with a store hash and access token instead?",
377
+ default: true
378
+ });
373
379
  if (!shouldFallback) {
374
380
  throw new LoginAbortedError();
375
381
  }
@@ -423,7 +429,7 @@ async function fetchProjects(storeHash, accessToken, apiHost) {
423
429
  assertAuthorized(response);
424
430
  if (response.status === 403) {
425
431
  throw new Error(
426
- "Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it."
432
+ "Infrastructure Projects API not enabled. If you are part of the beta, contact support@bigcommerce.com to enable it."
427
433
  );
428
434
  }
429
435
  if (!response.ok) {
@@ -474,7 +480,7 @@ async function createProject(name, storeHash, accessToken, apiHost) {
474
480
  }
475
481
  if (response.status === 403) {
476
482
  throw new Error(
477
- "Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it."
483
+ "Infrastructure Projects API not enabled. If you are part of the beta, contact support@bigcommerce.com to enable it."
478
484
  );
479
485
  }
480
486
  if (response.status === 502) {
@@ -495,7 +501,7 @@ async function deleteProject(projectUuid, storeHash, accessToken, apiHost) {
495
501
  assertAuthorized(response);
496
502
  if (response.status === 403) {
497
503
  throw new Error(
498
- "Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it."
504
+ "Infrastructure Projects API not enabled. If you are part of the beta, contact support@bigcommerce.com to enable it."
499
505
  );
500
506
  }
501
507
  if (response.status === 404) {
@@ -783,12 +789,17 @@ function getProjectState(cwd = process.cwd()) {
783
789
  }
784
790
 
785
791
  // src/cli/lib/wrangler-config.ts
792
+ function getCompatibilityDate(now = /* @__PURE__ */ new Date()) {
793
+ const date = new Date(now);
794
+ date.setUTCMonth(date.getUTCMonth() - 1);
795
+ return date.toISOString().slice(0, 10);
796
+ }
786
797
  function getWranglerConfig(projectUuid) {
787
798
  return {
788
799
  $schema: "node_modules/wrangler/config-schema.json",
789
800
  main: "../.open-next/worker.js",
790
801
  name: `project-${projectUuid}`,
791
- compatibility_date: "2025-09-15",
802
+ compatibility_date: getCompatibilityDate(),
792
803
  compatibility_flags: ["nodejs_compat", "global_fetch_strictly_public"],
793
804
  observability: {
794
805
  enabled: true,
@@ -935,7 +946,14 @@ Examples:
935
946
  });
936
947
 
937
948
  // src/cli/commands/channel.ts
938
- import { Command as Command3, Option as Option4 } from "commander";
949
+ import { select as select3 } from "@inquirer/prompts";
950
+ import { Command as Command3, InvalidArgumentError, Option as Option4 } from "commander";
951
+ import { colorize as colorize3 } from "consola/utils";
952
+ import { outputFileSync } from "fs-extra/esm";
953
+ import { join as join6 } from "path";
954
+
955
+ // src/cli/lib/channel-site-flow.ts
956
+ import { select as select2 } from "@inquirer/prompts";
939
957
 
940
958
  // src/cli/lib/channels.ts
941
959
  import { z as z6 } from "zod";
@@ -958,6 +976,20 @@ var channelSchema = z6.object({
958
976
  var channelsResponseSchema = z6.object({
959
977
  data: z6.array(channelSchema)
960
978
  });
979
+ var CHANNEL_PLATFORM_ORDER = ["catalyst", "next", "bigcommerce"];
980
+ function sortChannelsByPlatform(channels) {
981
+ return [...channels].sort((a, b) => {
982
+ const aIndex = CHANNEL_PLATFORM_ORDER.indexOf(a.platform);
983
+ const bIndex = CHANNEL_PLATFORM_ORDER.indexOf(b.platform);
984
+ if (aIndex === -1 && bIndex === -1) return 0;
985
+ if (aIndex === -1) return 1;
986
+ if (bIndex === -1) return -1;
987
+ return aIndex - bIndex;
988
+ });
989
+ }
990
+ function channelPlatformLabel(platform) {
991
+ return platform === "bigcommerce" ? "Stencil" : platform.charAt(0).toUpperCase() + platform.slice(1);
992
+ }
961
993
  var initResponseSchema = z6.object({
962
994
  data: z6.object({
963
995
  storefront_api_token: z6.string(),
@@ -1078,17 +1110,9 @@ async function updateChannelSiteUrl(channelId, siteUrl, storeHash, accessToken,
1078
1110
  }
1079
1111
 
1080
1112
  // src/cli/lib/commerce-hosting.ts
1081
- import { input, select, Separator } from "@inquirer/prompts";
1113
+ import { confirm as confirm2, input as input2, select, Separator } from "@inquirer/prompts";
1082
1114
  import { colorize as colorize2 } from "consola/utils";
1083
- import {
1084
- existsSync as existsSync2,
1085
- lstatSync,
1086
- mkdirSync,
1087
- readFileSync as readFileSync2,
1088
- symlinkSync,
1089
- unlinkSync,
1090
- writeFileSync
1091
- } from "fs";
1115
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
1092
1116
  import { dirname as dirname2, join as join5 } from "path";
1093
1117
  import { z as z7 } from "zod";
1094
1118
 
@@ -1128,58 +1152,43 @@ var writeJson = (path, value) => {
1128
1152
  writeFileSync(path, `${JSON.stringify(value, null, 2)}
1129
1153
  `);
1130
1154
  };
1131
- var symlinkRootEnvToCore = (projectDir) => {
1132
- const coreEnvPath = join5(projectDir, "core", ".env.local");
1133
- if (lstatSync(coreEnvPath, { throwIfNoEntry: false })) return;
1134
- try {
1135
- symlinkSync("../.env.local", coreEnvPath);
1136
- } catch (error) {
1137
- const message = error instanceof Error ? error.message : "unknown error";
1138
- consola.warn(
1139
- `Could not create symlink at core/.env.local: ${message}
1140
- On Windows, enable Developer Mode or run as administrator to allow symlinks.
1141
- You will need to keep .env.local and core/.env.local in sync manually.`
1142
- );
1143
- }
1144
- };
1145
1155
  var convertProxyToMiddleware = (projectDir) => {
1146
- const proxyPath = join5(projectDir, "core", "proxy.ts");
1147
- const middlewarePath = join5(projectDir, "core", "middleware.ts");
1156
+ const proxyPath = join5(projectDir, "proxy.ts");
1157
+ const middlewarePath = join5(projectDir, "middleware.ts");
1148
1158
  if (!existsSync2(proxyPath)) return;
1149
1159
  const contents = readFileSync2(proxyPath, "utf-8").replace("export const proxy", "export const middleware").replace("export const config = {", "export const config = {\n runtime: 'experimental-edge',");
1150
1160
  writeFileSync(middlewarePath, contents);
1151
1161
  unlinkSync(proxyPath);
1152
1162
  };
1153
1163
  var cleanupCloudflareIncompatibilities = async (projectDir) => {
1154
- const instrumentationPath = join5(projectDir, "core", "instrumentation.ts");
1164
+ const instrumentationPath = join5(projectDir, "instrumentation.ts");
1155
1165
  if (!existsSync2(instrumentationPath)) return;
1156
1166
  if (!process.stdin.isTTY) {
1157
1167
  consola.warn(
1158
- "core/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."
1168
+ "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."
1159
1169
  );
1160
1170
  return;
1161
1171
  }
1162
- const shouldRemove = await consola.prompt(
1163
- 'Catalyst found core/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 core/package.json?',
1164
- { type: "confirm", initial: true }
1165
- );
1166
- consola.log("");
1172
+ const shouldRemove = await confirm2({
1173
+ 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?',
1174
+ default: true
1175
+ });
1167
1176
  if (!shouldRemove) {
1168
1177
  consola.info(
1169
- 'Leaving core/instrumentation.ts in place. The Cloudflare worker will continue to log "Failed to prepare server" at cold start until this is resolved manually.'
1178
+ 'Leaving instrumentation.ts in place. The Cloudflare worker will continue to log "Failed to prepare server" at cold start until this is resolved manually.'
1170
1179
  );
1171
1180
  return;
1172
1181
  }
1173
1182
  unlinkSync(instrumentationPath);
1174
- consola.info("Removed core/instrumentation.ts (incompatible with Cloudflare Workers).");
1175
- const corePackageJsonPath = join5(projectDir, "core", "package.json");
1183
+ consola.info("Removed instrumentation.ts (incompatible with Cloudflare Workers).");
1184
+ const corePackageJsonPath = join5(projectDir, "package.json");
1176
1185
  if (existsSync2(corePackageJsonPath)) {
1177
1186
  const pkg = corePackageJsonSchema.parse(JSON.parse(readFileSync2(corePackageJsonPath, "utf-8")));
1178
1187
  if (pkg.dependencies && "@vercel/otel" in pkg.dependencies) {
1179
1188
  const { "@vercel/otel": _removedVercelOtel, ...preservedDeps } = pkg.dependencies;
1180
1189
  pkg.dependencies = preservedDeps;
1181
1190
  writeJson(corePackageJsonPath, sortPackageJsonFields(pkg));
1182
- consola.info("Dropped @vercel/otel from core/package.json.");
1191
+ consola.info("Dropped @vercel/otel from package.json.");
1183
1192
  }
1184
1193
  }
1185
1194
  };
@@ -1190,7 +1199,7 @@ var setupCommerceHosting = async ({
1190
1199
  accessToken
1191
1200
  }) => {
1192
1201
  await cleanupCloudflareIncompatibilities(projectDir);
1193
- const corePackageJsonPath = join5(projectDir, "core", "package.json");
1202
+ const corePackageJsonPath = join5(projectDir, "package.json");
1194
1203
  const pkg = corePackageJsonSchema.parse(JSON.parse(readFileSync2(corePackageJsonPath, "utf-8")));
1195
1204
  pkg.dependencies = {
1196
1205
  ...pkg.dependencies,
@@ -1203,8 +1212,7 @@ var setupCommerceHosting = async ({
1203
1212
  };
1204
1213
  if (storeHash) projectJson.storeHash = storeHash;
1205
1214
  if (accessToken) projectJson.accessToken = accessToken;
1206
- writeJson(join5(projectDir, "core", ".bigcommerce", "project.json"), projectJson);
1207
- symlinkRootEnvToCore(projectDir);
1215
+ writeJson(join5(projectDir, ".bigcommerce", "project.json"), projectJson);
1208
1216
  convertProxyToMiddleware(projectDir);
1209
1217
  };
1210
1218
  var NoLinkedProjectError = class extends Error {
@@ -1214,15 +1222,8 @@ var NoLinkedProjectError = class extends Error {
1214
1222
  }
1215
1223
  };
1216
1224
  async function promptForNewProjectName(api) {
1217
- const newProjectName = await consola.prompt("Enter a name for the new project:", {
1218
- type: "text"
1219
- });
1220
- const data = await createProject(
1221
- String(newProjectName),
1222
- api.storeHash,
1223
- api.accessToken,
1224
- api.apiHost
1225
- );
1225
+ const newProjectName = await input2({ message: "Enter a name for the new project:" });
1226
+ const data = await createProject(newProjectName, api.storeHash, api.accessToken, api.apiHost);
1226
1227
  consola.success(`Project "${data.name}" created successfully.`);
1227
1228
  return { uuid: data.uuid, name: data.name, deployment_hostnames: [] };
1228
1229
  }
@@ -1231,31 +1232,31 @@ async function selectOrCreateInfrastructureProject(api, linkedProjectUuid) {
1231
1232
  const existingProjects = await fetchProjects(api.storeHash, api.accessToken, api.apiHost);
1232
1233
  consola.success("Projects fetched.");
1233
1234
  if (existingProjects.length === 0) {
1234
- const shouldCreate = await consola.prompt(
1235
- "There are not any hosting projects that you can link to yet. Would you like to create one?",
1236
- { type: "confirm", initial: true }
1237
- );
1235
+ const shouldCreate = await confirm2({
1236
+ message: "There are not any hosting projects that you can link to yet. Would you like to create one?",
1237
+ default: true
1238
+ });
1238
1239
  if (!shouldCreate) {
1239
1240
  throw new NoLinkedProjectError();
1240
1241
  }
1241
1242
  return promptForNewProjectName(api);
1242
1243
  }
1243
- const promptOptions = [
1244
+ const choices = [
1244
1245
  ...existingProjects.map((p) => ({
1245
- label: p.uuid === linkedProjectUuid ? `${p.name} ${colorize2("green", "[linked]")}` : p.name,
1246
+ name: p.uuid === linkedProjectUuid ? `${p.name} ${colorize2("green", "[linked]")}` : p.name,
1246
1247
  value: p.uuid,
1247
- hint: p.uuid
1248
+ description: p.uuid
1248
1249
  })),
1249
1250
  {
1250
- label: "Create a new project",
1251
+ name: "Create a new project",
1251
1252
  value: "create",
1252
- hint: "Create a new hosting project for this Catalyst storefront."
1253
+ description: "Create a new hosting project for this Catalyst storefront."
1253
1254
  }
1254
1255
  ];
1255
- const selected = await consola.prompt(
1256
- "Select a project or create a new project (Press <enter> to select).",
1257
- { type: "select", options: promptOptions, cancel: "reject" }
1258
- );
1256
+ const selected = await select({
1257
+ message: "Select a project or create a new project (Press <enter> to select).",
1258
+ choices
1259
+ });
1259
1260
  if (selected === "create") {
1260
1261
  return promptForNewProjectName(api);
1261
1262
  }
@@ -1325,7 +1326,7 @@ async function promptForCommerceHostingProject(api, defaultName, autoUseDefaultN
1325
1326
  return selected;
1326
1327
  }
1327
1328
  async function promptAndCreateCommerceHostingProject(api, takenNames, defaultName) {
1328
- const projectName = await input({
1329
+ const projectName = await input2({
1329
1330
  message: "What would you like to name your Commerce Hosting project?",
1330
1331
  default: defaultName,
1331
1332
  validate: (value) => {
@@ -1439,16 +1440,14 @@ async function resolveChannel(options) {
1439
1440
  "No Catalyst channels found on this store. Create one with `catalyst create` and try again."
1440
1441
  );
1441
1442
  }
1442
- const selectedId = await consola.prompt("Select the channel to update.", {
1443
- type: "select",
1444
- options: catalystChannels.map((c) => ({
1445
- label: c.name,
1446
- value: String(c.id),
1447
- hint: `id: ${c.id}`
1448
- })),
1449
- cancel: "reject"
1443
+ const id = await select2({
1444
+ message: "Select the channel to update.",
1445
+ choices: catalystChannels.map((c) => ({
1446
+ name: c.name,
1447
+ value: c.id,
1448
+ description: `id: ${c.id}`
1449
+ }))
1450
1450
  });
1451
- const id = Number(selectedId);
1452
1451
  const matched = catalystChannels.find((c) => c.id === id);
1453
1452
  return { id, name: matched?.name };
1454
1453
  }
@@ -1465,12 +1464,11 @@ async function resolveHostname(project2, options) {
1465
1464
  ...project2.deployment_hostnames.filter((h) => h === options.preferHostname),
1466
1465
  ...project2.deployment_hostnames.filter((h) => h !== options.preferHostname)
1467
1466
  ] : project2.deployment_hostnames;
1468
- const selected = await consola.prompt("Select the hostname to point the channel at.", {
1469
- type: "select",
1470
- options: ordered.map((h) => ({ label: h, value: h })),
1471
- cancel: "reject"
1467
+ const selected = await select2({
1468
+ message: "Select the hostname to point the channel at.",
1469
+ choices: ordered.map((h) => ({ name: h, value: h }))
1472
1470
  });
1473
- return String(selected);
1471
+ return selected;
1474
1472
  }
1475
1473
  async function runChannelSiteUrlFlow(options) {
1476
1474
  const project2 = await resolveProject(options);
@@ -1488,6 +1486,38 @@ async function runChannelSiteUrlFlow(options) {
1488
1486
  consola.success(`Updated channel ${channelLabel} site URL to ${siteUrl}.`);
1489
1487
  }
1490
1488
 
1489
+ // src/cli/lib/env-config.ts
1490
+ var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
1491
+ var parseEnvAssignment = (input5) => {
1492
+ const separatorIndex = input5.indexOf("=");
1493
+ if (separatorIndex === -1) {
1494
+ throw new Error(`Invalid env var format: ${input5}. Expected format: KEY=VALUE`);
1495
+ }
1496
+ const key = input5.slice(0, separatorIndex).trim();
1497
+ const value = input5.slice(separatorIndex + 1).trim();
1498
+ if (!key || !value) {
1499
+ throw new Error(`Invalid env var format: ${input5}. Expected format: KEY=VALUE`);
1500
+ }
1501
+ if (!ENV_KEY_PATTERN.test(key)) {
1502
+ throw new Error(
1503
+ `Invalid env var name: ${key}. Names must start with a letter or underscore and contain only letters, numbers, and underscores.`
1504
+ );
1505
+ }
1506
+ return { key, value };
1507
+ };
1508
+ var getStoredEnv = (config2) => {
1509
+ return config2.get("env") ?? {};
1510
+ };
1511
+ var toDeploymentSecrets = (envVars) => {
1512
+ return Object.entries(envVars).map(([key, value]) => ({ type: "secret", key, value }));
1513
+ };
1514
+ var mergeDeploymentSecrets = (persisted, flagSecrets) => {
1515
+ const byKey = /* @__PURE__ */ new Map();
1516
+ persisted.forEach((secret) => byKey.set(secret.key, secret));
1517
+ flagSecrets.forEach((secret) => byKey.set(secret.key, secret));
1518
+ return Array.from(byKey.values());
1519
+ };
1520
+
1491
1521
  // src/cli/lib/resolve-credentials.ts
1492
1522
  function resolveCredentials(options, config2) {
1493
1523
  const storeHash = options.storeHash ?? config2.get("storeHash");
@@ -1504,6 +1534,31 @@ function resolveCredentials(options, config2) {
1504
1534
  }
1505
1535
 
1506
1536
  // src/cli/commands/channel.ts
1537
+ var parseChannelId = (value) => {
1538
+ const parsed = Number.parseInt(value, 10);
1539
+ if (Number.isNaN(parsed)) {
1540
+ throw new InvalidArgumentError(`"${value}" is not a valid channel ID (expected a number).`);
1541
+ }
1542
+ return parsed;
1543
+ };
1544
+ async function resolveCredentialsWithLogin(options, config2) {
1545
+ const storeHash = options.storeHash ?? config2.get("storeHash");
1546
+ const accessToken = options.accessToken ?? config2.get("accessToken");
1547
+ if (storeHash && accessToken) {
1548
+ return { storeHash, accessToken };
1549
+ }
1550
+ try {
1551
+ const credentials = await login(options.loginUrl, options.apiHost);
1552
+ config2.set("storeHash", credentials.storeHash);
1553
+ config2.set("accessToken", credentials.accessToken);
1554
+ return credentials;
1555
+ } catch (error) {
1556
+ if (error instanceof LoginAbortedError) {
1557
+ return null;
1558
+ }
1559
+ throw error;
1560
+ }
1561
+ }
1507
1562
  var update = new Command3("update").configureHelp({ showGlobalOptions: true }).description(
1508
1563
  "Update a BigCommerce channel's site URL to point at one of your project's deployment hostnames."
1509
1564
  ).addHelpText(
@@ -1550,150 +1605,186 @@ Examples:
1550
1605
  }
1551
1606
  process.exit(0);
1552
1607
  });
1553
- var channel = new Command3("channel").configureHelp({ showGlobalOptions: true }).description("Manage BigCommerce channels.").addCommand(update);
1608
+ var link = new Command3("link").configureHelp({ showGlobalOptions: true }).description(
1609
+ "Link this Catalyst project to a BigCommerce channel and write its credentials to .env.local."
1610
+ ).addHelpText(
1611
+ "after",
1612
+ `
1613
+ Examples:
1614
+ # Pick a channel interactively (logs you in if needed)
1615
+ $ catalyst channel link
1554
1616
 
1555
- // src/cli/commands/create.ts
1556
- import { Command as Command4, InvalidArgumentError, Option as Option5 } from "@commander-js/extra-typings";
1557
- import { input as input2, select as select2 } from "@inquirer/prompts";
1558
- import { execSync as execSync4 } from "child_process";
1559
- import { colorize as colorize3 } from "consola/utils";
1560
- import { pathExistsSync } from "fs-extra/esm";
1561
- import kebabCase from "lodash.kebabcase";
1562
- import { join as join9 } from "path";
1617
+ # Non-interactive
1618
+ $ catalyst channel link --store-hash <hash> --access-token <token> --channel-id 123
1563
1619
 
1564
- // src/cli/lib/build-workspace-packages.ts
1565
- import { execSync } from "child_process";
1566
- import { existsSync as existsSync3 } from "fs";
1567
- import { join as join6 } from "path";
1568
- import yoctoSpinner2 from "yocto-spinner";
1569
- var WORKSPACE_PACKAGES = ["packages/catalyst", "packages/client"];
1570
- var buildWorkspacePackages = (projectDir) => {
1571
- const hasCore = existsSync3(join6(projectDir, "core"));
1572
- const hasAllWorkspacePackages = WORKSPACE_PACKAGES.every(
1573
- (pkg) => existsSync3(join6(projectDir, pkg))
1574
- );
1575
- if (!hasCore || !hasAllWorkspacePackages) return;
1576
- WORKSPACE_PACKAGES.forEach((pkg) => {
1577
- const spinner = yoctoSpinner2().start(`Building ${pkg}...`);
1578
- try {
1579
- execSync("pnpm build", { cwd: join6(projectDir, pkg), stdio: "ignore" });
1580
- spinner.success(`Built ${pkg}.`);
1581
- } catch (error) {
1582
- const message = error instanceof Error ? error.message : "unknown error";
1583
- spinner.error(`Failed to build ${pkg}: ${message}`);
1584
- throw error;
1620
+ # Append extra environment variables to .env.local
1621
+ $ catalyst channel link --channel-id 123 --env MY_FLAG=1`
1622
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(loginUrlOption()).addOption(
1623
+ new Option4("--channel-id <id>", "Link this channel directly, skipping the picker.").argParser(
1624
+ parseChannelId
1625
+ )
1626
+ ).option(
1627
+ "--env <vars...>",
1628
+ "Arbitrary environment variables to set in .env.local. Format: KEY=VALUE (repeatable)."
1629
+ ).addOption(
1630
+ new Option4("--cli-api-origin <origin>", "Catalyst CLI API origin").default("https://cxm-prd.bigcommerceapp.com").hideHelp()
1631
+ ).action(async (options) => {
1632
+ const config2 = getProjectConfig();
1633
+ const credentials = await resolveCredentialsWithLogin(options, config2);
1634
+ if (!credentials) {
1635
+ consola.info(
1636
+ "Login aborted. Re-run `catalyst channel link` when you have your credentials ready."
1637
+ );
1638
+ process.exit(0);
1639
+ return;
1640
+ }
1641
+ const { storeHash, accessToken } = credentials;
1642
+ await getTelemetry().identify(storeHash);
1643
+ let channelId = options.channelId;
1644
+ let channelName;
1645
+ if (channelId === void 0) {
1646
+ const channels = await fetchAvailableChannels(storeHash, accessToken, options.apiHost);
1647
+ if (channels.length === 0) {
1648
+ consola.info(
1649
+ "No storefront channels found on this store. Create one with `catalyst create` and try again."
1650
+ );
1651
+ process.exit(0);
1652
+ return;
1585
1653
  }
1586
- });
1587
- };
1588
-
1589
- // src/cli/lib/clone-catalyst.ts
1590
- import { execSync as execSync3 } from "child_process";
1654
+ channelId = await select3({
1655
+ message: "Which channel would you like to link?",
1656
+ choices: sortChannelsByPlatform(channels).map((c) => ({
1657
+ name: c.name,
1658
+ value: c.id,
1659
+ description: channelPlatformLabel(c.platform)
1660
+ }))
1661
+ });
1662
+ channelName = channels.find((c) => c.id === channelId)?.name;
1663
+ }
1664
+ const initData = await getChannelInit(channelId, storeHash, accessToken, options.cliApiOrigin);
1665
+ const envVars = { ...initData.envVars };
1666
+ if (options.env) {
1667
+ options.env.forEach((entry) => {
1668
+ const { key, value } = parseEnvAssignment(entry);
1669
+ envVars[key] = value;
1670
+ });
1671
+ }
1672
+ outputFileSync(
1673
+ join6(process.cwd(), ".env.local"),
1674
+ `${Object.entries(envVars).map(([key, value]) => `${key}=${value}`).join("\n")}
1675
+ `
1676
+ );
1677
+ const label = channelName ? `"${channelName}" (${channelId})` : `${channelId}`;
1678
+ consola.success(
1679
+ `Linked to channel ${label} and wrote ${colorize3("cyanBright", ".env.local")}.`
1680
+ );
1681
+ consola.log(`Next steps:
1591
1682
 
1592
- // src/cli/lib/checkout-ref.ts
1593
- import { sync as spawnSync } from "cross-spawn";
1683
+ ${colorize3("yellow", "pnpm run dev")}`);
1684
+ process.exit(0);
1685
+ });
1686
+ var channel = new Command3("channel").configureHelp({ showGlobalOptions: true }).description("Manage BigCommerce channels.").addCommand(link).addCommand(update);
1594
1687
 
1595
- // src/cli/lib/is-exec-exception.ts
1596
- function isExecException(error) {
1597
- return typeof error === "object" && error !== null && "stdout" in error && "stderr" in error;
1598
- }
1688
+ // src/cli/commands/create.ts
1689
+ import { Command as Command4, InvalidArgumentError as InvalidArgumentError2, Option as Option5 } from "@commander-js/extra-typings";
1690
+ import { checkbox, input as input3, select as select4 } from "@inquirer/prompts";
1691
+ import { execSync as execSync2 } from "child_process";
1692
+ import { colorize as colorize4 } from "consola/utils";
1693
+ import { pathExistsSync } from "fs-extra/esm";
1694
+ import kebabCase from "lodash.kebabcase";
1695
+ import { join as join11 } from "path";
1599
1696
 
1600
- // src/cli/lib/checkout-ref.ts
1601
- function checkoutRef(repoDir, ref) {
1602
- try {
1603
- const spawn = spawnSync("git", ["checkout", ref, "--"], {
1604
- cwd: repoDir,
1605
- encoding: "utf8",
1606
- shell: false
1607
- });
1608
- const stderr = spawn.stderr.trim();
1609
- if (spawn.status !== 0 && stderr) {
1610
- throw new Error(stderr);
1611
- }
1612
- consola.success(`Checked out ref ${ref} successfully.`);
1613
- } catch (error) {
1614
- if (isExecException(error)) {
1615
- const stderr = error.stderr ? error.stderr.toString() : "";
1616
- if (stderr.includes(`fatal: reference is not a tree: ${ref}`) || stderr.includes(`fatal: ambiguous argument '${ref}'`) || stderr.includes(`unknown revision or path not in the working tree`)) {
1617
- consola.error(`Ref '${ref}' not found in the repository.`);
1618
- } else {
1619
- consola.error(`Error checking out ref '${ref}':`, stderr.trim());
1620
- }
1621
- } else if (error instanceof Error) {
1622
- consola.error(`Error checking out ref '${ref}':`, error.message);
1623
- } else {
1624
- consola.error(`Unknown error occurred while checking out ref '${ref}'.`);
1625
- }
1626
- consola.warn(`Falling back to the default branch.`);
1697
+ // src/cli/lib/detect-package-manager.ts
1698
+ var detectPackageManager = () => {
1699
+ const userAgent = process.env.npm_config_user_agent ?? "";
1700
+ const name = userAgent.split(" ")[0]?.split("/")[0];
1701
+ if (name === "pnpm" || name === "yarn" || name === "bun") {
1702
+ return name;
1627
1703
  }
1628
- }
1704
+ if (process.versions.bun) {
1705
+ return "bun";
1706
+ }
1707
+ return "npm";
1708
+ };
1629
1709
 
1630
- // src/cli/lib/has-github-ssh.ts
1631
- import { execSync as execSync2 } from "child_process";
1632
- function hasGitHubSSH() {
1710
+ // src/cli/lib/extract-catalyst.ts
1711
+ import { mkdirSync as mkdirSync2 } from "fs";
1712
+ import { Readable } from "stream";
1713
+ import { pipeline } from "stream/promises";
1714
+ import { x } from "tar";
1715
+ var SUBDIR = "core";
1716
+ var MAX_ATTEMPTS = 3;
1717
+ var codeloadUrl = (repository, ref) => `https://codeload.github.com/${repository}/tar.gz/${encodeURIComponent(ref)}`;
1718
+ var wait = (ms) => new Promise((resolve2) => {
1719
+ setTimeout(resolve2, ms);
1720
+ });
1721
+ var downloadAndExtract = async (repository, ref, projectDir) => {
1722
+ const url = codeloadUrl(repository, ref);
1723
+ const response = await fetch(url);
1724
+ if (!response.ok || !response.body) {
1725
+ throw new Error(`Failed to download ${url} (HTTP ${response.status})`);
1726
+ }
1727
+ await pipeline(
1728
+ // Node's global fetch types `body` as the DOM ReadableStream; it is the same
1729
+ // object Readable.fromWeb expects at runtime.
1730
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
1731
+ Readable.fromWeb(response.body),
1732
+ x({
1733
+ cwd: projectDir,
1734
+ strip: 2,
1735
+ filter: (path) => path.split("/")[1] === SUBDIR
1736
+ })
1737
+ );
1738
+ };
1739
+ var attemptDownload = async (repository, ref, projectDir, attempt = 1) => {
1633
1740
  try {
1634
- const output = execSync2("ssh -T git@github.com", {
1635
- encoding: "utf8",
1636
- stdio: "pipe"
1637
- }).toString();
1638
- return output.includes("successfully authenticated");
1741
+ await downloadAndExtract(repository, ref, projectDir);
1639
1742
  } catch (error) {
1640
- if (isExecException(error)) {
1641
- const stdout = error.stdout ? error.stdout.toString() : "";
1642
- const stderr = error.stderr ? error.stderr.toString() : "";
1643
- const combinedOutput = stdout + stderr;
1644
- if (combinedOutput.includes("successfully authenticated")) {
1645
- return true;
1646
- }
1743
+ const message = error instanceof Error ? error.message : "unknown error";
1744
+ if (attempt >= MAX_ATTEMPTS) {
1745
+ throw new Error(`Failed to download Catalyst after ${MAX_ATTEMPTS} attempts: ${message}`);
1647
1746
  }
1648
- return false;
1649
- }
1650
- }
1651
-
1652
- // src/cli/lib/reset-branch-to-ref.ts
1653
- import { sync as spawnSync2 } from "cross-spawn";
1654
- function resetBranchToRef(projectDir, ghRef) {
1655
- const spawn = spawnSync2("git", ["reset", "--hard", ghRef, "--"], {
1656
- cwd: projectDir,
1657
- encoding: "utf8",
1658
- shell: false
1659
- });
1660
- const stderr = spawn.stderr.trim();
1661
- if (spawn.status !== 0 && stderr) {
1662
- throw new Error(stderr);
1747
+ consola.warn(`Download attempt ${attempt} failed (${message}). Retrying...`);
1748
+ await wait(attempt * 1e3);
1749
+ return attemptDownload(repository, ref, projectDir, attempt + 1);
1663
1750
  }
1664
- }
1665
-
1666
- // src/cli/lib/clone-catalyst.ts
1667
- var cloneCatalyst = ({
1751
+ };
1752
+ var extractCatalyst = async ({
1668
1753
  repository,
1669
- projectName,
1670
- projectDir,
1671
- ghRef,
1672
- resetMain = false
1754
+ ref,
1755
+ projectDir
1673
1756
  }) => {
1674
- const useSSH = hasGitHubSSH();
1675
- consola.info(`Cloning ${repository} using ${useSSH ? "SSH" : "HTTPS"}...`);
1676
- const cloneCommand = `git clone ${useSSH ? `git@github.com:${repository}` : `https://github.com/${repository}`}.git${projectName ? ` ${projectName}` : ""}`;
1677
- execSync3(cloneCommand, { stdio: "inherit" });
1678
- execSync3("git remote rename origin upstream", { cwd: projectDir, stdio: "inherit" });
1679
- if (ghRef) {
1680
- if (resetMain) {
1681
- execSync3("git checkout -b main", { cwd: projectDir, stdio: "inherit" });
1682
- resetBranchToRef(projectDir, ghRef);
1683
- consola.success(`Reset main to ${ghRef} successfully.`);
1684
- return;
1685
- }
1686
- checkoutRef(projectDir, ghRef);
1757
+ consola.info(`Downloading ${repository}#${ref}...`);
1758
+ mkdirSync2(projectDir, { recursive: true });
1759
+ await attemptDownload(repository, ref, projectDir);
1760
+ };
1761
+
1762
+ // src/cli/lib/init-git-repo.ts
1763
+ import { execSync } from "child_process";
1764
+ import { rmSync } from "fs";
1765
+ import { join as join7 } from "path";
1766
+ var run = (command, cwd) => execSync(command, { cwd, stdio: "ignore" });
1767
+ var initGitRepo = (projectDir) => {
1768
+ try {
1769
+ run("git init", projectDir);
1770
+ run("git add -A", projectDir);
1771
+ run('git commit -m "Initial commit from Catalyst"', projectDir);
1772
+ consola.success("Initialized a git repository.");
1773
+ } catch {
1774
+ rmSync(join7(projectDir, ".git"), { recursive: true, force: true });
1775
+ consola.warn("Could not initialize a git repository. Skipping.");
1687
1776
  }
1688
1777
  };
1689
1778
 
1690
1779
  // src/cli/lib/install-dependencies.ts
1691
1780
  import { installDependencies as installDeps } from "nypm";
1692
- import yoctoSpinner3 from "yocto-spinner";
1693
- var installDependencies = async (projectDir) => {
1694
- const spinner = yoctoSpinner3().start("Installing dependencies. This could take a minute...");
1781
+ import yoctoSpinner2 from "yocto-spinner";
1782
+ var installDependencies = async (projectDir, packageManager = "pnpm") => {
1783
+ const spinner = yoctoSpinner2().start(
1784
+ `Installing dependencies with ${packageManager}. This could take a minute...`
1785
+ );
1695
1786
  try {
1696
- await installDeps({ cwd: projectDir, silent: true, packageManager: "pnpm" });
1787
+ await installDeps({ cwd: projectDir, silent: true, packageManager });
1697
1788
  spinner.success("Dependencies installed successfully.");
1698
1789
  } catch (error) {
1699
1790
  const message = error instanceof Error ? error.message : "unknown error";
@@ -1756,22 +1847,94 @@ var getAvailableLocales = async (storeHash, accessToken, apiHost) => {
1756
1847
  return AvailableLocalesSuccessSchema.parse(await response.json()).data.filter(({ id }) => allowedLocales.includes(id)).map(({ name, id }) => ({ name: `${name} (${id})`, value: id }));
1757
1848
  };
1758
1849
 
1759
- // src/cli/lib/setup-core-project.ts
1760
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1761
- import { dirname as dirname3, join as join7 } from "path";
1850
+ // src/cli/lib/rewrite-core-package.ts
1851
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1852
+ import { join as join8 } from "path";
1762
1853
  import { z as z9 } from "zod";
1854
+ var NATIVE_BUILD_ALLOWLIST = [
1855
+ "@parcel/watcher",
1856
+ "@swc/core",
1857
+ "@vercel/speed-insights",
1858
+ "esbuild",
1859
+ "msw",
1860
+ "protobufjs",
1861
+ "puppeteer",
1862
+ "sharp",
1863
+ "workerd"
1864
+ ];
1865
+ var DEP_FIELDS = [
1866
+ "dependencies",
1867
+ "devDependencies",
1868
+ "optionalDependencies",
1869
+ "peerDependencies"
1870
+ ];
1763
1871
  var corePackageJsonSchema2 = z9.looseObject({
1872
+ version: z9.string().optional(),
1873
+ private: z9.boolean().optional(),
1764
1874
  scripts: z9.record(z9.string(), z9.string()).optional(),
1765
- dependencies: z9.record(z9.string(), z9.string()).optional()
1875
+ dependencies: z9.record(z9.string(), z9.string()).optional(),
1876
+ devDependencies: z9.record(z9.string(), z9.string()).optional(),
1877
+ optionalDependencies: z9.record(z9.string(), z9.string()).optional(),
1878
+ peerDependencies: z9.record(z9.string(), z9.string()).optional(),
1879
+ pnpm: z9.looseObject({}).optional(),
1880
+ catalyst: z9.looseObject({}).optional()
1881
+ });
1882
+ var registryVersionSchema = z9.object({ version: z9.string() });
1883
+ var resolvePublishedVersion = async (pkgName) => {
1884
+ const response = await fetch(`https://registry.npmjs.org/${pkgName}/latest`);
1885
+ if (!response.ok) {
1886
+ throw new Error(
1887
+ `Could not resolve a published version for "${pkgName}" (HTTP ${response.status}). The extracted project still references it via the workspace protocol and cannot be installed.`
1888
+ );
1889
+ }
1890
+ const { version: version2 } = registryVersionSchema.parse(await response.json());
1891
+ return `^${version2}`;
1892
+ };
1893
+ var rewriteCorePackage = async (projectDir, ref) => {
1894
+ const packageJsonPath = join8(projectDir, "package.json");
1895
+ const pkg = corePackageJsonSchema2.parse(JSON.parse(readFileSync3(packageJsonPath, "utf-8")));
1896
+ const workspaceDeps = DEP_FIELDS.flatMap((field) => {
1897
+ const deps = pkg[field];
1898
+ if (!deps) return [];
1899
+ return Object.entries(deps).filter(([, range]) => range.startsWith("workspace:")).map(([name]) => ({ deps, name }));
1900
+ });
1901
+ await Promise.all(
1902
+ workspaceDeps.map(async ({ deps, name }) => {
1903
+ const resolved = await resolvePublishedVersion(name);
1904
+ deps[name] = resolved;
1905
+ consola.info(`Resolved ${name} to ${resolved}`);
1906
+ })
1907
+ );
1908
+ delete pkg.private;
1909
+ if (pkg.scripts) {
1910
+ pkg.scripts = Object.fromEntries(
1911
+ Object.entries(pkg.scripts).filter(([, command]) => !command.includes("turbo"))
1912
+ );
1913
+ }
1914
+ pkg.pnpm = { ...pkg.pnpm, onlyBuiltDependencies: NATIVE_BUILD_ALLOWLIST };
1915
+ pkg.trustedDependencies = NATIVE_BUILD_ALLOWLIST;
1916
+ pkg.catalyst = { ...pkg.catalyst, version: pkg.version, ref };
1917
+ writeFileSync2(packageJsonPath, `${JSON.stringify(sortPackageJsonFields(pkg), null, 2)}
1918
+ `);
1919
+ writeFileSync2(join8(projectDir, ".npmrc"), "legacy-peer-deps=true\n");
1920
+ };
1921
+
1922
+ // src/cli/lib/setup-core-project.ts
1923
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1924
+ import { dirname as dirname3, join as join9 } from "path";
1925
+ import { z as z10 } from "zod";
1926
+ var corePackageJsonSchema3 = z10.looseObject({
1927
+ scripts: z10.record(z10.string(), z10.string()).optional(),
1928
+ dependencies: z10.record(z10.string(), z10.string()).optional()
1766
1929
  });
1767
1930
  var writeJson2 = (path, value) => {
1768
- mkdirSync2(dirname3(path), { recursive: true });
1769
- writeFileSync2(path, `${JSON.stringify(value, null, 2)}
1931
+ mkdirSync3(dirname3(path), { recursive: true });
1932
+ writeFileSync3(path, `${JSON.stringify(value, null, 2)}
1770
1933
  `);
1771
1934
  };
1772
1935
  var setupCoreProject = (projectDir) => {
1773
- const corePackageJsonPath = join7(projectDir, "core", "package.json");
1774
- const pkg = corePackageJsonSchema2.parse(JSON.parse(readFileSync3(corePackageJsonPath, "utf-8")));
1936
+ const corePackageJsonPath = join9(projectDir, "package.json");
1937
+ const pkg = corePackageJsonSchema3.parse(JSON.parse(readFileSync4(corePackageJsonPath, "utf-8")));
1775
1938
  pkg.scripts = {
1776
1939
  ...pkg.scripts,
1777
1940
  build: "npm run generate && catalyst build",
@@ -1786,11 +1949,11 @@ var setupCoreProject = (projectDir) => {
1786
1949
  };
1787
1950
 
1788
1951
  // src/cli/lib/write-env.ts
1789
- import { outputFileSync } from "fs-extra/esm";
1790
- import { join as join8 } from "path";
1952
+ import { outputFileSync as outputFileSync2 } from "fs-extra/esm";
1953
+ import { join as join10 } from "path";
1791
1954
  var writeEnv = (projectDir, envVars) => {
1792
- outputFileSync(
1793
- join8(projectDir, "core", ".env.local"),
1955
+ outputFileSync2(
1956
+ join10(projectDir, ".env.local"),
1794
1957
  `${Object.entries(envVars).map(([key, value]) => `${key}=${value}`).join("\n")}
1795
1958
  `
1796
1959
  );
@@ -1801,41 +1964,41 @@ function getPlatformCheckCommand(command) {
1801
1964
  const isWindows = process.platform === "win32";
1802
1965
  return isWindows ? `where.exe ${command}` : `which ${command}`;
1803
1966
  }
1804
- function parseChannelId(value) {
1967
+ function parseChannelId2(value) {
1805
1968
  const parsed = parseInt(value, 10);
1806
1969
  if (Number.isNaN(parsed)) {
1807
- throw new InvalidArgumentError(`"${value}" is not a valid channel ID (expected a number).`);
1970
+ throw new InvalidArgumentError2(`"${value}" is not a valid channel ID (expected a number).`);
1808
1971
  }
1809
1972
  return parsed;
1810
1973
  }
1811
1974
  function parseEnvFlag(value, previous = {}) {
1812
1975
  const eqIdx = value.indexOf("=");
1813
1976
  if (eqIdx <= 0) {
1814
- throw new InvalidArgumentError(`Expected KEY=VALUE, got "${value}".`);
1977
+ throw new InvalidArgumentError2(`Expected KEY=VALUE, got "${value}".`);
1815
1978
  }
1816
1979
  const key = value.substring(0, eqIdx);
1817
1980
  const val = value.substring(eqIdx + 1);
1818
1981
  if (!val) {
1819
- throw new InvalidArgumentError(`Expected KEY=VALUE with non-empty value, got "${value}".`);
1982
+ throw new InvalidArgumentError2(`Expected KEY=VALUE with non-empty value, got "${value}".`);
1820
1983
  }
1821
1984
  return { ...previous, [key]: val };
1822
1985
  }
1823
1986
  async function handleChannelCreation(storeHash, accessToken, apiHost, cliApiOrigin) {
1824
- const newChannelName = await input2({
1987
+ const newChannelName = await input3({
1825
1988
  message: "What would you like to name your new channel?"
1826
1989
  });
1827
1990
  const availableLocales = await getAvailableLocales(storeHash, accessToken, apiHost);
1828
- const storefrontLocale = await select2({
1991
+ const storefrontLocale = await select4({
1829
1992
  message: "Which default language would you like to set for your channel?",
1830
1993
  default: "en",
1831
1994
  choices: availableLocales,
1832
1995
  theme: {
1833
1996
  style: {
1834
- help: () => colorize3("dim", "(Select locale from the list or start typing the name)")
1997
+ help: () => colorize4("dim", "(Select locale from the list or start typing the name)")
1835
1998
  }
1836
1999
  }
1837
2000
  });
1838
- const shouldAddAdditionalLocales = await select2({
2001
+ const shouldAddAdditionalLocales = await select4({
1839
2002
  message: "Would you like to add additional languages?",
1840
2003
  choices: [
1841
2004
  { name: "Yes", value: true },
@@ -1844,21 +2007,14 @@ async function handleChannelCreation(storeHash, accessToken, apiHost, cliApiOrig
1844
2007
  });
1845
2008
  let additionalLocales = [];
1846
2009
  if (shouldAddAdditionalLocales) {
1847
- const localeOptions = availableLocales.filter(({ value }) => value !== storefrontLocale).map(({ name, value }) => ({ label: name, value, hint: value }));
1848
- const pickLocales = async () => {
1849
- const selected = await consola.prompt(
1850
- "Which additional languages would you like to add to your channel?",
1851
- { type: "multiselect", options: localeOptions, cancel: "reject" }
1852
- );
1853
- if (selected.length > 4) {
1854
- consola.warn("You can only select up to 4 additional languages. Please try again.");
1855
- return pickLocales();
1856
- }
1857
- return selected;
1858
- };
1859
- additionalLocales = await pickLocales();
2010
+ const localeChoices = availableLocales.filter(({ value }) => value !== storefrontLocale).map(({ name, value }) => ({ name, value, description: value }));
2011
+ additionalLocales = await checkbox({
2012
+ message: "Which additional languages would you like to add to your channel?",
2013
+ choices: localeChoices,
2014
+ validate: (items) => items.length <= 4 || "You can only select up to 4 additional languages."
2015
+ });
1860
2016
  }
1861
- const shouldInstallSampleData = await select2({
2017
+ const shouldInstallSampleData = await select4({
1862
2018
  message: "Would you like to install sample data?",
1863
2019
  choices: [
1864
2020
  { name: "Yes", value: true },
@@ -1876,23 +2032,13 @@ async function handleChannelCreation(storeHash, accessToken, apiHost, cliApiOrig
1876
2032
  );
1877
2033
  }
1878
2034
  async function handleChannelSelection(storeHash, accessToken, apiHost) {
1879
- const channelSortOrder = ["catalyst", "next", "bigcommerce"];
1880
2035
  const channels = await fetchAvailableChannels(storeHash, accessToken, apiHost);
1881
- const existingChannel = await select2({
2036
+ const existingChannel = await select4({
1882
2037
  message: "Which channel would you like to use?",
1883
- choices: channels.sort((a, b) => {
1884
- const aIndex = channelSortOrder.indexOf(a.platform);
1885
- const bIndex = channelSortOrder.indexOf(b.platform);
1886
- if (aIndex === -1 && bIndex === -1) {
1887
- return 0;
1888
- }
1889
- if (aIndex === -1) return 1;
1890
- if (bIndex === -1) return -1;
1891
- return aIndex - bIndex;
1892
- }).map((ch) => ({
2038
+ choices: sortChannelsByPlatform(channels).map((ch) => ({
1893
2039
  name: ch.name,
1894
2040
  value: ch,
1895
- description: `Channel Platform: ${ch.platform === "bigcommerce" ? "Stencil" : ch.platform.charAt(0).toUpperCase() + ch.platform.slice(1)}`
2041
+ description: `Channel Platform: ${channelPlatformLabel(ch.platform)}`
1896
2042
  }))
1897
2043
  });
1898
2044
  return existingChannel.id;
@@ -1905,7 +2051,7 @@ async function setupProject(options) {
1905
2051
  }
1906
2052
  if (projectName) {
1907
2053
  projectName = kebabCase(projectName);
1908
- projectDir = join9(options.projectDir, projectName);
2054
+ projectDir = join11(options.projectDir, projectName);
1909
2055
  if (pathExistsSync(projectDir)) {
1910
2056
  consola.error(`${projectDir} already exists`);
1911
2057
  process.exit(1);
@@ -1915,13 +2061,13 @@ async function setupProject(options) {
1915
2061
  const validateProjectName = (i) => {
1916
2062
  const formatted = kebabCase(i);
1917
2063
  if (!formatted) return "Project name is required";
1918
- const targetDir = join9(options.projectDir, formatted);
2064
+ const targetDir = join11(options.projectDir, formatted);
1919
2065
  if (pathExistsSync(targetDir)) return `Destination '${targetDir}' already exists`;
1920
2066
  projectName = formatted;
1921
2067
  projectDir = targetDir;
1922
2068
  return true;
1923
2069
  };
1924
- await input2({
2070
+ await input3({
1925
2071
  message: "What do you want to name your project directory?",
1926
2072
  default: "my-catalyst-app",
1927
2073
  validate: validateProjectName
@@ -1933,19 +2079,11 @@ async function setupProject(options) {
1933
2079
  }
1934
2080
  function checkRequiredTools() {
1935
2081
  try {
1936
- execSync4(getPlatformCheckCommand("git"), { stdio: "ignore" });
2082
+ execSync2(getPlatformCheckCommand("git"), { stdio: "ignore" });
1937
2083
  } catch {
1938
2084
  consola.error("git is required to create a Catalyst project");
1939
2085
  process.exit(1);
1940
2086
  }
1941
- try {
1942
- execSync4(getPlatformCheckCommand("pnpm"), { stdio: "ignore" });
1943
- } catch {
1944
- consola.error(
1945
- "pnpm is required to create a Catalyst project. Enable it by running `corepack enable pnpm`."
1946
- );
1947
- process.exit(1);
1948
- }
1949
2087
  }
1950
2088
  var create = new Command4("create").configureHelp({ showGlobalOptions: true }).description("Scaffold and connect a Catalyst storefront to your BigCommerce store.").addHelpText(
1951
2089
  "after",
@@ -1959,11 +2097,11 @@ Examples:
1959
2097
 
1960
2098
  # Eagerly set up Commerce Hosting at create time
1961
2099
  $ catalyst create --project-name my-store --hosting commerce`
1962
- ).option("--project-name <name>", "Name of your Catalyst project").option("--project-dir <dir>", "Directory in which to create your project", process.cwd()).addOption(storeHashOption()).addOption(accessTokenOption()).option("--channel-id <id>", "BigCommerce channel ID", parseChannelId).option("--storefront-token <token>", "BigCommerce storefront token").option(
2100
+ ).option("--project-name <name>", "Name of your Catalyst project").option("--project-dir <dir>", "Directory in which to create your project", process.cwd()).addOption(storeHashOption()).addOption(accessTokenOption()).option("--channel-id <id>", "BigCommerce channel ID", parseChannelId2).option("--storefront-token <token>", "BigCommerce storefront token").option(
1963
2101
  "--gh-ref <ref>",
1964
- "Clone a specific ref from the source repository",
2102
+ "Extract a specific ref (tag, branch, or commit) from the source repository",
1965
2103
  "@bigcommerce/catalyst-core@latest"
1966
- ).option("--reset-main", "Reset the main branch to the gh-ref").option("--repository <repository>", "GitHub repository to clone from", "bigcommerce/catalyst").option(
2104
+ ).option("--repository <repository>", "GitHub repository to extract from", "bigcommerce/catalyst").option(
1967
2105
  "--env <vars...>",
1968
2106
  "Arbitrary environment variables to set in .env.local. Format: KEY=VALUE (repeatable).",
1969
2107
  parseEnvFlag
@@ -2034,7 +2172,7 @@ Examples:
2034
2172
  }
2035
2173
  let shouldCreateChannel;
2036
2174
  if (eligibility.eligible) {
2037
- shouldCreateChannel = await select2({
2175
+ shouldCreateChannel = await select4({
2038
2176
  message: "Would you like to create a new channel?",
2039
2177
  choices: [
2040
2178
  { name: "Yes", value: true },
@@ -2093,8 +2231,10 @@ Examples:
2093
2231
  );
2094
2232
  }
2095
2233
  consola.info(`Creating '${projectName}' at '${projectDir}'`);
2234
+ const packageManager = detectPackageManager();
2096
2235
  try {
2097
- cloneCatalyst({ repository, projectName, projectDir, ghRef, resetMain: options.resetMain });
2236
+ await extractCatalyst({ repository, ref: ghRef, projectDir });
2237
+ await rewriteCorePackage(projectDir, ghRef);
2098
2238
  setupCoreProject(projectDir);
2099
2239
  if (useCommerceHosting && commerceHostingProject && storeHash && accessToken) {
2100
2240
  await setupCommerceHosting({
@@ -2105,8 +2245,8 @@ Examples:
2105
2245
  });
2106
2246
  }
2107
2247
  writeEnv(projectDir, envVars);
2108
- await installDependencies(projectDir);
2109
- buildWorkspacePackages(projectDir);
2248
+ await installDependencies(projectDir, packageManager);
2249
+ initGitRepo(projectDir);
2110
2250
  } catch (error) {
2111
2251
  if (pathExistsSync(projectDir)) {
2112
2252
  consola.warn(
@@ -2116,26 +2256,28 @@ Examples:
2116
2256
  throw error;
2117
2257
  }
2118
2258
  consola.success(`Created '${projectName}' at '${projectDir}'`);
2119
- consola.info("Next steps:");
2120
- consola.info(colorize3("yellow", ` cd ${projectName}/core && pnpm run dev`));
2259
+ const steps = [`cd ${projectName} && ${packageManager} run dev`];
2121
2260
  if (useCommerceHosting) {
2122
- consola.info(
2123
- colorize3(
2124
- "yellow",
2125
- ` Run 'cd ${projectName}/core && pnpm run deploy' when ready to deploy to Commerce Hosting.`
2126
- )
2261
+ steps.push(
2262
+ `Run 'cd ${projectName} && ${packageManager} run deploy' when ready to deploy to Commerce Hosting.`
2127
2263
  );
2128
2264
  }
2265
+ consola.log(
2266
+ `Next steps:
2267
+
2268
+ ${steps.map((step) => ` ${colorize4("yellow", step)}`).join("\n")}`
2269
+ );
2129
2270
  });
2130
2271
 
2131
2272
  // src/cli/commands/deploy.ts
2273
+ import { confirm as confirm3 } from "@inquirer/prompts";
2132
2274
  import AdmZip from "adm-zip";
2133
2275
  import { Command as Command5, Option as Option6 } from "commander";
2134
- import { colorize as colorize4 } from "consola/utils";
2276
+ import { colorize as colorize5 } from "consola/utils";
2135
2277
  import { access, readdir, readFile } from "fs/promises";
2136
- import { dirname as dirname4, join as join10 } from "path";
2137
- import yoctoSpinner4 from "yocto-spinner";
2138
- import { z as z10 } from "zod";
2278
+ import { join as join12 } from "path";
2279
+ import yoctoSpinner3 from "yocto-spinner";
2280
+ import { z as z11 } from "zod";
2139
2281
 
2140
2282
  // src/cli/lib/deployment-errors.ts
2141
2283
  var ERROR_MESSAGES = {
@@ -2153,40 +2295,8 @@ function getDeploymentErrorMessage(code) {
2153
2295
  return ERROR_MESSAGES[code] ?? `Deployment failed with an unexpected error (code: ${code}). Please try again. If the issue persists, contact support.`;
2154
2296
  }
2155
2297
 
2156
- // src/cli/lib/env-config.ts
2157
- var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
2158
- var parseEnvAssignment = (input3) => {
2159
- const separatorIndex = input3.indexOf("=");
2160
- if (separatorIndex === -1) {
2161
- throw new Error(`Invalid env var format: ${input3}. Expected format: KEY=VALUE`);
2162
- }
2163
- const key = input3.slice(0, separatorIndex).trim();
2164
- const value = input3.slice(separatorIndex + 1).trim();
2165
- if (!key || !value) {
2166
- throw new Error(`Invalid env var format: ${input3}. Expected format: KEY=VALUE`);
2167
- }
2168
- if (!ENV_KEY_PATTERN.test(key)) {
2169
- throw new Error(
2170
- `Invalid env var name: ${key}. Names must start with a letter or underscore and contain only letters, numbers, and underscores.`
2171
- );
2172
- }
2173
- return { key, value };
2174
- };
2175
- var getStoredEnv = (config2) => {
2176
- return config2.get("env") ?? {};
2177
- };
2178
- var toDeploymentSecrets = (envVars) => {
2179
- return Object.entries(envVars).map(([key, value]) => ({ type: "secret", key, value }));
2180
- };
2181
- var mergeDeploymentSecrets = (persisted, flagSecrets) => {
2182
- const byKey = /* @__PURE__ */ new Map();
2183
- persisted.forEach((secret) => byKey.set(secret.key, secret));
2184
- flagSecrets.forEach((secret) => byKey.set(secret.key, secret));
2185
- return Array.from(byKey.values());
2186
- };
2187
-
2188
2298
  // src/cli/commands/deploy.ts
2189
- var stepsEnum = z10.enum([
2299
+ var stepsEnum = z11.enum([
2190
2300
  "initializing",
2191
2301
  "downloading",
2192
2302
  "unzipping",
@@ -2204,41 +2314,41 @@ var STEPS = {
2204
2314
  finalizing: "Finalizing...",
2205
2315
  complete: "Complete"
2206
2316
  };
2207
- var UploadSignatureSchema = z10.object({
2208
- data: z10.object({
2209
- upload_url: z10.url(),
2210
- upload_uuid: z10.string()
2317
+ var UploadSignatureSchema = z11.object({
2318
+ data: z11.object({
2319
+ upload_url: z11.url(),
2320
+ upload_uuid: z11.string()
2211
2321
  })
2212
2322
  });
2213
- var CreateDeploymentSchema = z10.object({
2214
- data: z10.object({
2215
- deployment_uuid: z10.uuid()
2323
+ var CreateDeploymentSchema = z11.object({
2324
+ data: z11.object({
2325
+ deployment_uuid: z11.uuid()
2216
2326
  })
2217
2327
  });
2218
- var ProjectSchema = z10.object({
2219
- data: z10.array(
2220
- z10.object({
2221
- uuid: z10.uuid(),
2222
- name: z10.string()
2328
+ var ProjectSchema = z11.object({
2329
+ data: z11.array(
2330
+ z11.object({
2331
+ uuid: z11.uuid(),
2332
+ name: z11.string()
2223
2333
  })
2224
2334
  )
2225
2335
  });
2226
- var DeploymentStatusSchema = z10.object({
2227
- deployment_uuid: z10.uuid(),
2228
- deployment_status: z10.enum(["queued", "in_progress", "failed", "completed"]),
2229
- event: z10.object({
2336
+ var DeploymentStatusSchema = z11.object({
2337
+ deployment_uuid: z11.uuid(),
2338
+ deployment_status: z11.enum(["queued", "in_progress", "failed", "completed"]),
2339
+ event: z11.object({
2230
2340
  step: stepsEnum,
2231
- progress: z10.number()
2341
+ progress: z11.number()
2232
2342
  }).nullable(),
2233
- deployment_hostnames: z10.array(z10.string()).optional(),
2234
- error: z10.object({
2235
- code: z10.number()
2343
+ deployment_hostnames: z11.array(z11.string()).optional(),
2344
+ error: z11.object({
2345
+ code: z11.number()
2236
2346
  }).optional()
2237
2347
  });
2238
2348
  var generateBundleZip = async () => {
2239
2349
  consola.info("Generating bundle...");
2240
- const bigcommerceDir = join10(process.cwd(), ".bigcommerce");
2241
- const distDir = join10(process.cwd(), ".bigcommerce", "dist");
2350
+ const bigcommerceDir = join12(process.cwd(), ".bigcommerce");
2351
+ const distDir = join12(process.cwd(), ".bigcommerce", "dist");
2242
2352
  try {
2243
2353
  await access(distDir);
2244
2354
  } catch {
@@ -2248,7 +2358,7 @@ var generateBundleZip = async () => {
2248
2358
  if (buildDirContents.length === 0) {
2249
2359
  throw new Error(`Dist directory is empty: ${distDir}`);
2250
2360
  }
2251
- const outputZip = join10(bigcommerceDir, "bundle.zip");
2361
+ const outputZip = join12(bigcommerceDir, "bundle.zip");
2252
2362
  const zip = new AdmZip();
2253
2363
  zip.addLocalFolder(distDir, "output");
2254
2364
  zip.writeZip(outputZip);
@@ -2280,7 +2390,7 @@ var generateUploadSignature = async (storeHash, accessToken, apiHost) => {
2280
2390
  };
2281
2391
  var uploadBundleZip = async (uploadUrl) => {
2282
2392
  consola.info("Uploading bundle...");
2283
- const zipPath = join10(process.cwd(), ".bigcommerce", "bundle.zip");
2393
+ const zipPath = join12(process.cwd(), ".bigcommerce", "bundle.zip");
2284
2394
  const fileBuffer = await readFile(zipPath);
2285
2395
  const response = await fetch(uploadUrl, {
2286
2396
  method: "PUT",
@@ -2335,7 +2445,7 @@ var createDeployment = async (projectUuid, uploadUuid, storeHash, accessToken, a
2335
2445
  };
2336
2446
  var getDeploymentStatus = async (deploymentUuid, storeHash, accessToken, apiHost) => {
2337
2447
  consola.info("Fetching deployment status...");
2338
- const spinner = yoctoSpinner4().start("Fetching...");
2448
+ const spinner = yoctoSpinner3().start("Fetching...");
2339
2449
  const response = await fetch(
2340
2450
  `https://${apiHost}/stores/${storeHash}/v3/infrastructure/deployments/${deploymentUuid}/events`,
2341
2451
  {
@@ -2391,7 +2501,7 @@ var getDeploymentStatus = async (deploymentUuid, storeHash, accessToken, apiHost
2391
2501
  spinner.success("Deployment completed successfully.");
2392
2502
  if (deploymentHostname) {
2393
2503
  consola.success(
2394
- `View your deployment at: ${colorize4("blue", `https://${deploymentHostname}`)}`
2504
+ `View your deployment at: ${colorize5("blue", `https://${deploymentHostname}`)}`
2395
2505
  );
2396
2506
  }
2397
2507
  return deploymentHostname;
@@ -2488,23 +2598,23 @@ Example:
2488
2598
  consola.success(`Linked project "${selected.name}".`);
2489
2599
  }
2490
2600
  if (!getProjectState().isTransformed) {
2491
- const shouldSetup = await consola.prompt(
2492
- "Your project is not yet set up for Commerce Hosting deployments. Would you like to run the Commerce Hosting setup now?",
2493
- { type: "confirm", initial: true }
2494
- );
2601
+ const shouldSetup = await confirm3({
2602
+ message: "Your project is not yet set up for Commerce Hosting deployments. Would you like to run the Commerce Hosting setup now?",
2603
+ default: true
2604
+ });
2495
2605
  if (!shouldSetup) {
2496
2606
  consola.info("When you're ready to deploy, re-run `catalyst deploy` to complete setup.");
2497
2607
  process.exit(0);
2498
2608
  }
2499
- const projectDir = dirname4(process.cwd());
2609
+ const projectDir = process.cwd();
2500
2610
  await setupCommerceHosting({ projectDir, projectUuid, storeHash, accessToken });
2501
2611
  consola.success("Commerce Hosting setup complete.");
2502
2612
  await installDependencies(projectDir);
2503
2613
  } else {
2504
- await cleanupCloudflareIncompatibilities(dirname4(process.cwd()));
2614
+ await cleanupCloudflareIncompatibilities(process.cwd());
2505
2615
  }
2506
2616
  if (options.prebuilt) {
2507
- const distDir = join10(process.cwd(), ".bigcommerce", "dist");
2617
+ const distDir = join12(process.cwd(), ".bigcommerce", "dist");
2508
2618
  try {
2509
2619
  await access(distDir);
2510
2620
  } catch {
@@ -2572,18 +2682,467 @@ Example:
2572
2682
  }
2573
2683
  });
2574
2684
 
2575
- // src/cli/commands/env.ts
2576
- import { Command as Command6 } from "commander";
2577
- var MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
2578
- var add = new Command6("add").configureHelp({ showGlobalOptions: true }).description(
2579
- "Add or update one or more deployment environment variables. Stored in .bigcommerce/project.json and sent as secrets on every `catalyst deploy`."
2580
- ).argument("<vars...>", "One or more environment variables in KEY=VALUE format.").addHelpText(
2581
- "after",
2582
- `
2583
- Example:
2584
- $ catalyst env add BIGCOMMERCE_STORE_HASH=abc123 BIGCOMMERCE_STOREFRONT_TOKEN=ey...`
2585
- ).action((vars) => {
2586
- const config2 = getProjectConfig();
2685
+ // src/cli/commands/domains.ts
2686
+ import { confirm as confirm4 } from "@inquirer/prompts";
2687
+ import { Command as Command6, Option as Option7 } from "commander";
2688
+ import { colorize as colorize7 } from "consola/utils";
2689
+
2690
+ // src/cli/lib/domains.ts
2691
+ import { z as z13 } from "zod";
2692
+
2693
+ // src/cli/lib/observability.ts
2694
+ import { colorize as colorize6 } from "consola/utils";
2695
+ import { z as z12 } from "zod";
2696
+ var LOG_LEVELS = ["debug", "info", "warn", "error"];
2697
+ var SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
2698
+ var logEntrySchema = z12.object({
2699
+ timestamp: z12.string().nullable().optional(),
2700
+ level: z12.string().optional(),
2701
+ messages: z12.array(z12.unknown()).optional(),
2702
+ is_exception: z12.boolean().optional(),
2703
+ exception_name: z12.string().optional(),
2704
+ request: z12.object({
2705
+ method: z12.string().optional(),
2706
+ url: z12.string().optional(),
2707
+ status_code: z12.number().optional()
2708
+ }).nullable().optional()
2709
+ });
2710
+ var queryLogsSchema = z12.object({
2711
+ data: z12.array(logEntrySchema)
2712
+ });
2713
+ var v3ErrorSchema = z12.object({
2714
+ title: z12.string().optional(),
2715
+ errors: z12.record(z12.string(), z12.string()).optional()
2716
+ });
2717
+ function formatV3Error(body) {
2718
+ const parsed = v3ErrorSchema.safeParse(body);
2719
+ if (!parsed.success) return void 0;
2720
+ const { title, errors } = parsed.data;
2721
+ const fieldErrors = errors && Object.keys(errors).length > 0 ? Object.entries(errors).map(([field, message]) => `${field}: ${message}`).join("; ") : void 0;
2722
+ if (title && fieldErrors) return `${title} (${fieldErrors})`;
2723
+ return title ?? fieldErrors;
2724
+ }
2725
+ function parseTimeInput(value) {
2726
+ const trimmed = value.trim();
2727
+ if (/^\d+$/.test(trimmed)) {
2728
+ return Number(trimmed) * 1e3;
2729
+ }
2730
+ const ms = Date.parse(trimmed);
2731
+ return Number.isNaN(ms) ? null : ms;
2732
+ }
2733
+ var DURATION_UNIT_MS = {
2734
+ s: 1e3,
2735
+ m: 60 * 1e3,
2736
+ h: 60 * 60 * 1e3,
2737
+ d: 24 * 60 * 60 * 1e3
2738
+ };
2739
+ function parseDuration(value) {
2740
+ const match = /^(\d+)([smhd])$/.exec(value.trim());
2741
+ if (!match) return null;
2742
+ return Number(match[1]) * DURATION_UNIT_MS[match[2]];
2743
+ }
2744
+ function resolveTimeWindow(opts, nowMs = Date.now()) {
2745
+ const { since } = opts;
2746
+ let { start: start2, end } = opts;
2747
+ let startMs;
2748
+ let endMs;
2749
+ if (end) {
2750
+ endMs = parseTimeInput(end);
2751
+ if (endMs === null) {
2752
+ throw new Error(
2753
+ `Invalid --end value "${end}". Provide an ISO-8601 timestamp or a Unix epoch (seconds).`
2754
+ );
2755
+ }
2756
+ } else {
2757
+ endMs = nowMs;
2758
+ end = new Date(endMs).toISOString();
2759
+ }
2760
+ if (since) {
2761
+ const durationMs = parseDuration(since);
2762
+ if (durationMs === null) {
2763
+ throw new Error(
2764
+ `Invalid --since value "${since}". Provide a duration like 30m, 6h, or 2d (units: s, m, h, d).`
2765
+ );
2766
+ }
2767
+ startMs = endMs - durationMs;
2768
+ start2 = new Date(startMs).toISOString();
2769
+ } else if (start2) {
2770
+ startMs = parseTimeInput(start2);
2771
+ if (startMs === null) {
2772
+ throw new Error(
2773
+ `Invalid --start value "${start2}". Provide an ISO-8601 timestamp or a Unix epoch (seconds).`
2774
+ );
2775
+ }
2776
+ } else {
2777
+ throw new Error("Provide a time window with --since <duration> or --start <time>.");
2778
+ }
2779
+ if (startMs > endMs) {
2780
+ throw new Error("Invalid time window: --start must be before or equal to --end.");
2781
+ }
2782
+ if (endMs - startMs > SEVEN_DAYS_MS) {
2783
+ throw new Error("Invalid time window: the range must not exceed 7 days.");
2784
+ }
2785
+ return { start: start2, end };
2786
+ }
2787
+ var LEVEL_COLORS = {
2788
+ debug: "gray",
2789
+ info: "green",
2790
+ warn: "yellow",
2791
+ error: "red"
2792
+ };
2793
+ var UNKNOWN_TIME = "unknown";
2794
+ function isRedundantExceptionName(name, message) {
2795
+ const parts = name.split("<*>").map((part) => part.trim()).filter((part) => part.length > 0);
2796
+ return parts.length > 0 && parts.every((part) => message.includes(part));
2797
+ }
2798
+ var stringifyMessage = (message) => typeof message === "string" ? message : JSON.stringify(message);
2799
+ function formatLogEntry(entry, format = "default") {
2800
+ const message = (entry.messages ?? []).map(stringifyMessage).join(" ").replace(/\s+/g, " ").trim();
2801
+ if (format === "short") return message;
2802
+ const level = entry.level ?? "unknown";
2803
+ const coloredLevel = colorize6(LEVEL_COLORS[level.toLowerCase()] ?? "white", level.toUpperCase());
2804
+ const exceptionStr = entry.is_exception && entry.exception_name && !isRedundantExceptionName(entry.exception_name, message) ? ` ${colorize6("red", `[${entry.exception_name}]`)}` : "";
2805
+ let requestStr = "";
2806
+ if (format === "request" && entry.request) {
2807
+ const parts = [];
2808
+ if (entry.request.method) parts.push(entry.request.method);
2809
+ if (entry.request.url) parts.push(entry.request.url);
2810
+ const status = entry.request.status_code ? ` (${entry.request.status_code})` : "";
2811
+ if (parts.length > 0) requestStr = ` ${parts.join(" ")}${status}`;
2812
+ }
2813
+ return `[${entry.timestamp ?? UNKNOWN_TIME}] [${coloredLevel}]${requestStr}${exceptionStr} ${message}`;
2814
+ }
2815
+ async function queryLogs(projectUuid, storeHash, accessToken, apiHost, params) {
2816
+ const search = new URLSearchParams();
2817
+ search.set("start", params.start);
2818
+ search.set("end", params.end);
2819
+ if (params.method) search.set("method", params.method);
2820
+ if (params.statusCode != null) search.set("status_code", String(params.statusCode));
2821
+ if (params.urlLike) search.set("url:like", params.urlLike);
2822
+ if (params.levelMin) search.set("level:min", params.levelMin);
2823
+ if (params.limit != null) search.set("limit", String(params.limit));
2824
+ const response = await fetch(
2825
+ `https://${apiHost}/stores/${storeHash}/v3/infrastructure/logs/${projectUuid}?${search.toString()}`,
2826
+ {
2827
+ method: "GET",
2828
+ headers: {
2829
+ "X-Auth-Token": accessToken,
2830
+ "X-Correlation-Id": getTelemetry().correlationId,
2831
+ Accept: "application/json"
2832
+ }
2833
+ }
2834
+ );
2835
+ assertAuthorized(response);
2836
+ if (response.status === 403) {
2837
+ throw new Error(
2838
+ "Infrastructure Logs API not enabled. If you are part of the beta, contact support@bigcommerce.com to enable it."
2839
+ );
2840
+ }
2841
+ if (response.status === 404) {
2842
+ throw new Error("Project not found. Check the project UUID.");
2843
+ }
2844
+ if (response.status === 400 || response.status === 422) {
2845
+ const body = await response.json().catch(() => null);
2846
+ throw new Error(formatV3Error(body) ?? "Invalid log query.");
2847
+ }
2848
+ if (!response.ok) {
2849
+ throw new Error(`Failed to fetch logs: ${response.status} ${response.statusText}`);
2850
+ }
2851
+ const res = await response.json();
2852
+ return queryLogsSchema.parse(res);
2853
+ }
2854
+
2855
+ // src/cli/lib/domains.ts
2856
+ var domainStatusSchema = z13.enum(["pending", "verified", "failed", "unknown"]);
2857
+ var domainSchema = z13.object({
2858
+ domain: z13.string(),
2859
+ project_uuid: z13.string(),
2860
+ verification_status: domainStatusSchema
2861
+ });
2862
+ var domainResponseSchema = z13.object({
2863
+ data: domainSchema
2864
+ });
2865
+ var domainListResponseSchema = z13.object({
2866
+ data: z13.array(domainSchema)
2867
+ });
2868
+ var DOMAINS_API_NOT_ENABLED = "Infrastructure Domains API not enabled. If you are part of the beta, contact support@bigcommerce.com to enable it.";
2869
+ function domainsUrl(storeHash, projectUuid, apiHost) {
2870
+ return `https://${apiHost}/stores/${storeHash}/v3/infrastructure/projects/${projectUuid}/domains`;
2871
+ }
2872
+ function domainUrl(storeHash, projectUuid, domain, apiHost) {
2873
+ return `${domainsUrl(storeHash, projectUuid, apiHost)}/${encodeURIComponent(domain)}`;
2874
+ }
2875
+ function authHeaders3(accessToken) {
2876
+ return {
2877
+ Accept: "application/json",
2878
+ "X-Auth-Token": accessToken,
2879
+ "X-Correlation-Id": getTelemetry().correlationId
2880
+ };
2881
+ }
2882
+ function formatResponseStatus(response, message) {
2883
+ return [response.status, response.statusText || message].filter(Boolean).join(" ");
2884
+ }
2885
+ async function getErrorMessage(response, action) {
2886
+ const body = await response.json().catch(() => null);
2887
+ const message = formatV3Error(body);
2888
+ if (response.status >= 500) {
2889
+ const status = formatResponseStatus(response, message);
2890
+ return `${action}: ${status}. This is a server-side response from the Domains API.`;
2891
+ }
2892
+ return message ?? `${action}: ${response.statusText}`;
2893
+ }
2894
+ async function assertDomainResponse(response, action) {
2895
+ assertAuthorized(response);
2896
+ if (response.status === 403) {
2897
+ throw new Error(DOMAINS_API_NOT_ENABLED);
2898
+ }
2899
+ if (!response.ok) {
2900
+ throw new Error(await getErrorMessage(response, action));
2901
+ }
2902
+ }
2903
+ async function createDomain(domain, projectUuid, storeHash, accessToken, apiHost) {
2904
+ const response = await fetch(domainsUrl(storeHash, projectUuid, apiHost), {
2905
+ method: "POST",
2906
+ headers: {
2907
+ ...authHeaders3(accessToken),
2908
+ "Content-Type": "application/json"
2909
+ },
2910
+ body: JSON.stringify({ domain })
2911
+ });
2912
+ await assertDomainResponse(response, "Failed to add domain");
2913
+ const result = await response.json();
2914
+ return domainResponseSchema.parse(result).data;
2915
+ }
2916
+ async function getDomain(domain, projectUuid, storeHash, accessToken, apiHost) {
2917
+ const response = await fetch(domainUrl(storeHash, projectUuid, domain, apiHost), {
2918
+ method: "GET",
2919
+ headers: authHeaders3(accessToken)
2920
+ });
2921
+ await assertDomainResponse(response, "Failed to fetch domain");
2922
+ const result = await response.json();
2923
+ return domainResponseSchema.parse(result).data;
2924
+ }
2925
+ async function listDomains(projectUuid, storeHash, accessToken, apiHost, filters = {}) {
2926
+ const search = new URLSearchParams();
2927
+ if (filters.domains && filters.domains.length > 0) {
2928
+ search.set("domain:in", filters.domains.join(","));
2929
+ }
2930
+ if (filters.verificationStatus) {
2931
+ search.set("verification_status", filters.verificationStatus);
2932
+ }
2933
+ const query2 = search.toString();
2934
+ const url = `${domainsUrl(storeHash, projectUuid, apiHost)}${query2 ? `?${query2}` : ""}`;
2935
+ const response = await fetch(url, {
2936
+ method: "GET",
2937
+ headers: authHeaders3(accessToken)
2938
+ });
2939
+ await assertDomainResponse(response, "Failed to fetch domains");
2940
+ const result = await response.json();
2941
+ return domainListResponseSchema.parse(result).data;
2942
+ }
2943
+ async function deleteDomain(domain, projectUuid, storeHash, accessToken, apiHost) {
2944
+ const response = await fetch(domainUrl(storeHash, projectUuid, domain, apiHost), {
2945
+ method: "DELETE",
2946
+ headers: authHeaders3(accessToken)
2947
+ });
2948
+ await assertDomainResponse(response, `Failed to remove domain: ${response.statusText}`);
2949
+ }
2950
+
2951
+ // src/cli/commands/domains.ts
2952
+ var WAIT_INTERVAL_MS = 5e3;
2953
+ var WAIT_TIMEOUT_MS = 5 * 60 * 1e3;
2954
+ var DOMAIN_STATUS_FILTERS = ["pending", "verified", "failed"];
2955
+ var STATUS_COLORS = {
2956
+ pending: "yellow",
2957
+ verified: "green",
2958
+ failed: "red",
2959
+ unknown: "gray"
2960
+ };
2961
+ var STATUS_LABELS = {
2962
+ pending: "pending",
2963
+ verified: "active",
2964
+ failed: "failed",
2965
+ unknown: "unknown"
2966
+ };
2967
+ var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
2968
+ function resolveDomainCommandContext(options) {
2969
+ const config2 = getProjectConfig();
2970
+ const { storeHash, accessToken } = resolveCredentials(options, config2);
2971
+ const projectUuid = resolveProjectUuid(options);
2972
+ return {
2973
+ projectUuid,
2974
+ storeHash,
2975
+ accessToken,
2976
+ apiHost: options.apiHost
2977
+ };
2978
+ }
2979
+ function formatDomainStatus(status) {
2980
+ return colorize7(STATUS_COLORS[status], STATUS_LABELS[status]);
2981
+ }
2982
+ function formatDomain(domain) {
2983
+ return `${domain.domain} ${formatDomainStatus(domain.verification_status)}`;
2984
+ }
2985
+ async function waitForDomainVerification({
2986
+ domain,
2987
+ projectUuid,
2988
+ storeHash,
2989
+ accessToken,
2990
+ apiHost,
2991
+ intervalMs = WAIT_INTERVAL_MS,
2992
+ timeoutMs = WAIT_TIMEOUT_MS
2993
+ }) {
2994
+ const startedAt = Date.now();
2995
+ let current = await getDomain(domain, projectUuid, storeHash, accessToken, apiHost);
2996
+ while (current.verification_status === "pending" && Date.now() - startedAt < timeoutMs) {
2997
+ await sleep(intervalMs);
2998
+ current = await getDomain(domain, projectUuid, storeHash, accessToken, apiHost);
2999
+ }
3000
+ return current;
3001
+ }
3002
+ var add = new Command6("add").configureHelp({ showGlobalOptions: true }).description("Add a custom domain to the current Native Hosting project.").argument("<domain>", "Custom domain to add.").addHelpText(
3003
+ "after",
3004
+ `
3005
+ Examples:
3006
+ $ catalyst domains add www.example.com
3007
+
3008
+ # Wait until the domain leaves pending verification
3009
+ $ catalyst domains add www.example.com --wait`
3010
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option("--wait", "Poll until domain verification completes or times out.").action(async (domain, options) => {
3011
+ const context = resolveDomainCommandContext(options);
3012
+ await getTelemetry().identify(context.storeHash);
3013
+ consola.start(`Adding domain ${domain}...`);
3014
+ let result = await createDomain(
3015
+ domain,
3016
+ context.projectUuid,
3017
+ context.storeHash,
3018
+ context.accessToken,
3019
+ context.apiHost
3020
+ );
3021
+ consola.success(`Domain ${result.domain} added.`);
3022
+ if (options.wait && result.verification_status === "pending") {
3023
+ consola.start(`Waiting for ${result.domain} to verify...`);
3024
+ result = await waitForDomainVerification({ domain: result.domain, ...context });
3025
+ }
3026
+ consola.log(formatDomain(result));
3027
+ process.exit(0);
3028
+ });
3029
+ var list = new Command6("list").configureHelp({ showGlobalOptions: true }).description("List custom domains for the current Native Hosting project.").addHelpText(
3030
+ "after",
3031
+ `
3032
+ Examples:
3033
+ $ catalyst domains list
3034
+
3035
+ # Show pending domains only
3036
+ $ catalyst domains list --status pending`
3037
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option("--domain <domain>", "Only show a specific domain.").addOption(
3038
+ new Option7("--status <status>", "Filter by verification status.").choices(
3039
+ DOMAIN_STATUS_FILTERS
3040
+ )
3041
+ ).action(async (options) => {
3042
+ const context = resolveDomainCommandContext(options);
3043
+ await getTelemetry().identify(context.storeHash);
3044
+ consola.start("Fetching domains...");
3045
+ const result = await listDomains(
3046
+ context.projectUuid,
3047
+ context.storeHash,
3048
+ context.accessToken,
3049
+ context.apiHost,
3050
+ {
3051
+ domains: options.domain ? [options.domain] : void 0,
3052
+ verificationStatus: options.status
3053
+ }
3054
+ );
3055
+ consola.success("Domains fetched.");
3056
+ if (result.length === 0) {
3057
+ consola.info("No custom domains found.");
3058
+ process.exit(0);
3059
+ return;
3060
+ }
3061
+ result.forEach((item) => consola.log(formatDomain(item)));
3062
+ process.exit(0);
3063
+ });
3064
+ var showStatus = new Command6("status").configureHelp({ showGlobalOptions: true }).description("Show the status of a custom domain on the current Native Hosting project.").argument("<domain>", "Custom domain to check.").addHelpText(
3065
+ "after",
3066
+ `
3067
+ Examples:
3068
+ $ catalyst domains status www.example.com
3069
+
3070
+ # Wait until the domain leaves pending verification
3071
+ $ catalyst domains status www.example.com --wait`
3072
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option("--wait", "Poll until domain verification completes or times out.").action(async (domain, options) => {
3073
+ const context = resolveDomainCommandContext(options);
3074
+ await getTelemetry().identify(context.storeHash);
3075
+ consola.start(`Fetching status for ${domain}...`);
3076
+ let result = await getDomain(
3077
+ domain,
3078
+ context.projectUuid,
3079
+ context.storeHash,
3080
+ context.accessToken,
3081
+ context.apiHost
3082
+ );
3083
+ consola.success("Domain status fetched.");
3084
+ if (options.wait && result.verification_status === "pending") {
3085
+ consola.start(`Waiting for ${result.domain} to verify...`);
3086
+ result = await waitForDomainVerification({ domain: result.domain, ...context });
3087
+ }
3088
+ consola.log(formatDomain(result));
3089
+ process.exit(0);
3090
+ });
3091
+ var remove = new Command6("remove").configureHelp({ showGlobalOptions: true }).description("Remove a custom domain from the current Native Hosting project.").argument("<domain>", "Custom domain to remove.").addHelpText(
3092
+ "after",
3093
+ `
3094
+ Examples:
3095
+ $ catalyst domains remove www.example.com
3096
+
3097
+ # Skip confirmation for an active domain
3098
+ $ catalyst domains remove www.example.com --force`
3099
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option("--force", "Skip the confirmation prompt before removing an active domain.").action(async (domain, options) => {
3100
+ const context = resolveDomainCommandContext(options);
3101
+ await getTelemetry().identify(context.storeHash);
3102
+ consola.start(`Fetching status for ${domain}...`);
3103
+ const current = await getDomain(
3104
+ domain,
3105
+ context.projectUuid,
3106
+ context.storeHash,
3107
+ context.accessToken,
3108
+ context.apiHost
3109
+ );
3110
+ if (current.verification_status === "verified" && !options.force) {
3111
+ const confirmed = await confirm4({
3112
+ message: `Remove active domain ${current.domain}? Traffic may stop routing to this project.`,
3113
+ default: false
3114
+ });
3115
+ if (!confirmed) {
3116
+ consola.info("Aborted. No domain was removed.");
3117
+ process.exit(0);
3118
+ return;
3119
+ }
3120
+ }
3121
+ consola.start(`Removing domain ${current.domain}...`);
3122
+ await deleteDomain(
3123
+ current.domain,
3124
+ context.projectUuid,
3125
+ context.storeHash,
3126
+ context.accessToken,
3127
+ context.apiHost
3128
+ );
3129
+ consola.success(`Domain ${current.domain} removed.`);
3130
+ process.exit(0);
3131
+ });
3132
+ var domains = new Command6("domains").configureHelp({ showGlobalOptions: true }).description("Manage custom domains for the current Native Hosting project.").addCommand(add).addCommand(list).addCommand(showStatus).addCommand(remove);
3133
+
3134
+ // src/cli/commands/env.ts
3135
+ import { Command as Command7 } from "commander";
3136
+ var MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
3137
+ var add2 = new Command7("add").configureHelp({ showGlobalOptions: true }).description(
3138
+ "Add or update one or more deployment environment variables. Stored in .bigcommerce/project.json and sent as secrets on every `catalyst deploy`."
3139
+ ).argument("<vars...>", "One or more environment variables in KEY=VALUE format.").addHelpText(
3140
+ "after",
3141
+ `
3142
+ Example:
3143
+ $ catalyst env add BIGCOMMERCE_STORE_HASH=abc123 BIGCOMMERCE_STOREFRONT_TOKEN=ey...`
3144
+ ).action((vars) => {
3145
+ const config2 = getProjectConfig();
2587
3146
  const stored = { ...getStoredEnv(config2) };
2588
3147
  const parsed = vars.map((entry) => parseEnvAssignment(entry));
2589
3148
  parsed.forEach(({ key, value }) => {
@@ -2597,7 +3156,7 @@ Example:
2597
3156
  keys.forEach((key) => consola.log(` ${key}=${MASK}`));
2598
3157
  process.exit(0);
2599
3158
  });
2600
- var remove = new Command6("remove").configureHelp({ showGlobalOptions: true }).description("Remove one or more stored deployment environment variables.").argument("<keys...>", "One or more environment variable names to remove.").addHelpText(
3159
+ var remove2 = new Command7("remove").configureHelp({ showGlobalOptions: true }).description("Remove one or more stored deployment environment variables.").argument("<keys...>", "One or more environment variable names to remove.").addHelpText(
2601
3160
  "after",
2602
3161
  `
2603
3162
  Example:
@@ -2623,7 +3182,7 @@ Example:
2623
3182
  }
2624
3183
  process.exit(0);
2625
3184
  });
2626
- var list = new Command6("list").configureHelp({ showGlobalOptions: true }).description("List stored deployment environment variables (values are masked).").addHelpText(
3185
+ var list2 = new Command7("list").configureHelp({ showGlobalOptions: true }).description("List stored deployment environment variables (values are masked).").addHelpText(
2627
3186
  "after",
2628
3187
  `
2629
3188
  Example:
@@ -2640,41 +3199,41 @@ Example:
2640
3199
  keys.forEach((key) => consola.log(`${key}=${MASK}`));
2641
3200
  process.exit(0);
2642
3201
  });
2643
- var env = new Command6("env").configureHelp({ showGlobalOptions: true }).description(
3202
+ var env = new Command7("env").configureHelp({ showGlobalOptions: true }).description(
2644
3203
  "Manage persistent deployment environment variables. These are sent as secrets on every `catalyst deploy`, so you no longer need to pass `--secret` each time."
2645
- ).addCommand(add).addCommand(remove).addCommand(list);
3204
+ ).addCommand(add2).addCommand(remove2).addCommand(list2);
2646
3205
 
2647
3206
  // src/cli/commands/logs.ts
2648
- import { Command as Command7, Option as Option7 } from "commander";
2649
- import { colorize as colorize5 } from "consola/utils";
2650
- import { z as z11 } from "zod";
3207
+ import { Command as Command8, InvalidArgumentError as InvalidArgumentError3, Option as Option8 } from "commander";
3208
+ import { colorize as colorize8 } from "consola/utils";
3209
+ import { z as z14 } from "zod";
2651
3210
  var telemetry = new Telemetry();
2652
3211
  var DEFAULT_CONNECTION_TTL_MS = 1 * 60 * 1e3;
2653
3212
  var MAX_RETRIES = 5;
2654
3213
  var isFatalStatusCode = (status) => status >= 400 && status < 500;
2655
- var LEVEL_COLORS = {
3214
+ var LEVEL_COLORS2 = {
2656
3215
  INFO: "green",
2657
3216
  WARN: "yellow",
2658
3217
  ERROR: "red",
2659
3218
  DEBUG: "gray"
2660
3219
  };
2661
- var LogEventSchema = z11.object({
2662
- uuid: z11.string(),
2663
- project_uuid: z11.string(),
2664
- request: z11.object({
2665
- method: z11.string(),
2666
- url: z11.string(),
2667
- status_code: z11.number()
3220
+ var LogEventSchema = z14.object({
3221
+ uuid: z14.string(),
3222
+ project_uuid: z14.string(),
3223
+ request: z14.object({
3224
+ method: z14.string(),
3225
+ url: z14.string(),
3226
+ status_code: z14.number()
2668
3227
  }),
2669
- logs: z11.array(
2670
- z11.object({
2671
- timestamp: z11.string(),
2672
- level: z11.string(),
2673
- messages: z11.array(z11.unknown())
3228
+ logs: z14.array(
3229
+ z14.object({
3230
+ timestamp: z14.string(),
3231
+ level: z14.string(),
3232
+ messages: z14.array(z14.unknown())
2674
3233
  })
2675
3234
  ),
2676
- exceptions: z11.array(z11.unknown()),
2677
- timestamp: z11.string()
3235
+ exceptions: z14.array(z14.unknown()),
3236
+ timestamp: z14.string()
2678
3237
  }).loose();
2679
3238
  var StreamError = class extends Error {
2680
3239
  constructor(message, fatal) {
@@ -2688,7 +3247,7 @@ var formatLogEvent = (event, format) => {
2688
3247
  logEntries.forEach((entry) => {
2689
3248
  const msg = formatMessages(entry.messages);
2690
3249
  const level = entry.level.toUpperCase();
2691
- const coloredLevel = colorize5(LEVEL_COLORS[level] ?? "white", level);
3250
+ const coloredLevel = colorize8(LEVEL_COLORS2[level] ?? "white", level);
2692
3251
  switch (format) {
2693
3252
  case "short":
2694
3253
  consola.log(msg);
@@ -2831,7 +3390,7 @@ var tailLogs = async (projectUuid, storeHash, accessToken, apiHost, format, conn
2831
3390
  }
2832
3391
  }
2833
3392
  };
2834
- var tail = new Command7("tail").configureHelp({ showGlobalOptions: true }).description("Tail live logs from your deployed application.").addHelpText(
3393
+ var tail = new Command8("tail").configureHelp({ showGlobalOptions: true }).description("Tail live logs from your deployed application.").addHelpText(
2835
3394
  "after",
2836
3395
  `
2837
3396
  Examples:
@@ -2843,7 +3402,7 @@ Examples:
2843
3402
  # Tail logs as raw JSON (useful for piping to other tools)
2844
3403
  $ catalyst logs tail --format json`
2845
3404
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).addOption(
2846
- new Option7("--format <format>", "Output format for log events.").choices(["json", "pretty", "default", "short", "request"]).default("default")
3405
+ new Option8("--format <format>", "Output format for log events.").choices(["json", "pretty", "default", "short", "request"]).default("default")
2847
3406
  ).action(async (options) => {
2848
3407
  try {
2849
3408
  const config2 = getProjectConfig();
@@ -2856,33 +3415,115 @@ Examples:
2856
3415
  process.exit(1);
2857
3416
  }
2858
3417
  });
2859
- var query = new Command7("query").configureHelp({ showGlobalOptions: true }).description("Query historical logs from your deployed application.").addHelpText(
3418
+ var parseIntInRange = (flag, min, max) => (value) => {
3419
+ const parsed = Number(value);
3420
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
3421
+ throw new InvalidArgumentError3(`${flag} must be an integer between ${min} and ${max}.`);
3422
+ }
3423
+ return parsed;
3424
+ };
3425
+ var query = new Command8("query").configureHelp({ showGlobalOptions: true }).description("Query historical logs from your deployed application.").addHelpText(
2860
3426
  "after",
2861
3427
  `
2862
- Example:
2863
- $ catalyst logs query`
2864
- ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).action((options) => {
2865
- const config2 = getProjectConfig();
2866
- resolveCredentials(options, config2);
2867
- consola.error("The query command is not yet implemented.");
2868
- process.exit(1);
3428
+ Specify a time window with \`--since\` (relative to now) or \`--start\`/\`--end\`
3429
+ (ISO-8601 timestamps or Unix epoch seconds, UTC). The window may not exceed
3430
+ 7 days. Entries print oldest-first; timestamps are UTC.
3431
+
3432
+ Examples:
3433
+ # Last hour of logs
3434
+ $ catalyst logs query --since 1h
3435
+
3436
+ # Everything from today (UTC)
3437
+ $ catalyst logs query --start 2026-06-11T00:00:00Z
3438
+
3439
+ # Errors only for a specific path
3440
+ $ catalyst logs query --since 24h --level-min error --url-like /cart
3441
+
3442
+ # Errors with request details (method, URL, status)
3443
+ $ catalyst logs query --since 1h --level-min error --format request
3444
+
3445
+ # Raw JSON (NDJSON) for piping to other tools
3446
+ $ catalyst logs query --since 2d --format json`
3447
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).addOption(
3448
+ new Option8(
3449
+ "--since <duration>",
3450
+ "Relative window ending at --end (default: now), e.g. 30m, 6h, 2d (units: s, m, h, d)."
3451
+ ).conflicts("start")
3452
+ ).option("--start <time>", "Window start: ISO-8601 timestamp or Unix epoch (seconds).").option(
3453
+ "--end <time>",
3454
+ "Window end: ISO-8601 timestamp or Unix epoch (seconds). Defaults to now. The window must not exceed 7 days."
3455
+ ).option("--method <method>", "Filter by request HTTP method (case-insensitive).").addOption(
3456
+ new Option8("--status-code <code>", "Filter by response status code (100-599).").argParser(
3457
+ parseIntInRange("--status-code", 100, 599)
3458
+ )
3459
+ ).option("--url-like <substring>", "Filter by URL substring (case-sensitive).").addOption(
3460
+ new Option8("--level-min <level>", "Minimum log level.").choices(Array.from(LOG_LEVELS))
3461
+ ).addOption(
3462
+ new Option8("--limit <n>", "Maximum number of entries to return (1-500).").argParser(parseIntInRange("--limit", 1, 500)).default(100)
3463
+ ).addOption(
3464
+ new Option8("--format <format>", "Output format for log entries.").choices(["json", "pretty", "default", "short", "request"]).default("default")
3465
+ ).action(async (options) => {
3466
+ try {
3467
+ const config2 = getProjectConfig();
3468
+ const { storeHash, accessToken } = resolveCredentials(options, config2);
3469
+ await telemetry.identify(storeHash);
3470
+ const projectUuid = resolveProjectUuid(options);
3471
+ const { start: start2, end } = resolveTimeWindow(options);
3472
+ const result = await queryLogs(projectUuid, storeHash, accessToken, options.apiHost, {
3473
+ start: start2,
3474
+ end,
3475
+ method: options.method,
3476
+ statusCode: options.statusCode,
3477
+ urlLike: options.urlLike,
3478
+ levelMin: options.levelMin,
3479
+ limit: options.limit
3480
+ });
3481
+ const entries = result.data;
3482
+ const ordered = [...entries].reverse();
3483
+ if (options.format === "json") {
3484
+ ordered.forEach((entry) => process.stdout.write(`${JSON.stringify(entry)}
3485
+ `));
3486
+ return;
3487
+ }
3488
+ if (options.format === "pretty") {
3489
+ ordered.forEach((entry) => consola.log(JSON.stringify(entry, null, 2)));
3490
+ return;
3491
+ }
3492
+ if (entries.length === 0) {
3493
+ consola.info("No log entries found for the given window and filters.");
3494
+ return;
3495
+ }
3496
+ const lineFormat = options.format;
3497
+ ordered.forEach((entry) => consola.log(formatLogEntry(entry, lineFormat)));
3498
+ consola.info(
3499
+ `${entries.length} ${entries.length === 1 ? "entry" : "entries"} shown (oldest first, times in UTC).`
3500
+ );
3501
+ if (entries.length === options.limit) {
3502
+ consola.info(
3503
+ `Limit of ${options.limit} reached; older entries may exist. Narrow the time window or raise --limit (max 500) to see more.`
3504
+ );
3505
+ }
3506
+ } catch (error) {
3507
+ consola.error(error instanceof Error ? error.message : error);
3508
+ process.exit(1);
3509
+ }
2869
3510
  });
2870
- var logs = new Command7("logs").configureHelp({ showGlobalOptions: true }).description("View logs from your deployed application.").addCommand(tail, { isDefault: true }).addCommand(query);
3511
+ var logs = new Command8("logs").configureHelp({ showGlobalOptions: true }).description("View logs from your deployed application.").addCommand(tail, { isDefault: true }).addCommand(query);
2871
3512
 
2872
3513
  // src/cli/commands/project.ts
2873
- import { Command as Command8 } from "commander";
2874
- import { colorize as colorize6 } from "consola/utils";
2875
- import { dirname as dirname5 } from "path";
3514
+ import { confirm as confirm5, input as input4, select as select5 } from "@inquirer/prompts";
3515
+ import { Command as Command9 } from "commander";
3516
+ import { colorize as colorize9 } from "consola/utils";
2876
3517
  async function offerCommerceHostingSetup(projectUuid, credentials) {
2877
- const projectDir = dirname5(process.cwd());
3518
+ const projectDir = process.cwd();
2878
3519
  if (getProjectState().isTransformed) {
2879
3520
  await cleanupCloudflareIncompatibilities(projectDir);
2880
3521
  return;
2881
3522
  }
2882
- const shouldSetup = await consola.prompt(
2883
- "Your project has been linked, but is not fully set up for Commerce Hosting deployments yet. Would you like to run the setup now?",
2884
- { type: "confirm", initial: true }
2885
- );
3523
+ const shouldSetup = await confirm5({
3524
+ 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?",
3525
+ default: true
3526
+ });
2886
3527
  if (!shouldSetup) return;
2887
3528
  await setupCommerceHosting({
2888
3529
  projectDir,
@@ -2893,7 +3534,7 @@ async function offerCommerceHostingSetup(projectUuid, credentials) {
2893
3534
  consola.success("Commerce Hosting setup complete.");
2894
3535
  await installDependencies(projectDir);
2895
3536
  }
2896
- var list2 = new Command8("list").configureHelp({ showGlobalOptions: true }).description("List BigCommerce infrastructure projects for your store.").addHelpText(
3537
+ var list3 = new Command9("list").configureHelp({ showGlobalOptions: true }).description("List BigCommerce infrastructure projects for your store.").addHelpText(
2897
3538
  "after",
2898
3539
  `
2899
3540
  Example:
@@ -2911,21 +3552,17 @@ Example:
2911
3552
  return;
2912
3553
  }
2913
3554
  const linkedProjectUuid = config2.get("projectUuid");
2914
- projects.forEach((p) => {
2915
- const marker = p.uuid === linkedProjectUuid ? ` ${colorize6("green", "[linked]")}` : "";
2916
- consola.log(`${p.name} (${p.uuid})${marker}`);
2917
- if (p.deployment_hostnames.length === 0) {
2918
- consola.log(" (not deployed)");
2919
- } else {
2920
- p.deployment_hostnames.forEach((hostname) => {
2921
- consola.log(` ${colorize6("blue", `https://${hostname}`)}`);
2922
- });
2923
- }
2924
- consola.log("");
3555
+ const projectList = projects.map((p) => {
3556
+ const marker = p.uuid === linkedProjectUuid ? ` ${colorize9("green", "[linked]")}` : "";
3557
+ const hostnames = p.deployment_hostnames.length === 0 ? [" (not deployed)"] : p.deployment_hostnames.map(
3558
+ (hostname) => ` ${colorize9("blue", `https://${hostname}`)}`
3559
+ );
3560
+ return [`${p.name} (${p.uuid})${marker}`, ...hostnames].join("\n");
2925
3561
  });
3562
+ consola.log(projectList.join("\n\n"));
2926
3563
  process.exit(0);
2927
3564
  });
2928
- var create2 = new Command8("create").configureHelp({ showGlobalOptions: true }).description(
3565
+ var create2 = new Command9("create").configureHelp({ showGlobalOptions: true }).description(
2929
3566
  "Create a new BigCommerce infrastructure project and link it to your local Catalyst project. Walks you through logging in if you are not authenticated yet."
2930
3567
  ).addHelpText(
2931
3568
  "after",
@@ -2959,9 +3596,7 @@ Example:
2959
3596
  }
2960
3597
  }
2961
3598
  await getTelemetry().identify(storeHash);
2962
- const newProjectName = await consola.prompt("Enter a name for the new project:", {
2963
- type: "text"
2964
- });
3599
+ const newProjectName = await input4({ message: "Enter a name for the new project:" });
2965
3600
  const data = await createProject(newProjectName, storeHash, accessToken, options.apiHost);
2966
3601
  consola.success(`Project "${data.name}" created successfully.`);
2967
3602
  consola.start("Writing project UUID to .bigcommerce/project.json...");
@@ -2971,7 +3606,7 @@ Example:
2971
3606
  consola.success("Project UUID written to .bigcommerce/project.json.");
2972
3607
  process.exit(0);
2973
3608
  });
2974
- var link = new Command8("link").configureHelp({ showGlobalOptions: true }).description(
3609
+ var link2 = new Command9("link").configureHelp({ showGlobalOptions: true }).description(
2975
3610
  "Link your local Catalyst project to a BigCommerce infrastructure project. You can provide a project UUID directly, or fetch and select from available projects using your store credentials."
2976
3611
  ).addHelpText(
2977
3612
  "after",
@@ -3021,7 +3656,7 @@ Examples:
3021
3656
  await offerCommerceHostingSetup(selected.uuid, { storeHash, accessToken });
3022
3657
  process.exit(0);
3023
3658
  });
3024
- var del = new Command8("delete").configureHelp({ showGlobalOptions: true }).description(
3659
+ var del = new Command9("delete").configureHelp({ showGlobalOptions: true }).description(
3025
3660
  "Permanently delete a BigCommerce infrastructure project. This action is irreversible."
3026
3661
  ).addHelpText(
3027
3662
  "after",
@@ -3051,21 +3686,17 @@ Examples:
3051
3686
  return;
3052
3687
  }
3053
3688
  const linkedProjectUuid = config2.get("projectUuid");
3054
- const selected = await consola.prompt(
3055
- "Select a project to delete (Press <enter> to select).",
3056
- {
3057
- type: "select",
3058
- options: [
3059
- ...projects.map((p) => ({
3060
- label: p.uuid === linkedProjectUuid ? `${p.name} ${colorize6("green", "[linked]")}` : p.name,
3061
- value: p.uuid,
3062
- hint: p.uuid
3063
- })),
3064
- { label: "Cancel", value: "cancel", hint: "Exit without deleting any project." }
3065
- ],
3066
- cancel: "reject"
3067
- }
3068
- );
3689
+ const selected = await select5({
3690
+ message: "Select a project to delete (Press <enter> to select).",
3691
+ choices: [
3692
+ ...projects.map((p) => ({
3693
+ name: p.uuid === linkedProjectUuid ? `${p.name} ${colorize9("green", "[linked]")}` : p.name,
3694
+ value: p.uuid,
3695
+ description: p.uuid
3696
+ })),
3697
+ { name: "Cancel", value: "cancel", description: "Exit without deleting any project." }
3698
+ ]
3699
+ });
3069
3700
  if (selected === "cancel") {
3070
3701
  consola.info("Aborted. No project was deleted.");
3071
3702
  process.exit(0);
@@ -3080,10 +3711,10 @@ Examples:
3080
3711
  }
3081
3712
  if (!options.force) {
3082
3713
  const label = targetName ? `"${targetName}" (${targetUuid})` : targetUuid;
3083
- const confirmed = await consola.prompt(
3084
- `Are you sure you want to delete project ${label}? This action is irreversible and will permanently destroy the project and all of its data.`,
3085
- { type: "confirm", initial: false }
3086
- );
3714
+ const confirmed = await confirm5({
3715
+ 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.`,
3716
+ default: false
3717
+ });
3087
3718
  if (!confirmed) {
3088
3719
  consola.info("Aborted. No project was deleted.");
3089
3720
  process.exit(0);
@@ -3099,14 +3730,14 @@ Examples:
3099
3730
  }
3100
3731
  process.exit(0);
3101
3732
  });
3102
- var project = new Command8("project").configureHelp({ showGlobalOptions: true }).description("Manage your BigCommerce infrastructure project.").addCommand(create2).addCommand(list2).addCommand(link).addCommand(del);
3733
+ var project = new Command9("project").configureHelp({ showGlobalOptions: true }).description("Manage your BigCommerce infrastructure project.").addCommand(create2).addCommand(list3).addCommand(link2).addCommand(del);
3103
3734
 
3104
3735
  // src/cli/commands/start.ts
3105
- import { Command as Command9 } from "commander";
3736
+ import { Command as Command10 } from "commander";
3106
3737
  import { execa as execa2 } from "execa";
3107
- import { existsSync as existsSync4, lstatSync as lstatSync2, symlinkSync as symlinkSync2 } from "fs";
3108
- import { join as join11, relative } from "path";
3109
- var start = new Command9("start").configureHelp({ showGlobalOptions: true }).description(
3738
+ import { existsSync as existsSync3, lstatSync, symlinkSync } from "fs";
3739
+ import { join as join13, relative } from "path";
3740
+ var start = new Command10("start").configureHelp({ showGlobalOptions: true }).description(
3110
3741
  "Start a local preview of your Catalyst storefront using the OpenNext Cloudflare adapter."
3111
3742
  ).addHelpText(
3112
3743
  "after",
@@ -3123,14 +3754,14 @@ Example:
3123
3754
  });
3124
3755
  return;
3125
3756
  }
3126
- const envLocal = join11(process.cwd(), ".env.local");
3127
- const devVars = join11(process.cwd(), ".bigcommerce", ".dev.vars");
3128
- if (existsSync4(envLocal)) {
3129
- if (!existsSync4(devVars)) {
3130
- const target = relative(join11(process.cwd(), ".bigcommerce"), envLocal);
3131
- symlinkSync2(target, devVars);
3757
+ const envLocal = join13(process.cwd(), ".env.local");
3758
+ const devVars = join13(process.cwd(), ".bigcommerce", ".dev.vars");
3759
+ if (existsSync3(envLocal)) {
3760
+ if (!existsSync3(devVars)) {
3761
+ const target = relative(join13(process.cwd(), ".bigcommerce"), envLocal);
3762
+ symlinkSync(target, devVars);
3132
3763
  consola.success("Symlinked .bigcommerce/.dev.vars \u2192 .env.local");
3133
- } else if (!lstatSync2(devVars).isSymbolicLink()) {
3764
+ } else if (!lstatSync(devVars).isSymbolicLink()) {
3134
3765
  consola.warn(
3135
3766
  ".bigcommerce/.dev.vars already exists and is not a symlink. Wrangler will use it instead of .env.local."
3136
3767
  );
@@ -3147,7 +3778,7 @@ Example:
3147
3778
  "opennextjs-cloudflare",
3148
3779
  "preview",
3149
3780
  "--config",
3150
- join11(".bigcommerce", "wrangler.jsonc")
3781
+ join13(".bigcommerce", "wrangler.jsonc")
3151
3782
  ],
3152
3783
  {
3153
3784
  stdio: "inherit",
@@ -3157,11 +3788,11 @@ Example:
3157
3788
  });
3158
3789
 
3159
3790
  // src/cli/commands/telemetry.ts
3160
- import { Argument, Command as Command10, Option as Option8 } from "commander";
3161
- import { colorize as colorize7 } from "consola/utils";
3791
+ import { Argument, Command as Command11, Option as Option9 } from "commander";
3792
+ import { colorize as colorize10 } from "consola/utils";
3162
3793
  var telemetryService = getTelemetry();
3163
3794
  var isEnabled = telemetryService.isEnabled();
3164
- var telemetry2 = new Command10("telemetry").configureHelp({ showGlobalOptions: true }).description(
3795
+ var telemetry2 = new Command11("telemetry").configureHelp({ showGlobalOptions: true }).description(
3165
3796
  "View or change CLI telemetry collection status. Enabling telemetry helps BigCommerce support diagnose and troubleshoot errors you encounter when using the CLI."
3166
3797
  ).addHelpText(
3167
3798
  "after",
@@ -3175,7 +3806,7 @@ Examples:
3175
3806
 
3176
3807
  # Disable telemetry collection
3177
3808
  $ catalyst telemetry --disable`
3178
- ).addArgument(new Argument("[arg]").choices(["disable", "enable", "status"])).addOption(new Option8("--enable", `Enables CLI telemetry collection.`).conflicts("disable")).option("--disable", `Disables CLI telemetry collection.`).action((arg, options) => {
3809
+ ).addArgument(new Argument("[arg]").choices(["disable", "enable", "status"])).addOption(new Option9("--enable", `Enables CLI telemetry collection.`).conflicts("disable")).option("--disable", `Disables CLI telemetry collection.`).action((arg, options) => {
3179
3810
  if (options.enable || arg === "enable") {
3180
3811
  telemetryService.setEnabled(true);
3181
3812
  isEnabled = true;
@@ -3192,7 +3823,7 @@ Examples:
3192
3823
  consola.info("Catalyst CLI Telemetry\n");
3193
3824
  }
3194
3825
  consola.info(
3195
- `Status: ${colorize7("bold", isEnabled ? colorize7("green", "Enabled") : colorize7("red", "Disabled"))}`
3826
+ `Status: ${colorize10("bold", isEnabled ? colorize10("green", "Enabled") : colorize10("red", "Disabled"))}`
3196
3827
  );
3197
3828
  if (!isEnabled) {
3198
3829
  consola.info(
@@ -3201,9 +3832,649 @@ Examples:
3201
3832
  }
3202
3833
  });
3203
3834
 
3835
+ // src/cli/commands/upgrade.ts
3836
+ import { Command as Command12, Option as Option10 } from "@commander-js/extra-typings";
3837
+ import { confirm as confirm6 } from "@inquirer/prompts";
3838
+ import { execa as execa3 } from "execa";
3839
+ import {
3840
+ access as access2,
3841
+ copyFile as copyFile2,
3842
+ cp as cp2,
3843
+ mkdir,
3844
+ mkdtemp,
3845
+ readdir as readdir2,
3846
+ readFile as readFile2,
3847
+ rename,
3848
+ rm as rm2,
3849
+ writeFile as writeFile2
3850
+ } from "fs/promises";
3851
+ import { homedir, tmpdir } from "os";
3852
+ import { dirname as dirname4, join as join14, relative as relative2 } from "path";
3853
+ import yoctoSpinner4 from "yocto-spinner";
3854
+ import { z as z15 } from "zod";
3855
+ var CorePackageJson = z15.object({
3856
+ name: z15.string().optional(),
3857
+ version: z15.string(),
3858
+ catalyst: z15.object({ version: z15.string(), ref: z15.string() }).optional()
3859
+ });
3860
+ var DEFAULT_REPOSITORY = "bigcommerce/catalyst";
3861
+ var MOVING_TAGS = /* @__PURE__ */ new Set(["latest", "canary", "alpha"]);
3862
+ var KNOWN_FAMILIES = /* @__PURE__ */ new Set([
3863
+ "@bigcommerce/catalyst-core",
3864
+ "@bigcommerce/catalyst-makeswift",
3865
+ "@bigcommerce/catalyst-b2b-makeswift",
3866
+ "@bigcommerce/catalyst-b2b"
3867
+ ]);
3868
+ var CACHE_DIR = join14(homedir(), ".cache", "catalyst-cli", "cores");
3869
+ var pathExists = (p) => access2(p).then(() => true).catch(() => false);
3870
+ async function listFiles(dir, prefix = "") {
3871
+ const entries = await readdir2(dir, { withFileTypes: true }).catch(() => []);
3872
+ const nested = await Promise.all(
3873
+ entries.map((entry) => {
3874
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
3875
+ return entry.isDirectory() ? listFiles(join14(dir, entry.name), rel) : Promise.resolve([rel]);
3876
+ })
3877
+ );
3878
+ return nested.flat();
3879
+ }
3880
+ async function filesEqual(a, b) {
3881
+ const [ba, bb] = await Promise.all([
3882
+ readFile2(a).catch(() => null),
3883
+ readFile2(b).catch(() => null)
3884
+ ]);
3885
+ return ba !== null && bb !== null && ba.equals(bb);
3886
+ }
3887
+ async function computeBaseSimilarity(baseDir, destDir) {
3888
+ const baseFiles = await listFiles(baseDir);
3889
+ if (baseFiles.length === 0) return 0;
3890
+ const matches = await Promise.all(
3891
+ baseFiles.map((rel) => filesEqual(join14(baseDir, rel), join14(destDir, rel)))
3892
+ );
3893
+ return matches.filter(Boolean).length / baseFiles.length;
3894
+ }
3895
+ async function isBinary(p) {
3896
+ const buf = await readFile2(p).catch(() => null);
3897
+ if (!buf) return false;
3898
+ return buf.subarray(0, 8e3).includes(0);
3899
+ }
3900
+ async function copyInto(src, dest) {
3901
+ await mkdir(dirname4(dest), { recursive: true });
3902
+ await copyFile2(src, dest);
3903
+ }
3904
+ function parseRef(ref) {
3905
+ const lastAt = ref.lastIndexOf("@");
3906
+ if (lastAt <= 0) throw new Error(`Cannot parse ref "${ref}" \u2014 expected <package>@<version>.`);
3907
+ return { packageName: ref.slice(0, lastAt), version: ref.slice(lastAt + 1) };
3908
+ }
3909
+ async function readResolvedVersion(coreDir) {
3910
+ const raw = await readFile2(join14(coreDir, "package.json"), "utf-8");
3911
+ const pkg = CorePackageJson.parse(JSON.parse(raw));
3912
+ return pkg.catalyst?.version ?? pkg.version;
3913
+ }
3914
+ async function mergeFile(oursPath, basePath, theirsPath) {
3915
+ const merged = await execa3(
3916
+ "git",
3917
+ [
3918
+ "merge-file",
3919
+ "-p",
3920
+ "-L",
3921
+ "ours",
3922
+ "-L",
3923
+ "base",
3924
+ "-L",
3925
+ "theirs",
3926
+ oursPath,
3927
+ basePath,
3928
+ theirsPath
3929
+ ],
3930
+ { reject: false, stripFinalNewline: false }
3931
+ );
3932
+ if (merged.stderr) throw new Error(`git merge-file failed: ${merged.stderr}`);
3933
+ await writeFile2(oursPath, merged.stdout);
3934
+ return (merged.exitCode ?? 0) > 0;
3935
+ }
3936
+ async function mergeModified(rel, baseDir, theirsDir, catalystRoot) {
3937
+ const basePath = join14(baseDir, rel);
3938
+ const theirsPath = join14(theirsDir, rel);
3939
+ const oursPath = join14(catalystRoot, rel);
3940
+ if (await filesEqual(basePath, theirsPath)) return null;
3941
+ if (!await pathExists(oursPath)) {
3942
+ await copyInto(theirsPath, oursPath);
3943
+ return "conflicted";
3944
+ }
3945
+ if (await isBinary(basePath) || await isBinary(theirsPath) || await isBinary(oursPath)) {
3946
+ if (await filesEqual(oursPath, basePath)) {
3947
+ await copyInto(theirsPath, oursPath);
3948
+ return "applied";
3949
+ }
3950
+ return "conflicted";
3951
+ }
3952
+ return await mergeFile(oursPath, basePath, theirsPath) ? "conflicted" : "applied";
3953
+ }
3954
+ async function mergeAdded(rel, theirsDir, catalystRoot, emptyFile) {
3955
+ const theirsPath = join14(theirsDir, rel);
3956
+ const oursPath = join14(catalystRoot, rel);
3957
+ if (!await pathExists(oursPath)) {
3958
+ await copyInto(theirsPath, oursPath);
3959
+ return "added";
3960
+ }
3961
+ if (await filesEqual(oursPath, theirsPath)) return "applied";
3962
+ if (await isBinary(oursPath) || await isBinary(theirsPath)) return "conflicted";
3963
+ const hadConflict = await mergeFile(oursPath, emptyFile, theirsPath);
3964
+ return hadConflict ? "conflicted" : "applied";
3965
+ }
3966
+ async function mergeDeleted(rel, baseDir, catalystRoot) {
3967
+ const basePath = join14(baseDir, rel);
3968
+ const oursPath = join14(catalystRoot, rel);
3969
+ if (!await pathExists(oursPath)) return null;
3970
+ if (await filesEqual(oursPath, basePath)) {
3971
+ await rm2(oursPath, { force: true });
3972
+ return "deleted";
3973
+ }
3974
+ return "conflicted";
3975
+ }
3976
+ async function mergeCorePerFile(baseDir, theirsDir, catalystRoot, emptyFile) {
3977
+ const baseFiles = new Set(await listFiles(baseDir));
3978
+ const theirsFiles = new Set(await listFiles(theirsDir));
3979
+ const result = { applied: [], added: [], deleted: [], conflicted: [] };
3980
+ const decide = async (rel) => {
3981
+ const inBase = baseFiles.has(rel);
3982
+ const inTheirs = theirsFiles.has(rel);
3983
+ let outcome;
3984
+ if (inBase && inTheirs) {
3985
+ outcome = await mergeModified(rel, baseDir, theirsDir, catalystRoot);
3986
+ } else if (inTheirs) {
3987
+ outcome = await mergeAdded(rel, theirsDir, catalystRoot, emptyFile);
3988
+ } else {
3989
+ outcome = await mergeDeleted(rel, baseDir, catalystRoot);
3990
+ }
3991
+ if (outcome) result[outcome].push(rel);
3992
+ };
3993
+ await Promise.all([.../* @__PURE__ */ new Set([...baseFiles, ...theirsFiles])].map(decide));
3994
+ return result;
3995
+ }
3996
+ async function gitSupportsMergeTree() {
3997
+ try {
3998
+ const { stdout } = await execa3("git", ["--version"]);
3999
+ const match = /(\d+)\.(\d+)/.exec(stdout);
4000
+ if (!match) return false;
4001
+ const [major, minor] = [Number(match[1]), Number(match[2])];
4002
+ return major > 2 || major === 2 && minor >= 38;
4003
+ } catch {
4004
+ return false;
4005
+ }
4006
+ }
4007
+ async function resolveStrategy(strategy) {
4008
+ if (strategy !== "auto") return strategy;
4009
+ if (await gitSupportsMergeTree()) return "tree";
4010
+ consola.warn("git < 2.38 \u2014 using the per-file merge engine (no `git merge-tree`).");
4011
+ return "per-file";
4012
+ }
4013
+ async function mergeCoreTree(baseDir, theirsDir, catalystRoot) {
4014
+ const scratch = await mkdtemp(join14(tmpdir(), "catalyst-merge-"));
4015
+ try {
4016
+ await execa3("git", ["init", "-q", scratch]);
4017
+ await execa3("git", ["config", "core.autocrlf", "false"], {
4018
+ env: { ...process.env, GIT_DIR: join14(scratch, ".git") }
4019
+ });
4020
+ const env2 = {
4021
+ ...process.env,
4022
+ GIT_DIR: join14(scratch, ".git"),
4023
+ GIT_AUTHOR_NAME: "catalyst",
4024
+ GIT_AUTHOR_EMAIL: "catalyst@bigcommerce.com",
4025
+ GIT_COMMITTER_NAME: "catalyst",
4026
+ GIT_COMMITTER_EMAIL: "catalyst@bigcommerce.com"
4027
+ };
4028
+ const writeTree = async (workTree, tag) => {
4029
+ const sideEnv = {
4030
+ ...env2,
4031
+ GIT_WORK_TREE: workTree,
4032
+ GIT_INDEX_FILE: join14(scratch, `index.${tag}`)
4033
+ };
4034
+ await execa3("git", ["add", "-A"], { env: sideEnv });
4035
+ return (await execa3("git", ["write-tree"], { env: sideEnv })).stdout.trim();
4036
+ };
4037
+ const commitTree = (tree, parent) => execa3("git", ["commit-tree", tree, "-m", "x", ...parent ? ["-p", parent] : []], {
4038
+ env: env2
4039
+ }).then((r) => r.stdout.trim());
4040
+ const baseTree = await writeTree(baseDir, "base");
4041
+ const oursTree = await writeTree(catalystRoot, "ours");
4042
+ const theirsTree = await writeTree(theirsDir, "theirs");
4043
+ const baseCommit = await commitTree(baseTree);
4044
+ const [oursCommit, theirsCommit] = await Promise.all([
4045
+ commitTree(oursTree, baseCommit),
4046
+ commitTree(theirsTree, baseCommit)
4047
+ ]);
4048
+ await Promise.all([
4049
+ execa3("git", ["branch", "ours", oursCommit], { env: env2 }),
4050
+ execa3("git", ["branch", "theirs", theirsCommit], { env: env2 })
4051
+ ]);
4052
+ const merge = await execa3(
4053
+ "git",
4054
+ [
4055
+ "merge-tree",
4056
+ "--write-tree",
4057
+ "-z",
4058
+ "--name-only",
4059
+ `--merge-base=${baseCommit}`,
4060
+ "ours",
4061
+ "theirs"
4062
+ ],
4063
+ { env: env2, reject: false }
4064
+ );
4065
+ if ((merge.exitCode ?? 0) > 1)
4066
+ throw new Error(merge.stderr || merge.stdout.slice(0, 500) || "git merge-tree failed");
4067
+ const fields = merge.stdout.split("\0");
4068
+ const mergedTree = fields[0];
4069
+ const afterOid = fields.slice(1);
4070
+ const emptyAt = afterOid.indexOf("");
4071
+ const conflicted = afterOid.slice(0, emptyAt === -1 ? afterOid.length : emptyAt);
4072
+ const conflictedSet = new Set(conflicted);
4073
+ const outEnv = {
4074
+ ...env2,
4075
+ GIT_WORK_TREE: catalystRoot,
4076
+ GIT_INDEX_FILE: join14(scratch, "index.out")
4077
+ };
4078
+ await execa3("git", ["read-tree", mergedTree], { env: outEnv });
4079
+ await execa3("git", ["checkout-index", "-a", "-f"], { env: outEnv });
4080
+ const diff = await execa3(
4081
+ "git",
4082
+ ["diff", "-z", "--no-renames", "--name-status", oursTree, mergedTree],
4083
+ { env: env2 }
4084
+ );
4085
+ const tokens = diff.stdout.split("\0");
4086
+ const changes = [];
4087
+ for (let i = 0; i + 1 < tokens.length; i += 2) {
4088
+ const status = tokens[i];
4089
+ const path = tokens[i + 1];
4090
+ if (status && path && !conflictedSet.has(path)) {
4091
+ changes.push({ status, path });
4092
+ }
4093
+ }
4094
+ const deleted = changes.filter((change) => change.status === "D").map((change) => change.path);
4095
+ await Promise.all(deleted.map((path) => rm2(join14(catalystRoot, path), { force: true })));
4096
+ return {
4097
+ applied: changes.filter((change) => change.status !== "A" && change.status !== "D").map((change) => change.path),
4098
+ added: changes.filter((change) => change.status === "A").map((change) => change.path),
4099
+ deleted,
4100
+ conflicted
4101
+ };
4102
+ } finally {
4103
+ await rm2(scratch, { recursive: true, force: true, maxRetries: 10, retryDelay: 500 });
4104
+ }
4105
+ }
4106
+ async function applyIndexState(gitRoot, relDir, baseDir, theirsDir, result, stampedPkg) {
4107
+ const toRepoPath = (rel) => relDir === "." ? rel : `${relDir}/${rel}`;
4108
+ const pkgRel = "package.json";
4109
+ const stillConflicted = result.conflicted.filter((rel) => !(stampedPkg && rel === pkgRel));
4110
+ const conflictedRepoPaths = new Set(stillConflicted.map(toRepoPath));
4111
+ const clean = [...result.applied, ...result.added, ...result.deleted].map(toRepoPath);
4112
+ if (stampedPkg) clean.push(toRepoPath(pkgRel));
4113
+ const toStage = [...new Set(clean)].filter((path) => !conflictedRepoPaths.has(path));
4114
+ if (toStage.length) {
4115
+ await execa3("git", ["add", "-A", "--", ...toStage], { cwd: gitRoot });
4116
+ }
4117
+ if (stillConflicted.length === 0) return;
4118
+ const OID = /^[0-9a-f]{40,64}$/;
4119
+ const gitFormat = (await execa3("git", ["rev-parse", "--show-object-format"], { cwd: gitRoot, reject: false })).stdout.trim();
4120
+ const NULL_OID = gitFormat === "sha256" ? "0".repeat(64) : "0".repeat(40);
4121
+ const hashBlob = async (file) => {
4122
+ if (!await pathExists(file)) return null;
4123
+ const oid = (await execa3("git", ["hash-object", "-w", "--", file], { cwd: gitRoot })).stdout.trim();
4124
+ return OID.test(oid) ? oid : null;
4125
+ };
4126
+ const headEntry = async (repoPath) => {
4127
+ const { stdout } = await execa3("git", ["ls-tree", "HEAD", "--", repoPath], {
4128
+ cwd: gitRoot,
4129
+ reject: false
4130
+ });
4131
+ const match = /^(\d{6}) blob ([0-9a-f]+)\t/.exec(stdout);
4132
+ return match ? { mode: match[1], oid: match[2] } : null;
4133
+ };
4134
+ const records = await Promise.all(
4135
+ stillConflicted.map(async (rel) => {
4136
+ const repoPath = toRepoPath(rel);
4137
+ const [base, ours, theirs] = await Promise.all([
4138
+ hashBlob(join14(baseDir, rel)),
4139
+ headEntry(repoPath),
4140
+ hashBlob(join14(theirsDir, rel))
4141
+ ]);
4142
+ return [
4143
+ `0 ${NULL_OID} ${repoPath}`,
4144
+ ...base ? [`100644 ${base} 1 ${repoPath}`] : [],
4145
+ ...ours ? [`${ours.mode} ${ours.oid} 2 ${repoPath}`] : [],
4146
+ ...theirs ? [`100644 ${theirs} 3 ${repoPath}`] : []
4147
+ ].join("\n");
4148
+ })
4149
+ );
4150
+ await execa3("git", ["update-index", "--index-info"], {
4151
+ cwd: gitRoot,
4152
+ input: `${records.join("\n")}
4153
+ `
4154
+ });
4155
+ }
4156
+ async function resolveProject2(cwd) {
4157
+ let gitRoot;
4158
+ try {
4159
+ gitRoot = (await execa3("git", ["rev-parse", "--show-toplevel"], { cwd })).stdout.trim();
4160
+ } catch {
4161
+ return null;
4162
+ }
4163
+ const candidateDirs = [.../* @__PURE__ */ new Set([join14(gitRoot, "core"), gitRoot, join14(cwd, "core"), cwd])];
4164
+ const parsed = (await Promise.all(
4165
+ candidateDirs.map(async (dir) => {
4166
+ const pkgPath = join14(dir, "package.json");
4167
+ const raw = await readFile2(pkgPath, "utf-8").catch(() => null);
4168
+ if (!raw) return null;
4169
+ let pkgJson;
4170
+ try {
4171
+ pkgJson = JSON.parse(raw);
4172
+ } catch {
4173
+ return null;
4174
+ }
4175
+ const result = CorePackageJson.safeParse(pkgJson);
4176
+ return result.success ? { dir, pkgPath, raw, pkg: result.data } : null;
4177
+ })
4178
+ )).filter((entry) => entry !== null);
4179
+ if (parsed.length === 0) return null;
4180
+ const chosen = parsed.find((p) => p.pkg.catalyst?.ref) ?? parsed.find((p) => p.pkg.name !== void 0 && KNOWN_FAMILIES.has(p.pkg.name)) ?? parsed[0];
4181
+ return {
4182
+ gitRoot,
4183
+ catalystRoot: chosen.dir,
4184
+ relDir: relative2(gitRoot, chosen.dir) || ".",
4185
+ pkgPath: chosen.pkgPath,
4186
+ rawContent: chosen.raw,
4187
+ pkg: chosen.pkg
4188
+ };
4189
+ }
4190
+ async function downloadCore(repository, ref, destDir, token) {
4191
+ const cacheable = !MOVING_TAGS.has(parseRef(ref).version);
4192
+ const cacheKey = `${repository.replace(/[^a-zA-Z0-9._-]/g, "_")}__${ref.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
4193
+ const cachePath = join14(CACHE_DIR, cacheKey);
4194
+ if (cacheable && await pathExists(cachePath) && await pathExists(join14(cachePath, "package.json"))) {
4195
+ await cp2(cachePath, destDir, { recursive: true });
4196
+ return;
4197
+ }
4198
+ const url = `https://api.github.com/repos/${repository}/tarball/${encodeURIComponent(ref)}`;
4199
+ const headers = {
4200
+ "User-Agent": "catalyst-cli",
4201
+ Accept: "application/vnd.github+json"
4202
+ };
4203
+ if (token) headers.Authorization = `Bearer ${token}`;
4204
+ const res = await fetch(url, { headers });
4205
+ if (!res.ok) {
4206
+ let hint = "";
4207
+ if (res.status === 404) hint = ` \u2014 tag "${ref}" not found in ${repository}`;
4208
+ if (res.status === 403) hint = " \u2014 GitHub rate limit hit; set GITHUB_TOKEN to raise it";
4209
+ throw new Error(`GitHub returned ${res.status} for ${ref}${hint}`);
4210
+ }
4211
+ const tarballPath = `${destDir}.tgz`;
4212
+ const rawDir = `${destDir}.raw`;
4213
+ await writeFile2(tarballPath, Buffer.from(await res.arrayBuffer()));
4214
+ await mkdir(rawDir, { recursive: true });
4215
+ await execa3("tar", ["-xzf", tarballPath, "-C", rawDir, "--strip-components=1"]);
4216
+ const coreDir = join14(rawDir, "core");
4217
+ const sourceDir = await pathExists(coreDir) ? coreDir : rawDir;
4218
+ await rename(sourceDir, destDir);
4219
+ await rm2(tarballPath, { force: true });
4220
+ await rm2(rawDir, { recursive: true, force: true });
4221
+ if (cacheable) {
4222
+ await mkdir(CACHE_DIR, { recursive: true });
4223
+ await cp2(destDir, cachePath, { recursive: true }).catch(() => {
4224
+ });
4225
+ }
4226
+ }
4227
+ async function resolveBaseRef(project2, fromOption, assumeYes) {
4228
+ if (project2.pkg.catalyst?.ref) return project2.pkg.catalyst.ref;
4229
+ if (fromOption) return fromOption;
4230
+ const family = project2.pkg.name && KNOWN_FAMILIES.has(project2.pkg.name) ? project2.pkg.name : "@bigcommerce/catalyst-core";
4231
+ const guess = `${family}@${project2.pkg.version}`;
4232
+ consola.warn(`No \`catalyst.ref\` found in package.json. Inferred base: ${guess}`);
4233
+ if (assumeYes) return guess;
4234
+ const ok = await confirm6({ message: `Upgrade from ${guess}?`, default: true }).catch(() => false);
4235
+ if (!ok) {
4236
+ consola.info("Re-run with `--from <ref>` to set the base version explicitly.");
4237
+ return null;
4238
+ }
4239
+ return guess;
4240
+ }
4241
+ function printSummary(result, relDir, stampedPkg) {
4242
+ const unresolved = result.conflicted.filter((f) => !(stampedPkg && f === "package.json"));
4243
+ const parts = [
4244
+ `${result.applied.length} updated`,
4245
+ `${result.added.length} added`,
4246
+ `${result.deleted.length} removed`
4247
+ ];
4248
+ if (unresolved.length) parts.push(`${unresolved.length} with conflicts`);
4249
+ consola.log(`
4250
+ ${parts.join(", ")}`);
4251
+ if (unresolved.length) {
4252
+ const files = unresolved.map((f) => ` ${f}`).join("\n");
4253
+ consola.warn(
4254
+ `
4255
+ Staged the clean changes. ${unresolved.length} file(s) need conflict resolution (the <<<ours/===/theirs>>> markers):
4256
+ ${files}
4257
+
4258
+ Resolve them, then: git add ${relDir} && git commit`
4259
+ );
4260
+ } else {
4261
+ consola.success("Staged all changes \u2014 review with `git diff --cached`, then commit.");
4262
+ }
4263
+ }
4264
+ var upgrade = new Command12("upgrade").configureHelp({ showGlobalOptions: true }).aliases([
4265
+ "up",
4266
+ // These are hidden aliases
4267
+ "upgrayedd",
4268
+ "ugrad"
4269
+ ]).description("Upgrade your Catalyst project to a newer version by applying a 3-way merge.").argument("[version]", "Target version to upgrade to (default: latest)").addOption(
4270
+ new Option10(
4271
+ "--ref <ref>",
4272
+ "Full git tag to upgrade to, e.g. an integration family like @bigcommerce/catalyst-makeswift@1.7.0. Overrides [version]."
4273
+ )
4274
+ ).addOption(new Option10("--from <ref>", "Base ref to upgrade from (when catalyst.ref is missing)")).option("--dry-run", "Generate and display the diff without applying it").option("--yes", "Skip confirmation prompts (e.g. inferred base ref)").addOption(
4275
+ new Option10(
4276
+ "--strategy <strategy>",
4277
+ "Merge engine: tree (git merge-tree, full fidelity), per-file (git merge-file, no-history fallback), or auto (tree when git >= 2.38, else per-file)"
4278
+ ).choices(["auto", "tree", "per-file"]).default("auto").hideHelp()
4279
+ ).addOption(
4280
+ new Option10("--repository <owner/repo>", "GitHub repository to pull versions from").default(
4281
+ DEFAULT_REPOSITORY
4282
+ )
4283
+ ).addHelpText(
4284
+ "after",
4285
+ `
4286
+ Examples:
4287
+ # Upgrade to the latest Catalyst version
4288
+ $ catalyst upgrade
4289
+
4290
+ # Upgrade to a specific version
4291
+ $ catalyst upgrade 1.8.0
4292
+
4293
+ # Preview what would change without applying
4294
+ $ catalyst upgrade --dry-run
4295
+
4296
+ # Upgrade to an integration family (makeswift, b2b-makeswift, ...)
4297
+ $ catalyst upgrade --ref @bigcommerce/catalyst-makeswift@1.7.0
4298
+
4299
+ Conflicts are written as standard <<<ours/===/theirs>>> markers \u2014 the upgrade
4300
+ never aborts. Versions are git tags on the repository (not npm). Set GITHUB_TOKEN
4301
+ to raise the GitHub API rate limit.`
4302
+ ).action(async (version2, options) => {
4303
+ const project2 = await resolveProject2(process.cwd());
4304
+ if (!project2) {
4305
+ consola.error(
4306
+ "Run `catalyst upgrade` from inside a Catalyst git repository (flat core/ repo or a project containing core/)."
4307
+ );
4308
+ process.exit(1);
4309
+ }
4310
+ const { gitRoot, catalystRoot, relDir, pkgPath } = project2;
4311
+ const baseRef = await resolveBaseRef(project2, options.from, options.yes ?? false);
4312
+ if (!baseRef) process.exit(1);
4313
+ let basePackage;
4314
+ let upstreamRef;
4315
+ let upstreamTagVersion;
4316
+ try {
4317
+ ({ packageName: basePackage } = parseRef(baseRef));
4318
+ upstreamRef = options.ref ?? `${basePackage}@${version2 ?? "latest"}`;
4319
+ ({ version: upstreamTagVersion } = parseRef(upstreamRef));
4320
+ } catch {
4321
+ consola.error(
4322
+ `Invalid ref format \u2014 expected <package>@<version> (e.g. @bigcommerce/catalyst-core@1.7.0). Got: "${baseRef}"`
4323
+ );
4324
+ process.exit(1);
4325
+ }
4326
+ if (upstreamRef === baseRef) {
4327
+ consola.success("Already up to date.");
4328
+ return;
4329
+ }
4330
+ if (!options.dryRun) {
4331
+ const statusArgs = relDir === "." ? [] : ["--", relDir];
4332
+ const status = await execa3("git", ["status", "--porcelain", ...statusArgs], {
4333
+ cwd: gitRoot,
4334
+ reject: false
4335
+ });
4336
+ if (status.stdout.trim()) {
4337
+ const where = relDir === "." ? "The repo" : `${relDir}/`;
4338
+ consola.error(
4339
+ `${where} has uncommitted changes. Commit or stash them before upgrading, so the upgrade is isolated and reversible:
4340
+ git add ${relDir} && git commit -m "snapshot before upgrade"
4341
+ # or: git stash`
4342
+ );
4343
+ process.exit(1);
4344
+ }
4345
+ }
4346
+ if (!project2.pkg.catalyst?.ref && !options.from && !options.dryRun) {
4347
+ const { version: baseVersion } = parseRef(baseRef);
4348
+ const rawPkg = await readFile2(pkgPath, "utf-8");
4349
+ const parsedPkg = z15.record(z15.string(), z15.unknown()).parse(JSON.parse(rawPkg));
4350
+ parsedPkg.catalyst = { version: baseVersion, ref: baseRef };
4351
+ await writeFile2(pkgPath, `${JSON.stringify(parsedPkg, null, 2)}
4352
+ `);
4353
+ await execa3("git", ["add", "--", pkgPath], { cwd: gitRoot });
4354
+ consola.success(`Added catalyst.ref \u2192 ${baseRef}`);
4355
+ }
4356
+ const token = process.env.GITHUB_TOKEN;
4357
+ const tmpDir = await mkdtemp(join14(tmpdir(), "catalyst-upgrade-"));
4358
+ try {
4359
+ const baseDir = join14(tmpDir, "base");
4360
+ const theirsDir = join14(tmpDir, "theirs");
4361
+ const emptyFile = join14(tmpDir, ".empty");
4362
+ await writeFile2(emptyFile, "");
4363
+ const downloadSpinner = yoctoSpinner4().start(`Downloading ${baseRef} and ${upstreamRef}...`);
4364
+ try {
4365
+ await Promise.all([
4366
+ downloadCore(options.repository, baseRef, baseDir, token),
4367
+ downloadCore(options.repository, upstreamRef, theirsDir, token)
4368
+ ]);
4369
+ downloadSpinner.success("Downloaded both versions.");
4370
+ } catch (err) {
4371
+ downloadSpinner.error("Download failed");
4372
+ throw err;
4373
+ }
4374
+ if (!project2.pkg.catalyst?.ref && !options.from) {
4375
+ const similarity = await computeBaseSimilarity(baseDir, catalystRoot);
4376
+ const pct = Math.round(similarity * 100);
4377
+ if (similarity < 0.5) {
4378
+ consola.warn(
4379
+ `Low confidence in inferred base ${baseRef} (${pct}% of base files match your project). If the merge result looks off, re-run with \`--from <correct-version>\` to set the base explicitly.`
4380
+ );
4381
+ }
4382
+ }
4383
+ const resolvedVersion = MOVING_TAGS.has(upstreamTagVersion) ? await readResolvedVersion(theirsDir) : upstreamTagVersion;
4384
+ const newRef = MOVING_TAGS.has(upstreamTagVersion) ? `${basePackage}@${resolvedVersion}` : upstreamRef;
4385
+ if (options.dryRun) {
4386
+ const diffSpinner = yoctoSpinner4().start("Generating diff...");
4387
+ const diff = await execa3(
4388
+ "git",
4389
+ ["diff", "--no-index", "--binary", "--diff-algorithm=histogram", "base", "theirs"],
4390
+ { cwd: tmpDir, reject: false, stripFinalNewline: false }
4391
+ );
4392
+ if ((diff.exitCode ?? 0) > 1) {
4393
+ diffSpinner.error("Failed to generate diff");
4394
+ throw new Error(diff.stderr);
4395
+ }
4396
+ if (!diff.stdout.trim()) {
4397
+ diffSpinner.success("No differences between versions \u2014 already up to date.");
4398
+ return;
4399
+ }
4400
+ const fileCount = (diff.stdout.match(/^diff --git /gm) ?? []).length;
4401
+ diffSpinner.success(`Diff ready \u2014 ${fileCount} file(s) affected.`);
4402
+ consola.log("\nDiff preview (--dry-run, not applied):\n");
4403
+ consola.log(diff.stdout);
4404
+ return;
4405
+ }
4406
+ const strategy = await resolveStrategy(options.strategy);
4407
+ const mergeSpinner = yoctoSpinner4().start(`Merging changes (${strategy})...`);
4408
+ const result = strategy === "tree" ? await mergeCoreTree(baseDir, theirsDir, catalystRoot) : await mergeCorePerFile(baseDir, theirsDir, catalystRoot, emptyFile);
4409
+ const total = result.applied.length + result.added.length + result.deleted.length + result.conflicted.length;
4410
+ if (total === 0) {
4411
+ mergeSpinner.success("No differences between versions \u2014 already up to date.");
4412
+ return;
4413
+ }
4414
+ if (result.conflicted.length) {
4415
+ mergeSpinner.warning("Merged with conflicts \u2014 resolve the markers, then commit.");
4416
+ } else {
4417
+ mergeSpinner.success("Merged cleanly.");
4418
+ }
4419
+ const patchedRaw = await readFile2(pkgPath, "utf-8");
4420
+ let stampedPkg = false;
4421
+ try {
4422
+ const patchedPkg = z15.record(z15.string(), z15.unknown()).parse(JSON.parse(patchedRaw));
4423
+ patchedPkg.catalyst = { version: resolvedVersion, ref: newRef };
4424
+ await writeFile2(pkgPath, `${JSON.stringify(patchedPkg, null, 2)}
4425
+ `);
4426
+ stampedPkg = true;
4427
+ consola.success(`catalyst.ref updated \u2192 ${newRef}`);
4428
+ } catch {
4429
+ const catalystReplacement = JSON.stringify(
4430
+ { version: resolvedVersion, ref: newRef },
4431
+ null,
4432
+ 2
4433
+ ).split("\n").map((line, i) => i === 0 ? line : ` ${line}`).join("\n");
4434
+ const updated = patchedRaw.replace(
4435
+ /"catalyst":\s*\{[^}]*\}/,
4436
+ () => `"catalyst": ${catalystReplacement}`
4437
+ );
4438
+ if (updated !== patchedRaw) {
4439
+ await writeFile2(pkgPath, updated);
4440
+ consola.success(`catalyst.ref updated \u2192 ${newRef}`);
4441
+ consola.info(
4442
+ "package.json still has conflicts in other sections \u2014 resolve them, then: git add package.json"
4443
+ );
4444
+ } else {
4445
+ consola.warn(
4446
+ `package.json has conflicts \u2014 after resolving, add:
4447
+ "catalyst": { "version": "${resolvedVersion}", "ref": "${newRef}" }`
4448
+ );
4449
+ }
4450
+ }
4451
+ try {
4452
+ await applyIndexState(gitRoot, relDir, baseDir, theirsDir, result, stampedPkg);
4453
+ } catch (err) {
4454
+ consola.warn(
4455
+ `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.`
4456
+ );
4457
+ }
4458
+ const gitVersion = await execa3("git", ["--version"]).then((r) => r.stdout.trim()).catch(() => "unknown");
4459
+ await getTelemetry().track("upgrade", {
4460
+ strategy,
4461
+ gitVersion,
4462
+ dryRun: Boolean(options.dryRun),
4463
+ applied: result.applied.length,
4464
+ added: result.added.length,
4465
+ deleted: result.deleted.length,
4466
+ conflicts: result.conflicted.length,
4467
+ hasConflicts: result.conflicted.length > 0
4468
+ });
4469
+ printSummary(result, relDir, stampedPkg);
4470
+ } finally {
4471
+ await rm2(tmpDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 500 });
4472
+ }
4473
+ });
4474
+
3204
4475
  // src/cli/commands/version.ts
3205
- import { Command as Command11 } from "commander";
3206
- var version = new Command11("version").configureHelp({ showGlobalOptions: true }).description("Display detailed version information.").addHelpText(
4476
+ import { Command as Command13 } from "commander";
4477
+ var version = new Command13("version").configureHelp({ showGlobalOptions: true }).description("Display detailed version information.").addHelpText(
3207
4478
  "after",
3208
4479
  `
3209
4480
  Example:
@@ -3265,19 +4536,19 @@ var telemetryPostHook = async () => {
3265
4536
 
3266
4537
  // src/cli/program.ts
3267
4538
  var defaultEnvPath = resolve(process.cwd(), ".env.local");
3268
- if (existsSync5(defaultEnvPath)) {
4539
+ if (existsSync4(defaultEnvPath)) {
3269
4540
  config({ path: defaultEnvPath });
3270
4541
  }
3271
4542
  if (!process.env.CATALYST_STORE_HASH && process.env.BIGCOMMERCE_STORE_HASH) {
3272
4543
  process.env.CATALYST_STORE_HASH = process.env.BIGCOMMERCE_STORE_HASH;
3273
4544
  }
3274
- var program = new Command12();
3275
- consola.log(colorize8("cyanBright", `\u25E2 ${package_default.name} v${package_default.version}
4545
+ var program = new Command14();
4546
+ consola.log(colorize11("cyanBright", `\u25E2 ${package_default.name} v${package_default.version}
3276
4547
  `));
3277
4548
  program.name(package_default.name).version(package_default.version).summary("CLI tool for Catalyst development").description(
3278
4549
  "CLI tool for Catalyst development.\n\nConfiguration priority: flags > env file (--env-path) > process.env > .env.local (auto-loaded from cwd) > .bigcommerce/project.json.\n\nCATALYST_STORE_HASH falls back to BIGCOMMERCE_STORE_HASH if unset.\n\nRun `catalyst <command> --help` for details on a specific command."
3279
4550
  ).configureHelp({ showGlobalOptions: true }).addOption(
3280
- new Option9(
4551
+ new Option11(
3281
4552
  "--env-path <path>",
3282
4553
  "Path to environment file to load (relative to current working directory)"
3283
4554
  // We are using argParser, because commander loads in environment variables before executing hooks.
@@ -3293,12 +4564,12 @@ program.name(package_default.name).version(package_default.version).summary("CLI
3293
4564
  const message = errCode === "ENOENT" ? `Env file not found: ${envFilePath}` : `Failed to load --env-path ${value}: ${result.error.message}`;
3294
4565
  throw new Error(message);
3295
4566
  }
3296
- consola.log(colorize8("cyanBright", `Loaded environment variables from ${envFilePath}
4567
+ consola.log(colorize11("cyanBright", `Loaded environment variables from ${envFilePath}
3297
4568
  `));
3298
4569
  }
3299
4570
  return value;
3300
4571
  })
3301
- ).addCommand(version).addCommand(create).addCommand(start).addCommand(build).addCommand(deploy).addCommand(logs).addCommand(project).addCommand(env).addCommand(channel).addCommand(auth).addCommand(telemetry2).hook("preAction", telemetryPreHook).hook("postAction", telemetryPostHook);
4572
+ ).addCommand(version).addCommand(create).addCommand(start).addCommand(build).addCommand(deploy).addCommand(domains).addCommand(logs).addCommand(project).addCommand(env).addCommand(channel).addCommand(auth).addCommand(upgrade).addCommand(telemetry2).hook("preAction", telemetryPreHook).hook("postAction", telemetryPostHook);
3302
4573
 
3303
4574
  // src/cli/index.ts
3304
4575
  var handleFatalError = async (error) => {